Why does Windows draw its own min-max-close buttons, even if the WM_NCPAINT is correctly(?) reimplemented?

StackOverflow https://stackoverflow.com/questions/20440203

Question

As we can see from this screenshot I took:

http://www.picpaste.com/pics/skinned_window.1386408792.png

for some strange reason Windows draws its own (un-styled) minimize/maximize/close button in the titlebar on top of my skinned titlebar (yellow rectangle), as the red arrow points. It also draws an annoying 1pixel sized line on bottom of those buttons, as you can see from the screeenshot.

You can notice that I'm skinning the window: I'm drawing my own titlebar (yellow rectangle), and resize borders (cyan, magenta, red rectangles). They're just rectangles for now, but I can't understand why Windows draws on top of my yellow rectangle, which I draw in the non-client area, by simply drawing when the WM_NCPAINT happens. Everything works just fine, except for this weird thing.

This does not happen always, it will happen after a while using the skinned window, resizing it, and maximize/minimize it 2-3 times. In particular it happens when I click the mouse down on the tilebar, at a certain point of the execution of the little program. Indeed I thought the problem could have been something wrong in the WM_NCHITTEST message, but it appears not to be that. There is something wrong, maybe some Window Style or Window Extended Style flag that is wrong.

I can't explain this, I'm reimplementing the WM_NCPAINT message correctly (I guess), so shouldn't Windows understand that I'm drawing my own titlebar? Why does it overwrite my drawings?! Is it a bug of Windows XP? It seems not to happen in Windows 7, but I'm not so sure.

Maybe I just missed to reimplement a WM_* message for that. Someone can help me? This is getting me nuts!

NOTE: I can't use WinForms, Qt, or some other libraries which helps to skin a window, this is an old project and all must be accomplished straight in winapi, handling the correct WM_* messages. No libraries can be linked.

NOTE2: Using both SetWindowPos or RedrawWindow in the WM_NCACTIVATE message brings same results.

This is the code:

#include <windows.h>
#include <stdio.h>


LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);

char szClassName[ ] = "SkinTest";


int left_off;
int right_off;
int top_off;
int bottom_off;


int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow)
{
    HWND hwnd;               /* This is the handle for our window */
    MSG messages;            /* Here messages to the application are saved */
    WNDCLASSEX wincl;        /* Data structure for the windowclass */


    wincl.hInstance = hThisInstance;
    wincl.lpszClassName = szClassName;
    wincl.lpfnWndProc = WindowProcedure;
    wincl.style = CS_DBLCLKS;
    wincl.cbSize = sizeof (WNDCLASSEX);

    wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
    wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
    wincl.lpszMenuName = NULL;                
    wincl.cbClsExtra = 0;                     
    wincl.cbWndExtra = 0;                     
    wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;

    if (!RegisterClassEx (&wincl))
        return 0;

    DWORD style =  WS_OVERLAPPEDWINDOW;

    hwnd = CreateWindowEx (
       WS_EX_CLIENTEDGE,
       szClassName,
       "Code::Blocks Template Windows App",
       style,
       CW_USEDEFAULT,
       CW_USEDEFAULT,
       500,
       500,
       HWND_DESKTOP,
       NULL,
       hThisInstance,
       NULL
       );

    //
    // This prevent the round-rect shape of the overlapped window.
    //
    HRGN rgn = CreateRectRgn(0,0,500,500);
    SetWindowRgn(hwnd, rgn, TRUE);

    left_off = 4;
    right_off = 4;
    top_off = 23;
    bottom_off = 4;

    ShowWindow (hwnd, nCmdShow);

    while (GetMessage (&messages, NULL, 0, 0))
    {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }

    return messages.wParam;
}



#define COLOR_TITLEBAR        0
#define COLOR_LEFT_BORDER     2
#define COLOR_RIGHT_BORDER    4
#define COLOR_BOTTOM_BORDER   6

int win_x, win_y, win_width, win_height;
int win_is_not_active = 0;


COLORREF borders_colors[] =
{
    RGB(255,255,0), RGB(180,180,0),    // Active titlebar - Not active titlebar
    RGB(0,255,255), RGB(0,180,180),    // Active left border - Not active left border
    RGB(255,0,255), RGB(180,0,180),    // Active right border - Not Active right border
    RGB(255,0,0),   RGB(180,0,0)       // Active bottom border - Not active bottom border
};




void draw_titlebar(HDC hdc)
{
    HBRUSH tmp, br = CreateSolidBrush(borders_colors[COLOR_TITLEBAR + win_is_not_active]);
    tmp = (HBRUSH)SelectObject(hdc, br);
    Rectangle(hdc, 0, 0, win_width, top_off);
    SelectObject(hdc, tmp);
    DeleteObject(br);
}

void draw_left_border(HDC hdc)
{
    HBRUSH tmp, br = CreateSolidBrush(borders_colors[COLOR_LEFT_BORDER + win_is_not_active]);
    tmp = (HBRUSH)SelectObject(hdc, br);
    Rectangle(hdc, 0, top_off, left_off, win_height - bottom_off);
    SelectObject(hdc, tmp);
    DeleteObject(br);
}


void draw_right_border(HDC hdc)
{
    HBRUSH tmp, br = CreateSolidBrush(borders_colors[COLOR_RIGHT_BORDER + win_is_not_active]);
    tmp = (HBRUSH)SelectObject(hdc, br);
    Rectangle(hdc, win_width - right_off, top_off, win_width, win_height - bottom_off);
    SelectObject(hdc, tmp);
    DeleteObject(br);
}


void draw_bottom_border(HDC hdc)
{
    HBRUSH tmp, br = CreateSolidBrush(borders_colors[COLOR_BOTTOM_BORDER + win_is_not_active]);
    tmp = (HBRUSH)SelectObject(hdc, br);
    Rectangle(hdc, 0, win_height - bottom_off, win_width, win_height);
    SelectObject(hdc, tmp);
    DeleteObject(br);
}



LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
    case WM_DESTROY:
        PostQuitMessage (0);
        break;

    case WM_SIZE:
        {
            RECT rect;
            HRGN rgn;
            GetWindowRect(hwnd, &rect);
            win_x = rect.left;
            win_y = rect.top;
            win_width = rect.right - rect.left;
            win_height = rect.bottom - rect.top;
            //
            // I use this to set a rectangular region for the window, and not a round-rect one.
            //
            rgn = CreateRectRgn(0,0, rect.right, rect.bottom);
            SetWindowRgn(hwnd, rgn, TRUE);
            DeleteObject(rgn);
        }
        break;

    case WM_PAINT:
    {
        printf("WM_PAINT\n");
        PAINTSTRUCT ps;
        HDC hdc;
        HBRUSH hb;
        RECT rect;

        hdc = BeginPaint(hwnd, &ps);
        hb = CreateSolidBrush(RGB(rand()%255,rand()%255,rand()%255));

        GetClientRect(hwnd, &rect);

        FillRect(hdc, &rect, hb);

        DeleteObject(hb);
        EndPaint(hwnd, &ps);
        break;
    }

    case WM_NCPAINT:
    {
        printf("WM_NCPAINT\n");
        HDC hdc;
        HBRUSH br;
        RECT rect;
        HRGN rgn = (HRGN)wparam;

        if ((wparam == 0) || (wparam == 1))
            hdc = GetWindowDC(hwnd);
        else
            hdc = GetDCEx(hwnd, (HRGN)wparam, DCX_WINDOW|DCX_CACHE|DCX_LOCKWINDOWUPDATE|DCX_INTERSECTRGN);

        draw_titlebar(hdc);
        draw_left_border(hdc);
        draw_right_border(hdc);
        draw_bottom_border(hdc);
        ReleaseDC(hwnd, hdc);
        return 0;
    }

    case WM_NCACTIVATE:
            if (wparam)
                win_is_not_active = 0;
            else
                win_is_not_active = 1;
          // Force paint our non-client area otherwise Windows will paint its own.
          SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_NOZORDER|SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE|SWP_FRAMECHANGED);
          //RedrawWindow(hwnd, 0, 0, RDW_FRAME | RDW_UPDATENOW | RDW_NOCHILDREN);
          return 0;

    case WM_NCCALCSIZE:
        {
            if (wparam)
            {
                NCCALCSIZE_PARAMS * ncparams = (NCCALCSIZE_PARAMS *)lparam;
                printf("WM_NCCALCSIZE wparam:True\n");
                ncparams->rgrc[0].left   += left_off;
                ncparams->rgrc[0].top    += top_off;
                ncparams->rgrc[0].right  -= right_off;
                ncparams->rgrc[0].bottom -= bottom_off;
                return 0;
            } else {

                RECT * rect = (RECT *)lparam;
                return 0;
            }
        }


    case WM_NCHITTEST:
        {
            LRESULT result = DefWindowProc(hwnd, message, wparam, lparam);
            switch (result)
            {
                //
                // I have to set this, because i need to draw my own min/max/close buttons
                // in different coordinates where the system draws them, so let's consider
                // all the titlebar just a tilebar for now, ignoring those buttons.
                //
                case HTCLOSE:
                case HTMAXBUTTON:
                case HTMINBUTTON:
                case HTSYSMENU:
                case HTNOWHERE:
                case HTHELP:
                case HTERROR:
                    return HTCAPTION;
                default:
                    return  result;
            };
        }

    case WM_ERASEBKGND:
        return 1;

    default:
        return DefWindowProc (hwnd, message, wparam, lparam);
}

return 0;
}
Was it helpful?

Solution

You'll want to handle WM_NCHITTEST appropriately as Windows likes to also draw non-client elements from that message (in the default window procedure) as well.

After handling this message in some of my custom windows the issue you have goes away.

EDIT: I see you're already handling WM_NCHITTEST, you'll not want to call DefWindowProc if you're handling a particular hit as that is where it will try and draw those caption buttons.

OTHER TIPS

I know that this answer is very late in relation to when the question was first asked, but I hope that this answer will help someone out there.

If we disregard the evil oddities of WM_NCPAINT for a minute, and assume that you've implemented a functioning (and safe) handler for WM_NCPAINT, we can start to look at the minimize, maximize and close buttons. The non-client area (NC) involves a lot of potential windows messages to handle, such as WM_NCCALCSIZE, WM_NCHITTEST, WM_NCACTIVATE, and so on.

These messages are widely familiar in the Win32 community. However, one message that many disregard is the WM_NCLBUTTONDOWN message. In later implementations of Windows, this message not only distributes on-click events, but also draws default buttons in the NC. When the WM_NCLBUTTONDOWN message is sent, the wParam variable holds the hit-test value returned by the WindowProc function as a result of processing the WM_NCHITTEST message. The message can be handled in the following way:

case WM_NCLBUTTONDOWN:
    result = 0;

    if (wparam == HTMINBUTTON) {
        ShowWindow(hwnd, SW_MINIMIZE);
    }
    else if (wparam == HTMAXBUTTON) {
        WINDOWPLACEMENT wp;
        GetWindowPlacement(hwnd, &wp);
        ShowWindow(hwnd, wp.showCmd == SW_MAXIMIZE ? SW_RESTORE : SW_MAXIMIZE);
    }
    else if (wparam == HTCLOSE) {
        SendMessage(hwnd, WM_DESTROY, 0, 0);
    }
    else {
        result = DefWindowProc(hwnd, message, wparam, lparam);
    }

    return result;
    break;

The reason for calling GetWindowPlacement() is to allow for correct behaviour of the window when entering/exiting the fullscreen view.

Also, note that you will need to implement your own processing of WM_NCHITTEST and returning HTMINBUTTON, HTMAXBUTTON and HTCLOSE, respectively, in order for this to work.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top