65.9K
CodeProject 正在变化。 阅读更多。
Home

在GDI+中绘制橡皮筋线

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.40/5 (24投票s)

2001 年 12 月 12 日

5分钟阅读

viewsIcon

293855

downloadIcon

3779

在 GDI+ 中绘制自擦除线或橡皮筋线似乎是不可能的——本文将介绍如何实现。

Sample Image - RubberBandLine.jpg

引言

在 GDI+ 中绘制自擦除线或橡皮筋线从基点到当前鼠标点似乎是不可能的。这是过去曾多次被撰写过的著名且标准的 XOR 运算问题。在 Win32 API 中,可以通过调用 SetROP2 来设置 XOR 模式。GDI+ 中 SetROP2 函数的唯一等效项是 Graphics.CompositingMode 属性,它只支持两种模式:SourceOverSourceCopy。这两种模式都不能让你先绘制一条线,然后再“擦除”它。本文将解释你需要做什么来绘制和擦除线条。对其他形状执行相同的操作是同一技术的简单扩展。

背景

我从开始编程以来就一直对图形编程感兴趣,这已经远远超出了我手指的数量。几年前,我构建了一个例程库,建立在 GDI 之上,以允许我使用实际单位(如英寸和米)绘制对象。基本上,这些例程就像一个小型 CAD 引擎,隐藏了像素、第四象限坐标系、MoveToLineTo 函数以及使用椭圆绘制的圆形(这些椭圆绘制在由左上点到右下点界定的矩形区域内)的残酷怪异之处——这些愚蠢的 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;
        }
    }
}
© . All rights reserved.