Click here to Skip to main content
15,867,453 members
Articles / Web Development / HTML

A Basic Icon Editor Running on ReactOS (and Consequently on Windows XP and Newer Versions)

Rate me:
Please Sign up or sign in to vote.
4.96/5 (17 votes)
31 Jan 2021CPOL20 min read 30K   1.3K   16   11
Creation of a basic icon editor with as little code as possible, that is running on ReactOS and Windows, to check out the stability of application development capabilities on ReactOS
This project was started to gain and share experience in programming applications for ReactOS. It integrates many small things I've done already - like choosing the right IDE, preparing for source code documentation and developing user controls.

Contents

Introduction

ReactOS is an open source alternative to the Windows operation system. Even if the first version of ReactOS dates back to 1998, there is still no 'stable' version of ReactOS. Maybe, the most important reason is a lack of attention.

Driven by the tip, Introduction to Embedded Icons without Resource on ReactOS, I wanted to dive deeper into the topic of Windows icons and looked for a free icon editor for ReactOS, that was simple enough to use without a long study of the docs. The best match I've found has been the Junior Icon Editor. This icon editor works fine on all Microsoft Windows versions I've tested and it can be easily installed and started on ReactOS as well. The only drawback is that the color palet for 4 bpp icons (16 color icons) does not work (tested on ReactOS version 0.4.11 with Junior Icon Editor version 4.39).

The Junior Icon Editor's color palet for a 8 bpp icon (256 color icon) on ReactOS:    

The Junior Icon Editor's color palet for a 4 bpp icon (16 color) on ReactOS:    

My primary focus is on 4 bpp icons. Although it is more likely that the Junior Icon Editor's color palette problem is caused by a missing 4bpp default palette in ReactOS than a bug in the Junior Icon Editor itself, it is hardly possible to create or edit 4bpp icons.

Because of this drawback and since I wanted to test the findings from the tips, Introduction to OpenGL with C/C++ on ReactOS and Introduction to C# on ReactOS on a real application, I started to program my own basic icon editor - the ReactOS Icon Editor. This application is inspired by the simplicity and intuitive handling of the Visual Studio Icon Editor.

Updates

Version 0.2

  • New: The 16 colors of the 4bpp color palette can be individualized. A double click on the color opens the color dialog of ReactOS and the color value can be set individually. Advantage: The 4bpp palette can also be used to create appealing icons. Disadvantage: Not all icon editors support individualized colors and reset the colors to the default palette - e.g. the Visual Studio icon editor.
  • New: The Pipette Tool is implemented. Now colors can also be selected within the image.
  • New: The menu items now also support icons (13x13 pixels) including transparency. They are automatically switched to ownerdraw. Advantage: The native support of menu item bitmaps is originally intended for 1bpp images and does not support transparency for color images. With icons, transparency is also supported for colored images. Disadvantage: The switch to ownerdraw can lead to inconsistent results when using themes.
  • Fix: The "Save as" dialog works now correctly / generates no longer a segmentation fault (wrong parameter 4 in call to wcsncpy_s() fixed).
  • Fix: Segmentation faults have been fixed, that occurred when working with icons whose width/height is not a multiple of 8 (e.g. icons with 13x13 pixels for menu items).
  • Upgrade: Improved OWNERDRAW menus, which support accelerators now. Provision of accelerators for many menu items.

Version 0.3

This update took quite a long time - but that has to do with my learning curve. Firstly, I found the wonderful Win32++ from David Nash (whose simplicity and clarity impressed me as much as the preemption of inheritance and object orientation in the K&R C Athena Widget Set more than 30 years ago) and secondly, this was the first time I seriously looked into DoxyGen.

You may ask me: Why is my icon editor not based on Win32++?

  • I think Win32++ is cool, but I discovered Win32++ very late.
  • I am seriously considering to switch to Win32++ - especially because of the already existing stability - but I am not entirely happy with the implementation of the MenuBar and ToolBar in mainframe window.
  • The design of Win32++ impresses me with its clarity and simplicity - but the need for synchronization when developing own controls is still a bit daunting.

Maybe this will change the more professional - and thus the more similar in the program structure to Win32++ - my icon editor becomes.

  • New: My first attempt of a color picker uses one Button control per color. This is OK for 4bpp (16 colors) mode but leads to 256 controls for 8bpp (256 color) mode. To be ready for 8bpp, I implemented the new control ColorPicker, that is set up with 8 columns and 2 rows SetCellCount(SET_SIZE(8, 2)) for 4bpp (and looks the same as the previous solution).

    and will probably set up with 14 columns and 18 rows SetCellCount(SET_SIZE(14, 18)) for 8bpp (which makes the icon editor ready to be extended to 8bpp).

  • NEW: The fill tool is implemented now .
  • NEW: All code comments in the whole Ogww Library are revised in such a way that a meaningful documentation can be generated with DoxyGen. I use the DoxyBlocks plugin from Code::Blocks (and I'm very pleased about the resulting documentation). See also the section "Adding a documentation generator" in my tip Introduction to OpenGL with C/C++ on ReactOS for details on this.
  • FIX: Memory violation when using GetDIBits() has been fixed - at least on Microsoft Windows. Sometimes (based on my observation on color depths that require a color palette) GetDIBits() writes a minimum color palette into the BITMAPINFOHEADER. According to the Microsoft Forum, they are 3 colors (3* sizeof(WORD)). If this is not taken into account, memory violations occur.

Version 0.4

  • FIX: The overlapping display (outdated position) of the image buttons with wrong size/position after opening an icon file has been fixed, which must formerly be rectified manually by resizing the window. Fixed by the handover of the window size to ::SendMessage(hWnd, WM_SIZE, (WPARAM)SIZE_RESTORED, ...) after opening an icon file.
  • FIX: The display of the old color (outdated RGB value) in ColorPicker and PixelEdit after changing a color of the color palette was fixed, which must formerly be rectified manually by resizing the window. Fixed by a re-paint of the ColorPicker and PixelEdit with ::InvalidateRect(_pweakColorPicker->GetHWnd(), NULL, TRUE) and ::InvalidateRect(_pweakPixelEdit->GetHWnd(), NULL, TRUE).
  • NEW: Added memory leak detection. I've read the great article A Cross-Platform Memory Leak Detector and wondered how good my application is in terms of memory management. See also the section "Memory leak detection" in this article.
  • NEW: Added palette color change to the undo/redo chain.
  • NEW: All code comments in the whole IconEditor is revised in such a way that a meaningful documentation can be generated with DoxyGen. I use the DoxyBlocks plugin from Code::Blocks (and I'm very pleased about the resulting documentation). See also the section "Adding a documentation generator" in my tip Introduction to OpenGL with C/C++ on ReactOS for details on this.
  • NEW: Ownerdraw buttons support BS_RADIOBUTTON and BS_AUTORADIOBUTTON. This is a function which is supported by the original window controls for autodraw buttons only (because the bit mask of BS_OWNERDRAW overlaps other button styles and can't be used simultaneously with BS_CHECKBOX, BS_AUTOCHECKBOX, BS_RADIOBUTTON and BS_AUTORADIOBUTTON).
  • NEW: Added initial handling of multi-image icons: Load/save/save as for multi-image icons work. Images can be added to an existing icon (but currently not removed) and the image order can be changed.

Version 0.5

  • NEW: Delete image from icon is implemented (the last remaining image can't be removed).
  • NEW: Tool bar buttons support tool tips.
  • IMPROVED: Methods, that return dynamically allocated strings, no longer use StringMediator* but String. This ensures an automatic garbage collection as before and the code is immediately transparent for every C++ programmer. For details, see tip How to return a string class from a C++ function/method.
  • NEW: As an alternative to the simple ToolBar, it is now possible to use several ToolBars within one ReBar. The first ToolBar of the ReBar is automatically created as a default ToolBar.
  • IMPROVED: The image of disabled menu items and toolbar buttons are calculated as a gray scale image now, instead of an gray/white image with cast shadow.
  • FIXED: The short cuts for UNDO (Ctrl + z) and REDO (Ctrl + y) work now.
  • NEW: The select (lasso) tool has been added to capture a rectangle area of pixels as selected (the images below this list show the tool bar button and a sample rectangle area of selected pixels).
  • NEW: Basic COPY (Ctrl + c) of the current icon image (e.g., to Paint) and basic PASTE (Ctrl + v) into the current icon image (e.g., from Paint) are added. If a rectangle area of pixels is selected, PASTE affects the selection only. For details, see tip Approach to Paste a Bitmap into an Icon Image on ReactOS (and Consequently on Windows XP and Newer Versions, using Win32 API).

Version 0.6

  • FIXED: The methods CDDBitmap::CopyTo4bppColors() and CDDBitmap::CopyTo8bppColors() work now (they have sometimes thrown exceptions during redurction of the color vector for the final color table).
  • NEW: Small and big icons can be set now for the application farme window.
  • IMPROVED: The C API has been cleaned up to support C# P/Invoke (interop) for the whole API.
  • NEW: A C# equivalent to the C++ application hast been added for ReactOS (editor: Notepad++ with NppExec plugin, compiler: MONO installation mono-4.3.2.467-gtksharp-2.12.30.1-win32-0.msi, see tip Introduction to C# on ReactOS for details) and Windows (Visual Studio).
  • IMPROVED: The undo/redo chains is no longer connected to the pixel editor control, and thus generic for all icon images, but every icon image has now a local undo/redo chains. In addition, the redo/undo chains remain intact when the icon image is changed.
  • IMPROVED: The application now has its own icon - the first meaningful icon created with the application itself.
  • FIXED: A memory leak during PASTE (Ctrl + v) into the current icon image has been eliminated.
  • NEW: The selected rectangle area of pixels can be scaled now.

Using the Code

The Application

The ReactOS Icon Editor is based on the OpenGL Windows Wrapper (Ogww) DLL, introduced by the tip Introduction to OpenGL with C/C++ on ReactOS. Meanwhile, the DLL has evolved to meet significantly more requirements on a professional UI, but the DLL is still far away from a release state. However, it is still designed to support application development with C/C++ and C#.

Now, the inclined reader will ask: Why Ogww - yet another wrapper around the Win32 API?
Simple answer: Because I discovered Win32++ only later.

Win32++ does a great job! I will try to replace Ogww as completely as possible by Win32++ bit by bit. Although I already know that there will be challenges: CString is not based on CObject and CObject offers no typeof() operator / no GetType() method or any alternative easy access to RTTI.

The ReactOS Icon Editor is currently very limited but designed to be developed step by step into a full-fledged icon editor. The current limitations are:

  • support for 4bpp (16 colors) mode only
  • support for pen, eraser, fill and pipette tool only (copy/paste tools are planned)

And that's how it currently looks:

The Tool Bar

Starting with version 0.5, it's possible to choose between a simple ToolBar and a ReBar, that provides the option to handle multiple ToolBars. This is the code, that shows both opportunities at once:

C++
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();
}

The Pixel Edit Control

I have added the new window class PixelEditWindow to the OpenGL Windows Wrapper (Ogww) DLL - the picture above shows the control in action, right in the center of the application. It is based on the ICONIMAGE structure (see chapter "The inside of an icon" below) and designed to display and edit the image pixels.

Masked pixels (pixels that are not displayed) are shown in the defined color but crossed out. Unmasked pixels (pixels that are displayed) are shown in the defined color.
The masking is set with the erase tool - it also sets the color.
The pen tool sets the color and deletes the masking.

I have decided

  • to set the defined color when masking a pixel, and
  • to show masked pixels in the defined color and not in the complementary color or any fixed color.

Pencil tool and erase tool work when the left mouse button is held down - as long as the mouse button is held down, multiple pixels can be set via mouse move.

Icon Design Recommendations

Since disabled toolbar buttons and disabled menu items typically display their images gray/white with cast shadow, toolbar and menu item images should always be designed to keep the bottom and right pixel stripes unused.
The image to the left illustrates the procedure by which disabled images (second row) are automatically calculated from normal images (first row).

Update with version 0.5: I changed the look of the disabled icons from the Win32 standard (gray/white with cast shadow) to user specific with a more attractive look (greyscale).

The image to the left illustrates the new procedure by which disabled images (second row) are automatically calculated from normal images (first row).

The Projects

I've developed the ReactOS Icon Editor entirely using Code::Blocks on ReactOS. The solution currently has the following structure:

The download (at the top of this article) contains a folder structure with the three folders OGWW, OGWW_Wrapper and ReactOS_Icon_Editor.

The Code::Blocks solution consists of these two projects

  • OGWW (the OpenGL Windows Wrapper DLL), completely contained in folder OGWW and
  • ReactOS Icon Editor (the icon editor application), ivided into the folders OGWW_Wrapper (contains the glue code, which makes OGWW usable for ReactOS Icon Editor) and WWOG (the application itself).

To open the projects on a new environment for the first time, these two files must be opened:

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

The OGWW project uses a sub-folder structure to organize the code files.

  • The folder Images contains BYTE[] of icons and bitmaps, that are provided by the DLL to applications.
  • The folder Layouter contains all layouter classes.
  • The folder Tests contains rudimentary unit and performance tests.
  • The folder Windows contains all classes, that are based on a Win32 window (have an own HWND).

The ReactOS Icon Editor project also integrates the folder OGWW_Wrapper, that holds all wrapper classes to provide an object oriented interface of the OGWW DLL.

In order to check whether the current build environment is well configured, here is an example call to 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

The -std=c++11 switch lifts the g++ to the ANSI C++2011 standard. This enables among other thing the use of

  • snwprintf() instead of swprintf(),
  • std::chrono::high_resolution_clock::now() instead of GetTickCount() and
  • auto.

The -D_UNICODE and -DUNICODE switch ensure the general use of wchar_t instead of char.

The -D__MSVCRT__ switch announces the availability of msvcrt.dll, that enables among other things, the use of _wgetenv() instead of getenv().

The Inside of an Icon

The most important class of the ReactOS Icon Editor is the class OgwwIconData. This class provides convenient access to the current image and manages the selected mask as well as the selected color. It is derived from the class OgwwIcon, that manages the icon as a whole. The basic structure of an icon is:

● Icon directory stored to ICONDIR structure
● Icon directory entries stored to ICONDIRENTRY[] within ICONDIR structure
    □ Image directory entry 1...n stored to ICONDIRENTRY structure
● Images stored to ICONIMAGE[]
    □ Image 1...n stored to ICONIMAGE structure
       Bitmap info header stored to BITMAPINFOHEADER within ICONIMAGE structure
       ◦ Bitmap color palet stored to AARRGGBB[] within ICONIMAGE structure
       ◦ Color bitmap bytes stored to BYTE[] within ICONIMAGE structure
       ◦ Mask bitmap bytes stored to BYTE[] within ICONIMAGE structure

Only BITMAPINFOHEADER is a standard Windows data structure. I would like to briefly introduce the other structures and types ICONDIR, ICONDIRENTRY and ICONIMAGE:

C++
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;
C++
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;

The size of an image (deBytesInRes) can be calculated this way: sizeof(BITMAPINFOHEADER) + sizeof(ICONIMAGE::iiColors) + sizeof(ICONIMAGE::iiXOR) + sizeof(ICONIMAGE::iiAND)

C++
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;

To get full control over all data of an icon, I implemented my own methods for reading OgwwIcon::ConstructFromFile(LPCWSTR wszFilePath) and writing OgwwIcon::SaveAs(LPCWSTR wszFilePath) icons.

At first, I focused on supporting 16 color images. A further development to 256 and 16777216 colors is already planned.

Flickering Minimization

There are two essential problem areas regarding the flickering:

  • Redrawing of the controls (e.g., during resize)
  • Editing the image pixels (window class PixelEditWindow / control OgwwPixelEdit)

Since redrawing the controls happens relatively rarely, my focus is on editing the image pixels. To edit the image pixels, the control OgwwPixelEdit is used, which is an owner-drawn control. Either the WS_EX_COMPOSITED window style does not solve the problem or it does not work as expected on ReactOS (double-buffering). The solution to this problem is an optimized implementation of the message handlers for WM_ERASEBKGND and WM_PAINT, as shown below:

C++
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;
}

I moved the message handler for WM_PAINT to the OnPaint() method:

C++
/// <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);
    }
}

I have added comments at the three most important points of the optimization:

  • DON'T DRAW on WM_ERASEBKGND (integrate background drawing into WM_PAINT instead)
  • DRAW BACKGROUND (only outside the pixel field - it will be updated later)
  • DRAW PIXEL FACE (without any overlap with the borders)

There is a good article "Flicker free drawing of any control" about this topic, which I used as a basis for my solution. The use of clip regions as described in the article "A Guide to WIN32 Clipping Regions" could be an equivalent alternative.

Memory Leak Detection

After I accidentally came across the article, A Cross-Platform Memory Leak Detector, I wondered how good my application is in terms of storage management. The answer was disillusioning. I almost lost faith in myself and in C++. But C++ wouldn't be C++ if it couldn't pull itself out of this mess.

First and foremost: Respect to all programmers who write stable professional applications in C or C++, which do a hard job for days, weeks or months without complaint and without having to be restarted!

If my Icon Editor (before I started to revise the memory management) would be an application that is in use continuously for a long time and processes large amounts of data, the operating system would certainly run out of breath at some point. As it turned out that it was mainly about small omissions, the memory leaks were cleared away when the application was terminated and were not noticed any further. In the long run, however, the memory leaks would still cause considerable memory consumption.

I've learned the following things while searching for memory leaks:

  1. You definitely need easy-to-use yet effective tools. The on-board tools of gcc or VS are not enough. Special versions (with logging/tracing) of ::GlobalAlloc() and ::GlobalFree() as well as new and delete are highly recommended.
  2. Make a simple determination for using ::GlobalAlloc() and ::GlobalFree() respective new and delete - and stick to it! I used new and delete only for objects that have constructor(s) / destructors. In all other cases, I used ::GlobalAlloc() and ::GlobalFree() (because I'll never know whether I have to pasds (parts) of the memory block to Window API calls).
  3. Design all dynamic memory usage intuitively! Any method that allocates dynamic memory should have a natural/intuitive counterpart to release the dynamic memory and both should be found instinctively.
  4. Never stop improving memory management. Even 2 months after I first thought all memory leaks were fixed, I still found new ones.

I finally ended up with 4 macros and one class, with which I could detect all memory leaks - fixing them was a breeze afterwards. They are much simpler and far from being as beautiful as described in A Cross-Platform Memory Leak Detector - but they do their job well. Here is a screenshot from the final phase of my fixes:

The macros

  • ALLOC_REGISTRATION_CALL___DBG (wraps ::GlobalAlloc()) /
  • FREE_CODE_BLOCK___DBG (replaces ::GlobalFree()) and
  • NEW_REGISTRATION_CALL___DBG (wraps new) /
  • DELETE_CODE_BLOCK___DBG (replaces delete)

are declared in MemoryDebug.hpp and OgwwAPI.hpp and are used for all requests or releases of dynamic memory. And here exemplary use cases for ::GlobalAlloc()/::GlobalFree() and new/delete:

C++
...
// 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;
...
C++
...
// 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++) Compatibility

I have written the DLL and the application completely in Code::Blocks 17.12 on ReactOS 0.4.11. Now I was wondering if this can be transferred 1:1 to Microsoft World with Windows 10 and Visual Studio 2017. After some initial difficulties, the creation of full compatibility for the entire source code of the DLL and the application took one day. For those of you who have something similar in mind, here are some hints on how to make the necessary adjustments.

1. First, I created a new solution with Visual Studio and two new projects. One project based on the template "Other Languages | Visual C++ | Windows Desktop Application - Visual C++" for the application and one project based on the template "Other Languages | Visual C++ | Dynamic-Link Library (DLL) - Visual C++" for the DLL.

1.1. Any new Visual Studio C++ project is prepared to use precompiled headers. This is a feature I don't use with Code::Blocks. To remove this feature, the project properties must be edited for both projects.

Switch the "Configuration Properties | C/C++ | Precompiled Headers" property "Precompiled Headers" from "Create (/Yc)" to "Not Using Precompiled Headers" and empty the properties "Precompiled Header File" and "Precompiled Header Output File" for "All Configurations".

1.2. To resolve all external references, two libraries must be added to both new solutions: opengl32.lib;comctl32.lib;

This is done via the "Configuration Properties | Linker | Input" property "Additional Dependencies" for "All Configurations".

2. All code and resource files except dllmain.cpp, generated with the two project templates, must be deleted from the projects and all code files from Code::Blocks 17.12 must be registered to the appropriate project. Within dllmain.cpp, the #include "stdafx.h" must be replaced by #include "windows.h".

3. Unfortunately, Code::Blocks comes with an older version of MinGW that does not yet include the security enhancements of string functions. However, Visual Studio demands the use of these functions and therefore a solution must be found. A local deactivation using "#pragma warning (disable: 4996)" does not work for Code::Blocks. And I couldn't warm up for a global deactivation. So I decided to emulate the security enhancements for the demanded string functions in Code::Blocks.

These are the additional declarations within SimpleWString.hpp:

C++
#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

And this is how the additional implementations look like within SimpleWString.cpp:

C++
#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. What remains now is diligent work to remove the warnings within Visual Studio because the Microsoft compiler checks the syntax much stricter than MinGW.

Points of interest

After years of exclusive programming in C#, it was once again great fun to program in C++ and to have full control over every bit and every function call - even if you have to act more carefully than in C#.
It is a pity that the C/C++ Windows libraries are either over-engineered (e.g. MFC) or/and not equipped with a comfort comparable to C# (e.g. LINQ).

History

  • 12th December, 2019: Initial version
  • 9th January, 2020: Update to version 0.2
  • 10th June, 2020: Update to version 0.3
  • 10th October, 2020: Update to version 0.4
  • 27th November, 2020: Update to version 0.5
  • 31st January, 2021: Update to version 0.6

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader Celonis SA
Germany Germany
I am currently the CEO of Symbioworld GmbH and as such responsible for personnel management, information security, data protection and certifications. Furthermore, as a senior programmer, I am responsible for the automatic layout engine, the simulation (Activity Based Costing), the automatic creation of Word/RTF reports and the data transformation in complex migration projects.

The main focus of my work as a programmer is the development of Microsoft Azure Services using C# and Visual Studio.

Privately, I am interested in C++ and Linux in addition to C#. I like the approach of open source software and like to support OSS with own contributions.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1412838429-Dec-20 12:30
Member 1412838429-Dec-20 12:30 
QuestionMalwareBytes informa Malware!? Pin
FAFILI11-Jun-20 22:12
FAFILI11-Jun-20 22:12 
AnswerRe: MalwareBytes informa Malware!? Pin
Steffen Ploetz11-Oct-20 0:40
mvaSteffen Ploetz11-Oct-20 0:40 
Questionmany errors for VS2017 compilation Pin
Southmountain26-Jan-20 9:37
Southmountain26-Jan-20 9:37 
AnswerRe: many errors for VS2017 compilation Pin
Steffen Ploetz11-Oct-20 0:26
mvaSteffen Ploetz11-Oct-20 0:26 
GeneralRe: many errors for VS2017 compilation Pin
Southmountain4-Jan-21 9:10
Southmountain4-Jan-21 9:10 
GeneralMy vote of 5 Pin
Member 1412838410-Jan-20 4:29
Member 1412838410-Jan-20 4:29 
QuestionCommon mistake when writing sprite editors Pin
Tomaž Štih10-Jan-20 4:08
Tomaž Štih10-Jan-20 4:08 
AnswerRe: Common mistake when writing sprite editors Pin
Steffen Ploetz13-Jan-20 23:50
mvaSteffen Ploetz13-Jan-20 23:50 
QuestionBUG WARNING for Version 0.1 Pin
Steffen Ploetz13-Dec-19 17:03
mvaSteffen Ploetz13-Dec-19 17:03 
AnswerRe: BUG WARNING for Version 0.1 Pin
learn_more10-Oct-20 6:32
learn_more10-Oct-20 6:32 
GeneralRe: BUG WARNING for Version 0.1 Pin
Steffen Ploetz10-Oct-20 23:42
mvaSteffen Ploetz10-Oct-20 23:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.