Search code examples
c++winapibuttonhoverpaint

paint button when detect hovering Winapi


I'm trying to create a button and handle possible events, like clicking the button, and hovering.

I have found many useful articles, and have so far been able to verify the events. The problem I'm having is with drawing.

In the code that I am presenting to you, I created a subclass for the button, and I want to process all events through it and not through the mainwindproc of the parent window. The original idea was to use WM_DRAWITEM or WM_NOTIFY, but these messages are sent to the parent window and not to the button.

Returning to the problem, I want the color of the button to change when hover is detected, so I used the isHover variable to control the color that will be chosen in WM_PAINT.

However, although the value of isHover is changed as it should in WM_MOUSEHOVER and WM_MOUSELEAVE, this is not done in WM_PAINT, and you can notice this through the texts printed on the console.

I don't know what the problem is, and I hope you can help me solve it.

Note: I am ready to try any suggestion if it leads me to the desired result, which is to perform all the actions related to the button outside the mainwindowproc as much as possible.

#define UNICODE
#include <windows.h>
#include <CommCtrl.h>
#include <windowsx.h>
#include <iostream>
LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK ButtonWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{

    
    const wchar_t g_szClassName[] = L"myWindowClass";
    WNDCLASSEX wc = {0};
    HWND hwnd;
    MSG Msg;

    
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = 0;
    wc.lpfnWndProc = MainWndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));
    wc.lpszMenuName = NULL;
    wc.lpszClassName = g_szClassName;
    wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    
    if (!RegisterClassEx(&wc))
    {
        MessageBox(NULL, L"فشل تسجيل فئة النافذة", L"خطأ", MB_OK);
        return 1;
    }

    
    hwnd = CreateWindowEx(0, L"MyWindowClass", L"نافذة جديدة",
                          WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                          500, 350, NULL, NULL, wc.hInstance, NULL);

    if (hwnd == NULL)
    {
        MessageBox(NULL, L"فشل إنشاء النافذة", L"خطأ", MB_OK);
        return 1;
    }
    HWND hButton1 = CreateWindowEx(0, L"BUTTON", L"button1", WS_VISIBLE | WS_CHILD | BS_FLAT,
                                   200, 200, 100, 40, hwnd, (HMENU)101, NULL, NULL);

    if (hButton1 == NULL)
    {
        MessageBox(NULL, L"فشل إنشاء الزر", L"خطأ", MB_OK);
        return 1;
    }

    SetWindowSubclass(hButton1, &ButtonWndProc, 1, 0);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    
    while (Msg.message != WM_QUIT)
    {
        if (GetMessage(&Msg, NULL, 0, 0))
        {
            TranslateMessage(&Msg);
            DispatchMessage(&Msg);
        }
    }
    return 0;
}

LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {

    case WM_DESTROY:
    {
        PostQuitMessage(0);
        break;
    }
    case WM_CLOSE:
    {
        
        DestroyWindow(hwnd);
        break;
    }

    default:
      
        return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }

    return 0;
}

LRESULT CALLBACK ButtonWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{

    bool isTracking;
    bool isHover;
    switch (uMsg)
    {

    case WM_NCDESTROY:
        RemoveWindowSubclass(hWnd, &ButtonWndProc, uIdSubclass);
        break;

    case WM_MOUSEMOVE:
    {
        std::cout << "WM_MOUSEMOVE RECEIVED " << std::endl;        
        std::cout << "hovered =  " << isHover << std::endl; // to check the value of isHover
        if (!isTracking)
        {
            TRACKMOUSEEVENT tme = {sizeof(TRACKMOUSEEVENT)};
            tme.dwFlags = TME_LEAVE | TME_HOVER;
            tme.hwndTrack = hWnd;  // hWnd;
            tme.dwHoverTime = 10; // milliseconds
            isTracking = TrackMouseEvent(&tme);
        }

        return 0 ;
    }
    case WM_MOUSELEAVE:
    {
        std::cout << "(" << GET_X_LPARAM(lParam) << ", " << GET_Y_LPARAM(lParam) << ")" << std::endl;
        isTracking = FALSE;
        isHover = FALSE;
        std::cout << "WM_MOUSELEAVE RECEIVED " << std::endl; 
        std::cout << "hovered =  " << isHover << std::endl; // to check the value of isHover
        InvalidateRect(hWnd, NULL, true);
        UpdateWindow(hWnd);

        return 0;
    }
    case WM_MOUSEHOVER:
    {
        std::cout << "(" << GET_X_LPARAM(lParam) << ", " << GET_Y_LPARAM(lParam) << ")" << std::endl;
        isTracking = TRUE;
        isHover = TRUE;
        std::cout << "WM_MOUSEHOVER RECEIVED " << std::endl;        
        std::cout << "hovered =  " << isHover << std::endl; // to check the value of isHover
        InvalidateRect(hWnd, NULL, true);
        UpdateWindow(hWnd);
        return 0;
    }
    case WM_PAINT:
    {

        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        HBRUSH brush;
        std::cout << "WM_PAINT RECEIVED" << std::endl;
        std::cout << "paint hovered =  " << isHover << std::endl; // to check the value of isHover when receiving WM_PAINT message

        if (isHover)
        {
            brush = CreateSolidBrush(RGB(255, 0, 0));
        }
        else
        {
            brush = CreateSolidBrush(RGB(0, 255, 0));
        }

        FillRect(hdc, &ps.rcPaint, brush);
        DeleteObject(brush);
        EndPaint(hWnd, &ps);

        break;
    }
    }

    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

Solution

  • Your variables are local to ButtonWndProc(), so they will be lost and reset every time ButtonWndProc() is called. You need to move the variables out of ButtonWndProc() so they can persist between messages.

    Create a class/struct object instance to hold the variables, and then pass a pointer to that object to SetWindowSubclass() so the pointer can be passed into ButtonWndProc() for each message received. This is especially important if you ever plan to use the same ButtonWndProc() for more than 1 button at a time. You will need a separate object to hold the variables of each button.

    On a side note: when you update the value of your variables, you do not need to call UpdateWindow() to force an immediate repaint. Calling InvalidateRect() by itself will suffice to signal to the OS that the button needs to be repainted. A new WM_PAINT message will happen automatically when the OS is actually ready to repaint the button.

    For example, try something like this:

    #define UNICODE
    #include <windows.h>
    #include <CommCtrl.h>
    #include <windowsx.h>
    #include <iostream>
    
    LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
    LRESULT CALLBACK ButtonWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
    
    struct ButtonVars
    {
        bool isTracking;
        bool isHover; 
    };
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
    {    
        const wchar_t g_szClassName[] = L"myWindowClass";
        WNDCLASSEX wc = {0};
        HWND hwnd;
        MSG Msg;
        
        wc.cbSize = sizeof(WNDCLASSEX);
        wc.style = 0;
        wc.lpfnWndProc = MainWndProc;
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = hInstance;
        wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wc.hCursor = LoadCursor(NULL, IDC_ARROW);
        wc.hbrBackground = CreateSolidBrush(RGB(30, 30, 30));
        wc.lpszMenuName = NULL;
        wc.lpszClassName = g_szClassName;
        wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
        
        if (!RegisterClassEx(&wc))
        {
            MessageBox(NULL, L"فشل تسجيل فئة النافذة", L"خطأ", MB_OK);
            return 1;
        }
        
        hwnd = CreateWindowEx(0, L"MyWindowClass", L"نافذة جديدة",
                              WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                              500, 350, NULL, NULL, wc.hInstance, NULL);
    
        if (hwnd == NULL)
        {
            MessageBox(NULL, L"فشل إنشاء النافذة", L"خطأ", MB_OK);
            return 1;
        }
    
        ShowWindow(hwnd, nCmdShow);
        UpdateWindow(hwnd);
        
        while (Msg.message != WM_QUIT)
        {
            if (GetMessage(&Msg, NULL, 0, 0))
            {
                TranslateMessage(&Msg);
                DispatchMessage(&Msg);
            }
        }
        return 0;
    }
    
    LRESULT CALLBACK MainWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        switch (uMsg)
        {
            case WM_CREATE:
            {
                HWND hButton = CreateWindowEx(0, L"BUTTON", L"button1", WS_VISIBLE | WS_CHILD | BS_FLAT, 200, 200, 100, 40, hwnd, (HMENU)101, NULL, NULL);
                if (hButton == NULL)
                    return -1;
    
                ButtonVars *vars = new ButtonVars;
                vars->isTracking = false;
                vars->isHover = false;
    
                if (!SetWindowSubclass(hButton, &ButtonWndProc, 1, reinterpret_cast<DWORD_PTR>(vars)))
                {
                    delete vars;
                    return -1;
                } 
    
                break;
            }
    
            case WM_DESTROY:
            {
                PostQuitMessage(0);
                break;
            }
    
            case WM_CLOSE:
            {        
                DestroyWindow(hwnd);
                break;
            }
    
            default:      
                return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
    
        return 0;
    }
    
    LRESULT CALLBACK ButtonWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
    {
        ButtonVars *vars = reinterpret_cast<ButtonVars*>(dwRefData);
    
        switch (uMsg)
        {
            case WM_NCDESTROY:
            {
                RemoveWindowSubclass(hWnd, &ButtonWndProc, uIdSubclass);
                delete vars;
                return DefSubclassProc(hWnd, uMsg, wParam, lParam);
            }
    
            case WM_MOUSEMOVE:
            {
                std::cout << "WM_MOUSEMOVE RECEIVED " << std::endl;        
                std::cout << "hovered =  " << vars->isHover << std::endl; // to check the value of isHover
                if (!vars->isTracking)
                {
                    TRACKMOUSEEVENT tme = {sizeof(TRACKMOUSEEVENT)};
                    tme.dwFlags = TME_LEAVE | TME_HOVER;
                    tme.hwndTrack = hWnd;  // hWnd;
                    tme.dwHoverTime = 10; // milliseconds
                    vars->isTracking = TrackMouseEvent(&tme);
                }
    
                break;
            }
    
            case WM_MOUSELEAVE:
            {
                std::cout << "(" << GET_X_LPARAM(lParam) << ", " << GET_Y_LPARAM(lParam) << ")" << std::endl;
                vars->isTracking = false;
                vars->isHover = false;
                std::cout << "WM_MOUSELEAVE RECEIVED " << std::endl; 
                std::cout << "hovered =  " << vars->isHover << std::endl; // to check the value of isHover
                InvalidateRect(hWnd, NULL, true);
                break;
            }
    
            case WM_MOUSEHOVER:
            {
                std::cout << "(" << GET_X_LPARAM(lParam) << ", " << GET_Y_LPARAM(lParam) << ")" << std::endl;
                vars->isTracking = true;
                vars->isHover = true;
                std::cout << "WM_MOUSEHOVER RECEIVED " << std::endl;        
                std::cout << "hovered =  " << vars->isHover << std::endl; // to check the value of isHover
                InvalidateRect(hWnd, NULL, true);
                break;
            }
    
            case WM_PAINT:
            {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hWnd, &ps);
                HBRUSH brush;
                std::cout << "WM_PAINT RECEIVED" << std::endl;
                std::cout << "paint hovered =  " << vars->isHover << std::endl; // to check the value of isHover when receiving WM_PAINT message
    
                if (vars->isHover)
                {
                    brush = CreateSolidBrush(RGB(255, 0, 0));
                }
                else
                {
                    brush = CreateSolidBrush(RGB(0, 255, 0));
                }
    
                FillRect(hdc, &ps.rcPaint, brush);
                DeleteObject(brush);
                EndPaint(hWnd, &ps);
    
                break;
            }
    
            default:
                return DefSubclassProc(hWnd, uMsg, wParam, lParam);
        }
    
        return 0;
    }
    

    Regarding the WM_DRAWITEM/WM_NOTIFY issue, that message carries the HWND of the child control who issued it. You can have your parent WndProc reflect the message back to the original child control if you want the child WndProc to handle its own notifications. Same with WM_COMMAND, too.