2004 年 2 月 17 日
在进行 Internet 或基于 Windows 的开发方面,您遇到过问题或难题吗?这时,您可以求助于 GUI 博士 (drgui@microsoft.com);GUI 博士每个月会两次做客 MSDN,在线回答您的问题。虽然博士忙碌的工作安排使他无法回复所有的问题,但他会尽可能地在这里回答更多的问题。如果恰好选中了您的问题,那么博士会送您一件 GUI 博士 T 恤衫!
摘要:GUI 博士介绍了如何使用 .NET 框架来绘制橡皮带线条。(16 页打印页)
内容
| 位、字节和 Blog | |
| 您从未听到过成功绘制出橡皮带的人发出的欢呼声 |
“您是否具有支持它们的 RSS?”
“您的 webblog 地址是什么?”
GUI 博士发现许多人都在询问有关 blog 和 RSS 的问题,因此他决定深入探讨一下这方面的问题。
weblog(网络日记)是一人或多人定期更新的 Web 页,通常包含带有时间戳的项。它可以是个人页面、技术页面或只是有关任一主题的页面。许多人将 weblog 缩写为 blog。GUI 博士对在缩写词的基础上再加以缩写感到有些奇怪,但您只需了解这一点即可。此外,该缩写词为何还这么长呢?您可以将它看作是另一种 Web 页,也可以将其视为一种日记(如果您是“大人”,也可以将其视为杂志),亦或认为它是一种向人们表述您所掌握的信息的全新方式。例如,GUI 博士很喜欢在 Gizmodo weblog 上了解他买不起的最新小玩意儿的信息,而其他人则喜欢在其 blog 上了解来自 Dan Gilmor最新的至理名言。
虽然成天“浪费时间”阅读 blog 很可能会使您的老板感到十分愤怒,但这也是一种学习知识的好方法。例如,您知道吗,Microsoft 有许多员工都在浏览 blog?您只需快速浏览一下 http://blogs.msdn.com,肯定会有收获。例如,现在博士看到又出现一个 WS 规范、有关 Microsoft Word HTML 文档清洁器的信息以及有关 Steve Ballmer 先生本人简介的公告。过去,GUI 博士也曾在某些 .NET 体系结构文章中看到过有关 Microsoft .NET 高深主题的疑难术语。
RSS(它是一个缩略语,但不同的人会把它展开成不同的词;它可以表示 Really Simple Syndication,也可以表示 RDF Site Summary)是一种包含 Web 站点最近几次更新的 XML 格式。GUI 博士认为,这类似于 Microsoft Internet Explorer 4.0 中期的活动通道。但是,它与通道的不同之处在于,它得到了广泛的认可。通过观察 Web 站点的 RSS(确切地说是通过一个程序来监视它),您可以轻松得知它何时发生了变化。您再也不需要浪费时间来访问各个站点;只要它更新了,您就会知道。
大多数 blog 也支持 RSS:因此,您可以使用程序(如 SharpReader 或 NewsGator)向 RSS 订阅许多 blog。然后在空闲时阅读其内容。
了解 weblog 的最佳方式是开始阅读它们。您可以在以下网站上找到许多有关 .NET 及相关信息的出色 weblog:
| • | |
| • | |
| • |
此外,如果您想提供某些信息,或许也可以从 blog 开始,与全世界的人分享您的知识。您会逐渐喜欢上它的。
作为一位 Visual Basic 程序员,我与其他许多人一样,总会丢失 GDI+ 中的光栅操作,这主要是因为橡皮带图形不太容易创建。您为何不向我们演示一下如何使用 InteropServices 来调用 Win32 API 呢?我打算使用 Win32 API 函数,而不是创建工作区的缓冲区,并在每次发生 MOUSEMOVE 事件时将缓冲区复制到工作区中。这种旧方法容易得多,也清晰得多。非常感谢!
来自巴西的 Meireles
我们的足球队曾五次获得世界足球冠军
下面是我经常执行的代码:
Case MOUSEMOVE
m_lXn = oEventoDoMouse.X
m_lYn = oEventoDoMouse.Y
Call m_oContextoGrafico.Obter
With m_oContextoGrafico.CanetaElastica
Caneta = CreatePen(.Estilo, .Espessura, .Cor)
End With
CanetaAnterior = SelectObject(m_oContextoGrafico.hdc, Caneta)
g_lRetorno = SetROP2(m_oContextoGrafico.hdc, R2_XORPEN)
g_lRetorno = MoveToEx(m_oContextoGrafico.hdc, _
m_lXi, m_lYi, UltimoPonto)
g_lRetorno = LineTo(m_oContextoGrafico.hdc, m_lXv, m_lYv)
g_lRetorno = MoveToEx(m_oContextoGrafico.hdc, _
m_lXi, m_lYi, UltimoPonto)
g_lRetorno = LineTo(m_oContextoGrafico.hdc, m_lXn, m_lYn)
g_lRetorno = SelectObject(m_oContextoGrafico.hdc, CanetaAnterior)
g_lRetorno = DeleteObject(Caneta)
Call m_oContextoGrafico.Liberar
m_lXv = m_lXn
m_lYv = m_lYn
Obrigado por teu pergunta!首先,GUI 博士对 pentacampeona 所取得的成就表示最真挚的祝贺;这项记录必然会保持一段时间,才会被打破!(除非 Sele?ao 在 2006 年第六次夺冠。)
现在来看一下您提出的问题...GUI 博士对您在创建工作区的缓冲区并在每次发生 MouseMove 事件时将该缓冲区复制到工作区时所涉及到的系统开销方面所产生的疑虑表示理解。请放心,正如 GUI 博士将在本说明后面部分解释的那样,使用 .NET 框架来绘制橡皮带线条并不是唯一的方法。
让我们从您所提出问题的简化版开始。毫无疑问,您可以与非托管 Win32 API 函数进行互操作,以使用 Microsoft Windows 图形设备接口 (GDI)。要进一步了解互操作,请查阅 Interoperating with Unmanaged Code。实际上,这与您的一贯做法几乎完全相同,除了可能做了几处细微的修改。为了复习一下 GDI 的基本知识,您可以利用在线 MSDN 库 Windows GDI Start Page 中的某些信息文档。
首先,在 Microsoft Visual Studio .NET 中,使用您选择的 Microsoft Visual Basic .NET 或 C# 来创建 Windows 应用程序。GUI 博士将使用 Visual Basic .NET,因为这可以说明您的问题是如何产生的。但是,那些不愿使用 Visual Basic .NET 的用户可以使用 C# 的任一版本。为了使这种解决方法尽可能接近 Win32 编程,请将 Form 的 BackColor 属性更改为白色。接下来,声明您计划调用的所有非托管函数。当然,在这些声明之前,您还需要在 Form 中声明非托管的 POINT 结构,该结构将连同 MoveToEx 函数一起使用。下面说明了如何在 Visual Basic .NET 中声明 POINT 结构:
Private Structure POINTAPI Public x As Int32 Public y As Int32 End Structure
请注意,您使用的是 POINTAPI,而不是 POINT。这是为了避免混淆您所使用的结构;请记住有一个名为 Point 的 .NET 结构,该结构是在 System.Drawing 命名空间中声明的。
一旦您声明了 POINTAPI 结构,就需要为将用于 Form 的所有 Win32 API 函数添加声明。您可以通过向 Form 添加以下声明来实现这一点:
Private Declare Auto Function SetROP2 Lib "gdi32.dll" _
Alias "SetROP2" (ByVal hDC As IntPtr, _
ByVal nDrawMode As Int32) As Int32
Private Declare Auto Function MoveToEx Lib "gdi32.dll" _
Alias "MoveToEx" (ByVal hDC As IntPtr, _
ByVal x As Int32, ByVal y As Int32, _
ByRef lpPoint As POINTAPI) As Boolean
Private Declare Auto Function LineTo Lib "gdi32.dll" _
Alias "LineTo" (ByVal hDC As IntPtr, _
ByVal x As Int32, ByVal y As Int32) As Boolean
如果您想更详细地了解上述声明可达到的实际效果,可以阅读有关声明语句的更多信息。
R2_NOT 能够以最简单的橡皮带图形格式满足调用 SetROP2 函数语句中的 nDrawMode 参数要求。R2_NOT 绘图模式可以用与其现有颜色相反的颜色来设置像素。当您声明非托管函数时,GUI 博士建议您同时将 R2_NOT 声明为常量:
Private Const R2_NOT As Int32 = 6
现在,您已完成了最初的程式化声明,GUI 博士将把此处理方法应用到下一级操作:实际绘制橡皮带线条。
为了在窗口的工作区内进行绘图,您需要获得用于窗口工作区的显示设备上下文。在 Win32 编程操作中,您可以通过调用 GetDC 函数来实现这一点。在 .NET 编程操作中,您可以通过将 Graphics 对象与您的 Form 相关联来实现这一点。Graphics 类是在 System.Drawing 命名空间中定义的。使用 GDI 函数将 Graphics 对象与特定的显示设备上下文相关联,从而允许您在 Form 上进行绘图。因此,您需要创建一个 Graphics 对象来表示 Form 的绘图图面。在调用 InitializeComponent 后,通过向 Form 的构造函数添加以下代码,可以实现这一点。
m_oDrawingSurface = Me.CreateGraphics()
显而易见,您还必须将 m_oDrawingSurface 声明为 Form 类的成员,如下所示:
Private m_oDrawingSurface As System.Drawing.Graphics
现在,您已拥有自己的 Graphics 对象,GUI 博士将说明如何添加重写方法,以处理您的鼠标事件。您可能想知道为何 GUI 博士主张重写这些方法,而不使用事件处理程序。其原因之一即为性能:使用重写方法时可以获得各种性能优势。每当在屏幕上处理图形时,博士都会推荐您将性能作为主要目标之一。为了实现橡皮带操作,您需要添加的重写方法是 OnMouseDown 和 OnMouseMove。
在 OnMouseDown 中,如果按下鼠标左键,则需要存储橡皮带线条的起点。您还需要存储橡皮带线条的上一个终点,在本例中,即为与起点相同的点。您可以从传递给 OnMouseDown 的 MouseEventArgs 参数的 X 和 Y 属性中获得所需的 x 和 y 坐标。您需要再次声明相应的成员,如下所示:
Private m_ptsStart As POINTAPI Private m_ptsPrevious As POINTAPI
并且我们不要忘了 OnMouseDown 的实际实现操作。
Protected Overrides Sub OnMouseDown(ByVal e As _
System.Windows.Forms.MouseEventArgs)
' Check whether the left mouse button
' has generated the associated event.
If e.Button = MouseButtons.Left Then
' Store the starting point for your rubber-band line.
m_ptsStart.x = e.X
m_ptsStart.y = e.Y
' Store the previous endpoint for your rubber-band line.
m_ptsPrevious = m_ptsStart
End If
End Sub
您会注意到,与 Win32 编程不同的是,它无需显式调用 SetCapture 和 ReleaseCapture。这是因为您使用的是从 System.Windows.Forms.Control 派生出来的 Form,反过来,System.Windows.Forms.Control 又会自动为您执行该实现的所有细节。
继续来看一看 OnMouseMove,在这里您需要执行橡皮带线条的实际绘图操作。再次使用传递给该方法的 MouseEventArgs 参数,以便确定是否按下了鼠标左键。然后,您需要从 m_oDrawingSurface Graphics 对象获得相应设备上下文的句柄,以执行绘图操作。随后,为将使用此设备上下文的绘图操作设置光栅模式。现在,您可以使用 MoveToEx 和 LineTo 函数来绘制橡皮带线条。
您可以再次从传递给 OnMouseMove 的 MouseEventArgs 参数的 X 和 Y 属性中获得橡皮带线条的当前终点。完成该操作后,不要忘了释放从 m_oDrawingSurface 获得的设备上下文的句柄!最后,将当前终点存储到 m_ptsPrevious 成员变量中。
注 使用 R2_NOT 光栅模式的优势在于:即使未指定用于绘图操作的绘图笔,您也可以实现橡皮带图形。这是因为 R2_NOT 光栅模式仅处理反转屏幕像素颜色的操作,而不像 R2_XORPEN 模式那样,将屏幕像素颜色与绘图笔结合在一起使用。其关键点在于:R2_NOT 更易于入门。当然,如果您需要其他功能,如笔型和颜色,则最好使用 R2_XORPEN。要想进一步了解 SetROP2 及其他光栅模式,请访问在线 MSDN 库中的 SetROP2。
GUI 博士的重写 OnMouseMove 如下所示:
Protected Overrides Sub OnMouseMove(ByVal e As _
System.Windows.Forms.MouseEventArgs)
' Check if the left mouse button is pressed.
If e.Button = MouseButtons.Left Then
' Declare local variables.
' handle to an appropriate device context obtained
' from m_oDrawingSurface
Dim hDC As IntPtr
' previous mix mode; returned from SetROP2
Dim Mix As Int32
' temporary variables used for rubber-banding logic
Dim ptsCurrent As POINTAPI
Dim ptsOld As POINTAPI
' determines whether MoveToEx and LineTo calls are successful
Dim Success As Boolean
' Store the current endpoint of your rubber-band line.
ptsCurrent.x = e.X
ptsCurrent.y = e.Y
' Perform the requisite graphical operations.
' Obtain a handle to an appropriate device context
hDC = m_oDrawingSurface.GetHdc()
' Set the raster mode to invert the screen color
Mix = SetROP2(hDC, R2_NOT)
' Move to the starting point of the rubber-band line
Success = MoveToEx(hDC, m_ptsStart.x, m_ptsStart.y, ptsOld)
' Erase the previous line
Success = LineTo(hDC, m_ptsPrevious.x, m_ptsPrevious.y)
' Move to the starting point of the rubber-band line
Success = MoveToEx(hDC, m_ptsStart.x, m_ptsStart.y, ptsOld)
' Draw the new line
Success = LineTo(hDC, ptsCurrent.x, ptsCurrent.y)
' Release the obtained handle to a device context
m_oDrawingSurface.ReleaseHdc(hDC)
' Store the current endpoint for the next erase operation
m_ptsPrevious = ptsCurrent
End If
End Sub
生成您的应用程序,然后再运行它。此时应出现 Form,并且您应能够绘制和观察橡皮带线条。
现在,您已经实现了形式最简单的橡皮带线条,GUI 博士将向您介绍如何使用 R2_XORPEN 光栅模式来获得更多功能。例如,使用 R2_XORPEN 模式,您可以实现采用各种笔型和颜色的橡皮带。为此,您需要声明其他一些非托管 GDI 函数。它们是:
Public Declare Auto Function CreatePen Lib "gdi32.dll" _
Alias "CreatePen" (ByVal nPenStyle As Int32, _
ByVal nWidth As Int32, ByVal crColor As Int32) As IntPtr
Public Declare Auto Function SelectObject Lib "gdi32.dll" _
Alias "SelectObject" (ByVal hDC As IntPtr, _
ByVal hObject As IntPtr) As IntPtr
Public Declare Function DeleteObject Lib "gdi32.dll" _
Alias "DeleteObject" (ByVal hObject As IntPtr) As Boolean
您需要声明与 R2_XORPEN 光栅模式相对应的常量。为此,请使用以下声明。
Private Const R2_XORPEN As Int32 = 7
您还需要声明在调用 CreatePen 时将用于 nPenStyle 参数的常量。例如,如果您计划实现点式笔型,则下面介绍了如何声明关联的常量:
Private Const PS_DOT As Int32 = 2
有关其他光栅模式和笔型的完整列表,请查阅 WinGDI.h 文件。WinGDI.h 是 Windows 头文件,其中包含用于定义光栅模式和笔型的预处理器指令。WinGDI.h 通常位于以下路径。
%Program Files%\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include
最后,您需要修改 OnMouseMove 方法,以便创建具有您所需特性的绘图笔,然后将该绘图笔与您的设备上下文相关联。完成此操作后,请将光栅模式设置为 R2_XORPEN 并绘制您的橡皮带线条。最后,不要忘了将最初的绘图笔与您的设备上下文重新关联,删除您指定的绘图笔,然后通过释放您的设备上下文来完成所有操作。在本例中,您的 OnMouseMove 如下所示:
Protected Overrides Sub OnMouseMove(ByVal e As _
System.Windows.Forms.MouseEventArgs)
' Check if the left mouse button is pressed.
If e.Button = MouseButtons.Left Then
' Declare local variables.
' handle to an appropriate device context
' obtained from m_oDrawingSurface
Dim hDC As IntPtr
' previous mix mode; returned from SetROP2
Dim Mix As Int32
' temporary variables used for rubber-banding logic
Dim ptsCurrent As POINTAPI
Dim ptsOld As POINTAPI
' determines whether MoveToEx and LineTo calls are successful
Dim Success As Boolean
' handles to rubber-band and original pens
Dim hPen As IntPtr
Dim hOldPen As IntPtr
' Store the current endpoint of your rubber-band line
ptsCurrent.x = e.X
ptsCurrent.y = e.Y
' Perform the requisite graphical operations.
' Obtain a handle to an appropriate device context
hDC = m_oDrawingSurface.GetHdc()
' Create a dotted, one pixel thick, blue pen
hPen = CreatePen(PS_DOT, 1, &HFF0000)
' Associate hPen with your device context
hOldPen = SelectObject(hDC, hPen)
' Set the raster mode to the XOR mode
Mix = SetROP2(hDC, R2_XORPEN)
' Move to the starting point of the rubber-band line
Success = MoveToEx(hDC, m_ptsStart.x, m_ptsStart.y, ptsOld)
' Erase the previous line
Success = LineTo(hDC, m_ptsPrevious.x, m_ptsPrevious.y)
' Move to the starting point of the rubber-band line
Success = MoveToEx(hDC, m_ptsStart.x, m_ptsStart.y, ptsOld)
' Draw the new line
Success = LineTo(hDC, ptsCurrent.x, ptsCurrent.y)
' Re-associate the original pen with your device context
hPen = SelectObject(hDC, hOldPen)
' Delete your rubber-band pen
Success = DeleteObject(hPen)
' Release the obtained handle to a device context
m_oDrawingSurface.ReleaseHdc(hDC)
' Store the current endpoint for the next erase operation.
m_ptsPrevious = ptsCurrent
End If
End Sub
再次生成您的应用程序,然后运行它。此时应出现 Form,并且您应能够绘制橡皮带线条,该线条显示为一个像素大小的黄黑色点线。当然,如果 Form 的 BackColor 是除白色以外的某种颜色,则橡皮带线条的颜色将由相应 R2_XORPEN 操作的结果来确定。
需要注意的一点是,GUI 博士已经在调用 CreatePen 时将 &HFF0000 用于 crColor 参数。这是因为 CreatePen 应使用 COLORREF 变量,而该变量只不过是一个 32 位的无符号整数。第二个最高有效字节表示蓝色的相对亮度,而 FF 是表示最大蓝色亮度的十六进制表示形式。因此,蓝色可以用十六进制的形式表示为 &HFF0000。
继续看其他的情况...您曾经提到过,由于缺乏对 GDI+ 中光栅操作的支持,因此很难在 Microsoft Visual Basic .NET 中创建橡皮带图形。我们暂且不谈 GDI+ 实现 — GUI 博士将在适当的课程里加以介绍!GUI 博士将对症下药,说明如何使用内置 Microsoft .NET 框架方法在 Visual Basic .NET 中创建橡皮带线条。
使用这种方法的主要优势在于:您不必与任何非托管代码进行互操作;所有代码均为托管代码并以公共语言运行库为目标。这直接消除了前面的互操作示例中遇到的许多副作用,例如,在函数声明中使用正确的数据类型,或在实际调用非托管函数时使用正确的参数。从抽象角度来看,GUI 博士还说道,对于全面处理托管代码或非托管代码,在概念上都非常清晰。如果希望通过混合这两者来获得这两个方面的最佳优势,可能会导致严重问题!
System.Windows.Forms 命名空间包含 ControlPaint 类,您可以使用该类来实现橡皮带操作,因为 ControlPaint.DrawReversibleLine 方法可以直接在屏幕上绘图。DrawReversibleLine 可以在指定的点之间绘制线条,并使用 backColor 参数来计算线条的填充颜色,以便该线条在背景的反衬下始终清晰可见。通过再次绘制同一线条可以取消这种方法的效果。您可以在此处找到有关 DrawReversibleLine 的更多详细信息。
但是,您无法指定要用于绘制线条的确切颜色;这是使用 DrawReversibleLine 的缺点之一。此外,您还必须将点坐标从工作区坐标转换成屏幕坐标,而使用 PointToScreen 方法可以轻松实现这一点。最后,主要的障碍是,由于 DrawReversibleLine 绘制到屏幕上,而不是 Form 上,因此您会发现线条可能会超出 Form 的边界。这个问题可以通过实现某种自定义剪辑逻辑来轻松克服,以确保线条位于 Form 的范围内。
用于获得橡皮带效果的编程逻辑与 GUI 博士前面提到的在 Visual Basic .NET 与非托管 GDI 函数之间进行互操作所使用的方法几乎完全相同。通过将几个 System.Drawing.Point 变量声明为 Form 的成员,然后再实现重写 OnMouseDown 和 OnMouseMove 方法,可以满足您所有的需求。
因此,在本例中,您的两个成员变量如下:
Private ptsStart As System.Drawing.Point Private ptsPrevious As System.Drawing.Point
GUI 博士介绍的编程逻辑实质上与您的编程逻辑完全相同;您必须重写 OnMouseDown 和 OnMouseMove 方法。在 OnMouseDown 中,如前所述,如果按下了鼠标左键,则需要存储橡皮带线条的起点以及上一个终点,即与起点相同的点。您可以再次从传递给 OnMouseDown 的 MouseEventArgs 参数的 X 和 Y 属性中获得所需的 x 和 y 坐标。
因此,下面所示的 OnMouseDown,您应该非常熟悉:
Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
' Check if the left mouse button has
' generated the associated event.
If e.Button = MouseButtons.Left Then
' Store the starting point for your rubber-band line.
ptsStart.X = e.X
ptsStart.Y = e.Y
' Store the previous endpoint for your rubber-band line.
ptsPrevious = ptsStart
End If
End Sub
同样,在 OnMouseMove 中,您需要检查是否按下了鼠标左键。如果是,请继续操作并通过调用 DrawReversibleLine 两次来绘制橡皮带线条。您可以再次从传递给 OnMouseMove 的 MouseEventArgs 参数的 X 和 Y 属性中获得橡皮带线条的当前终点。最后,您需要将橡皮带线条的当前终点存储到 ptsPrevious 变量中,以供下次调用 OnMouseMove 时使用。不要忘了,您必须使用 PointToScreen 方法将您的 Point 变量从其相对工作区坐标转换成绝对屏幕坐标。
因此,下面是您的重写 OnMouseMove 方法:
Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
' Check if the left mouse button is pressed.
If e.Button = MouseButtons.Left Then
' Declare a local variable for the current
'endpoint of your rubber-band line.
Dim ptsCurrent As System.Drawing.Point
' Store the current endpoint of your rubber-band line.
ptsCurrent.X = e.X
ptsCurrent.Y = e.Y
' Draw your actual rubber-band lines.
ControlPaint.DrawReversibleLine(PointToScreen(ptsStart), _
PointToScreen(ptsPrevious), Color.Black)
ControlPaint.DrawReversibleLine(PointToScreen(ptsStart), _
PointToScreen(ptsCurrent), Color.Black)
' Store the current endpoint for the next call to OnMouseMove.
ptsPrevious = ptsCurrent
End If
End Sub
注 GUI 博士的方法并不包括为了确保橡皮带线条位于 Form 的范围之内,而不是蔓延到整个桌面所要实现的自定义剪辑逻辑。如果涉及到这种剪辑逻辑,请尽管使用最符合您要求的逻辑。
现在,您已经完成了编码操作,请继续;生成并运行您的应用程序。如前所述,您将能够绘制橡皮带线条。但是,如果您尚未实现任何剪辑逻辑,毫无疑问,您将发现橡皮带线条也会显示在桌面上。
您的问题可能存在的另一种解决方法(在特定环境下,这可能是最合适的方法)是:将 Graphics 对象与您的 Form 相关联,并将此 Graphics 对象用作绘图图面,而不是使用 DrawReversibleLine 直接在屏幕上绘图。如何执行这种解决方法呢?
在向您介绍这种方法之前,GUI 博士将列举诸多优势。首先,这种方法还具有附加优势,可以用作仅处理托管代码的解决方案。此外,这种机制还克服了前面介绍的 DrawReversibleLine 机制的所有缺点。
对于初学者,您不必为任何可能的难题而感到困扰,尽管它在实质上是一些极为简单的剪辑逻辑。这是因为您是在与 Form 对应的 Graphics 图面上绘图,该图面可以自动将橡皮带线条限制在 Form 的范围之内。
接下来,您还会发现自己不必为调用 PointToScreen 方法而感到烦恼。这也是因为您是在 Graphics 对象上绘图,该对象的坐标与您的工作区坐标保持同步,而在前面的例子中,您必须将这两个坐标进行同步。
最后,您会发现自己实际上控制了橡皮带线条所显示的确切颜色。实际上,在这种情况下,不管 Form 的 BackColor 属性的值是什么,绘制出的橡皮带线条均为蓝色。而它们是否可见则是另一回事,这取决于 Form 的 BackColor 的实际值。不过,这允许您从总体上控制屏幕输出,而在前面的例子中,橡皮带线条始终在屏幕上可见,并且不一定显示为您所选择的颜色。
要处理这种情况,您需要一个表示 Graphics 对象的成员变量。您还需要表示两种类型 System.Drawing.Pen 对象的变量,您将使用这两个变量在 Graphics 对象上绘图。最后,与前面的例子一样,您还需要表示橡皮带线条的起点和上一个终点的变量。
因此,它们是:
Private DrawingSurface As System.Drawing.Graphics Private ErasePen As System.Drawing.Pen Private DrawPen As System.Drawing.Pen Private ptsStart As System.Drawing.Point Private ptsPrevious As System.Drawing.Point
如前所述,您需要在 Form 的构造函数中实例化 Graphics 对象。而且,您还需要创建 Pen 对象;一个用于清除原来的线条,一个用于绘制新线条。因此,在调用 InitializeComponent 之后,请继续操作并将此代码添加到 Form 的构造函数中。
DrawingSurface = Me.CreateGraphics()
' Pen to erase a previous line
ErasePen = New System.Drawing.Pen(Me.BackColor)
' Pen to draw a new line
DrawPen = New System.Drawing.Pen(Color.Blue)
您的重写 OnMouseDown 方法与前面例子中的方法几乎完全相同:
Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
' Check if the left mouse button has
' generated the associated event.
If e.Button = MouseButtons.Left Then
' Store the starting point for your rubber-band line.
ptsStart.X = e.X
ptsStart.Y = e.Y
' Store the previous endpoint for your rubber-band line.
ptsPrevious = ptsStart
End If
End Sub
这时,通过该重写 OnMouseMove 方法,您可以实际绘制出橡皮带线条;正如前面的例子所示:
Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
' Check if the left mouse button is pressed.
If e.Button = MouseButtons.Left Then
' Declare a local variable for the current endpoint '
' of your rubber-band line.
Dim ptsCurrent As System.Drawing.Point
' Store the current endpoint of your rubber-band line.
ptsCurrent.X = e.X
ptsCurrent.Y = e.Y
' Draw your actual rubber-band lines.
DrawingSurface.DrawLine(ErasePen, _
ptsStart.X, ptsStart.Y, ptsPrevious.X, ptsPrevious.Y)
DrawingSurface.DrawLine(DrawPen, _
ptsStart.X, ptsStart.Y, ptsCurrent.X, ptsCurrent.Y)
' Store the current endpoint for the next call to OnMouseMove.
ptsPrevious = ptsCurrent
End If
End Sub
这与实际的代码差别不大;实际上,唯一的更改是对 DrawLine 的两次调用。但此时,当您实际开始生成并运行应用程序时,还是会发现存在相当大的区别。
因此,在 GUI 博士介绍的这三种方法(实质上,使用 R2_NOT 或 R2_XORPEN 进行互操作是同一回事,我们将其视为一种解决方案)中,使用互操作方法似乎可以很好地满足您的需求,而这正是您所需要的方法。作为几种辅助方法,GUI 博士希望针对您的问题采用纯托管代码的其他两种方法(使用 ControlPaint.DrawReversibleLine 方法和使用 Graphics.DrawLine)能够长期解决您的问题。博士再次强调,在一般的橡皮带方案中,Graphics.DrawLine 方法优于其他两种方法。
尽管 GUI 博士提出了这几种方法,但他仍要指出,这三种方法中均存在注意事项。如果您最小化 Form 或在 Form 前面放置了其他窗口,则当您恢复为具有输入焦点的 Form 时,您将看到以下两种情况之一:您以前绘制的橡皮带线条全部消失,或覆盖窗口所隐藏的线条部分被清除。如果在最小化操作后恢复您的 Form,则系统会向您的 Form 发送一则 WM_PAINT 消息。但是,由于您不是处理 Form 的 Paint 事件,因此在默认情况下会调用 Form.OnPaint 方法来处理 WM_PAINT 消息。但是,这会导致您 Form 的整个工作区被重绘。由于 Form.OnPaint 方法不“了解”您的橡皮带线条,因此重绘的工作区将不包含原来的内容。
如果用另一个窗口覆盖了 Form 的部分内容,然后再将输入焦点重新设为 Form,则也会发生类似的事件。如前所述,要解决这一问题,您需要使用某种技术来保留 Form 的状态,并在每次需要时重绘必需的区域。
在最简单的可能解决方案中,您可以使用重写 OnPaint 方法来执行所有的绘图操作。这就要求您在所处理的各种事件之间进行同步,以确保您不会遇到因重绘逻辑不一致而产生的任何问题。但是,从您最初的提问可以看出,您的特定方案似乎不适合处理这种“重绘操作”。因此,GUI 博士对您的特定方案得出的结论是:这三种方法至少可以暂时解决您的问题。
GUI 博士希望他的讲解再加上您自身的努力能够帮助您找到最佳的解决方法!
致谢!
GUI 博士在此感谢出色的专家小组,包括 Ajay Abraham 及其专门助手 Maura Baughman。如果 GUI 博士在回答问题时没有大家的帮助,问题的解答就不会这么令人满意。
向 GUI 博士请教
著名的问题解决专家“GUI 博士”很高兴在 Internet 和基于 Windows 的开发方面提供百科全书式的知识,使各地的开发人员从中受益。如果您有无法解决的问题,请将您的疑问发送到 drgui@microsoft.com。虽然博士忙碌的工作安排使他无法回复所有的问题,但他会尽可能地在这里回答更多的问题。如果恰好选中了您的问题,那么博士会送您一件 GUI 博士 T 恤衫!(请注意:问题可能经过整理,以确保语法正确,逻辑清晰。)