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

2

11

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;
}
Beatrisbeatrisa answered 7/12, 2013 at 10:22 Comment(5)
Good question, I upvote. For the problem, maybe I am incorrect, but Windows may handle this in deeper level, etc. Win32k.sys. Maybe. :-)Halitosis
A typical solution would be to draw your custom titlebar in the client area instead of the non-client area, then hide the default non-client area, such as with SetWindowRgn(), so it does not matter what Windows decides to put in it.Orvil
But in that case I have to reimplement all of the window resize facilities, e.g. top-left, top-right resize etc. I choosen to not use client area on purpose to avoid all of that mess, which is far more difficult to struggle with!Beatrisbeatrisa
@MarcoPagliaricci: Hey. I know it's been awhile, still I'm curious, how did you find out how to process WM_NCPAINT by catching wParam being 1 or 0 and calling GetWindowDC instead of GetDCEx? This seems to work, but it's not what WM_NCPAINT suggests.Notwithstanding
Sorry man, lot of years ago..I don't remember how I did solve that! :-)Beatrisbeatrisa
E
13

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.

Ebner answered 13/12, 2013 at 16:58 Comment(8)
Who at Microsoft decided that drawing in response to WM_NCHITTEST was a good idea? Thanks for this non-intuitive answer.Berard
I also found it necessary to handle WM_SETCURSOR too.Berard
@MarkRansom how did you handle WM_SETCURSOR? I found it to be the cause of error for me too. If handling WM_SETCURSOR and blocking default behavior the problem goes away but so does also the change to resizing cursors.Beutler
@Beutler when I said "handle" I meant doing the same actions that Windows would normally do, i.e. looking at the hit test code and setting the proper cursor. There are no shortcuts unfortunately.Berard
@MarkRansom Thanks for your quick reply. I already did handle it myself, but was hoping there was a shortcut. One last question, WM_SETCURSOR was pretty straight forward, WM_NCHITTEST seems to be another story. Do you have any example on what is the proper way to handle WM_NCHITTEST?Beutler
@Beutler Sorry, no examples that I can share. I hate how Windows punishes you the moment you try to do something even a little out of the ordinary.Berard
@MarkRansom Even so, thanks for your time. I have something that seems to be working pretty good now, after finding this: chromium.googlesource.com/chromium/src/+/master/ui/views/win/…Beutler
@MarkRansom: by the way, it's not just that they draw non-client area parts in response to WM_NCHITTEST, but also in WM_NCCALCSIZE for some controls (at least on Windows 10.)Notwithstanding
D
0

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.

Deviant answered 13/7, 2022 at 23:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.