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

Win11下SetROP2(R2_XORPEN)橡皮筋线条闪烁问题求助

Win11下GDI XOR橡皮筋线条闪烁问题分析与修复

问题描述

我开发了一个采用Win32 GDI经典XOR模式(SetROP2(R2_XORPEN)方法)绘制橡皮筋线条的程序,在XP/Vista/Win7/Win8/Win10系统中运行正常,但升级到Win11后,橡皮筋线条出现了之前从未有过的闪烁问题。这是不是Win11的Bug?该怎么修复?

注:我知道处理WM_ERASEBKGND消息和双缓冲能避免窗口闪烁,但这次只有橡皮筋线条闪烁,不是整个窗口闪烁。

复现代码如下:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <tchar.h>

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

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                       _In_opt_ HINSTANCE hPrevInstance,
                       _In_ LPWSTR    lpCmdLine,
                       _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    WNDCLASSEX wcex;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = MyWndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = NULL;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = _T("MyRubberBandLine");
    wcex.hIconSm = NULL;
    RegisterClassEx(&wcex);

    HWND hWnd = CreateWindow(_T("MyRubberBandLine"), _T("RubberBandLine"), WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    MSG msg;
    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int) msg.wParam;
}

//
//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  PURPOSE: Processes messages for the main window.
//
//  WM_COMMAND  - process the application menu
//  WM_PAINT    - Paint the main window
//  WM_DESTROY  - post a quit message and return
//
//
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);

            RECT rect;
            GetClientRect(hWnd, &rect);
            COLORREF clrbak = SetBkColor(hdc, RGB(255, 0, 0));
            ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rect, NULL, 0, NULL);

            int x = (rect.right - rect.left ) / 8;
            int y = (rect.bottom - rect.top) / 8;
            rect.left += x; rect.right -= x;
            rect.top += y; rect.bottom -= y;
            SetBkColor(hdc, RGB(0, 255, 0));
            ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rect, NULL, 0, NULL);

            rect.left += x; rect.right -= x;
            rect.top += y; rect.bottom -= y;
            SetBkColor(hdc, RGB(0, 0, 255));
            ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rect, NULL, 0, NULL);

            rect.left += x; rect.right -= x;
            rect.top += y; rect.bottom -= y;
            SetBkColor(hdc, RGB(0, 0, 0));
            ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rect, NULL, 0, NULL);

            SetBkColor(hdc, clrbak);
            EndPaint(hWnd, &ps);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    case WM_MOUSEMOVE:
        {
           static POINT ptPrev1{ -1,-1 };
           static POINT ptPrev2{ -1,-1 };

           HDC hdc = GetDC(hWnd);
           int modebak = SetROP2(hdc, R2_XORPEN);

           HPEN hPen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
           HGDIOBJ hOld = SelectObject(hdc, hPen);

           // erase prev line
           if (ptPrev1.x != -1 && ptPrev2.x != -1)
           {
               MoveToEx(hdc, ptPrev1.x, ptPrev1.y, NULL);
               LineTo(hdc, ptPrev2.x, ptPrev2.y);
               ptPrev1.x = ptPrev1.y = ptPrev2.x = ptPrev2.y = -1;
           }

           // draw new line
           RECT rect;
           GetClientRect(hWnd, &rect);
           POINT ptCenter{ (rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2 };
           POINT ptMouse{ LOWORD(lParam), HIWORD(lParam) };
           if ((ptMouse.x != ptCenter.x) || (ptMouse.y != ptCenter.y))
           {
               MoveToEx(hdc, ptCenter.x, ptCenter.y, NULL);
               LineTo(hdc, ptMouse.x, ptMouse.y);

               ptPrev1 = ptCenter;
               ptPrev2 = ptMouse;
           }

           SelectObject(hdc, hOld);
           SetROP2(hdc, modebak);
           ReleaseDC(hWnd, hdc);
           DeleteObject(hPen);
        }
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

问题原因分析

这并非Win11的Bug,而是Win11对GDI绘制的底层渲染机制做了优化调整,尤其是在**桌面合成(DWM)**的处理逻辑上。传统XOR绘制依赖直接在屏幕DC上进行两次异或操作来擦除旧线条,但Win11的DWM可能会对屏幕DC的绘制做额外的缓存或同步处理,导致两次XOR操作的像素状态不一致,进而出现闪烁。

修复方案

针对这个问题,有两种可靠的修复方式:

方案1:使用内存DC双缓冲绘制橡皮筋

不再直接在屏幕DC上做XOR操作,而是先在内存DC中绘制背景和橡皮筋,再一次性拷贝到屏幕,避免直接操作屏幕DC带来的同步问题。修改WM_MOUSEMOVE中的绘制逻辑:

case WM_MOUSEMOVE:
{
    static POINT ptPrev1{ -1,-1 };
    static POINT ptPrev2{ -1,-1 };

    HDC hdcScreen = GetDC(hWnd);
    HDC hdcMem = CreateCompatibleDC(hdcScreen);
    RECT clientRect;
    GetClientRect(hWnd, &clientRect);
    HBITMAP hBmpMem = CreateCompatibleBitmap(hdcScreen, clientRect.right, clientRect.bottom);
    HGDIOBJ hOldBmp = SelectObject(hdcMem, hBmpMem);

    // 先把当前窗口内容拷贝到内存DC
    BitBlt(hdcMem, 0, 0, clientRect.right, clientRect.bottom, hdcScreen, 0, 0, SRCCOPY);

    int modebak = SetROP2(hdcMem, R2_XORPEN);
    HPEN hPen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
    HGDIOBJ hOldPen = SelectObject(hdcMem, hPen);

    // 绘制新的橡皮筋线条
    POINT ptCenter{ (clientRect.left + clientRect.right)/2, (clientRect.top + clientRect.bottom)/2 };
    POINT ptMouse{ LOWORD(lParam), HIWORD(lParam) };
    if ((ptMouse.x != ptCenter.x) || (ptMouse.y != ptCenter.y))
    {
        MoveToEx(hdcMem, ptCenter.x, ptCenter.y, NULL);
        LineTo(hdcMem, ptMouse.x, ptMouse.y);
    }

    // 将内存DC内容拷贝到屏幕
    BitBlt(hdcScreen, 0, 0, clientRect.right, clientRect.bottom, hdcMem, 0, 0, SRCCOPY);

    // 清理资源
    SelectObject(hdcMem, hOldPen);
    SetROP2(hdcMem, modebak);
    SelectObject(hdcMem, hOldBmp);
    DeleteObject(hBmpMem);
    DeleteDC(hdcMem);
    DeleteObject(hPen);
    ReleaseDC(hWnd, hdcScreen);

    // 更新之前的坐标(这里不再需要擦除,因为每次都是重绘背景+新线条)
    ptPrev1 = ptCenter;
    ptPrev2 = ptMouse;
}
break;

方案2:使用SetWindowPos禁用窗口的重绘优化

通过设置窗口的SWP_NOCOPYBITS标志,强制Win11不对窗口DC做缓存处理,让XOR操作的两次绘制能正确同步。可以在窗口创建后添加:

SetWindowPos(hWnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOCOPYBITS);

或者在WM_MOUSEMOVE中每次获取DC前临时设置,但前者更高效。

额外说明

  • 方案1虽然代码稍复杂,但兼容性更好,且能避免其他潜在的渲染问题;
  • 方案2更轻量,但依赖系统行为,未来Win11更新可能会再次改变机制。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 11:01:36