Home > Software design >  WM_PAINT: Manage hovering on an item of a tab control
WM_PAINT: Manage hovering on an item of a tab control

Time:07-12

So I'm overriding the WM_PAINT message of a tab control to add a close button, and to make a consistent look with the other controls of my application, I need to highlight the currently hovered item. The problem is the repainting does not work as expected, and I don't know how to manage the hovering state. The hovered item doesn't know when the mouse cursor has left it.

Here is a piece of code:

    switch (msg) {
    case WM_PAINT:
    {
        auto style = GetWindowLongPtr(m_self, GWL_STYLE);
        // Let the system process the WM_PAINT message if the Owner Draw Fixed style is set.
        if (style & TCS_OWNERDRAWFIXED) {
            break;
        }
        PAINTSTRUCT ps{};
        HDC hdc = BeginPaint(m_self, &ps);
        RECT rc{};
        GetClientRect(m_self, &rc);

        // Paint the background
        HBRUSH bkgnd_brush = GetSysColorBrush(COLOR_BTNFACE);
        FillRect(hdc, &rc, bkgnd_brush);
        DeleteObject(bkgnd_brush);

        // Get infos about the control
        int tabsCount = TabCtrl_GetItemCount(m_self);
        int tabsSelect = TabCtrl_GetCurSel(m_self);
        int ctl_identifier = GetDlgCtrlID(m_self);

        // Draw each items
        for (int i = 0; i < tabsCount;   i) {
            DRAWITEMSTRUCT dis{ ODT_TAB, ctl_identifier, static_cast<UINT>(i), 
            ODA_DRAWENTIRE, 0, m_self, hdc, RECT{}, 0 };
            TabCtrl_GetItemRect(m_self, i, &dis.rcItem);
            const UINT buffSize = 128;
            wchar_t buff[buffSize];
            TCITEM ti{};
            ti.mask = TCIF_TEXT;
            ti.pszText = buff;
            ti.cchTextMax = buffSize;
            this->Notify<int, LPTCITEM>(TCM_GETITEM, i, &ti); // Template class == SendMessageW

            // Get item state
            bool isHover = false;
            HBRUSH hBrush = NULL;
            POINT pt{};
            GetCursorPos(&pt);
            ScreenToClient(m_self, &pt);
            // Item's bounds
            if ((pt.x >= dis.rcItem.left && pt.x <= dis.rcItem.right) && (pt.y >= dis.rcItem.top && pt.y <= dis.rcItem.bottom)) {
                m_hoveredTab = dis.rcItem;
                isHover = true;
            }

            // Paint item according to its current state
            hBrush = CreateSolidBrush(
            (i == tabsSelect) ? 
            RGB(255, 131, 10) : isHover ? 
            RGB(255, 10, 73) : RGB(102, 10, 255));
            
            FillRect(hdc, &dis.rcItem, hBrush);
            DeleteObject(hBrush);
            
            // Draw Text
            SetBkMode(hdc, TRANSPARENT);
            SetTextColor(hdc, RGB(0, 0, 0));
            DrawTextW(hdc, buff, lstrlen(buff), &dis.rcItem, DT_SINGLELINE | DT_LEFT | DT_VCENTER);
        }
        EndPaint(m_self, &ps);
        return 0;
    }

    // MOUSE EVENTS
    case WM_MOUSEMOVE:
    {
        if (m_mouseTracking == FALSE) {
            TRACKMOUSEEVENT trackMouseStruct{};
            trackMouseStruct.cbSize = sizeof(trackMouseStruct);
            trackMouseStruct.dwFlags = TME_HOVER | TME_LEAVE;
            trackMouseStruct.hwndTrack = m_self;
            trackMouseStruct.dwHoverTime = 1; // Shorter hover time to instantly hover a tab item
            m_mouseTracking = TrackMouseEvent(&trackMouseStruct);
        }
        break;
    }

    case WM_MOUSEHOVER:
    {
        m_lostFocus = false;
        break;
    }

    case WM_MOUSELEAVE:
    {
        m_mouseTracking = FALSE;
        m_lostFocus = true;
        break;
    }

  . . .

CodePudding user response:

TrackMouseEvent detects hovering and leaving a window. The tab control is a single window that shows multiple tabs. If the cursor is hovering on one tab and is then moved to another tab (or to dead space in the tab control window), the TrackMouseEvent technique will not notify you.

Also, TrackMouseEvent could be interfering with the mouse tracking the tab control tries to do itself. Unfortunately, subclassing controls to modify their behavior usually requires knowing details of their implementation. For example, if you hadn't replaced the handling of WM_MOUSEMOVE, the tab control would probably do its own mouse tracking there in order to decide when to show its tooltip.

I think your best bet is to let the control process its messages as designed and to use the owner-draw mechanism to customize its appearance.

CodePudding user response:

I finally managed to get something closer to the original control (even if there's a little bit of flicker, but it's pretty evident because the code below is just a "test" to understand how the tab control works.)

LRESULT TabsWindow::HandleMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    // Track the mouse cursor to check if it has hit a tab.
    case WM_MOUSEMOVE:
    {
        if (_enableMouseTracking == FALSE) {
            TRACKMOUSEEVENT trackMouseStruct{};
            trackMouseStruct.cbSize = sizeof(trackMouseStruct);
            trackMouseStruct.dwFlags = TME_LEAVE;
            trackMouseStruct.hwndTrack = m_self;
            _enableMouseTracking = TrackMouseEvent(&trackMouseStruct);
        }
        _prevHoverTabIndex = _hoverTabIndex;
        POINT coordinates{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
        _hoverTabIndex = this->GetTabIndexFrom(coordinates);
        if (_hoverTabIndex != _prevHoverTabIndex) {
            // We need to loop over tabs as we don't know which tab has the 
            // highest height, and of course, the width of each tab can vary 
            // depending on many factors such as the text width (Assuming the 
            // TCS_FIXEDWIDTH style was not set, but it'll work too...)
            int count = this->Notify(TCM_GETITEMCOUNT, 0, 0);
            RECT rc{ 0, 0, 0, 0 };
            for (int i = 0; i != count;   i) {
                RECT currItm{};
                this->Notify(TCM_GETITEMRECT, i, &currItm);
                UnionRect(&rc, &currItm, &rc);
                _tabBarRc = rc;
            }
            InvalidateRect(m_self, &rc, FALSE);
            UpdateWindow(m_self);
        }
    }
    return 0;

    case WM_MOUSELEAVE: // The tab bar must be redrawn
    {
        _hoverTabIndex = -1;
        InvalidateRect(m_self, &_tabBarRc, FALSE);
        UpdateWindow(m_self);
        _enableMouseTracking = FALSE;
    }
    return 0;

    case WM_ERASEBKGND:
    {
        return TRUE;
    }

    case WM_PAINT:
    {
        auto style = GetWindowLongPtr(m_self, GWL_STYLE);
        if ((style & TCS_OWNERDRAWFIXED)) {
            break;
        }
        PAINTSTRUCT ps{};
        HDC hdc = BeginPaint(m_self, &ps);
        // Total Size
        RECT rc{};
        GetClientRect(m_self, &rc);

        // Paint the background
        HBRUSH bkgnd = GetSysColorBrush(COLOR_BTNFACE);
        FillRect(hdc, &rc, bkgnd);

        // Get some infos about tabs
        int tabsCount = TabCtrl_GetItemCount(m_self);
        int tabsSelect = TabCtrl_GetCurSel(m_self);
        int ctl_identifier = GetDlgCtrlID(m_self);

        for (int i = 0; i < tabsCount;   i) {
            DRAWITEMSTRUCT dis{ ODT_TAB, ctl_identifier, static_cast<UINT>(i), ODA_DRAWENTIRE, 0, m_self, hdc, RECT{}, 0 };
            TabCtrl_GetItemRect(m_self, i, &dis.rcItem);
            RECT intersect{}; // Draw the relevant items that needs to be redrawn
            if (IntersectRect(&intersect, &ps.rcPaint, &dis.rcItem)) {
                HBRUSH hBrush = CreateSolidBrush
                (
                    (i == tabsSelect) ? RGB(255, 0, 255) : (i == _hoverTabIndex) ? RGB(0, 0, 255) : RGB(0, 255, 255)
                );
                FillRect(hdc, &dis.rcItem, hBrush);
                DeleteObject(hBrush);
            }
        }
        EndPaint(m_self, &ps);
        return 0;
    }
    return DefSubclassProc(m_self, msg, lParam, wParam);
}

    // Helpers to get the current hovered tab
    int TabsWindow::GetTabIndexFrom(POINT& pt)
    {
        TCHITTESTINFO hit_test_info{};
        hit_test_info.pt = pt;
        return static_cast<int>(this->Notify(TCM_HITTEST, 0, &hit_test_info));
    }

    bool TabsWindow::GetItemRectFrom(int index, RECT& out)
    {
        if (index < -1) {
            return false;
        }
        return this->Notify(TCM_GETITEMRECT, index, &out);
    }

Explanations


The Tab Control, fortunately, provides ways to get the hovered item. First, TCM_HITTEST allows us to get the current index based on the passed RECT. TCM_GETITEMRECT does the opposite. We need to track the mouse cursor to detect whether it is on a tab or not. The tab control does not seem to use WM_MOUSEHOVER at all, but it effectively uses WM_MOUSELEAVE as we can see on Spy with a standard tab control.

Firstly, I tried to "disable" the WM_MOUSEMOVE message, and the tab control was not responsible. However, when WM_MOUSELEAVE is disabled, it works as expected but does not update the window if the mouse cursor leaves the tab control (so, the focus effect sticks on the previously hovered tab, which is no longer). Finally, everything depends on the WM_MOUSEMOVE event, and the WM_MOUSELEAVE message is not so big because it only handles the "focus exit state" of the tab control but is necessary to exit the hovered state of a tab. Again, the above code is just a skeleton filled with problems, but it works and reproduces the same behavior as the stock control.

  • Related