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

基于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+尝试中的问题

尝试用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;
}

GDI+初始尝试效果

调整画笔宽度后,单个椭圆绘制正常,但椭圆重叠时,下方的椭圆会被背景色误擦除:

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;
}

GDI+调整后重叠问题效果

三、最优解决方案

方案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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 01:29:51