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
相关产品推荐
相关产品推荐

