Custom Titlebar

How to insert buttons into a window's caption area

This article will teach you the techniques used to insert one or more buttons into the title bar (caption area) of any window. The source code download implements a complete library which supports all types of window, including standard frame windows, and tool windows with smaller captions.

Important

Note that this article was originally written in 20001 - it is only relevant for Windows 95/98/ME and Windows NT/2000/XP. Customizing the titlebar in Vista and Windows 7 (using the Aero theme) requires overlaying windows on top of the titlebar.

The techniques presented here are fairly straight-forward. The same techniques can be applied to perform any title bar customization, although this article will concentrate just on inserting buttons next to the standard minimize, maximize and close caption buttons.

Sample screen-shot

Subclassing the window

As with most other window customizations, this method will subclass the window in question in order to provide the additional functionality.

Every inserted button is going to need some form of state. The structure definition below describes the necessary attributes for a button.

typedef struct
{
    UINT     uCmd;          //Command to send when clicked (WM_COMMAND)
    int      nRightBorder;  //Pixels between this button and buttons to the right
    HBITMAP  hBmp;          //Bitmap to display
    BOOL     fPressed;      //Is the button pressed in or out?.
} CaptionButton;

Whilst this is sufficient to describe one button, there is still some additional information we need to keep track of. We need to know the number of buttons that are inserted, an array of these buttons, and somewhere to store the window's old window procedure. The following structure contains these details, and it is this structure that we associate with the subclassed window.

typedef struct
{
    CaptionButton  buttons[MAX_TITLE_BUTTONS];
    int            nNumButtons;
    BOOL           fMouseDown;        // is the mouse button being clicked?
    WNDPROC        wpOldProc;         // old window procedure
    int            iActiveButton;     // the button index being clicked.
} CustomCaption;

We're now ready to implement an InsertButton function. This function will perform a number of steps to insert a button into a window's titlebar.

  1. Allocate memory for a CustomCaption structure if this is the first button being inserted.
  2. Associate this structure with the window (using SetProp API, if necessary)
  3. Replace the window procedure with our custom one.
  4. Fill in one entry in the CaptionButton array to represent a button.

Note that there aren't any physical changes to the window when we insert a button. Unlike the edit-control button tutorial, where space was allocated for a button in the non-client area using WM_NCCALCSIZE, in this case there is no need. This is because the caption bar already exists, and we will be drawing our buttons over the top of that.

BOOL Caption_InsertButton(HWND hwnd, UINT uCmd, int nBorder, HBITMAP hBmp);

Calculating the button position

Our custom titlebar buttons don't have a fixed position or size. Their location and size are completely dependent on the current size of the window, and the current system setting for the titlebar height. It will therefore be necessary to write a function which calculates the current location of a button at any given time. This location is required when we come to draw the buttons, and also when we come to process mouse messages for them.

The function will look like this:

void GetButtonRect(CustomCaption *ctp, HWND hwnd, int idx, RECT *rect, BOOL fWindowRelative);

We need to pass a pointer to the CustomCaption structure for a given window, the window handle itself, the index of the button we want the location for and a pointer to a RECT structure to receive the coordinates. The last parameter, fWindowRelative, controls what the returned coordinates will be relative to. A value of TRUE results in window-relative coordintes (from the top-left of the window). A value of FALSE results in screen coordinates.

The GetButtonRect function needs to take a few things into consideration in order to return a correct button location.

  1. Titlebar button size (in pixels).This information can be retrieved using the GetSystemMetrics API call, but we need to test if the window is a standard window or a tool-window (with a narrow titlebar).
  2. Current caption buttons.A window can contain four different caption buttons. These are the Close [x], Minimize [-], Maximize [+] and Context Help [?] buttons. If we want to preserve these buttons then we need to make sure that our inserted buttons never overlap any existing buttons. The GetButtonRect function tests the window styles to determine how many buttons are already in place.
  3. Window border size.A window can have many different border sizes, depending on if it has a resizing or fixed border, but also on current system settings. GetButtonRect calculates the current border size and takes these dimensions into account.
  4. Previously inserted buttons.The GetButtonRect function also needs to take into account any buttons that were inserted before the one we are interested in, and calculate not only their sizes (based on the system settings), but also the border spacing setting that our buttons have.

Now that we can precisely locate any one of our custom buttons, we can proceed onto the next step: actually drawing the buttons.

Drawing the inserted buttons

The custom buttons live in the non-client area of a window (more precisely, the caption bar). Therefore this requires us to handle the WM_NCPAINT message.

Drawing the custom buttons is going to be a little more difficult than usual. We ideally want to keep the actual drawing to a minimum. If possible, we want the default window procedure to paint as much as possible before we step in and perform our customization. The problem is that we have to preserve the existing window title bar whilst drawing our own buttons on top. And we have to make sure that there is no flickering or painting glitches.

The basic strategy will be to get the original window procedure to draw the whole window frame and caption, but with the button areas masked out. Once the window has been painted, all that is left to do is to paint each button in turn into the masked-out areas. I will break the process down into steps so that you can see how this is achieved.

The first step is to build up a region which describes the whole window, minus the areas our buttons will take. This is achieved with the CreateRectRgn API call to obtain the whole window region. The button areas are then masked out with calls to the CombineRgn API call, specifying the RGN_XOR flag to "cut" button-shaped rectangles out of the window region.

One very important thing to mention is that all regions and rectangles must be in screen coordinates. The WM_NCPAINT handler will look something like this.

HRGN hrgn, temprgn;

// Get the SCREEN coordinates of this window.
GetWindowRect(hwnd, &rect);

// Create a region which covers the whole window. 
if(wParam == 1)
    hrgn = CreateRectRgnIndirect(&rect);
else
    hrgn = (HRGN)wParam;

// Clip our custom buttons out of the way...
for(i = 0; i < ctp->nNumButtons; i++)
{
    //Get button rectangle in screen coords
    GetButtonRect(ctp, hwnd, i, &rect, FALSE);

    temprgn = CreateRectRgnIndirect(&rect);

    //Cut out a button-shaped hole
    CombineRgn(hrgn, hrgn, temprgn, RGN_XOR);
    DeleteObject(temprgn);
}

That's the tricky part out of the way. The next task is to get the original window procedure to paint the entire window frame and caption (like it would do normally).

The wParam argument to WM_NCPAINT is a handle to an update region. MSDN states that when wParam is 1, this indicates that the entire non-client area is to be updated. Our paint handler needs to test for this occurance and create a valid window region when this happens. Otherwise, wParam already represents a valid update region, so we can use that instead when we mask our buttons out.

Now, when we call the original window procedure, instead of passing the original handle specified by wParam, we can pass in our modified update region. It works like a charm. The window is drawn normally, but with our inserted buttons masked out of the way.

CallWindowProc(ctp->wpOldProc, hwnd, WM_NCPAINT, (WPARAM)hrgn, 0);

All that is left is to draw our buttons! To do this we need to obtain a device context to the window, using the GetWindowDC API call. Next, the buttons are drawn in a simple for-loop. Another important note here. When we draw into a window device-context, all coordinates must now be window relative. That is, relative to the top-left of the window.

The code below just draws a blank button, either pressed or normal:

HDC hdc = GetWindowDC(hwnd);

// Draw buttons in a loop
for(i = 0; i < ctp->nNumButtons; i++)
{
    //Get Button rect in window coords
    GetButtonRect(ctp, hwnd, i, &rect1, TRUE);
        
    if(ctp->buttons[i].fPressed)
        DrawFrameControl(hdc, &rect1, DFC_BUTTON, DFCS_BUTTONPUSH | DFCS_PUSHED);
    else
        DrawFrameControl(hdc, &rect1, DFC_BUTTON, DFCS_BUTTONPUSH);

   //Draw the bitmap on top...
}

ReleaseDC(hwnd, hdc);

if(wParam == 1)
    DeleteObject(hrgn);

We have to be careful to release the update region if we allocated it ourselves. If wParam was a valid region, then we don't need to because Windows owns the update region. The code download also draws a bitmap into the button, but I wanted to keep the code as simple as possible here.

Making the buttons work.

What we have so far is a window with extra buttons drawn into the title bar. They don't actually do anything yet - so far, they're just decoration. It's pretty simple to make the buttons behave just like the standard caption buttons. In fact, the process is almost exactly the same as the one I described in another article (Inserting a button into an edit control).

Mouse button down (WM_NCLBUTTONDOWN)

When the left mouse button is clicked, and the mouse cursor is within one of the areas occupied by our buttons, then we need to react to this event and give the button a depressed look. We will also set the mouse capture so we can receive all further mouse messages whilst the mouse button is held down. This means that we can move the mouse outside the window and can still retrieve mouse messages.

The code below checks each inserted button in turn to see if the mouse is inside one as the mouse button is clicked. If a positive intersection is found then the mouse capture is set, and a variable set (iActiveButton) to record what the active button is. The caption is redrawn to reflect the button in a "pushed" state.

int i;
RECT rect;
POINT pt;

// retrieve the mouse coordinates
pt.x = (short)LOWORD(lParam);
pt.y = (short)HIWORD(lParam);

// Loop over all buttons to see if one has been clicked
for(i = 0; i < ctp->nNumButtons; i++)
{
    //get screen coordinates of each button
    GetButtonRect(ctp, hwnd, i, &rect, FALSE);
    InflateRect(&rect, 0, 2);
            
    //if clicked in a custom button
    if(PtInRect(&rect, pt))
    {
        ctp->iActiveButton = i;
        ctp->buttons[i].fPressed = TRUE;
        ctp->fMouseDown = TRUE;
                
        SetCapture(hwnd);
                
        RedrawCaption(hwnd);
                
        return 0;
    }
}

return CallWindowProc(ctp->wpOldProc, hwnd, msg, wParam, lParam);

Mouse Movement (WM_MOUSEMOVE)

The WM_MOUSEMOVE message will be received whenever the mouse moves within the client area of a window, but will be also be received outside the client area when the mouse capture is set. The mouse-move handler is very similar to the left-click handler shown above. The only difference is a small amount of code to convert the client-relative mouse coordinates to screen-relative coordinates (a simple ClientToScreen call). The other change is a test to see if the mouse is inside or outside the active button's region. If it is inside, then the button is drawn with a sunken appearance. When the mouse is outside the active button, then the button is drawn normally. This behaviour causes the button to be pushed in and out as the mouse moves over it.

GetButtonRect(ctp, hwnd, ctp->iActiveButton, &rect, FALSE);

if(PtInRect(&rect, pt))
    ctp->buttons[ctp->iActiveButton].fPressed = TRUE;
else
    ctp->buttons[ctp->iActiveButton].fPressed = FALSE;

Mouse button up (WM_LBUTTONUP)

The left-button-up handler is also very similar to the mouse-move handler. The only difference being the button is restored to its non-pressed state, and a WM_COMMAND message is sent to the window if the mouse is still within the active button when the mouse button is released. This WM_COMMAND basically simulates a standard button press - the command identifier is stored in wParam.

GetButtonRect(ctp, hwnd, ctp->iActiveButton, &rect, FALSE);

if(PtInRect(&rect, pt))
    SendMessage(hwnd, WM_COMMAND, ctp->buttons[ctp->iActiveButton].uCmd, 0);

Hit-testing (WM_NCHITTEST)

Last one! This message handler must check if the mouse is inside any one of the inserted buttons. Because our inserted buttons are inside the caption area, the default handler for this message will return HTCAPTION. We need to override this, so we will return HTBORDER instead to prevent any caption-related behaviour occuring when the user clicks our button.

for(i = 0; i < ctp->nNumButtons; i++)
{
    GetButtonRect(ctp, hwnd, i, &rect, FALSE);

    if(PtInRect(&rect, pt))
        return HTBORDER;

}

return CallWindowProc(ctp->wpOldProc, hwnd, msg, wParam, lParam);

One last step

We're almost there! We now have fully functional buttons inside the caption area of a window. There are two more messages that our subclass procedure needs to handle. The messages are WM_NCACTIVATE and WM_SETTEXT. Both of these messages cause a window caption to be repainted, which is going to cause our inserted buttons to be painted over. In the case of WM_NCACTIVATE, the caption is painted in either active or inactive colours whenever the window is activated or deactivated. As for WM_SETTEXT, this message is received whenever the window text is to be changed, causing the caption to repainted.

Unfortunately for us neither of these repaints occur through the WM_NCPAINT message, which is a design flaw in Windows as far as I can make out. The problem still remains though. The solution is to prevent both messages from performing any painting whilst letting them do everything else. This is achieved in four steps:

  1. Turn off the WS_VISIBLE style for the window, which stops any painting from occuring.
  2. Calling the old window procedure to handle the message.
  3. Turn WS_VISIBLE back on again.
  4. Paint the window frame and caption using our custom code.

The code looks like this.

LRESULT foobar(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    DWORD ret, dwStyle;

    dwStyle = GetWindowLong(hwnd, GWL_STYLE);

    // turn OFF WS_VISIBLE
    SetWindowLong(hwnd, GWL_STYLE, dwStyle & ~WS_VISIBLE);

    // perform the default action, minus painting
    ret = CallWindowProc(ctp->wpOldProc, hwnd, msg, wParam, lParam);

    // turn ON WS_VISIBLE
    SetWindowLong(hwnd, GWL_STYLE, dwStyle);

    // perform custom painting
    Caption_NCPaint(hwnd, (HRGN)1);

    return ret;
}

This same code can be used to process WM_SETTEXT and WM_NCACTIVATE.

Conclusion

There is another option for placing buttons over the caption bar. You could simply create a button using CreateWindow with the WS_POPUP style. This button can be positioned over the window's title bar, and will look and act just like a normal button. This method will work, but it's not perfect. There will be window focus problems, clipping and painting problems, and the hassle of always making sure that the button is in the right place, and never overlapping any windows it shouldn't be.

The method of inserting titlebar buttons described here is the preferred way to perform this type of customization. Well, I hope you enjoyed this article and found it useful. If you have any feedback, I'd be glad to here it.