You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

重写Tab控件WM_PAINT时如何正确管理标签项悬停状态?

问题根因

悬停状态更新异常是4个逻辑漏洞共同导致的:

  • 状态变化后未触发重绘:鼠标移动、离开导致悬停项改变时,没有主动通知控件重绘对应区域,系统不会自动发送WM_PAINT消息,界面会一直停留在旧状态。
  • 悬停跟踪粒度过粗:仅跟踪鼠标是否在整个标签控件范围内,没有处理鼠标在不同标签项之间移动的场景——鼠标从标签A移到标签B时不会触发WM_MOUSELEAVE,旧标签的高亮无法被清除。
  • 悬停判断逻辑有误:鼠标离开控件后,仍然读取全局光标坐标做命中测试,只要光标坐标刚好落在某个标签矩形内(哪怕光标已经在其他窗口上),就会误判为悬停状态。
  • 存在GDI资源隐患:GetSysColorBrush返回的是系统全局画刷,不需要调用DeleteObject释放,强行删除会导致系统GDI状态异常。
修复方案

第一步:补充状态成员变量

替换原来存矩形的m_hoveredTab,改用索引记录悬停项,状态更稳定:

int m_hoveredTabIndex = -1; // -1 表示当前无悬停标签
bool m_mouseTracking = false;
bool m_lostFocus = true;

第二步:封装命中测试函数

单独实现标签命中逻辑,避免在多个消息分支重复写判断代码:

int HitTestTab() {
    POINT pt;
    GetCursorPos(&pt);
    ScreenToClient(m_self, &pt);
    
    int tabCount = TabCtrl_GetItemCount(m_self);
    for (int i = 0; i < tabCount; i++) {
        RECT rc;
        TabCtrl_GetItemRect(m_self, i, &rc);
        if (PtInRect(&rc, pt)) {
            return i;
        }
    }
    return -1;
}

第三步:修正鼠标消息处理

每次鼠标状态变化时更新悬停索引,仅重绘状态发生变化的区域,减少界面闪烁:

case WM_MOUSEMOVE:
{
    if (!m_mouseTracking) {
        TRACKMOUSEEVENT tme{};
        tme.cbSize = sizeof(tme);
        tme.dwFlags = TME_HOVER | TME_LEAVE;
        tme.hwndTrack = m_self;
        tme.dwHoverTime = 1;
        m_mouseTracking = TrackMouseEvent(&tme);
    }

    // 对比当前命中标签和已记录的悬停项
    int curHit = HitTestTab();
    if (curHit != m_hoveredTabIndex) {
        // 重绘旧悬停项,清除高亮
        if (m_hoveredTabIndex != -1) {
            RECT oldRc;
            TabCtrl_GetItemRect(m_self, m_hoveredTabIndex, &oldRc);
            InvalidateRect(m_self, &oldRc, TRUE);
        }
        // 重绘新悬停项,显示高亮
        if (curHit != -1) {
            RECT newRc;
            TabCtrl_GetItemRect(m_self, curHit, &newRc);
            InvalidateRect(m_self, &newRc, TRUE);
        }
        m_hoveredTabIndex = curHit;
    }
    break;
}

case WM_MOUSELEAVE:
{
    m_mouseTracking = false;
    m_lostFocus = true;
    // 鼠标离开整个控件,清除悬停状态并重绘旧项
    if (m_hoveredTabIndex != -1) {
        RECT oldRc;
        TabCtrl_GetItemRect(m_self, m_hoveredTabIndex, &oldRc);
        InvalidateRect(m_self, &oldRc, TRUE);
        m_hoveredTabIndex = -1;
    }
    break;
}

case WM_MOUSEHOVER:
{
    m_lostFocus = false;
    // 触发一次命中测试同步状态
    int curHit = HitTestTab();
    if (curHit != m_hoveredTabIndex) {
        if (m_hoveredTabIndex != -1) {
            RECT oldRc;
            TabCtrl_GetItemRect(m_self, m_hoveredTabIndex, &oldRc);
            InvalidateRect(m_self, &oldRc, TRUE);
        }
        m_hoveredTabIndex = curHit;
        if (curHit != -1) {
            RECT newRc;
            TabCtrl_GetItemRect(m_self, curHit, &newRc);
            InvalidateRect(m_self, &newRc, TRUE);
        }
    }
    break;
}

第四步:修正WM_PAINT逻辑

删除绘制流程里临时计算悬停状态的代码,直接使用提前维护好的m_hoveredTabIndex判断状态,同时移除错误的系统画刷释放逻辑:

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

    // 绘制背景:系统画刷不需要手动释放
    HBRUSH bkgnd_brush = GetSysColorBrush(COLOR_BTNFACE);
    FillRect(hdc, &rc, bkgnd_brush);

    int tabsCount = TabCtrl_GetItemCount(m_self);
    int tabsSelect = TabCtrl_GetCurSel(m_self);

    for (int i = 0; i < tabsCount; ++i) {
        RECT itemRc;
        TabCtrl_GetItemRect(m_self, i, &itemRc);
        const UINT buffSize = 128;
        wchar_t buff[buffSize];
        TCITEM ti{};
        ti.mask = TCIF_TEXT;
        ti.pszText = buff;
        ti.cchTextMax = buffSize;
        SendMessageW(m_self, TCM_GETITEM, i, (LPARAM)&ti);

        // 直接用已维护的状态判断,不需要重复做命中测试
        bool isHover = (i == m_hoveredTabIndex) && !m_lostFocus;
        COLORREF brushColor;
        if (i == tabsSelect) {
            brushColor = RGB(255, 131, 10);
        } else if (isHover) {
            brushColor = RGB(255, 10, 73);
        } else {
            brushColor = RGB(102, 10, 255);
        }
        HBRUSH hBrush = CreateSolidBrush(brushColor);
        FillRect(hdc, &itemRc, hBrush);
        DeleteObject(hBrush);
        
        // 绘制标签文字
        SetBkMode(hdc, TRANSPARENT);
        SetTextColor(hdc, RGB(0, 0, 0));
        DrawTextW(hdc, buff, lstrlen(buff), &itemRc, DT_SINGLELINE | DT_LEFT | DT_VCENTER);
    }
    EndPaint(m_self, &ps);
    return 0;
}
额外优化建议

在切换选中标签、插入/删除标签、控件大小变化的场景下,主动调用一次HitTestTab更新m_hoveredTabIndex,可以避免标签布局变化后悬停状态错位。

内容的提问来源于stack exchange,提问作者Chadribleu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 02:15:28