65.9K
CodeProject 正在变化。 阅读更多。
Home

一个基本的图标编辑器, 在 ReactOS 上运行( 因此也在 Windows XP 及更高版本上运行)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (17投票s)

2019年12月12日

CPOL

20分钟阅读

viewsIcon

33248

downloadIcon

1980

创建一个代码量尽可能少的基本图标编辑器,使其在ReactOS和Windows上运行,以检查ReactOS上应用程序开发能力的稳定性。

目录

引言

ReactOS 是 Windows 操作系统的开源替代品。即使 ReactOS 的第一个版本可以追溯到 1998 年,目前仍然没有“稳定”版本的 ReactOS。也许最重要的原因是缺乏关注。

受《ReactOS 上无需资源嵌入图标的介绍》一文启发,我希望深入研究 Windows 图标主题,并寻找一个足够简单易用、无需长时间阅读文档的免费 ReactOS 图标编辑器。我找到的最佳匹配是 Junior Icon Editor。这款图标编辑器在我测试过的所有 Microsoft Windows 版本上都能正常工作,并且也可以轻松地在 ReactOS 上安装和启动。唯一的缺点是 4bpp 图标(16 色图标)的调色板无法工作(在 ReactOS 0.4.11 版和 Junior Icon Editor 4.39 版上测试)。

在 ReactOS 上 Junior Icon Editor 的 8 bpp 图标 (256 色图标) 的调色板    

在 ReactOS 上 Junior Icon Editor 的 4 bpp 图标 (16 色) 的调色板    

我主要关注 4bpp 图标。尽管 Junior Icon Editor 的调色板问题更可能是由于 ReactOS 中缺少 4bpp 默认调色板而不是 Junior Icon Editor 本身的错误造成的,但创建或编辑 4bpp 图标仍然非常困难。

由于这个缺点,并且为了在一个实际应用程序中测试从《ReactOS 上使用 C/C++ 介绍 OpenGL》和《ReactOS 上使用 C# 介绍》这些技巧中学到的知识,我开始编写自己的基本图标编辑器——ReactOS Icon Editor。这个应用程序的灵感来源于 Visual Studio 图标编辑器的简洁性和直观操作。

更新

版本 0.2

  • 新增:4bpp 调色板的 16 种颜色可以进行个性化设置。双击颜色会打开 ReactOS 的颜色对话框,并且可以单独设置颜色值。优点:4bpp 调色板也可以用于创建吸引人的图标。缺点:并非所有图标编辑器都支持个性化颜色,并会将颜色重置为默认调色板——例如 Visual Studio 图标编辑器。
  • 新增:吸管工具已实现。现在也可以在图像内部选择颜色。
  • 新增:菜单项现在也支持带透明度的图标(13x13 像素)。它们会自动切换到自绘模式。优点:菜单项位图的原始原生支持仅适用于 1bpp 图像,不支持彩色图像的透明度。使用图标,彩色图像也支持透明度。缺点:切换到自绘模式在使用主题时可能导致不一致的结果。
  • 修复:“另存为”对话框现在可以正常工作/不再产生分段错误(修复了调用 wcsncpy_s() 时参数 4 错误的问题)。
  • 修复:修复了在处理宽度/高度不是 8 的倍数的图标(例如菜单项的 13x13 像素图标)时出现的分段错误。
  • 升级:改进了支持加速键的 OWNERDRAW 菜单。为许多菜单项提供了加速键。

版本 0.3

这次更新花了很多时间——但这与我的学习曲线有关。首先,我发现了 David Nash 编写的精彩的 Win32++(它的简洁和清晰给我留下了深刻印象,就像 30 多年前 K&R C Athena Widget Set 中对继承和面向对象的先发制人一样),其次,这是我第一次认真研究 DoxyGen

你可能会问我:为什么我的图标编辑器不是基于 Win32++ 的?

  • 我认为 Win32++ 很酷,但我发现 Win32++ 的时间很晚。
  • 我正在认真考虑切换到 Win32++——特别是考虑到其已有的稳定性——但我对主框架窗口中 MenuBarToolBar 的实现并不完全满意。
  • Win32++ 的设计以其清晰和简洁给我留下了深刻印象——但是开发自己的控件时对同步的需求仍然有点令人望而却步。

也许我的图标编辑器变得更专业——因此在程序结构上更像 Win32++——这种情况会改变。

  • 新增:我的第一个颜色选择器尝试为每种颜色使用一个 Button 控件。这对于 4bpp(16 色)模式来说没问题,但对于 8bpp(256 色)模式会导致 256 个控件。为了支持 8bpp,我实现了新的 ColorPicker 控件,它为 4bpp 设置了 8 列 2 行 SetCellCount(SET_SIZE(8, 2))(并且看起来与之前的解决方案相同)。

    并可能为 8bpp 设置 14 列 18 行 SetCellCount(SET_SIZE(14, 18)) (这将使图标编辑器能够扩展到 8bpp)。

  • 新增:填充工具现已实现
  • 新增:整个 Ogww 库中的所有代码注释都已修改,以便可以使用 DoxyGen 生成有意义的文档。我使用 Code::Blocks 的 DoxyBlocks 插件(并且对生成的文档非常满意)。有关此内容的详细信息,请参阅我的技巧文章 ReactOS 上 C/C++ OpenGL 简介 中的“添加文档生成器”部分。
  • 修复:使用 GetDIBits() 时的内存冲突问题已修复——至少在 Microsoft Windows 上是这样。有时(根据我对需要调色板的颜色深度的观察),GetDIBits() 会将一个最小调色板写入 BITMAPINFOHEADER。根据 Microsoft 论坛,它们是 3 种颜色(3* sizeof(WORD))。如果未考虑这一点,就会发生内存冲突。

版本 0.4

  • 修复:打开图标文件后,图像按钮重叠显示(过时的位置)以及尺寸/位置不正确的问题已修复,此前必须通过调整窗口大小手动纠正。通过在打开图标文件后将窗口大小传递给 ::SendMessage(hWnd, WM_SIZE, (WPARAM)SIZE_RESTORED, ...) 来修复。
  • 修复:在更改调色板颜色后,ColorPickerPixelEdit 中旧颜色(过时的 RGB 值)的显示问题已修复,此前必须通过调整窗口大小手动纠正。通过使用 ::InvalidateRect(_pweakColorPicker->GetHWnd(), NULL, TRUE)::InvalidateRect(_pweakPixelEdit->GetHWnd(), NULL, TRUE) 重新绘制 ColorPickerPixelEdit 来修复。
  • 新增:添加了内存泄漏检测。我阅读了这篇很棒的文章 一个跨平台的内存泄漏检测器,并思考我的应用程序在内存管理方面表现如何。另请参阅本文中的“内存泄漏检测”部分。
  • 新增:将调色板颜色更改添加到撤销/重做链。
  • 新增:整个 IconEditor 中的所有代码注释都已修改,以便可以使用 DoxyGen 生成有意义的文档。我使用 Code::Blocks 的 DoxyBlocks 插件(并且对生成的文档非常满意)。有关此内容的详细信息,请参阅我的技巧文章 ReactOS 上 C/C++ OpenGL 简介 中的“添加文档生成器”部分。
  • 新增:自绘按钮支持 BS_RADIOBUTTONBS_AUTORADIOBUTTON。这是一个只有原生窗口控件的自动绘制按钮才支持的功能(因为 BS_OWNERDRAW 的位掩码与其他按钮样式重叠,不能与 BS_CHECKBOXBS_AUTOCHECKBOXBS_RADIOBUTTONBS_AUTORADIOBUTTON 同时使用)。
  • 新增:添加了多图像图标的初始处理:多图像图标的加载/保存/另存为功能可用。图像可以添加到现有图标(但目前无法移除),并且图像顺序可以更改。

版本 0.5

  • 新增:已实现从图标中删除图像功能(最后一个剩余图像无法删除)。
  • 新增:工具栏按钮支持工具提示。
  • 改进:返回动态分配字符串的方法不再使用 StringMediator*,而是使用 String。这确保了与以前一样的自动垃圾回收,并且代码对于任何 C++ 程序员来说都是一目了然的。有关详细信息,请参阅技巧 如何从 C++ 函数/方法返回字符串类
  • 新增:作为简单 ToolBar 的替代方案,现在可以在一个 ReBar 中使用多个 ToolBarReBar 的第一个 ToolBar 会自动创建为默认 ToolBar
  • 改进:禁用菜单项和工具栏按钮的图像现在计算为灰度图像,而不是带阴影的灰/白图像。
  • 修复:撤消(Ctrl + z)和重做(Ctrl + y)的快捷方式现在可以正常工作。
  • 新增:已添加选择(套索)工具,用于捕获像素的矩形区域作为选定区域(此列表下方的图像显示了工具栏按钮和选定像素的示例矩形区域)。
  • 新增:添加了当前图标图像的基本复制(Ctrl + c)(例如,复制到画图)和当前图标图像的基本粘贴(Ctrl + v)(例如,从画图粘贴)。如果选择了像素的矩形区域,则粘贴只影响选择。有关详细信息,请参阅技巧 在 ReactOS(以及 Windows XP 和更高版本,使用 Win32 API)上将位图粘贴到图标图像中的方法

版本 0.6

  • 已修复:方法 CDDBitmap::CopyTo4bppColors()CDDBitmap::CopyTo8bppColors() 现在可以正常工作(它们有时在缩减最终颜色表的颜色向量时会抛出异常)。
  • 新增:现在可以为应用程序主窗口设置小图标和大图标。
  • 改进:C API 已清理,以支持整个 API 的 C# P/Invoke(互操作)。
  • 新增:已为 ReactOS(编辑器:Notepad++ 附带 NppExec 插件,编译器:MONO 安装 mono-4.3.2.467-gtksharp-2.12.30.1-win32-0.msi,详情请参阅技巧 ReactOS 上 C# 简介)和 Windows (Visual Studio) 添加了一个与 C++ 应用程序等效的 C# 版本。
  • 改进:撤销/重做链不再与像素编辑器控件连接,因此不再对所有图标图像通用,而是每个图标图像现在都拥有本地的撤销/重做链。此外,当图标图像发生更改时,重做/撤销链保持完整。
  • 改进:该应用程序现在拥有自己的图标——这是使用该应用程序本身创建的第一个有意义的图标。
  • 修复:在粘贴(Ctrl + v)到当前图标图像时出现的内存泄漏已被消除。
  • 新增:现在可以缩放选定的像素矩形区域。

Using the Code

应用程序

ReactOS 图标编辑器基于《ReactOS 上 C/C++ OpenGL 简介》中介绍的 OpenGL Windows Wrapper (Ogww) DLL。同时,该 DLL 已经发展到满足了专业 UI 的更多要求,但该 DLL 距离发布状态仍有很长的路要走。然而,它仍然旨在支持 C/C++ 和 C# 的应用程序开发。

现在,有兴趣的读者会问:为什么是 Ogww——又一个对 Win32 API 的包装器?
简单回答:因为我后来才发现了 Win32++

Win32++ 做得非常出色!我将尝试一点一点地用 Win32++ 尽可能完全地替换 Ogww。尽管我已经知道会面临挑战:CString 不基于 CObject,并且 CObject 不提供 typeof() 运算符 / GetType() 方法或任何替代的简便方式访问 RTTI

ReactOS 图标编辑器目前功能非常有限,但旨在逐步开发成为一个功能齐全的图标编辑器。目前的限制包括:

  • 仅支持 4bpp(16 色)模式
  • 仅支持钢笔、橡皮、填充和吸管工具(计划支持复制/粘贴工具)

目前它看起来是这样的

工具栏

0.5 版开始,可以在一个简单的 ToolBar 和一个提供处理多个 ToolBar 选项的 ReBar 之间进行选择。这是同时显示这两种可能性的代码。

void CIconEditMainFrame::AddToolBar(HWND hWnd)
{
    HBITMAP hColorBmp = NULL;
    HBITMAP hMaskBmp  = NULL;

#ifdef USE_REBAR
    LPVOID  pweakReBarImp = ReBarCreateAndRegister(hWnd, TOOL_BAR_DEFAULT_ID, 16, 5);
    CReBar  aReBar(pweakReBarImp);
    _pweakToolBar = new CToolBar(aReBar.GetFistToolBarImplementation());
#else
    LPVOID pweakToolBarImp = ToolBarCreateAndRegister(hWnd, TOOL_BAR_DEFAULT_ID, 16, 5);
    _pweakToolBar = new CToolBar(pweakToolBarImp);
#endif

    CIcon::LoadIconBitmapsFromBytes(ICO_NEW2_16_Bytes(),    ICO_NEW2_16_ByteCount(),
        16, 16, true, &hColorBmp, &hMaskBmp);
    _pweakToolBar->AddButton(hColorBmp, hMaskBmp, MENU_FILE_NEW_ID,
        TBSTATE_ENABLED, TBSTYLE_BUTTON);
    ::DeleteObject(hColorBmp);
    ::DeleteObject(hMaskBmp);
    _pweakToolBar->SetButtonToolTip(MENU_FILE_NEW_ID, _(L"MAINFRAME|Toolbar",
        L"New\n\nStart with a new\ninitial icon file.", L"Tool bar item", __FILE__));

...

    _pweakToolBar->Show();
}

像素编辑控件

我已将新的窗口类 PixelEditWindow 添加到 OpenGL Windows Wrapper (Ogww) DLL 中——上图显示了该控件在应用程序中心正在运行。它基于 ICONIMAGE 结构(参见下面的“图标内部结构”章节),旨在显示和编辑图像像素。

被遮罩的像素(未显示的像素)以定义的颜色显示,但被划掉。未被遮罩的像素(显示的像素)以定义的颜色显示。
遮罩通过橡皮擦工具设置——它也设置颜色。
画笔工具设置颜色并删除遮罩。

我已决定

  • 在遮罩像素时设置定义的颜色,并且
  • 以定义的颜色显示被遮罩的像素,而不是互补色或任何固定颜色。

当按住鼠标左键时,铅笔工具和橡皮擦工具会工作——只要按住鼠标按钮,就可以通过鼠标移动设置多个像素。

图标设计建议

由于禁用状态的工具栏按钮和菜单项通常会以带阴影的灰/白图像显示,因此工具栏和菜单项图像应始终设计为保留底部和右侧的像素条纹未使用。
左图说明了禁用图像(第二行)自动从正常图像(第一行)计算的过程。

0.5 版更新:我将禁用图标的外观从 Win32 标准(带阴影的灰/白)更改为 用户自定义 的更具吸引力的外观(灰度)。

左图说明了从正常图像(第一行)自动计算禁用图像(第二行)的新过程。

项目

我完全使用 ReactOS 上的 Code::Blocks 开发了 ReactOS Icon Editor。该解决方案目前具有以下结构:

下载(本文顶部)包含一个文件夹结构,其中包含三个文件夹:OGWWOGWW_WrapperReactOS_Icon_Editor

Code::Blocks 解决方案包含以下两个项目:

  • OGWWOpenGL Windows Wrapper DLL),完全包含在 OGWW 文件夹中,以及
  • ReactOS 图标编辑器(图标编辑器应用程序),分为 OGWW_Wrapper 文件夹(包含粘合代码,使 OGWW 可用于 ReactOS 图标编辑器)和 WWOG(应用程序本身)。

首次在新环境中打开项目,必须打开以下两个文件:

  • ...\OGWW\OGWW.cbp
  • ...\ReactOS_Icon_Editor\ReactOS_Icon_Editor.cbp

OGWW 项目使用子文件夹结构来组织代码文件。

  • Images 文件夹包含 DLL 提供给应用程序的图标和位图的 BYTE[]
  • Layouter 文件夹包含所有布局器类。
  • Tests 文件夹包含基本的单元测试和性能测试。
  • Windows 文件夹包含所有基于 Win32 窗口的类(拥有自己的 HWND)。

ReactOS 图标编辑器项目还集成了 OGWW_Wrapper 文件夹,该文件夹包含所有包装器类,以提供 OGWW DLL 的面向对象接口。

为了检查当前构建环境是否配置良好,这里是一个 g++ 的示例调用:

mingw32-g++.exe -Wall -std=c++11 -pg -g -D_UNICODE -DUNICODE -D__MSVCRT__ -Wall -g -DBUILD_DLL
    -c C:\Projects\CodeBlocks\OGWW\Console.cpp -o obj\Debug\Console.o

-std=c++11 开关将 g++ 提升到 ANSI C++2011 标准。这使得除了其他功能外,还可以使用:

  • snwprintf() 而不是 swprintf()
  • std::chrono::high_resolution_clock::now() 而不是 GetTickCount(),以及
  • auto.

-D_UNICODE-DUNICODE 开关确保普遍使用 wchar_t 而不是 char

-D__MSVCRT__ 开关宣布 msvcrt.dll 可用,这使得除了其他功能外,还可以使用 _wgetenv() 而不是 getenv()

图标内部结构

ReactOS 图标编辑器最重要的类是 OgwwIconData 类。该类提供对当前图像的便捷访问,并管理选定的掩码和选定的颜色。它派生自 OgwwIcon 类,该类管理整个图标。图标的基本结构是:

● 图标目录 存储到 ICONDIR 结构中
● 图标目录项 存储到 ICONDIR 结构中的 ICONDIRENTRY[]
    □ 图像目录项 1...n 存储到 ICONDIRENTRY 结构中
● 图像 存储到 ICONIMAGE[]
    □ 图像 1...n 存储到 ICONIMAGE 结构中
       位图信息头 存储到 ICONIMAGE 结构中的 BITMAPINFOHEADER
       ◦ 位图调色板 存储到 ICONIMAGE 结构中的 AARRGGBB[]
       ◦ 颜色位图字节 存储到 ICONIMAGE 结构中的 BYTE[]
       ◦ 掩码位图字节 存储到 ICONIMAGE 结构中的 BYTE[]

只有 BITMAPINFOHEADER 是标准的 Windows 数据结构。我想简要介绍一下其他结构和类型 ICONDIRICONDIRENTRYICONIMAGE

typedef struct tagICONDIR
{
    WORD              idReserved;      // Reserved (must be 0)
    WORD              idType;          // Resource Type (1 for icons)
    WORD              idCount;         // How many images?
    LPICONDIRENTRY    idEntries;       // One entry for each image.
} ICONDIR, *LPICONDIR;
typedef struct tagICONDIRENTRY
{
    BYTE              deWidth;         // Width, in pixels, of the image
    BYTE              deHeight;        // Height, in pixels, of the image
    BYTE              deColorCount;    // Number of colors in image (0 if >=8bpp)
    BYTE              deReserved;      // Reserved ( must be 0)
    WORD              dePlanes;        // Color Planes
    WORD              deBitCount;      // Bits per pixel
    DWORD             deBytesInRes;    // How many bytes are in this image?
    DWORD             deImageOffset;   // Where in the file is this image?
} ICONDIRENTRY, *LPICONDIRENTRY;

图像的大小(deBytesInRes)可以通过以下方式计算:sizeof(BITMAPINFOHEADER) + sizeof(ICONIMAGE::iiColors) + sizeof(ICONIMAGE::iiXOR) + sizeof(ICONIMAGE::iiAND)

typedef DWORD AARRGGBB;

#define AARRGGBBtoCOLORREF(v)  ((DWORD) (((0xFF000000 & ((DWORD)v)) >>  0) |    \\
                                         ((0x00FF0000 & ((DWORD)v)) >> 16) |    \\
                                         ((0x0000FF00 & ((DWORD)v)) << 0)  |    \\
                                         ((0x000000FF & ((DWORD)v)) << 16)) )
#define AARRGGBBtoLUMINANCE(v)  ((WORD) (((0x00FF0000 & ((DWORD)v)) >> 16) +    \\
                                         ((0x0000FF00 & ((DWORD)v)) >>  8) +    \\
                                         ((0x000000FF & ((DWORD)v)) >> 0))  )   \\

typedef struct tagICONIMAGE
{
    BITMAPINFOHEADER   iiHeader;       // DIB header
    AARRGGBB*          iiColors;       // Color table as DWORD[]: AARRGGBB format
    BYTE*              iiXOR;          // DIB bits for XOR image: 4bpp, 8bpp or 32bpp format
    BYTE*              iiAND;          // DIB bits for AND mask
} ICONIMAGE, *LPICONIMAGE;

为了完全控制图标的所有数据,我实现了自己的读取 OgwwIcon::ConstructFromFile(LPCWSTR wszFilePath) 和写入 OgwwIcon::SaveAs(LPCWSTR wszFilePath) 图标的方法。

起初,我专注于支持 16 色图像。进一步开发到 256 色和 16777216 色已在计划中。

闪烁最小化

关于闪烁,存在两个主要问题:

  • 控件重绘(例如,在调整大小时)
  • 编辑图像像素(窗口类 PixelEditWindow / 控件 OgwwPixelEdit

由于控件重绘相对较少发生,我将重点放在编辑图像像素上。为了编辑图像像素,使用了 OgwwPixelEdit 控件,它是一个自绘控件。WS_EX_COMPOSITED 窗口样式要么不能解决问题,要么在 ReactOS 上无法按预期工作(双缓冲)。解决此问题的方案是对 WM_ERASEBKGNDWM_PAINT 消息处理程序进行优化实现,如下所示:

case WM_ERASEBKGND: // 20
{
    // Minimize flickering - part I:
    // * Move background erasing to WM_PAINT to integrate it with foreground drawing.
    return 0;
}
case WM_PAINT:    //  15
{
    PAINTSTRUCT        ps;
    ::BeginPaint(hWnd, &ps);

    RECT clientRect;
    ::GetWindowRect(hWnd, &clientRect);
    clientRect.right  -= clientRect.left; clientRect.left = 0;
    clientRect.bottom -= clientRect.top;  clientRect.top  = 0;

    OgwwPaintArgs      paintArgs(&ps, clientRect);
    OnPaint(paintArgs);

    ::EndPaint(hWnd, &ps);
    //Console::WriteText(Console::GRAY, L"Handle WM_PAINT for blank %d.\n", (int)hWnd);
    return 0;
}

我将 WM_PAINT 的消息处理程序移到了 OnPaint() 方法中。

/// <summary>
/// Processes the <see cref="WM_PAINT"> message.
/// </summary>
/// <param name="paintArgs">The <see cref="OgwwPaintArgs"> arguments.</param>
void OgwwPixelEdit::OnPaint(OgwwPaintArgs& paintArgs)
{
    RECT rcClientRect = paintArgs.GetClientRect();
    LPBITMAPINFOHEADER bmiHeader = NULL;

    // --- DRAW BACKGROUND
    // ===================
    // Minimize flickering - part II:
    // * Erase only the background, that will NOT be affected by foreground drawing.
    COLORREF crBackground = ::GetSysColor(COLOR_BTNFACE);
    HBRUSH hbrush = CreateSolidBrush(crBackground);
    if (_pIconImage == NULL)
    {
        ::FillRect(paintArgs.GetDC(), &rcClientRect, hbrush);
    }
    else
    {
        bmiHeader = &(_pIconImage->iiHeader);

        if (_rcPadding.left > 0)
        {
            RECT bgClipLeftArea    = rcClientRect;
            bgClipLeftArea.right   = _rcPadding.left;
            bgClipLeftArea.bottom  = bmiHeader->biHeight * (1 + _nZoom) + 1 + _rcPadding.top;
            ::FillRect(paintArgs.GetDC(), &bgClipLeftArea, hbrush);
        }

        if (_rcPadding.top > 0)
        {
            RECT bgClipTopArea    = rcClientRect;
            bgClipTopArea.bottom  = _rcPadding.top;
            bgClipTopArea.right   = bmiHeader->biWidth  * (1 + _nZoom) + 1 + _rcPadding.left;
            ::FillRect(paintArgs.GetDC(), &bgClipTopArea, hbrush);
        }

        RECT bgClipBottomArea  = rcClientRect;
        bgClipBottomArea.top   = bmiHeader->biHeight * (1 + _nZoom) + 1 + _rcPadding.top;
        bgClipBottomArea.right = bmiHeader->biWidth  * (1 + _nZoom) + 1 + _rcPadding.left;
        if (bgClipBottomArea.top < bgClipBottomArea.bottom)
            ::FillRect(paintArgs.GetDC(), &bgClipBottomArea, hbrush);

        RECT bgClipRightArea   = rcClientRect;
        bgClipRightArea.left   = bmiHeader->biWidth  * (1 + _nZoom) + 1 + _rcPadding.left;
        if (bgClipRightArea.left < bgClipRightArea.right)
            ::FillRect(paintArgs.GetDC(), &bgClipRightArea, hbrush);
    }
    ::DeleteObject(hbrush);

    // --- PREVENT VIOLATION
    // =====================
    if (_pIconImage == NULL)
        return;

    // DRAW PIXELS
    // ===========
    if (bmiHeader->biWidth > 0 && bmiHeader->biHeight > 0)
    {
        HPEN oldPen = (HPEN)::SelectObject(paintArgs.GetDC(), _hDarkMaskPen);

        int x = rcClientRect.left + _rcPadding.left;
        int y = rcClientRect.top  + _rcPadding.top;
        for (int nRowCount = 0; nRowCount < bmiHeader->biHeight; nRowCount++)
        {
            for (int nColCount = 0; nColCount < bmiHeader->biWidth; nColCount++)
            {
                // -- DETERMINE COLOR
                // ==================
                WORD   luminance    = 0;
                HBRUSH currentBrush = NULL;
                if ((bmiHeader->biBitCount == 4) && (_pIconImage->iiColors != NULL))
                {
                    DWORD colorIndex  = OgwwIcon::BmpXORgetColor4bpp (_pIconImage->iiXOR,
                        nColCount, nRowCount, bmiHeader->biWidth, bmiHeader->biHeight);
                    luminance    = AARRGGBBtoLUMINANCE(_pIconImage->iiColors[colorIndex]);
                    currentBrush = ::CreateSolidBrush(
                                     AARRGGBBtoCOLORREF(_pIconImage->iiColors[colorIndex]));
                }
                else if ((bmiHeader->biBitCount == 8) && (_pIconImage->iiColors != NULL))
                {
                    DWORD colorIndex = OgwwIcon::BmpXORgetColor8bpp (_pIconImage->iiXOR,
                        nColCount, nRowCount, bmiHeader->biWidth, bmiHeader->biHeight);
                    luminance    = AARRGGBBtoLUMINANCE(_pIconImage->iiColors[colorIndex]);
                    currentBrush = ::CreateSolidBrush(
                                     AARRGGBBtoCOLORREF(_pIconImage->iiColors[colorIndex]));
                }
                else
                {
                    int pixelIndex = nRowCount * bmiHeader->biWidth + nColCount;
                    luminance    = AARRGGBBtoLUMINANCE(_pIconImage->iiXOR[pixelIndex]);
                    currentBrush = ::CreateSolidBrush(
                                     AARRGGBBtoCOLORREF(_pIconImage->iiXOR[pixelIndex]));
                }

                // -- DRAW PIXEL FACE
                // ==================
                // Minimize flickering - part III:
                // * Prevent drawing pixel face over pixel border
                //   (by inflating the pixel face rectangle).
                RECT   currentRC;
                currentRC.left  = x;                currentRC.top    = y;
                currentRC.right = x + (1 + _nZoom); currentRC.bottom = y + (1 + _nZoom);
                // Inflate left/top but keep right/bottom.
                // FillRect doesn't include the right coordinate.
                currentRC.left += 1;
                currentRC.top  += 1;
                ::FillRect(paintArgs.GetDC(), &currentRC, currentBrush);
                // Restore the left/bottom.
                currentRC.left -= 1;
                currentRC.top  -= 1;
                ::DeleteObject(currentBrush);

                // -- DRAW MASK ON PIXEL FACE
                // ==========================
                bool   bMasked  = OgwwIcon::BmpXORgetMask(_pIconImage->iiAND, nColCount,
                                      nRowCount, bmiHeader->biWidth, bmiHeader->biHeight);
                if (bMasked)
                {
                    if      (luminance < 192)
                      ::SelectObject(paintArgs.GetDC(),_hLightMaskPen);
                    else if (luminance < 384)
                      ::SelectObject(paintArgs.GetDC(),(HGDIOBJ)::GetStockObject(WHITE_PEN));
                    else if (luminance < 576)
                      ::SelectObject(paintArgs.GetDC(),(HGDIOBJ)::GetStockObject(BLACK_PEN));
                    else
                      ::SelectObject(paintArgs.GetDC(),_hDarkMaskPen);

                    ::MoveToEx(paintArgs.GetDC(), x, y, NULL);
                    ::LineTo  (paintArgs.GetDC(), x + (1 + _nZoom), y + (1 + _nZoom));
                    if (_nZoom >= 6)
                    {
                        int nHalf = _nZoom / 2;
                        ::MoveToEx(paintArgs.GetDC(), x + nHalf, y, NULL);
                        ::LineTo  (paintArgs.GetDC(), x + (1+_nZoom), y - nHalf +(1+_nZoom));
                        ::MoveToEx(paintArgs.GetDC(), x, y + nHalf, NULL);
                        ::LineTo  (paintArgs.GetDC(), x - nHalf + (1+_nZoom), y +(1+_nZoom));
                    }
                }

                x += (1 + _nZoom);
            }
            x  = rcClientRect.left + _rcPadding.left;
            y += (1 + _nZoom);
        }

        // -- DRAW PIXEL BORDER
        // ====================
        int nLineWidth  = 1 + (1 + _nZoom) * bmiHeader->biWidth;
        int nLineHeight = 1 + (1 + _nZoom) * bmiHeader->biHeight;

        HPEN newPen = GetStockPen(BLACK_PEN);
        ::SelectObject(paintArgs.GetDC(), newPen);

        x = rcClientRect.left + _rcPadding.left;
        y = rcClientRect.top  + _rcPadding.top;
        for (int nRowCount = 0; nRowCount <= bmiHeader->biHeight; nRowCount++)
        {
            ::MoveToEx(paintArgs.GetDC(), x, y, NULL);
            ::LineTo(paintArgs.GetDC(), x + nLineWidth, y);
            y += (1 + _nZoom);
        }

        x = rcClientRect.left + _rcPadding.left;
        y = rcClientRect.top  + _rcPadding.top;
        for (int nColCount = 0; nColCount <= bmiHeader->biWidth; nColCount++)
        {
            ::MoveToEx(paintArgs.GetDC(), x, y, NULL);
            ::LineTo(paintArgs.GetDC(), x, y + nLineHeight);
            x += (1 + _nZoom);
        }

        ::SelectObject(paintArgs.GetDC(), oldPen);
    }
}

我在优化的三个最重要的地方添加了注释:

  • 请勿在 WM_ERASEBKGND 上绘图(而是将背景绘图集成到 WM_PAINT 中)
  • 绘制背景(仅在像素场外部——它将在稍后更新)
  • 绘制像素面(不与边框重叠)

有一篇关于这个主题的优秀文章《任何控件的无闪烁绘图》,我以此作为解决方案的基础。使用文章《WIN32 剪裁区域指南》中描述的剪裁区域可能是一个等效的替代方案。

内存泄漏检测

在我偶然发现文章《一个跨平台的内存泄漏检测器》后,我开始思考我的应用程序在存储管理方面的表现如何。结果令人失望。我几乎对我和 C++ 失去了信心。但如果 C++ 不能摆脱这种困境,它就不是 C++ 了。

首先也是最重要的:向所有用 C 或 C++ 编写稳定专业应用程序的程序员致敬,他们日以继夜、周复一周、月复一月地辛勤工作,毫无怨言,无需重启!

如果我的图标编辑器(在我开始修改内存管理之前)是一个长时间连续使用并处理大量数据的应用程序,操作系统肯定会在某个时候不堪重负。事实证明,这主要是由于一些小的疏忽,内存泄漏在应用程序终止时被清除,并且没有再被注意到。然而,从长远来看,内存泄漏仍然会导致大量的内存消耗。

我在查找内存泄漏时学到了以下几点:

  1. 您绝对需要易于使用且有效的工具。gccVS 的内置工具是不够的。强烈推荐使用特殊版本(带日志/跟踪)的 ::GlobalAlloc()::GlobalFree() 以及 newdelete
  2. 明确确定使用 ::GlobalAlloc()::GlobalFree()newdelete——并坚持下去!我只对具有构造函数/析构函数的对象使用 newdelete。在所有其他情况下,我使用 ::GlobalAlloc()::GlobalFree()(因为我永远不知道是否需要将内存块(部分)传递给 Window API 调用)。
  3. 直观地设计所有动态内存使用!任何分配动态内存的方法都应该有一个自然/直观的对应方法来释放动态内存,并且两者都应该能够本能地找到。
  4. 永远不要停止改进内存管理。即使在我第一次认为所有内存泄漏都已修复的 2 个月后,我仍然发现了新的泄漏。

我最终使用了 4 个宏和一个类,通过它们我可以检测到所有内存泄漏——之后修复它们简直是小菜一碟。它们比《一个跨平台内存泄漏检测器》中描述的更简单,也远没有那么美观——但它们很好地完成了任务。以下是我的修复工作最终阶段的截图:

  • ALLOC_REGISTRATION_CALL___DBG (封装 ::GlobalAlloc()) /
  • FREE_CODE_BLOCK___DBG (替换 ::GlobalFree()) 和
  • NEW_REGISTRATION_CALL___DBG (封装 new) /
  • DELETE_CODE_BLOCK___DBG (替换 delete)

MemoryDebug.hppOgwwAPI.hpp 中声明,用于所有动态内存的请求或释放。以下是 ::GlobalAlloc()/::GlobalFree()new/delete 的示例用例:

...
// Not very elegant, but 100% transparent (the application of ::GlobalAlloc() is visible):
// Separation of declaration and initialization not needed - because ::GlobalAlloc() returns
// an untyped memory block. (Yes. there is room for optimization.)
BYTE* pbIconBytes = (BYTE*)ALLOC_REGISTRATION_CALL___DBG(::GlobalAlloc(GPTR, dwBufferSize));

...

// Less transparent but very short: The ::GlobalFree() call is hidden within an {...} block.
if (pbIconBytes != NULL)
    FREE_CODE_BLOCK___DBG(pbIconBytes)
// I do that in principle. When reusing the variable it is clear, if it is already initialized.
pbIconBytes = NULL;
...
...
// Not very elegant, but 100% transparent (the application of new operator is visible):
// I separate the declaration from the initialization and only the initialization is monitored -
// because the new operator returns a typed memory block. (Yes, there is room for optimization.)
CColLayouter* pColLayouter = NULL;
NEW_REGISTRATION_CALL___DBG(pColLayouter = new CColLayouter());

...

// Less transparent but very short: The delete operator call is hidden within an {...} block.
if (pColLayouter != NULL)
    DELETE_CODE_BLOCK___DBG(pColLayouter)
// I do that in principle. When reusing the variable it is clear, if it is already initialized.
pColLayouter = NULL;
...

Microsoft Visual Studio (VC++) 兼容性

我完全使用 Code::Blocks 17.12 在 ReactOS 0.4.11 上编写了 DLL 和应用程序。现在我想知道这是否可以 1:1 移植到带有 Windows 10 和 Visual Studio 2017 的 Microsoft World。在遇到一些最初的困难后,DLL 和应用程序的整个源代码的完全兼容性创建花了一天时间。对于那些有类似想法的人,这里有一些关于如何进行必要调整的提示。

1. 首先,我使用 Visual Studio 创建了一个新解决方案和两个新项目。一个项目基于“其他语言 | Visual C++ | Windows 桌面应用程序 - Visual C++”模板用于应用程序,另一个项目基于“其他语言 | Visual C++ | 动态链接库 (DLL) - Visual C++”模板用于 DLL。

1.1. 任何新的 Visual Studio C++ 项目都已准备好使用预编译头。这是我不在 Code::Blocks 中使用的功能。要删除此功能,必须编辑两个项目的项目属性。

将“配置属性 | C/C++ | 预编译头”属性“预编译头”从“创建 (/Yc)”切换到“不使用预编译头”,并清空“所有配置”的“预编译头文件”和“预编译头输出文件”属性。

1.2. 为了解决所有外部引用,必须向这两个新解决方案添加两个库:opengl32.lib;comctl32.lib;

这是通过“配置属性 | 链接器 | 输入”属性“附加依赖项”针对“所有配置”完成的。

2. 除了 dllmain.cpp 之外,所有使用两个项目模板生成的代码和资源文件都必须从项目中删除,并且 Code::Blocks 17.12 中的所有代码文件都必须注册到相应的项目。在 dllmain.cpp 中,#include "stdafx.h" 必须替换为 #include "windows.h"

3. 遗憾的是,Code::Blocks 附带的 MinGW 版本较旧,尚未包含字符串函数的安全增强功能。然而,Visual Studio 要求使用这些函数,因此必须找到解决方案。使用“#pragma warning (disable: 4996)”进行局部停用不适用于 Code::Blocks。而且我也不喜欢全局停用。因此,我决定在 Code::Blocks 中模拟所需字符串函数的安全增强功能。

以下是 SimpleWString.hpp 中的额外声明:

#if defined(__GNUC__) || defined(__MINGW32__)

typedef int errno_t;

/// <summary>
/// Copies not more than <c>nCopyCnt</c> characters of the character array pointed to
/// by <c>wszSrc</c> to the character array pointed to by <c>wszDst<c>, stopping at the
/// first <c>0</c> character. Zeroes out the rest of the character array pointed to
/// by <c>wszDst</c>, which can be a performance concern.
/// </summary>
/// <param name="wszDst">The copy target. Must not be <c>NULL</c>.</param>
/// <param name="nDstSize">The max. capacity of the copy target.</param>
/// <param name="wszSrc">The copy source. Can be <c>NULL</c>.</param>
/// <param name="nCopyCnt">The max. number of characters to copy.</param>
/// <returns>Returns <c>0</c> on success, or an error number otherwise.</returns>
errno_t wcsncpy_s(wchar_t* wszDst, size_t nDstSize, const wchar_t* wszSrc, size_t nCopyCnt);

/// <summary>
/// Copies the character array pointed to by <c>wszSrc</c> to the character array pointed to
/// by <c>wszDst<c>, stopping at the first <c>0</c> character. Zeroes out the rest of
/// the character array pointed to by <c>wszDst</c>, which can be a performance concern.
/// </summary>
/// <param name="wszDst">The copy target. Must not be <c>NULL</c>.</param>
/// <param name="nDstSize">The max. capacity of the copy target.</param>
/// <param name="wszSrc">The copy source. Can be <c>NULL</c>.</param>
/// <returns>Returns <c>0</c> on success, or an error number otherwise.</returns>
errno_t wcscpy_s(wchar_t* wszDst, size_t nDstSize, const wchar_t* wszSrc);

#define swprintf_s(wszDst, nDstSize, wszFormat, ...) swprintf(wszDst, wszFormat, __VA_ARGS__)

#endif

以下是 SimpleWString.cpp 中额外的实现:

#if defined(__GNUC__) || defined(__MINGW32__)

errno_t wcsncpy_s(wchar_t* wszDst, size_t nDstSize, const wchar_t* wszSrc, size_t nCopyCnt)
{
    // Prevent segment violation.
    if (wszDst == NULL)
        return ENOMEM;

    // Prevent overlapping.
    if (&(wszDst[nDstSize]) > wszSrc  && wszDst < &(wszSrc[nCopyCnt]))
        return EPERM;
    if (&(wszSrc[nCopyCnt]) > wszDst && wszSrc  < &(wszDst[nDstSize]))
        return EPERM;

    // Process the standard case.
    if (nDstSize > nCopyCnt)
    {
        wcsncpy(wszDst, wszSrc, nCopyCnt);
        for (size_t nIndex = nCopyCnt; nIndex < nDstSize; nIndex++)
            wszDst[nIndex] = (wchar_t)0;
        return 0;
    }

    // Prevent overrun (by missing string termination).
    if (nDstSize == nCopyCnt && wszSrc[nCopyCnt] == (wchar_t)0)
    {
        wcsncpy(wszDst, wszSrc, nCopyCnt);
        return 0;
    }

    // Prevent overflow.
    return ERANGE;
}

errno_t wcscpy_s(wchar_t* wszDst, size_t nDstSize, const wchar_t* wszSrc)
{
    return wcsncpy_s(wszDst, nDstSize, wszSrc, wcslen(wszSrc));
}

#endif

4. 现在剩下的是努力工作,以消除 Visual Studio 中的警告,因为 Microsoft 编译器比 MinGW 对语法检查更严格。

关注点

在多年 exclusively 使用 C# 编程之后,再次使用 C++ 编程并完全控制每个位和每个函数调用,即使您必须比 C# 更小心,也是一件非常有趣的事情。
遗憾的是,C/C++ Windows 库要么过度设计(例如 MFC),要么/并且没有配备与 C# 相当的舒适性(例如 LINQ)。

历史

  • 2019年12月12日:初始版本
  • 2020年1月9日:更新至0.2版
  • 2020年6月10日:更新至0.3版
  • 2020年10月10日:更新至0.4版
  • 2020年11月27日:更新至0.5版
  • 2021年1月31日:更新至0.6版
© . All rights reserved.