在GDI+中绘制橡皮筋线






4.40/5 (24投票s)
2001 年 12 月 12 日
5分钟阅读

293855

3779
在 GDI+ 中绘制自擦除线或橡皮筋线似乎是不可能的——本文将介绍如何实现。
引言
在 GDI+ 中绘制自擦除线或橡皮筋线从基点到当前鼠标点似乎是不可能的。这是过去曾多次被撰写过的著名且标准的 XOR 运算问题。在 Win32 API 中,可以通过调用 SetROP2
来设置 XOR 模式。GDI+ 中 SetROP2
函数的唯一等效项是 Graphics.CompositingMode
属性,它只支持两种模式:SourceOver
和 SourceCopy
。这两种模式都不能让你先绘制一条线,然后再“擦除”它。本文将解释你需要做什么来绘制和擦除线条。对其他形状执行相同的操作是同一技术的简单扩展。
背景
我从开始编程以来就一直对图形编程感兴趣,这已经远远超出了我手指的数量。几年前,我构建了一个例程库,建立在 GDI 之上,以允许我使用实际单位(如英寸和米)绘制对象。基本上,这些例程就像一个小型 CAD 引擎,隐藏了像素、第四象限坐标系、MoveTo
和 LineTo
函数以及使用椭圆绘制的圆形(这些椭圆绘制在由左上点到右下点界定的矩形区域内)的残酷怪异之处——这些愚蠢的 API 与现实世界对象几乎无关。
我开始使用 Visual Studio 7.0 Beta 2 版本将此代码移植到 C# 和 .NET。在那里有很多乐趣,这肯定会是另一篇文章的主题。(事实证明,使用模板和 STL 编写的 C++ 代码移植起来并不容易,但我坚持了下来。)在此过程中,我决定通过在对话框中添加按钮来设置绘图模式,以测试我正在构建的类:[直线]、[圆形]、[矩形] 等等。每个按钮在按下时都会实例化一个对象来执行“橡皮筋”操作,即从基点重复绘制对象到当前跟踪的鼠标位置,然后再擦除它,接着绘制下一个。
事实证明,这比我预期的要难得多。
橡皮筋线对象
RubberBandLine
类处理绘制和跟踪鼠标移动的所有鼠标事件。一个 RubberBandLine
类型的变量作为窗体的私有成员添加到窗体中,如下所示:
private RubberBandLine rubberBandLine = null;
在实例化窗体时,rubberBandLine
成员会像这样创建和初始化:
if (rubberBandLine == null)
{
rubberBandLine = new RubberBandLine();
rubberBandLine.Color = Color.Firebrick;
rubberBandLine.Width = 1;
rubberBandLine.DashStyle = DashStyle.Dash;
rubberBandLine.PictureBox = pictureBox1;
}
if (rubberBandLine != null)
{
rubberBandLine.InitializeComponent();
}
此代码创建一个新的 RubberBandLine
对象并将其分配给之前声明的成员,然后设置一些属性。最重要的属性是 PictureBox
属性。这对于实现此功能至关重要。PictureBox
有一个 Image
成员,我们使用它来捕获屏幕,以便正确恢复屏幕。鉴于创建和属性设置成功,将调用 InitializeComponent()
方法。此处复制了该方法。
/// <summary>
/// Instance variable for handling the mouse up event
/// for a single rubber-band object.
/// </summary>
protected System.Windows.Forms.MouseEventHandler mu = null;
/// <summary>
/// Instance variable for handling the mouse move event for
/// a single rubber-band object.
/// </summary>
protected System.Windows.Forms.MouseEventHandler mm = null;
/// <summary>
/// Instance variable for handling the mouse down event for
/// a single rubber-band object.
/// </summary>
protected System.Windows.Forms.MouseEventHandler md = null;
public void InitializeComponent()
{
this.mu = new System.Windows.Forms.MouseEventHandler(
this.pictureBox_MouseUp);
this.mm = new System.Windows.Forms.MouseEventHandler(
this.pictureBox_MouseMove);
this.md = new System.Windows.Forms.MouseEventHandler(
this.pictureBox_MouseDown);
this.pictureBox.MouseUp += this.mu;
this.pictureBox.MouseMove += this.mm;
this.pictureBox.MouseDown += this.md;
}
你可能会问,为什么 MouseEventHandlers
没有内联到 MouseUp += xxx
语句中?对于单个橡皮筋类,这是一个非常有效的问题,但一旦你将其扩展到橡皮筋任何其他东西,就必须有一种方法来关闭事件处理,否则你将得到所有处理程序同时运行。(事实上,在我微调此代码时,我无意中调用了两次 InitializeComponent()
,并看到了这种行为,因为 UnInitializeComponent()
方法只被调用了一次。此外,这表明事件的 +=
运算符可以使用 -=
运算符进行反向操作,这一点在我翻阅的所有 C# 和 .NET 书籍中都极为轻描淡写。我认为,需要注意的是,你必须缓存用于 +=
操作的原始对象,以便在撤销操作时使用。IMHO,这是一个小小的代价。)调用 UnInitializeComponent()
方法会暂停橡皮筋功能,直到再次调用 InitializeComponent()
。
public void UnInitializeComponent()
{
this.pictureBox.MouseUp -= this.mu;
this.pictureBox.MouseMove -= this.mm;
this.pictureBox.MouseDown -= this.md;
mu = null;
mm = null;
md = null;
}
在鼠标按下事件上,会捕获 PictureBox
当前 Image
的快照。这将用于创建 TextureBrush
。此 TextureBrush
将用于创建一个可以绘制背景图像的笔。无论你绘制什么形状,该形状都会留下快照时存在的像素。这是真正棘手的部分!这意味着,如果你在一张你最好的朋友的照片上画一条黑线,然后用这个 TextureBrush
笔画出完全相同的线,结果将如同你什么都没做一样。这正是我们想要的!
拍摄快照的基本算法来自 Mike Gold,他回复了我发送给他的关于如何实现此功能的电子邮件,现已转述如下。
Rectangle r = pictureBox.ClientRectangle;
Graphics g1 = pictureBox.CreateGraphics();
Image i = new Bitmap(r.Width, r.Height, g1);
pictureBox.Image = i;
Graphics g2 = Graphics.FromImage(i);
IntPtr dc1 = g1.GetHdc();
IntPtr dc2 = g2.GetHdc();
BitBlt(dc2, 0, 0, r.Width, r.Height, dc1,
0, 0, 0x00CC0020 /* dest = source*/);
g1.ReleaseHdc(dc1);
g2.ReleaseHdc(dc2);
有关此算法的更详细的注释,请参阅下面的源代码。
故事的其余部分
其余部分相当简单。编译项目,运行它,然后在 PictureBox
中拖动以绘制你想要绘制的最漂亮的线条!:-) 只要鼠标正在移动并且你没有松开鼠标,一条线将从鼠标按下事件发生点开始绘制,并且会使用此技术反复绘制和擦除线条,直到你释放鼠标按钮。重复一遍又一遍。为微软的部下再次被打败而欢呼——我们将会拥有我们的 XOR 运算,非常感谢!
我希望这对其他人来说像对我一样有趣。
橡皮筋线对象源代码
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Diagnostics;
namespace RubberBandLine_demo
{
/// <summary>
/// Summary description for RubberBandLine.
/// </summary>
public class RubberBandLine
{
public RubberBandLine()
{
Debug.WriteLine("Creating RubberBandLine");
}
public void InitializeComponent()
{
this.mu = new MouseEventHandler(
this.pictureBox_MouseUp);
this.mm = new MouseEventHandler(
this.pictureBox_MouseMove);
this.md = new MouseEventHandler(
this.pictureBox_MouseDown);
this.pictureBox.MouseUp += this.mu;
this.pictureBox.MouseMove += this.mm;
this.pictureBox.MouseDown += this.md;
}
public void UnInitializeComponent()
{
this.pictureBox.MouseUp -= this.mu;
this.pictureBox.MouseMove -= this.mm;
this.pictureBox.MouseDown -= this.md;
mu = null;
mm = null;
md = null;
}
/// <summary>
/// Private variable to hold the width of the object
/// being drawn.
/// </summary>
private int width = 1;
/// <summary>
/// Width is the attribute holding the width of the
/// object being drawn.
/// The Width attribute is read/write.
/// </summary>
public int Width
{
get { return width; }
set { width = value; }
}
/// <summary>
/// Private variable to hold the color of the
/// object being drawn.
/// </summary>
private Color color = Color.FromArgb(255, 0, 0, 0);
/// <summary>
/// Color is the attribute holding the color of the
/// object being drawn.
/// The Color attribute is read/write.
/// </summary>
public Color Color
{
get { return color; }
set { color = value; }
}
/// <summary>
/// Private variable to hold the color of the object
/// being drawn.
/// </summary>
private DashStyle dashStyle = DashStyle.Dot;
/// <summary>
/// DashStyle is the attribute holding
/// the line style of the object being drawn.
/// The DashStyle attribute is read/write.
/// </summary>
public DashStyle DashStyle
{
get { return dashStyle; }
set { dashStyle = value; }
}
/// <summary>
/// Protected variable to hold the texture brush
/// that is used to create the erasing pen for the
/// object being drawn.
/// </summary>
protected TextureBrush staticTextureBrush = null;
/// <summary>
/// Protected variable to hold the
/// erasing pen for the object being drawn.
/// </summary>
protected Pen staticPenRubout = null;
/// <summary>
/// Protected variable to hold the drawing surface
/// or canvas.
/// </summary>
protected PictureBox pictureBox = null;
protected Point oldP1 = new Point(0, 0);
protected Point oldP2 = new Point(0, 0);
protected Point p1 = new Point(0, 0);
protected Point p2 = new Point(0, 0);
protected bool bDone = true;
/// <summary>
/// Instance variable for handling the mouse up
/// event for a single rubber-band object.
/// </summary>
protected MouseEventHandler mu = null;
/// <summary>
/// Instance variable for handling the mouse move
/// event for a single rubber-band object.
/// </summary>
protected MouseEventHandler mm = null;
/// <summary>
/// Instance variable for handling the mouse down
/// event for a single rubber-band object.
/// </summary>
protected MouseEventHandler md = null;
/// <summary>
/// PictureBox is the attribute holding the
/// PictureBox being used in the drawing of the
/// rubber-band object.
/// The PictureBox attribute is write only.
/// </summary>
public PictureBox PictureBox
{
// No getter
set { pictureBox = value; }
}
protected Graphics pg1 = null;
[System.Runtime.InteropServices.DllImportAttribute(
"gdi32.dll")]
private static extern bool BitBlt(
IntPtr hdcDest, // handle to destination DC
int nXDest, // x-coord of dest upper-left corner
int nYDest, // y-coord of dest upper-left corner
int nWidth, // width of destination rectangle
int nHeight, // height of destination rectangle
IntPtr hdcSrc, // handle to source DC
int nXSrc, // x-coord of source upper-left corner
int nYSrc, // y-coord of source upper-left corner
System.Int32 dwRop // raster operation code
);
/// <summary>
/// Handles the MouseDown event for the underlying
/// PictureBox that is used for drawing
/// rubber band lines (and other shapes).
/// A snapshot of the client area of the PictureBox
/// is taken, to be used when undrawing lines.
/// </summary>
protected void pictureBox_MouseDown(object sender,
System.Windows.Forms.MouseEventArgs e)
{
Debug.WriteLine("TSRubberBandObject - " +
"CreateGraphics in MouseDown");
// Get the client rectangle for use in sizing
// the image and the PictureBox
Rectangle r = pictureBox.ClientRectangle;
// Debug.WriteLine("1 pictureBox." +
// "ClientRectangle: " + r.ToString());
// Translate the top-left point of the
// PictureBox from client coordinates of the
// PictureBox to client coordinates of the
// parent of the
// PictureBox.
Point tl = new Point(r.Left, r.Top);
tl = pictureBox.PointToScreen(tl);
tl = pictureBox.Parent.PointToClient(tl);
// pictureBox.SetBounds(tl.X, tl.Y, r.Width,
// r.Height, BoundsSpecified.All);
// Get a Graphics object for the PictureBox,
// and create a bitmap to contain the current
// contents of the PictureBox.
Graphics g1 = pictureBox.CreateGraphics();
Image i = new Bitmap(r.Width, r.Height, g1);
// Reset the Bounds of the PictureBox
// There seems to be a bug in the Bounds
// attribute in the PictureBox control.
// The top-left point must be set to the
// top-left client point translated to screen
// then back to the parents client space, and
// (here's the weird part) the width and height
// must be increased by the offset from the
// origin, or the image will be clipped to the
// original PictureBox area.
pictureBox.Bounds = new Rectangle(tl.X, tl.Y,
r.Width + tl.X, r.Height + tl.Y);
// Now assign the new image area we just created.
// This will be the screen area that we capture
// via BitBlt for our rubout pen.
pictureBox.Image = i;
// Reset the bounds once again, although this
// time we can do it as it seems you should.
pictureBox.SetBounds(tl.X, tl.Y, r.Width,
r.Height, BoundsSpecified.All);
// Get a Graphics object for the image
Graphics g2 = Graphics.FromImage(i);
// Now get handles to device contexts, and
// perform the bit blitting operation.
IntPtr dc1 = g1.GetHdc();
IntPtr dc2 = g2.GetHdc();
BitBlt(dc2, 0, 0, r.Width, r.Height,
dc1, 0, 0, 0x00CC0020 /* dest = source*/);
// Clean up !!
g1.ReleaseHdc(dc1);
g2.ReleaseHdc(dc2);
p1 = new Point(e.X, e.Y);
bDone = false;
}
private void DrawXorLine(Graphics g, Point p1,
Point p2, Boolean bRubOut)
{
// Debug.WriteLine("DrawXorLine");
if (bDone)
{
return;
}
if (pictureBox == null)
{
throw new NullReferenceException(
"RubberBandLine.pictureBox is null.");
}
g.CompositingMode = CompositingMode.SourceOver;
if (bRubOut && staticTextureBrush == null &
& staticPenRubout == null)
{
if (pictureBox.Image != null)
{
staticTextureBrush = new TextureBrush(
pictureBox.Image);
staticPenRubout = new Pen(
staticTextureBrush, this.Width + 2);
// Debug.WriteLine(
// "Creating staticPenRubout");
}
else
{
g.Dispose();
Debug.WriteLine(
"Cannot create staticPenRubout");
}
}
if (bRubOut && staticPenRubout != null &&
!(p1 == p2))
{
g.DrawLine(staticPenRubout, p1, p2);
// Debug.WriteLine("Erase line");
}
else
{
Pen p = new Pen(this.Color, this.Width);
p.DashStyle = this.DashStyle;
g.DrawLine(p, p1, p2);
// Debug.WriteLine("Draw line");
}
}
/// <summary>
/// Handles the MouseMove event for the underlying
/// PictureBox that is used for drawing rubber band
/// lines (and other shapes).
/// </summary>
private void pictureBox_MouseMove(object sender,
System.Windows.Forms.MouseEventArgs e)
{
if (bDone)
{
return;
}
if (pg1 == null)
{
pg1 = pictureBox.CreateGraphics();
}
if (pictureBox.DisplayRectangle.Contains(e.X,
e.Y))
{
p2 = new Point(e.X, e.Y);
// We are going to be drawing a new line,
// so draw over the area we are about to
// impact with the cached background image
// taken before the first line is drawn.
if (oldP1 == oldP2)
{
DrawXorLine(pg1, p1, p2, false);
}
else
{
// Debug.WriteLine("Undrawing line");
DrawXorLine(pg1, oldP1, oldP2, true);
}
// Debug.WriteLine("Drawing line");
DrawXorLine(pg1, p1, p2, false);
oldP1 = p1;
oldP2 = p2;
}
}
/// <summary>
/// Handles the MouseUp event for the underlying
/// PictureBox that is used for drawing rubber band
/// lines (and other shapes).
/// </summary>
protected void pictureBox_MouseUp(object sender,
System.Windows.Forms.MouseEventArgs e)
{
p2 = new Point(e.X, e.Y);
oldP1 = oldP2;
bDone = true;
staticTextureBrush = null;
staticPenRubout = null;
}
}
}