基于GDI+实现鼠标拖拽绘制椭圆及解决重叠擦除问题
用GDI+实现鼠标拖拽绘制可擦除椭圆的最优方案
一、GDI的参考实现
通过修改官方GDI代码,可实现鼠标拖拽绘制10像素粗的红色椭圆,核心是利用SetROP2(hdc, R2_NOTXORPEN)让椭圆绘制两次时自动擦除旧图形:
case WM_MOUSEMOVE: { if(wParam && MK_LBUTTON) { hdc = GetDC(hwnd); SetROP2(hdc, R2_NOTXORPEN); HGDIOBJ obj_pen = SelectObject(hdc, CreatePen(PS_SOLID, 10, RGB(255,0,0))); // 擦除之前的椭圆 if(!IsRectEmpty(&rcTarget)) { Ellipse(hdc, rcTarget.left, rcTarget.top, rcTarget.right, rcTarget.bottom); } // 更新当前椭圆的矩形范围 if((pt.x < (LONG) LOWORD(lParam)) && (pt.y > (LONG) HIWORD(lParam))) { SetRect(&rcTarget, pt.x, HIWORD(lParam), LOWORD(lParam), pt.y); } else if ((pt.x > (LONG) LOWORD(lParam)) && (pt.y > (LONG) HIWORD(lParam))) { SetRect(&rcTarget, LOWORD(lParam), HIWORD(lParam), pt.x, pt.y); } else if ((pt.x > (LONG) LOWORD(lParam)) && (pt.y < (LONG) HIWORD(lParam))) { SetRect(&rcTarget, LOWORD(lParam), pt.y, pt.x, HIWORD(lParam)); } else { SetRect(&rcTarget, pt.x, pt.y, LOWORD(lParam), HIWORD(lParam)); } // 绘制新椭圆 Ellipse(hdc, rcTarget.left, rcTarget.top, rcTarget.right, rcTarget.bottom); SelectObject(hdc, obj_pen); ReleaseDC(hwnd, hdc); } return 0; }

二、GDI+尝试中的问题
尝试用GDI+实现时,因无直接对应SetROP2的API,改用背景色画笔擦除旧椭圆,初始尝试效果不符合预期:
case WM_MOUSEMOVE: { if(wParam && MK_LBUTTON) { hdc = GetDC(hwnd); Gdiplus::Graphics graphics(hdc); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); // 尝试用背景色擦除旧椭圆 if(!IsRectEmpty(&rcTarget)) { Gdiplus::Pen pen(Gdiplus::Color(255, 236, 236, 236), 10); // 背景色 graphics.DrawEllipse(&pen, (int) rcTarget.left, (int) rcTarget.top, (int) rcTarget.right-(int)rcTarget.left, (int)rcTarget.bottom-(int)rcTarget.top); } // 更新当前椭圆范围 if((pt.x < (LONG) LOWORD(lParam)) && (pt.y > (LONG) HIWORD(lParam))) { SetRect(&rcTarget, pt.x, HIWORD(lParam), LOWORD(lParam), pt.y); } else if ((pt.x > (LONG) LOWORD(lParam)) && (pt.y > (LONG) HIWORD(lParam))) { SetRect(&rcTarget, LOWORD(lParam), HIWORD(lParam), pt.x, pt.y); } else if ((pt.x > (LONG) LOWORD(lParam)) && (pt.y < (LONG) HIWORD(lParam))) { SetRect(&rcTarget, LOWORD(lParam), pt.y, pt.x, HIWORD(lParam)); } else { SetRect(&rcTarget, pt.x, pt.y, LOWORD(lParam), HIWORD(lParam)); } // 绘制新椭圆 Gdiplus::Pen pen(Gdiplus::Color(255, 255, 0, 0), 10); // 红色画笔 graphics.DrawEllipse(&pen, (int) rcTarget.left, (int) rcTarget.top, (int) rcTarget.right-(int)rcTarget.left, (int)rcTarget.bottom-(int)rcTarget.top); ReleaseDC(hwnd, hdc); } return 0; }

调整画笔宽度后,单个椭圆绘制正常,但椭圆重叠时,下方的椭圆会被背景色误擦除:
case WM_MOUSEMOVE: { if(wParam && MK_LBUTTON) { hdc = GetDC(hwnd); Gdiplus::Graphics graphics(hdc); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); // 用更宽的背景色画笔擦除旧椭圆 if(!IsRectEmpty(&rcTarget)) { Gdiplus::Pen pen(Gdiplus::Color(255, 236, 236, 236), 12); // 背景色,加宽画笔 graphics.DrawEllipse(&pen, (int) rcTarget.left, (int) rcTarget.top, (int) rcTarget.right-(int)rcTarget.left, (int)rcTarget.bottom-(int)rcTarget.top); } // 更新当前椭圆范围 if((pt.x < (LONG) LOWORD(lParam)) && (pt.y > (LONG) HIWORD(lParam))) { SetRect(&rcTarget, pt.x, HIWORD(lParam), LOWORD(lParam), pt.y); } else if ((pt.x > (LONG) LOWORD(lParam)) && (pt.y > (LONG) HIWORD(lParam))) { SetRect(&rcTarget, LOWORD(lParam), HIWORD(lParam), pt.x, pt.y); } else if ((pt.x > (LONG) LOWORD(lParam)) && (pt.y < (LONG) HIWORD(lParam))) { SetRect(&rcTarget, LOWORD(lParam), pt.y, pt.x, HIWORD(lParam)); } else { SetRect(&rcTarget, pt.x, pt.y, LOWORD(lParam), HIWORD(lParam)); } // 绘制新椭圆 Gdiplus::Pen pen2(Gdiplus::Color(255, 255, 0, 0), 10); // 红色画笔 graphics.DrawEllipse(&pen2, (int) rcTarget.left, (int) rcTarget.top, (int) rcTarget.right-(int)rcTarget.left, (int)rcTarget.bottom-(int)rcTarget.top); ReleaseDC(hwnd, hdc); } return 0; }

三、最优解决方案
方案1:用GDI+混合模式模拟ROP2效果
GDI+虽无直接的SetROP2,但可通过CompositingModeXor模拟XOR绘图模式,实现绘制两次自动擦除的效果:
case WM_MOUSEMOVE: { if(wParam && MK_LBUTTON) { hdc = GetDC(hwnd); Gdiplus::Graphics graphics(hdc); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); // 设置XOR混合模式,模拟R2_NOTXORPEN的核心逻辑 graphics.SetCompositingMode(Gdiplus::CompositingModeXor); graphics.SetCompositingQuality(Gdiplus::CompositingQualityHighSpeed); Gdiplus::Pen pen(Gdiplus::Color(255, 255, 0, 0), 10); // 红色画笔 // 擦除旧椭圆(XOR模式下绘制同图形会还原背景) if(!IsRectEmpty(&rcTarget)) { graphics.DrawEllipse(&pen, rcTarget.left, rcTarget.top, rcTarget.right - rcTarget.left, rcTarget.bottom - rcTarget.top); } // 更新当前椭圆范围 POINT curr_pt = { LOWORD(lParam), HIWORD(lParam) }; SetRect(&rcTarget, min(pt.x, curr_pt.x), min(pt.y, curr_pt.y), max(pt.x, curr_pt.x), max(pt.y, curr_pt.y)); // 绘制新椭圆 graphics.DrawEllipse(&pen, rcTarget.left, rcTarget.top, rcTarget.right - rcTarget.left, rcTarget.bottom - rcTarget.top); ReleaseDC(hwnd, hdc); } return 0; }
注意:XOR模式下的颜色表现可能和GDI的
R2_NOTXORPEN略有差异,若需要完全一致的逻辑,可结合颜色反转操作,但此方案已能满足基础拖拽擦除需求。
方案2:双缓冲+全量重绘(推荐,解决重叠问题)
背景色擦除的核心问题是会破坏已绘制的图形,采用双缓冲+保存所有已完成图形的方式,从根源解决重叠误擦除问题,同时获得更好的抗锯齿效果:
步骤1:定义全局变量
#include <vector> #include <Gdiplus.h> using namespace Gdiplus; std::vector<RECT> g_completed_ellipses; // 保存所有已绘制完成的椭圆 RECT g_temp_ellipse = {0}; // 拖拽中的临时椭圆 POINT g_start_pt = {0}; // 拖拽起始点 bool g_is_dragging = false; // 是否处于拖拽状态 COLORREF g_bg_color = RGB(236,236,236); // 背景色
步骤2:处理鼠标消息
// WM_LBUTTONDOWN:开始拖拽 case WM_LBUTTONDOWN: g_start_pt.x = LOWORD(lParam); g_start_pt.y = HIWORD(lParam); SetRect(&g_temp_ellipse, g_start_pt.x, g_start_pt.y, g_start_pt.x, g_start_pt.y); g_is_dragging = true; return 0; // WM_MOUSEMOVE:更新临时椭圆并触发重绘 case WM_MOUSEMOVE: if(g_is_dragging && (wParam & MK_LBUTTON)) { POINT curr_pt = { LOWORD(lParam), HIWORD(lParam) }; SetRect(&g_temp_ellipse, min(g_start_pt.x, curr_pt.x), min(g_start_pt.y, curr_pt.y), max(g_start_pt.x, curr_pt.x), max(g_start_pt.y, curr_pt.y)); // 触发重绘临时椭圆区域,减少闪烁 InvalidateRect(hwnd, &g_temp_ellipse, TRUE); } return 0; // WM_LBUTTONUP:完成拖拽,保存椭圆 case WM_LBUTTONUP: if(g_is_dragging) { g_completed_ellipses.push_back(g_temp_ellipse); SetRectEmpty(&g_temp_ellipse); g_is_dragging = false; InvalidateRect(hwnd, NULL, TRUE); } return 0;
步骤3:WM_PAINT中双缓冲绘制
case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); // 创建双缓冲DC和位图 HDC mem_dc = CreateCompatibleDC(hdc); RECT client_rc; GetClientRect(hwnd, &client_rc); HBITMAP mem_bmp = CreateCompatibleBitmap(hdc, client_rc.right, client_rc.bottom); HGDIOBJ old_bmp = SelectObject(mem_dc, mem_bmp); // 绘制背景 Gdiplus::Graphics graphics(mem_dc); graphics.Clear(Gdiplus::Color(g_bg_color)); graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); // 绘制所有已完成的椭圆 Gdiplus::Pen pen(Gdiplus::Color(255,255,0,0), 10); for(const auto& rc : g_completed_ellipses) { graphics.DrawEllipse(&pen, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top); } // 绘制拖拽中的临时椭圆 if(g_is_dragging && !IsRectEmpty(&g_temp_ellipse)) { graphics.DrawEllipse(&pen, g_temp_ellipse.left, g_temp_ellipse.top, g_temp_ellipse.right - g_temp_ellipse.left, g_temp_ellipse.bottom - g_temp_ellipse.top); } // 将双缓冲内容贴到窗口DC BitBlt(hdc, 0, 0, client_rc.right, client_rc.bottom, mem_dc, 0, 0, SRCCOPY); // 释放资源 SelectObject(mem_dc, old_bmp); DeleteObject(mem_bmp); DeleteDC(mem_dc); EndPaint(hwnd, &ps); return 0; }
方案优势
- 彻底解决重叠椭圆的误擦除问题,所有图形都基于背景重新绘制,互不干扰
- 双缓冲消除拖拽时的闪烁,抗锯齿效果稳定
- 可轻松扩展更多功能(如撤销、修改椭圆属性等)
内容的提问来源于stack exchange,提问作者Niteya
相关产品推荐
相关产品推荐

