C# 橡皮筋矩形
在 C# 中实现橡皮矩形。
引言
我最近一直在试验 GDI+ 绘图方法,以便在 C# 中创建一个绘图控件。目标是构建一个绘图器,能够显示多个图形、自适应屏幕、放大/缩小、平移等。它必须非常容易地整合到现有项目中,实际上创建它的目的是完全取代一个用 LISP 编写的类似旧版绘图器。
我遇到的最大的问题之一是如何创建橡皮矩形。互联网上流传着一些想法,也有很多 hack,我最终决定采用一种我认为对我来说效果很好的方法。不幸的是,使用 Mono 的人不会像我这样轻松地让它工作,但对于大多数使用 Windows 的人来说,应该完全没有问题。
问题
想象一个屏幕,上面有很多颜色数据。您希望以非破坏性方式绘制可调整大小的框的轮廓。您首先想到的可能是绘制一个没有填充颜色的框,边框为 1 像素宽的黑线。但是,当您调整该框的大小时,您会发现您会在后面留下一串黑线。然后您可以尝试用白线擦除黑线,但这只会留下白线,仍然会有效地破坏您的绘图。
解决方案
使用 XOR 画笔绘制一条线,它将异或 (XOR) 所有像素颜色信息。因此,白色将变为黑色,黑色将变为白色,等等。然后,用第二个 XOR 画笔重新绘制您的框,以将您的绘图恢复到之前的状态。这是非破坏性绘图!不幸的是,在 GDI+ 中没有(据我所知)创建 XOR 画笔的方法。
我解决这个 XOR 画笔缺失的方法是使用 gdi32.dll,然后从 C# 调用 GDI 方法。我就是这样做的。
GDI32 类
我首先创建了一个名为 GDI32
的新类。然后,我使用 Interop 获取外部方法。
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool Ellipse(IntPtr hdc, int x1, int y1, int x2, int y2);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool Rectangle(IntPtr hdc, int X1, int Y1, int X2, int Y2);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr MoveToEx(IntPtr hdc, int x, int y, IntPtr lpPoint);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool LineTo(IntPtr hdc, int x, int y);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr CreatePen(PenStyles enPenStyle, int nWidth, int crColor);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr CreateSolidBrush(BrushStyles enBrushStyle, int crColor);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern bool DeleteObject(IntPtr hObject);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern IntPtr GetStockObject(int brStyle);
[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
private static extern int SetROP2(IntPtr hdc, int enDrawMode);
您在这里真正需要的只有 SetROP2
、SelectObject
、DeleteObject
、CreatePen
、Rectangle
和 CreateSolidBrush
。
然后,我创建了一个可以初始化画笔和画刷的方法,以及一个在它们使用后处理画笔和画刷的方法。需要注意的一点是,R2_XORPEN
是 XOR 画笔。如果您只想使用 XOR 画笔,可以传递 (int)7
作为等价物。
/// <summary>
/// Initializes the pen and brush objects. Stores the old pen
/// and brush so they can be recovered later.
/// </summary>
protected void InitPenAndBrush(Graphics g)
{
hdc = g.GetHdc();
gdiPen = CreatePen(penStyle, lineWidth, GetRGBFromColor(PenColor));
gdiBrush = CreateSolidBrush(brushStyle, GetRGBFromColor(fillColor));
if (PenColor == Color.Transparent) SetROP2(hdc, (int)RasterOps.R2_XORPEN);
oldPen = SelectObject(hdc, gdiPen);
oldBrush = SelectObject(hdc, gdiBrush);
}
/// <summary>
/// Reloads the old pen and brush.
/// Deletes the pen that was created by InitPenAndBrush(g).
/// Releases the handle to the device context
/// and then disposes of the Graphics object.
/// </summary>
protected void Dispose(Graphics g)
{
SelectObject(hdc, oldBrush);
SelectObject(hdc, oldPen);
DeleteObject(gdiPen);
DeleteObject(gdiBrush);
g.ReleaseHdc(hdc);
g.Dispose();
}
最后,我构建了用于绘制线条、矩形、椭圆等对象的方法。
/// <summary>
/// Draws a rectangle with the pen and brush
/// that have been set by the user. Uses gdi32->Rectangle
/// </summary>
/// <param name="g">Graphics object. You can use CreateGraphics().</param>
/// <param name="p1">First corner of rectangle.</param>
/// <param name="p2">Second corner of rectangle.</param>
public void DrawRectangle(Graphics g, Point p1, Point p2)
{
InitPenAndBrush(g);
Rectangle(hdc, p1.X, p1.Y, p2.X, p2.Y);
Dispose(g);
}
Using the Code
要使用代码,只需将其包含在您的项目中并创建一个新的 GDI32
对象。
GDI32 gdi = new GDI32();
然后,您可以使用 GDI32
类的任何公共方法。所有方法都已记录,并且很容易扩展。
gdi.DrawRectangle(CreateGraphics(), mouseDown, new Point(e.X, e.Y));
gdi.DrawRectangle(CreateGraphics(), _mouseDown, oldMouse);
oldMouse = new Point(e.X, e.Y);
上述代码将放在 OnMouseMove(MouseEventArgs e)
中,并将从 mouseDown
点绘制一个黑色矩形到由 MouseEventArgs
提供的新的点。
关注点
这里没有什么超级有趣或硬核的东西... 我只是发现这是一个令人讨厌的问题,互联网上有很多 hack 可用。我认为这是一种非常干净的方法,尽管它确实迫使您远离 GDI+。
橡皮矩形
以下是使用此类构建您自己的橡皮矩形所需的。您需要覆盖 OnMouseDown
、OnMouseMove
和 OnMouseUp
方法。当鼠标第一次按下时,您需要存储原始鼠标位置。
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button == MouseButtons.Left)
oldMouse = mouseDownAt = new Point(e.X, e.Y);
}
然后,您需要绘制两个矩形。一个绘制可见边框,一个擦除之前的边框。这发生在 OnMouseMove
中。
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.Button == MouseButtons.Left)
{
gdi.DrawRectangle(CreateGraphics(), mouseDownAt, new Point(e.X, e.Y));
gdi.DrawRectangle(CreateGraphics(), mouseDownAt, oldMouse);
oldMouse = new Point(e.X, e.Y);
}
}
最后,您需要在鼠标抬起时擦除最后一个矩形。
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (e.Button == MouseButtons.Left)
{
gdi.DrawRectangle(CreateGraphics(), mouseDownAt, oldMouse);
}
}
历史
- 2008 年 6 月 30 日 - 首次发布。
- 2008 年 6 月 30 日 - 添加了一个演示项目。