Search

An easy way to use Skins in a Dialog based Application

0 views

Preparing the Project and Adding Resources

Before any custom skin can appear on a dialog, the foundation must be solid. In an MFC application that follows the dialog‑based model, the majority of visual work happens inside the dialog class derived from CDialog. The first step is to create a new project in Visual Studio using the “MFC AppWizard” and choose the “Dialog based” template. Once the project skeleton is in place, you can add a bitmap that will serve as the skin. The bitmap must be placed in the resource file (.rc) and given a unique identifier, such as IDB_MAIN. This image can be any shape or style you like – a simple rectangle, a rounded corner splash, or a full‑screen texture.

With the resource ready, the dialog header file needs to declare a handful of members that will hold the bitmap, the client region, and the window region. These members are essential for creating the non‑rectangular window that matches the shape of the skin image. The header might look like this:

Prompt
class CSkinDlg : public CDialog</p> <p>{</p> <p>public:</p> <p> CSkinDlg(CWnd* pParent = nullptr);</p> <p> // Skin components</p> <p> HBITMAP m_hBmp; // Handle to the skin bitmap</p> <p> HRGN m_hClientRgn; // Region for the client area (optional)</p> <p> HRGN m_hWndRgn; // Region that defines the whole window shape</p> <p> // Utility to convert a bitmap into a region</p> <p> HRGN DIBToRgn(HBITMAP hBmp, COLORREF BkColor, BOOL Direct);</p> <p>};</p>

The m_hBmp member will hold the loaded bitmap. The client region can be used to clip controls or to create a custom shape for the dialog’s client area, while m_hWndRgn is the actual region applied to the dialog window, making it non‑rectangular. The DIBToRgn function will later translate a bitmap into an HRGN by examining pixel colors and building a region that matches the opaque parts of the image.

In the constructor of the dialog, the bitmap is loaded using LoadImage with the LR_CREATEDIBSECTION flag so that it can be accessed in 32‑bit format. After loading, the code creates an elliptical client region and converts the bitmap into a window region. The elliptical region is optional; it simply demonstrates how you can create a custom client shape before the skin is applied. A typical constructor might look like this:

Prompt
CSkinDlg::CSkinDlg(CWnd<em> pParent /</em>=nullptr*/)</p> <p> : CDialog(IDD_SKINDLG, pParent)</p> <p>{</p> <p> // Load the bitmap resource into a DIB section</p> <p> m_hBmp = (HBITMAP)LoadImage(</p> <p> AfxGetInstanceHandle(),</p> <p> MAKEINTRESOURCE(IDB_MAIN),</p> <p> IMAGE_BITMAP,</p> <p> 0, 0,</p> <p> LR_CREATEDIBSECTION);</p> <p> // Optional: define a small ellipse for the client area</p> <p> m_hClientRgn = ::CreateEllipticRgn(33, 34, 34, 35);</p> <p> // Convert the bitmap into a window region that matches the skin shape</p> <p> m_hWndRgn = DIBToRgn(m_hBmp, RGB(0, 255, 0), FALSE);</p> <p>}</p>

Here, RGB(0, 255, 0) is the background color that the DIBToRgn function will treat as transparent. The Direct flag tells the function whether to treat matching pixels as a continuous run or to evaluate each pixel individually; passing FALSE creates a more precise region.

With these steps, the project is set up: the dialog class knows how to load the skin, it holds a handle to the bitmap, and it has the necessary infrastructure to generate a region that will later shape the window. This preparation stage keeps the code clean and modular, letting you focus on the more intricate region‑generation logic in the next section.

Implementing Skin Support in the Dialog

Having the basic members and constructor ready, the heart of the skinning process is the DIBToRgn function. This routine walks through every pixel of the bitmap, determines whether it belongs to the opaque part of the skin, and collects a list of rectangles that will be merged into a single region. The function is designed to be efficient: it scans line by line, groups contiguous opaque pixels into horizontal runs, and stores those runs as RECT structures in a buffer. Once enough rectangles are collected, the function merges them into an HRGN using ExtCreateRegion and the RGNDATA structure.

The function starts by setting a tolerance value that controls how strict the color comparison is. Pixels whose color falls within a range defined by the tolerance around the background color are treated as transparent. This approach handles minor color variations caused by anti‑aliasing or compression artifacts. The following excerpt shows the initial setup and the color range calculation:

Prompt
HRGN CSkinDlg::DIBToRgn(HBITMAP hBmp, COLORREF BkColor, BOOL Direct)</p> <p>{</p> <p> HRGN hRgn = nullptr;</p> <p> const int MAX_ALLOC_RECTS = 100;</p> <p> COLORREF Tolerance = RGB(0x10, 0x10, 0x10);</p> <p> if (!hBmp) return nullptr;</p> <p> // Create a memory DC for the bitmap</p> <p> HDC hMemDC = ::CreateCompatibleDC(nullptr);</p> <p> if (!hMemDC) return nullptr;</p> <p> BITMAP bm;</p> <p> ::GetObject(hBmp, sizeof(bm), &bm);</p> <p> // Prepare a 32‑bit DIB section to work with</p> <p> BITMAPINFOHEADER bi = {0};</p> <p> bi.biSize = sizeof(BITMAPINFOHEADER);</p> <p> bi.biWidth = bm.bmWidth;</p> <p> bi.biHeight = bm.bmHeight;</p> <p> bi.biPlanes = 1;</p> <p> bi.biBitCount = 32;</p> <p> bi.biCompression = BI_RGB;</p> <p> VOID* pBits = nullptr;</p> <p> HBITMAP hDib32 = ::CreateDIBSection(</p> <p> hMemDC, (BITMAPINFO*)&bi, DIB_RGB_COLORS, &pBits, nullptr, 0);</p>

Once the 32‑bit DIB is ready, the function copies the original bitmap into it and begins scanning the pixels. It uses a loop over the height and width of the bitmap. For each row, it walks through the pixels, comparing their RGB values to the background range. If a pixel matches the tolerance, it either starts a new run or continues an existing one, depending on the Direct flag. When a run ends, the function records the run’s start and end coordinates as a RECT and stores it in the region buffer. If the buffer fills up, the function creates a temporary region from the buffer and merges it into the main region using CombineRgn. This step ensures that the region never consumes too much memory at once.

After all rows are processed, a final region is built from any remaining rectangles. If the Direct flag is FALSE, the function also XORs the region with a rectangle covering the whole bitmap. This step effectively removes the background area, leaving only the opaque skin shape. The final region is then returned and stored in m_hWndRgn

In addition to region creation, the dialog needs to paint its background correctly. Overriding OnEraseBkgnd allows us to blit the bitmap directly onto the dialog’s client area. The implementation below creates a temporary memory DC, selects the skin bitmap into it, and uses BitBlt to copy the image onto the dialog. If the bitmap exists, the function returns TRUE to indicate that the background has been handled; otherwise, it falls back to the default MFC implementation.

Prompt
BOOL CSkinDlg::OnEraseBkgnd(CDC* pDC)</p> <p>{</p> <p> if (m_hBmp)</p> <p> {</p> <p> BITMAP bm;</p> <p> ::GetObject(m_hBmp, sizeof(bm), &bm);</p> <p> HDC hMemDC = ::CreateCompatibleDC(pDC->GetSafeHdc());</p> <p> if (!hMemDC) return CDialog::OnEraseBkgnd(pDC);</p> <p> HBITMAP hOld = (HBITMAP)::SelectObject(hMemDC, m_hBmp);</p> <p> ::BitBlt(pDC->GetSafeHdc(), 0, 0, bm.bmWidth, bm.bmHeight,</p> <p> hMemDC, 0, 0, SRCCOPY);</p> <p> ::SelectObject(hMemDC, hOld);</p> <p> ::DeleteDC(hMemDC);</p> <p> return TRUE;</p> <p> }</p> <p> return CDialog::OnEraseBkgnd(pDC);</p> <p>}</p>

Finally, the OnInitDialog override applies the generated region to the window. Calling SetWindowRgn with the region handle tells Windows to reshape the dialog. The second parameter, TRUE, forces the window to redraw immediately, so the skin appears as soon as the dialog becomes visible.

Prompt
BOOL CSkinDlg::OnInitDialog()</p> <p>{</p> <p> CDialog::OnInitDialog();</p> <p> // Apply the custom region if it was created</p> <p> if (m_hWndRgn)</p> <p> ::SetWindowRgn(m_hWndRgn, TRUE);</p> <p> return TRUE; // Let MFC handle the focus</p> <p>}</p>

These pieces together give the dialog a custom shape and a background that matches the bitmap. The code is straightforward, but it demonstrates the power of working directly with device contexts, bitmap data, and regions to create UI elements that look polished and unique.

Running and Verifying the Skinned Dialog

With the skin logic in place, building and launching the application should now produce a dialog that matches the shape of the bitmap. When you press Ctrl+F5 or click the debug start button in Visual Studio, the dialog window appears. If everything worked correctly, the window will have no standard window borders, the title bar may be missing or partially transparent, and the controls inside the dialog will be clipped to the custom region. The bitmap should be rendered exactly as it appears in the resource file.

During the first run, it’s useful to verify that the bitmap is loaded correctly. If m_hBmp is null, the background will be painted by the default MFC routine, resulting in a plain gray dialog. Double‑check the resource ID and the build action for the bitmap file. In a typical project, the bitmap resource is added to the .rc file with a line similar to:

Prompt
IDB_MAIN BITMAP "Skin.bmp"</p>

Once the bitmap is confirmed, you can test the interaction between the region and user input. Click on various parts of the dialog to ensure that controls respond correctly. For instance, if a button sits in a translucent area of the bitmap, clicking it should still register the click. The region created by DIBToRgn ensures that only the opaque portions of the bitmap are interactive; if you click outside that area, the click is ignored by the dialog, allowing clicks to pass through to windows underneath.

To further validate the skin, experiment with different background colors in DIBToRgn. If you use a pure white background in your bitmap, passing RGB(255, 255, 255) to DIBToRgn will make those white pixels transparent. If your skin uses a custom color, adjust the tolerance value to fine‑tune which pixels become part of the region. This flexibility lets you design skins that have subtle shading or anti‑aliasing without compromising the overall shape.

Once satisfied, you can integrate the skinning logic into other dialogs or even into a full‑featured application. The key takeaway is that the approach relies on standard Windows GDI operations: loading a bitmap, converting it to a 32‑bit DIB, scanning pixels, building a region, and applying that region to a window. No external libraries are required, and the code remains lightweight and easy to maintain.

With the dialog now visually distinct, you’ve achieved a polished, non‑rectangular UI element that can be reused across projects. The same technique can be extended to tool windows, custom toolbars, or splash screens, providing a cohesive visual style throughout an application. By keeping the code modular and well‑documented, future developers can tweak the skinning behavior without deep dives into low‑level GDI functions.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles