home
Custom Window Captions (and shapes too)

Get the source code here.

  My latest tiny application program needed a custom title. I played with many things to do this, grabbing non-client messages trying to prevent paint events, or just making a window with no caption. Nothing was quite satisfactory. Quite accidentally while trying another window trick to round off the corners I stumbled on a very simple way to completely customize a windows complete appearance, color and shape.

  This may make more sense if you see the application it is intended for. Please have a look at Timer and get yourself a copy and run it a few times. Be advised, it's still a work in progress, though it seems not to crash your system, just lacks some appearance functionality (oh yeah, and it's timer accuracy is for crap).

  This method works by simply converting the entire displayed portion of the window to client area. The caption and border areas are still there, but never get displayed, and as an additional benefit you can access the whole window in one set of co-ordinates. This is done by applying a region to the window before we display it. I'm going to describe the code in general, even giving short pieces of it, while allowing you to see the entire program by downloading the source.


  The window I was making to looks like this:

  It's unconventional in several respects. First, the caption is the wrong color. And the corners are rounded. Gee, weird buttons too. And those numbers... yetch.

  Well, to start, all the window face consists of is a bitmap. In fact, the actual app bitmap is very similar to the one you see here. What we need to do is 

create a window with a client area just big enough to fit this bitmap,  and then we'll cut away the portions we don't need. Let's look at some system metrics (another yetch).

  We need to compute the window at run time because we can't be sure how our user will have his system set up. Caption heights can change, trust me. (Don't trust me? Right click the desktop, select Appearance, Scheme, pick "Windows Standard (extra large)"). The border and caption sizes are returned in pixel units with a GetSystemMetrics call, using the constants as indicated in this picture.

We need to compute the size of the smallest window to hold our bitmap, and the 4 corners of the 

client area (two x-y pairs), not in client co-ordinates, but in window co-ordinates. Given Bitmap width wBit height hBmp (measure bitmap size in your editor)

wWin == main window width
= 2 * SM_CYDLGFRAME + hBackBmp 
hWin == main window height
= SM_CYDLGFRAME + SM_CYCAPTION + hBackBmp
x1Reg == top left corner of client area in screen co-ords
= SM_CXDLGFRAME
x2Reg == lower right corner of client area in screen co-ords
= x1Reg + wBackBmp + 1
y1Reg == upper left corner of client area in screen co-ords
= SM_CYDLGFRAME + SM_CYCAPTION
y2Reg == lower right corner of client area in screen co-ords
= y1Reg + hBackBmp + 1

This can all be defined inside your WinMain procedure. Now, let's do this in assembly:
mov eax, wBackBmp
mov wWin, eax
mov x2Reg, eax
mov eax, hBackBmp
mov hWin, eax
mov y2Reg, eax
invoke GetSystemMetrics, SM_CYDLGFRAME
mov y1Reg, eax
add y2Reg, eax
shl eax, 1 ; SM_CYDLGFRAME * 2
add hWin, eax
add wWin, eax ; wWin = 2 * SM_CYDLGFRAME + hBackBmp
invoke GetSystemMetrics, SM_CXDLGFRAME
mov x1Reg, eax ; x1Reg = SM_CXDLGFRAME 
inc eax
add x2Reg, eax ; x2Reg = x1Reg + wBackBmp + 1
invoke GetSystemMetrics, SM_CYCAPTION
add hWin, eax ; hWin = SM_CYDLGFRAME + SM_CYCAPTION + hBackBmp
add y1Reg, eax ; y1Reg = SM_CYDLGFRAME + SM_CYCAPTION
inc eax
add y2Reg, eax ; y2Reg = y1Reg + hBackBmp + 1
; center the window
; X Dims
invoke GetSystemMetrics,SM_CXSCREEN
invoke TopXY,wWin,eax
mov xWin, eax
; Y Dims
invoke GetSystemMetrics,SM_CYSCREEN
invoke TopXY,hWin,eax
mov yWin, eax
; now make the main window
invoke CreateWindowEx, NULL, ADDR ClassName, ADDR AppName,
WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or CS_DBLCLKS,
xWin, yWin, wWin, hWin,
NULL, NULL, hInstance, NULL
mov hWndMain,eax


  Not too bad, huh? Just to create one window? OK, so we have the window, let's make the region and get this window displayed:

invoke CreateRoundRectRgn, x1Reg, y1Reg, x2Reg, y2Reg, 20, 20 
invoke SetWindowRgn, hWndMain, eax, FALSE
invoke ShowWindow, hWndMain,SW_SHOWNORMAL
invoke UpdateWindow, hWndMain
invoke SetFocus, hWndMain
  CreateRoundRectRgn is the key to this method. The region is in main window co-ordinates, which we so skillfully defined as the edges of the client area. It differs from a simple rectangle by allowing rounded corners, the rounding defined by an ellipse who's x-y is the last 2 parameters. I used 20,20 for a circular round over. Hard coding these numbers is OK, because the region size never varies.

  That's the window. We manipulate it in the message pump. At WM_CREATE time we create a device context for our bitmap, and load the bitmap from a resource. I'll explain later why I'm setting the text color (it's a special surprise). Of course, at WM_DESTROY time we release all our resources like a good application should.

; --------------------------------
.IF uMsg == WM_CREATE ; create GDI objects to hold our tools
invoke GetDC, hWnd
mov hdc, eax
invoke CreateCompatibleDC, hdc
mov hdcBuf, eax
invoke LoadBitmap,hInstance,IDB_BACKGROUND
mov hBmBackground, eax 
invoke SelectObject, hdcBuf, hBmBackground
mov hBmOldBuf, eax
invoke SetTextColor, hdcBuf, WHITE
invoke SetBkColor, hdcBuf, BROWN
invoke InvalidateRect, hWnd, NULL, FALSE
; --------------------------------
.ELSEIF uMsg == WM_DESTROY ; clean up the GDI objects we created
invoke SelectObject, hdcBuf, hBmOldBuf
invoke DeleteDC,hdcBuf
invoke DeleteObject, hBmBackground
invoke PostQuitMessage,NULL
; --------------------------------
  Now let's get the bitmap onto our window. Since it is already on a private device context, we can just BitBlit it to the screen. And since we lined up our region to the client area, they match 1 to 1 so no offsets are needed. Oh, and here's the surprise I was saving. I'll also draw the application name string at the top of the window. ; --------------------------------
.ELSEIF uMsg == WM_PAINT  invoke BeginPaint,hWnd,addr ps 
mov hdc,eax 
invoke DrawText, hdcBuf,ADDR AppName,-1, ADDR rectCAPTION, 
DT_SINGLELINE or DT_LEFT or DT_VCENTER
invoke BitBlt, hdc, 0, 0, 135,187, hdcBuf, 0, 0, SRCCOPY 
invoke EndPaint,hWnd,addr ps 
; --------------------------------
  That's the main work of getting the window to look right, but I thought I would give you a taste of what else you will need to do for a *real* application. As of now, all the window does is sit there. The min and close buttons on it just ain't buttons, as much as they look like it. Well, animating those is a subject for another tutorial. But we can do something simple to make the window move. Let's make it respond to the mouse so we can move it:
  ; --------------------------------
.ELSEIF uMsg == WM_LBUTTONDOWN  ; unpack and save mouse XY
mov eax,lParam ; lParam has mouse XY in packed form
shr eax,16 ; shift hi word to lo
mov lMouseY0, eax ; save Y
mov eax, lParam ; get lP back
and eax, 0FFFFh ; mask lo word
mov lMouseX0, eax ; save X
mov KeyDown, TRUE
invoke SetCapture, hWnd
; --------------------------------
.ELSEIF uMsg == WM_LBUTTONUP mov KeyDown, FALSE
invoke ReleaseCapture
; --------------------------------
.ELSEIF uMsg == WM_MOUSEMOVE .IF KeyDown == TRUE invoke GetWindowRect, hWnd, ADDR rect
mov eax,lParam
and eax,0FFFFh ; get the new mouse X co-ord
sub eax, lMouseX0 ; and compute dif from first down
add rect.left, eax ; and add offset to window left
mov eax,lParam 
shr eax,16 ; mouse Y co-ord
sub eax, lMouseY0 ; compute dif from start
add rect.top, eax ; and add offset to window top
invoke SetWindowPos, hWndMain, NULL, 
rect.left, rect.top, NULL, NULL, 
     .ENDIF
; --------------------------------
  While we're at it, let's make a way to close the test app. We can use WM_RBUTTONUP to post a WM_DESTROY message to ourselves, that will close us down.  ; --------------------------------
.ELSEIF uMsg == WM_RBUTTONUP invoke PostMessage,hWnd, WM_DESTROY, NULL, NULL ; --------------------------------
  And one final trick: As is, the app cannot visually indicate if it is active or not, as we gave it a static color in AppName... or did we? By checking the WM_NCACTIVATE message we can tell if we're being activated or de-activated, and paint out caption title as needed. Note that after we change the color, we invoke an InvalidateRectangle to forse a re-paint. Also note we set retval to 1. I use retval to hold a return value from WinProc. I default it to zero, as most message handlers should do. However, we need to send on the NCACTIVATE message as windows needs to see it for further processing. NC is Non Client, and these messages should be used with just a bit of caution. ; --------------------------------
.ELSEIF uMsg == WM_NCACTIVATE mov eax, wParam
and eax, 0FFFFH
.IF ( eax ) invoke SetTextColor, hdcBuf, WHITE .ELSE invoke SetTextColor, hdcBuf, LT_GRAY .ENDIF
invoke InvalidateRect, hWnd, NULL, FALSE
mov retval, 1
; --------------------------------
  That's it. Go run the sample and look over the full source there. Next time we meet I'll show you how to do hit testing so you can animate the buttons and make then do some real work.

  For any comments, I may be reached here.

home