Top

C++/Win32: Skinning

March 2, 2008

At the end of this lesson, you’ll find a link to the flipcode site, where you can read a tutorial there on skinning. That document is brilliantly prepared, and goes into quite a bit of detail on the subject, so I highly recommend you read it. I will just cover the bare minimum you’ll need to get a skin up on top of your app. After finishing here, you’ll know how to apply rectangular, polygonal, or elliptical skins to your applications — or any combination of the three. The flipcode tutorial goes even further, providing a function to read in shape data from a bitmap file and apply any shape you want to your application. Fun stuff.

But first, the basics.

The idea of skinning, of course, is to give you the option of creating applications that don’t look like every other Windows app (rectangular frame, blue title bar, etc.) You can alter the shape of the window, and apply an image on top of the window to change its appearance. Each of these operations are handled by two separate sets of functions in the API: Regioning and Skinning.

Win32 Regions

A region is just a shape that can be used to receive the same messages that a normal window would receive. To apply a region to a window, you must first declare a region handle (HRGN), define the region with calls to CreateRectRgn, CreateEllipticRgn, or CreatePolygonRgn (and there are others beyond this — see the Win32 documentation), combine your regions into one region with calls to CombineRgn, and then apply the final region to your window with a call to SetWindowRgn.

Because a region is an object, you should DeleteObject them when you are through with them.

Here’s an example, which creates a sort of “figure 8″ region, and applies it to our window (with handle hWnd):

HRGN hRegion1 = CreateEllipticRgn(0, 0  , 200, 200);
HRGN hRegion2 = CreateEllipticRgn(0, 180, 200, 200);

CombineRgn(hRegion1, hRegion1, hRegion2, RGN_OR);

SetWindowRgn(hWnd, hRegion1, true);

DeleteObject(hRegion1);
DeleteObject(hRegion2);

This code block defines two elliptical regions and combines them into a “figure 8″ shape by ORing them (RGN_OR) with the CombineRgn function. It then applies the region to the hWnd window with a call to SetWindowRgn.

Take a look at the source. I’ve added a member function to the PortScannerWindow class that creates a region the same size and shape of the app window. Because I’ll want my skin to cover the entire window, I’ll need to make sure the entire window is covered by this region.

The member function that does this is Regionize(). It takes no parameters, and its task is to figure out the window size and create the appropriate region to cover it.

Here’s the definition:

// Remove title bar and prevent resizing
DWORD dwStyle = GetWindowLong(m_hWnd, GWL_STYLE);
dwStyle &= ~(WS_CAPTION|WS_SIZEBOX);
SetWindowLong(m_hWnd, GWL_STYLE, dwStyle);

// Define and set the region
HRGN hRegion = CreateRectRgn(0, 0, m_pRect->right-m_pRect->left, m_pRect->bottom-m_pRect->top);

SetWindowRgn(m_hWnd, hRegion, true);

DeleteObject(hRegion);

return TRUE;

Notice also the bit of code that retrieves the window long from our app window, and removes the WS_CAPTION and WS_SIZEBOX styles if they exist. Essentially we are removing the title bar and preventing the window from being resized by the user — you can, of course, have skins that can be resized, but let’s not make this harder than it need be. And the title bar doesn’t do us any good, since we’re laying a bitmap over it in a few minutes, so… hasta.

We call this member function from WinMain, immediately after calling Show(). This function should definitely do some error checking, since the calls could fail, but we’ll leave that until later (so we’ll have something to do when we tighten this thing down for release!)

(If you want to have more fun than just creating a rectangular region, change CreateRectRegion to CreateEllipticRgn in the function to make your window a perfect circle. Neat!)

Skinning

So now that we have applied a shape to our window, we’d like to lay a skin over it. To do this, you must first create a bitmap big enough to cover the region, and then bitblt it to the window. In the case of our figure 8 shaped region (the first example I gave), you won’t be able to create a figure 8 shaped bitmap image. You’ll have to make a rectangular image containing the figure 8 shaped skin. Windows will handle the clipping for you. In that example, notice our region is enclosed in a rectangle that is 200 wide by 380 tall, so your image will have to be at least that large.

For this project, however, our region is just a rectangle. Specifically, we create a 500×500 window in WinMain. So we’ll need a 500×500 bitmap image to lay over the region.

There are two ways to get bitmaps into your program. You can either open it as a file and use it that way (which would be the way to go if you want to allow your users to create their own skins) or you can import it into your project as a resource. That’s what we’ll do here. You’ve seen resources already, so this shouldn’t be too tough.

1. From the Solution Explorer, double-click your .rc file

2. Right-click the .rc folder, and select Add Resource.

3. Select Bitmap and then Import, and find and open your bitmap file.

4. Visual Studio imports your bitmap, and opens the bitmap editor. Change it a little if you need to, otherwise close the editor and return to your project.

5. Do a File->Save All to get Visual Studio to remember to update your resource.h file.

6. Open resource.h and you’ll see something like the following:

#define IDB_BITMAP1                     102

That’s the resource ID for your bitmap resource. We’ll need it when we apply it to the region.

Now, we’ll create another member function of the PortScannerWindow class to handle blitting the bitmap to the region. We’ll call it Skin().

Here’s the definition:


BOOL PortScannerWindow::Skin() {

	// load the skin bitmap from resource
	HBITMAP hSkinBmp = LoadBitmap(m_hInstance,MAKEINTRESOURCE(IDB_BITMAP1));
	if (!hSkinBmp) return -1;

	// create a device context for the skin
	HDC dcSkin = CreateCompatibleDC(0);

	// select the skin bitmap
	HBITMAP hOldBmp = (HBITMAP)SelectObject(dcSkin, hSkinBmp);

	PAINTSTRUCT ps;
	BeginPaint(m_hWnd,&ps);

	BitBlt(ps.hdc, 0,0,500,500, dcSkin, 0,0, SRCCOPY);

	EndPaint(m_hWnd,&ps);
}

Don’t worry too much about the specifics here just yet. We’ll talk more about DCs when we discuss the GDI.

So where do we call Skin()? We should call it whenever our message handler receives a WM_PAINT message. So we’ll modify WinProc by adding the following case to the uMsg switch:

case WM_PAINT:
     Skin();
     break;

This will catch the WM_PAINT message and call Skin() when it does. WM_PAINT is sent whenever the window is resized, or if another window is dragged over the window, causing its display to become corrupted. Basically, Windows is aware of when the window needs to be redrawn, and it let’s you know. When the window display is disturbed, the skin gets disturbed with it, so we’ll need to make sure Skin gets called then. Also, WM_PAINT is one of the first messages sent to your window after it is created, so this also guarantees that the skin will be applied when the window is created.

A few more considerations

Well, now we’ve put an ugly skin on top of our app. But we’ve lost two important things: we can no longer move the window (because the title bar is gone) and we can no longer close the application, since the close button is hidden. To remedy this, we’ll want to create our own close button, and figure out a way to get the window movable.

Let’s start with movement.

First, I’ll give you the code, then explain why it works. We’ll need to add the following case to our message handler’s uMsg switch:

case WM_LBUTTONDOWN:
     SendMessage(hWnd, WM_NCLBUTTONDOWN, HTCAPTION,NULL);
     break;

WM_LBUTTONDOWN is sent whenever the user presses the left mouse button while the mouse is over the window. So when we catch this message, we send a WM_NCLBUTTONDOWN message to the title bar (HTCAPTION), tricking it into thinking we clicked it. It’s hidden, remember? So clicking it is impossible. But it still exists, and it still has a message handler waiting to process messages. So it won’t mind if we send a few to it.

Now, let’s add a close button. We need to add another HWND to the class (where all the other window elements’ HWNDs are declared) and create the button.

Here’s the HWND declaration (in PortScannerWindow.h):

HWND m_hCloseButton;

And here’s the CreateWindow call (in PortScanWindow.cpp, under BuildGUI()):

// Create our close button
m_hCloseButton = CreateWindow("BUTTON",
                              "X",
                              WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
                              400, 40, 80, 20,
                              m_hWnd,
                              (HMENU)IDB_CLOSEWINDOW,
                              m_hInstance,
                              NULL);

That will get the button up on the window, but we need to catch its messages and close the application when we receive the WM_COMMAND message for this button.

To do so, modify the WM_COMMAND case in your message handler:

case WM_COMMAND:
        switch(LOWORD(wParam))
	 {
		case IDB_GOSCAN:
			if (validateInput()) goScan();
			break;
		case IDB_CLOSEWINDOW:
			PostQuitMessage(0);
			break;
	 }
	 break;

When the IDB_CLOSEWINDOW button is clicked, we call PostQuitMessage to alert all windows that we want to quit.

And there you go — basic skinning. Again I’ll recommend reading the Flipcode tutorial now that you have a basic idea of how this all works. It really is worth reading.

Next: Threads.


Source Code:

win32tut_part7.zip [3.25MB zipped]

Additional Information:

· Flipcode tutorial - Win32 Window Skinning

Further Reading:

· Nitty Gritty Windows Programming with C++ by Henning Hansen.

· Thinking in C++ by Bruce Eckel.

Comments

Got something to say?





Bottom