动态绘图应用开发:从控件到对象的实现
1. 绘图程序类型概述
绘图程序主要分为两种基本类型:
-
位图绘画程序
:如 Microsoft Paint,用户可以创建具有静态内容的位图。一旦绘制形状或输入文本,就无法修改或重新排列。
-
矢量绘图程序
:像 Adobe Illustrator 和 Microsoft Visio 这类,用户的绘图实际上是对象的集合,用户可以随时点击、更改或完全移除任何对象。
创建基于位图的绘图程序相对容易,掌握 GDI+ 即可。但矢量绘图或图表程序则更复杂,需要单独跟踪每个对象及其位置。当用户点击绘图表面时,可能需要复杂逻辑来确定用户选择的对象并处理重叠绘制问题。解决此问题有两种方法:
- 使用子控件处理每个绘图元素:这是最简单的方法,但对于专业绘图应用不够灵活。
- 手动绘制和跟踪每个元素:提供最大的灵活性和功能,但需要编写更多代码。
2. 使用控件的绘图程序
2.1 基本应用功能
基本应用允许用户创建任意颜色的矩形、椭圆或三角形,然后调整大小或拖动到新位置。通过将每个形状转换为自定义控件,可以简化管理命中测试、选择和分层的逻辑。因为每个控件都有处理用户交互(如鼠标点击和按键)的内置智能,所以这种方法大大简化了开发。
2.2 形状控件(Shape Control)
2.2.1 关键特性
形状控件有三个关键特性:
- 提供
ShapeType
枚举,定义可表示的形状类型。程序员通过设置
Shape
属性选择形状。
- 使用私有成员变量引用关联形状的
GraphicsPath
对象。每当
Shape
属性修改时,控件创建新的
GraphicsPath
并添加相应形状,然后设置控件的
Region
属性以匹配形状的裁剪边界。
- 绘画逻辑简单,使用
FillPath()
方法绘制形状,
DrawPath()
方法绘制轮廓。
2.2.2 代码实现
Public Class Shape
Inherits System.Windows.Forms.Control
' 该控件支持的形状类型。
Public Enum ShapeType
Rectangle
Ellipse
Triangle
End Enum
' 当前形状。
Private shape As ShapeType = ShapeType.Rectangle
Private path As GraphicsPath
Public Property Type() As ShapeType
Get
Return shape
End Get
Set(ByVal value As ShapeType)
shape = value
RefreshPath()
Me.Invalidate()
End Set
End Property
' 为形状创建相应的 GraphicsPath,并通过设置 Region 属性将其应用于控件。
Private Sub RefreshPath()
If path IsNot Nothing Then path.Dispose()
path = New GraphicsPath()
Select Case shape
Case ShapeType.Rectangle
path.AddRectangle(Me.ClientRectangle)
Case ShapeType.Ellipse
path.AddEllipse(Me.ClientRectangle)
Case ShapeType.Triangle
Dim pt1 As Point = New Point(Me.Width / 2, 0)
Dim pt2 As Point = New Point(0, Me.Height)
Dim pt3 As Point = New Point(Me.Width, Me.Height)
path.AddPolygon(New Point(){pt1, pt2, pt3})
End Select
Me.Region = New Region(path)
End Sub
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
MyBase.OnPaint(e)
If path IsNot Nothing Then
Dim shapeBrush As New SolidBrush(Me.BackColor)
Dim shapePen As New Pen(Me.ForeColor, 5)
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
e.Graphics.FillPath(shapeBrush, path)
e.Graphics.DrawPath(shapePen, path)
shapePen.Dispose()
shapeBrush.Dispose()
End If
End Sub
Protected Overrides Sub OnResize(ByVal e As System.EventArgs)
MyBase.OnResize(e)
RefreshPath()
Me.Invalidate()
End Sub
End Class
2.3 绘图表面操作
2.3.1 创建形状
绘图应用从空画布开始,用户右键单击绘图区域,选择三个菜单项(新建矩形、新建椭圆、新建三角形)之一创建形状。点击事件触发相同的事件处理程序,根据选择设置
ShapeType
属性。
Private Sub mnuNewShape_Click(ByVal sender As Object, ByVal e As EventArgs) _
Handles mnuRectangle.Click, mnuTriangle.Click, mnuEllipse.Click
' 创建并配置具有一些默认值的形状。
Dim newShape As New Shape()
newShape.Size = New Size(40, 40)
newShape.ForeColor = Color.Coral
' 根据所选菜单项配置适当的形状。
If sender Is mnuRectangle Then
newShape.Type = Shape.ShapeType.Rectangle
ElseIf sender Is mnuEllipse Then
newShape.Type = Shape.ShapeType.Ellipse
ElseIf sender Is mnuTriangle Then
newShape.Type = Shape.ShapeType.Triangle
End If
' 确定形状的放置位置,需要将当前基于屏幕的鼠标坐标转换为相对窗体坐标。
newShape.Location = Me.PointToClient(Control.MousePosition)
' 为形状附加上下文菜单。
newShape.ContextMenuStrip = mnuSelectShape
' 将形状连接到所有事件处理程序。
AddHandler newShape.MouseDown, AddressOf ctrl_MouseDown
AddHandler newShape.MouseMove, AddressOf ctrl_MouseMove
AddHandler newShape.MouseUp, AddressOf ctrl_MouseUp
' 将形状添加到窗体。
Me.Controls.Add(newShape)
End Sub
2.3.2 形状操作
创建形状后,用户可以进行以下操作:
- 点击并拖动到新位置
- 点击右下角调整大小
- 右键单击显示上下文菜单,可更改颜色或删除对象
这些操作都响应
MouseDown
事件。代码检索触发事件的控件引用,检查右键是否点击(显示菜单),若左键点击,根据光标位置切换到调整大小或拖动模式。
graph TD;
A[MouseDown事件] --> B{右键点击?};
B -- 是 --> C[显示上下文菜单];
B -- 否 --> D{光标位置};
D -- 边或角 --> E[调整大小模式];
D -- 其他 --> F[拖动模式];
3. 使用形状对象的绘图程序
3.1 局限性与改进思路
使用控件的绘图程序虽然容易创建,但扩展时会遇到一些固有局限性,如渲染质量不佳、难以添加焦点提示、实现高级功能困难等。因此,可以采用手动使用 GDI+ 绘制形状并在集合中跟踪它们的方法,这种方法虽然需要依赖命中测试进行形状选择和操作,会比较复杂,但能提供更大的灵活性。
3.2 形状类(Shape Class)
3.2.1 属性添加
新的
Shape
类是必须继承的类,由于它从头开始绘制且不继承自
Control
,需要手动添加
ForeColor
、
BackColor
、
Location
和
Size
等属性。
Private _foreColor As Color
Public Property ForeColor() As Color
Get
Return _foreColor
End Get
Set(ByVal value As Color)
_foreColor = value
End Set
End Property
Private _backColor As Color
Public Property BackColor() As Color
Get
Return _backColor
End Get
Set(ByVal value As Color)
_backColor = value
End Set
End Property
Private _size As Size
Public Property Size() As Size
Get
Return _size
End Get
Set(ByVal value As Size)
_size = value
_path = Nothing
End Set
End Property
Private _location As Point
Public Property Location() As Point
Get
Return _location
End Get
Set(ByVal value As Point)
_location = value
_path = Nothing
End Set
End Property
3.2.2 路径管理
Location
和
Size
属性设置时会清除当前表示形状的
GraphicsPath
。使用延迟创建模式,当代码请求
Path
属性时才创建
GraphicsPath
。
Private _path As GraphicsPath
Public ReadOnly Property Path() As GraphicsPath
Get
' 路径根据需要自动刷新。
If _path Is Nothing Then
RefreshPath()
End If
Return _path
End Get
End Property
Private Sub RefreshPath()
_path = GeneratePath()
End Sub
Protected MustOverride Function GeneratePath() As GraphicsPath
3.2.3 焦点矩形
Shape
类新增了绘制焦点矩形的功能,通过
Selected
属性跟踪是否需要绘制。
Private _selected As Boolean
Public Property Selected() As Boolean
Get
Return _selected
End Get
Set
_selected = value
End Set
End Property
3.3 派生形状类
派生形状类(
RectangleShape
、
EllipseShape
和
TriangleShape
)只需实现
GeneratePath()
方法来确定控件区域。
Public Class RectangleShape
Inherits Shape
Protected Overrides Function GeneratePath() As GraphicsPath
Dim newPath As New GraphicsPath()
newPath.AddRectangle(New Rectangle( _
Location.X, Location.Y, Size.Width, Size.Height))
Return newPath
End Function
End Class
Public Class EllipseShape
Inherits Shape
Protected Overrides Function GeneratePath() As GraphicsPath
Dim newPath As New GraphicsPath()
NewPath.AddEllipse(Location.X, Location.Y, Size.Width, Size.Height)
Return newPath
End Sub
End Class
Public Class TriangleShape
Inherits Shape
Protected Overrides Function GeneratePath() As GraphicsPath
Dim newPath As New GraphicsPath()
Dim pt1 As New Point(Location.X + Size.Width / 2, Location.Y)
Dim pt2 As New Point(Location.X, Location.Y + Size.Height)
Dim pt3 As New Point(Location.X + Size.Width, Location.Y + Size.Height)
newPath.AddPolygon(New Point(){pt1, pt2, pt3})
Return newPath
End Function
End Class
3.4 绘图代码
Shape
类本身不能直接绘制,但将绘图逻辑集中在该类中。包含窗体可以通过调用
Render()
方法并传入
Graphics
对象来要求形状自行绘制。
Private penThickness As Integer = 5
Private focusBorderSpace As Integer = 5
Private outlinePen As Pen
Public Sub Render(ByVal g As Graphics)
If outlinePen IsNot Nothing Then outlinePen.Dispose()
outlinePen = New Pen(foreColor, penThickness)
Dim surfaceBrush As New SolidBrush(backColor)
' 绘制形状。
g.FillPath(surfaceBrush, Path)
g.DrawPath(outlinePen, Path)
' 如果需要,绘制焦点框。
If Selected Then
Dim rect As Rectangle = Rectangle.Round(Path.GetBounds())
rect.Inflate(New Size(focusBorderSpace, focusBorderSpace))
ControlPaint.DrawFocusRectangle(g, rect)
End If
surfaceBrush.Dispose()
End Sub
3.5 命中测试
形状需要具备命中测试任意点的能力,有三种命中测试类型:
- 检查点是否在形状内。
- 检查点是否在形状边缘。
- 检查点是否在形状周围的焦点提示(虚线矩形)上。
' 检查点是否在形状内。
Public Overridable Function HitTest(ByVal point As Point) As Boolean
Return Path.IsVisible(point)
End Function
' 检查点是否在形状的轮廓上。
Public Overridable Function HitTestBorder(ByVal point As Point) As Boolean
Return Path.IsOutlineVisible(point, outlinePen)
End Function
命中测试焦点边框更复杂,需要区分命中位置。通过枚举
HitSpot
表示不同可能性,并使用两个矩形(外矩形和内矩形)进行简单测试。
Public Enum HitSpot
Top
Bottom
Left
Right
TopLeftCorner
BottomLeftCorner
TopRightCorner
BottomRightCorner
None
End Enum
Public Function HitTestFocusBorder(ByVal point As Point, _
ByRef hitSpot As HitSpot) As Boolean
hitSpot = HitSpot.None
' 忽略没有焦点框的控件。
If Not selected Then
Return False
Else
Dim rectInner As Rectangle = Rectangle.Round(Path.GetBounds())
Dim rectOuter As Rectangle = rectInner
rectOuter.Inflate(New Size(focusBorderSpace, focusBorderSpace))
If rectOuter.Contains(point) And Not rectInner.Contains(point) Then
' 点在(或足够接近)焦点框上。
' 后续代码进行详细位置判断
...
Else
Return False
End If
End If
End Function
3.6 Z 顺序
控件有内置的分层支持,而
Shape
类需要手动实现此功能。首先定义
ZOrder
属性存储数值 z 索引值,然后实现
IComparable
接口的
CompareTo()
方法来比较形状对象的 z 顺序。
Private _zOrder As Integer
Public Property ZOrder() As Integer
Get
Return _zOrder
End Get
Set(ByVal value As Integer)
_zOrder = value
End Set
End Property
Public MustInherit Class Shape
Implements IComparable
Public Function CompareTo(ByVal obj As Object) As Integer _
Implements IComparable.CompareTo
Return ZOrder.CompareTo(CType(obj, Shape).ZOrder)
End Function
3.7 形状集合(Shape Collection)
创建自定义形状集合类
ShapeCollection
来处理形状对象的分组操作(如命中测试和重新排序)。该类继承自
CollectionBase
,添加了强类型的
Add()
和
Remove()
方法。
Public Class ShapeCollection
Inherits CollectionBase
Public Sub Remove(ByVal shapeToRemove As Shape)
List.Remove(shapeToRemove)
End Sub
Public ReadOnly Property Item(ByVal index as Integer) As Shape
Get
Return CType(List(index), Shape)
End Get
End Property
Public Sub Add(ByVal shapeToAdd As Shape)
' 重新排序形状,使新形状位于顶部。
For Each shape As Shape In List
shape.ZOrder += 1
Next
shapeToAdd.ZOrder = 0
List.Add(shapeToAdd)
End Sub
Public Sub BringShapeToFront(ByVal frontShape As Shape)
For Each shape As Shape In List
shape.ZOrder += 1
Next
frontShape.ZOrder = 0
End Sub
Public Sub SendShapeToBack(ByVal backShape As Shape)
Dim maxZOrder As Integer = 0
For Each shape As Shape in List
If shape.ZOrder > maxZOrder Then
maxZOrder = shape.ZOrder
End If
Next
maxZOrder += 1
backShape.ZOrder = maxZOrder
End Sub
Public Function HitTest(ByVal point As Point) As Shape
Sort()
For Each shape As Shape In List
If shape.HitTest(point) Or shape.HitTestBorder(point) Then
Return shape
End If
Next
Return Nothing
End Function
Public Sub Sort()
InnerList.Sort()
End Sub
Private ReverseComparer As New ReverseZOrderComparer()
Public Sub ReverseSort()
InnerList.Sort(ReverseComparer)
End Sub
End Class
创建
ReverseZOrderComparer
类实现反向排序
Public Class ReverseZOrderComparer
Implements IComparer
Public Function Compare(ByVal shapeA As Object, _
ByVal shapeB As Object) As Integer _
Implements IComparer.Compare
' 以相反顺序调用 CompareTo() 方法。
' 这将实现从高到低的排序。
Return CType(shapeB, Shape).CompareTo(CType(shapeA, Shape))
End Function
End Class
3.8 绘图表面操作
3.8.1 形状添加
绘图表面(窗体)通过
ShapeCollection
对象跟踪所有形状。添加形状时,设置相同的属性,将形状插入集合,并使窗体中添加新形状的部分无效。
Private shapes As New ShapeCollection()
shapes.Add(newShape)
Invalidate(newShape.GetLargestPossibleRegion())
3.8.2 绘制形状
窗体绘制时,按反向 z 顺序循环遍历形状,调用
Shape.Render()
方法绘制每个形状。
Private Sub DrawingSurface_Paint(ByVal sender As Object, _
ByVal e As PaintEventArgs) Handles MyBase.Paint
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias
' 擦除当前图像。
e.Graphics.Clear(Color.White)
' 确保顶部的形状遮挡底部的形状。
shapes.ReverseSort()
' 要求所有形状自行绘制。
For Each shape As Shape In shapes
shape.Render(e.Graphics)
Next
End Sub
3.8.3 优化绘制
为了优化绘制过程,可以检查无效区域是否与给定形状重叠,若不重叠则不绘制。还可以通过开启双缓冲(设置
Form.DoubleBuffered
属性为
True
)使渲染更平滑。
For Each shape As shape In shapes
If e.ClipRectangle.IntersectsWith(shape.GetLargestPossibleRegion()) Then
shape.Render(e.Graphics)
End If
Next
3.9 检测鼠标点击
处理鼠标点击是一个复杂的问题,最佳方法如下:
1. 检查是否有当前选中的形状,若有,测试焦点框是否命中,这具有最高优先级。
2. 若焦点框未命中,遍历所有形状进行命中测试(检查表面和边框)。
3. 若没有形状命中,清除最后选中的形状,并根据鼠标按钮显示菜单。
graph TD;
A[鼠标点击] --> B{有选中形状?};
B -- 是 --> C{焦点框命中?};
C -- 是 --> D[调整大小模式];
C -- 否 --> E[遍历形状命中测试];
B -- 否 --> E;
E --> F{有形状命中?};
F -- 是 --> G[选择新形状];
F -- 否 --> H[清除选中形状];
H --> I{右键点击?};
I -- 是 --> J[显示通用菜单];
I -- 否 --> K[无操作];
G --> L{右键点击?};
L -- 是 --> M[显示形状特定菜单];
L -- 否 --> N[拖动模式];
3.10 操作形状
选中形状后,可以进行更改背景颜色、删除形状、重新排序等操作。这些操作的代码与基于控件的版本类似,但使用
currentShape
变量引用形状对象,并使受影响的区域无效。
Private Sub mnuColorChange_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles mnuColorChange.Click
' 显示颜色对话框。
Dim dlgColor As New ColorDialog()
If dlgColor.ShowDialog() = DialogResult.OK Then
' 更改形状背景。
currentShape.BackColor = dlgColor.Color
Invalidate(currentShape.Region)
End If
End Sub
Private Sub mnuRemoveShape_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles mnuRemoveShape.Click
shapes.Remove(currentShape)
ClearSelectedShape()
End Sub
Private Sub mnuToFront_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles mnuToFront.Click
shapes.BringShapeToFront(currentShape)
Invalidate(currentShape.GetLargestPossibleRegion())
End Sub
Private Sub mnuToBack_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles mnuToBack.Click
shapes.SendShapeToBack(currentShape)
Invalidate(currentShape.GetLargestPossibleRegion())
End Sub
3.11 鼠标移动处理
鼠标移动时可能执行以下任务:
- 若拖动模式启用,移动控件。
- 若调整大小模式启用,调整控件大小。
- 若都未启用,检查鼠标指针是否靠近焦点框的边框,并相应更新鼠标指针。
Private Sub DrawingSurface_MouseMove(ByVal sender As Object, _
ByVal e As MouseEventArgs) Handles MyBase.MouseMove
If isDragging Then
Dim oldPosition, newPosition As Rectangle
oldPosition = currentShape.GetLargestPossibleRegion()
currentShape.Location = New Point(e.X - clickOffsetX, _
e.Y - clickOffsetY)
' 使包含旧位置和新位置的窗体部分无效。
newPosition = currentShape.GetLargestPossibleRegion()
Invalidate(Rectangle.Union(oldPosition, newPosition))
ElseIf isResizing Then
Dim minSize As Integer = 5
Dim oldPosition, newPosition As Rectangle
oldPosition = currentShape.GetLargestPossibleRegion()
' 根据调整大小模式调整控件大小。
Select Case resizingMode
Case Shape.HitSpot.Top, Shape.HitSpot.TopRightCorner
If e.Y < (currentShape.Location.Y + _
currentShape.Size.Height - minSize ) Then
currentShape.Size = New Size(currentShape.Size.Width, _
currentShape.Location.Y + currentShape.Size.Height - _
(e.Y - clickOffsetY))
currentShape.Location = New Point(currentShape.Location.X, _
e.Y - clickOffsetY)
End If
Case Shape.HitSpot.Bottom
If e.Y > (currentShape.Location.Y + minSize)
currentShape.Size = New Size(currentShape.Size.Width, _
e.Y - currentShape.Location.Y)
End If
Case Shape.HitSpot.Left, Shape.HitSpot.BottomLeftCorner, _
Shape.HitSpot.TopLeftCorner
If e.X < (currentShape.Location.X + _
currentShape.Size.Width - minSize) Then
currentShape.Size = New Size( _
(currentShape.Location.X + currentShape.Size.Width) - _
(e.X - clickOffsetX), currentShape.Size.Height)
currentShape.Location = New Point(e.X - clickOffsetX, _
currentShape.Location.Y)
End If
Case Shape.HitSpot.Right
If e.X > (currentShape.Location.X + minSize)
currentShape.Size = New Size(e.X - currentShape.Location.X, _
currentShape.Size.Height)
End If
Case Shape.HitSpot.BottomRightCorner
If e.Y > (currentShape.Location.Y + minSize) Then
currentShape.Size = New Size(currentShape.Size.Width, _
e.Y - currentShape.Location.Y)
End If
If e.X > (currentShape.Location.X + minSize) Then
currentShape.Size = New Size(e.X - currentShape.Location.X, _
currentShape.Size.Height)
End If
End Select
newPosition = currentShape.GetLargestPossibleRegion()
Invalidate(Rectangle.Union(oldPosition, newPosition))
Else
If currentShape IsNot Nothing AndAlso currentShape.Selected _
AndAlso currentShape.HitTestFocusBorder(New Point(e.X, e.Y), _
resizingMode) Then
Select Case resizingMode
Case Shape.HitSpot.Top, Shape.HitSpot.Bottom, _
Shape.HitSpot.TopRightCorner
Cursor = Cursors.SizeNS
Case Shape.HitSpot.Left, Shape.HitSpot.Right, _
Shape.HitSpot.BottomLeftCorner, Shape.HitSpot.TopLeftCorner
Cursor = Cursors.SizeWE
Case Shape.HitSpot.BottomRightCorner
Cursor = Cursors.SizeNWSE
Case Else
Cursor = Cursors.Arrow
End Select
Else
Cursor = Cursors.Arrow
End If
End If
End Sub
3.12 保存和加载图像
可以轻松实现将当前显示的所有形状保存到文件并在以后检索和重新显示的功能。通过为
Shape
和
ShapeCollection
类添加
Serializable
属性使其可序列化,对于不能序列化的成员添加
NonSerialized
属性。
<Serializable> _
Public Class Shape
Implements IComparable
...
End Class
<Serializable> _
Public Class ShapeCollection
Inherits CollectionBase
...
End Class
<NonSerialized> Private _path As GraphicsPath
3.12.1 序列化代码
Imports System.IO
Imports System.Runtime.Serialization.Formatters.Binary
Private Sub mnuSave_Click(ByVal sender As Object, ByVal e As EventArgs) _
Handles mnuSave.Click
If saveFileDialog.ShowDialog() = DialogResult.OK Then
Try
Dim fs As FileStream = File.Create(saveFileDialog.FileName)
Using fs
Dim f As New BinaryFormatter()
f.Serialize(fs, shapes)
End Using
Catch err As Exception
MessageBox.Show("Error while saving. " & err.Message)
End Try
End If
End Sub
3.12.2 反序列化代码
Private Sub mnuLoad_Click(ByVal sender As Object, ByVal e As EventArgs) _
Handles mnuLoad.Click
If openFileDialog.ShowDialog() = DialogResult.OK Then
Dim newShapes As ShapeCollection = Nothing
Try
Dim fs As FileStream = File.Open(openFileDialog.FileName, FileMode.Open)
Using fs
Dim f As New BinaryFormatter()
newShapes = CType(f.Deserialize(fs, Nothing), ShapeCollection)
End Using
Catch err As Exception
MessageBox.Show("Error while loading. " & err.Message)
Return
End Try
' 触发刷新。
shapes = newShapes
Invalidate()
End If
End Sub
4. 总结
开发动态绘图应用有两种方法:使用 .NET 控件支持构建程序和仅使用 GDI+ 手动构建。基于控件的方法是向业务应用添加绘图或图表功能的便捷方式,而底层方法则适合构建复杂的绘图应用。
4. 两种绘图方式对比总结
4.1 功能特性对比
| 特性 | 基于控件的绘图程序 | 基于形状对象的绘图程序 |
|---|---|---|
| 开发难度 | 相对较低,利用控件内置功能简化开发 | 较高,需手动实现诸多功能 |
| 渲染质量 | 处理重叠控件时效果不佳 | 借助 GDI+ 绘制完整图像,渲染更平滑 |
| 焦点提示 | 难以添加,受限于控件裁剪区域 | 可灵活实现,不受控件限制 |
| 高级功能扩展性 | 实现复杂功能较困难 | 便于添加分组、旋转、保存加载等高级功能 |
| 分层管理 | 控件有内置分层支持 | 需手动实现 Z 顺序管理 |
4.2 适用场景建议
- 基于控件的绘图程序 :适用于快速开发简单绘图功能,对绘图质量和高级功能要求不高的业务应用。例如,在企业内部的报表工具中添加简单的图形绘制功能。
- 基于形状对象的绘图程序 :适合开发专业绘图软件,需要处理复杂图形、高级功能和高质量渲染的场景。如建筑设计、机械制图等领域的绘图工具。
5. 优化与扩展建议
5.1 性能优化
-
减少无效绘制
:在绘制过程中,检查无效区域是否与形状重叠,避免不必要的绘制操作,如在
DrawingSurface_Paint方法中添加重叠检查逻辑。
For Each shape As shape In shapes
If e.ClipRectangle.IntersectsWith(shape.GetLargestPossibleRegion()) Then
shape.Render(e.Graphics)
End If
Next
-
双缓冲技术
:开启双缓冲(设置
Form.DoubleBuffered属性为True),减少绘图时的闪烁,提升用户体验。
Me.DoubleBuffered = True
5.2 功能扩展
-
添加更多形状类型
:在
ShapeType枚举中添加新的形状类型,并在派生形状类中实现相应的GeneratePath方法。
Public Enum ShapeType
Rectangle
Ellipse
Triangle
Pentagon ' 新增五边形
End Enum
Public Class PentagonShape
Inherits Shape
Protected Overrides Function GeneratePath() As GraphicsPath
Dim newPath As New GraphicsPath()
' 计算五边形的顶点
Dim centerX = Location.X + Size.Width / 2
Dim centerY = Location.Y + Size.Height / 2
Dim radius = Math.Min(Size.Width, Size.Height) / 2
Dim angle = 2 * Math.PI / 5
Dim points As New List(Of Point)()
For i As Integer = 0 To 4
Dim x = centerX + radius * Math.Cos(i * angle - Math.PI / 2)
Dim y = centerY + radius * Math.Sin(i * angle - Math.PI / 2)
points.Add(New Point(CInt(x), CInt(y)))
Next
newPath.AddPolygon(points.ToArray())
Return newPath
End Function
End Class
-
支持图形组合与分组
:实现图形组合功能,允许用户将多个形状组合成一个整体进行操作。可以创建一个
GroupShape类,继承自Shape类,内部管理多个子形状。
Public Class GroupShape
Inherits Shape
Private _shapes As New List(Of Shape)()
Public Sub AddShape(shape As Shape)
_shapes.Add(shape)
End Sub
Public Overrides Function GeneratePath() As GraphicsPath
Dim newPath As New GraphicsPath()
For Each shape In _shapes
newPath.AddPath(shape.Path, False)
Next
Return newPath
End Function
Public Overrides Sub Render(g As Graphics)
For Each shape In _shapes
shape.Render(g)
Next
End Sub
End Class
-
实现图形旋转与变形
:在
Shape类中添加旋转和变形的属性和方法,通过Graphics对象的变换功能实现图形的旋转和变形。
Public Class Shape
' 新增旋转角度属性
Private _rotationAngle As Double
Public Property RotationAngle() As Double
Get
Return _rotationAngle
End Get
Set(value As Double)
_rotationAngle = value
_path = Nothing
End Set
End Property
Public Overrides Sub Render(g As Graphics)
g.TranslateTransform(Location.X + Size.Width / 2, Location.Y + Size.Height / 2)
g.RotateTransform(_rotationAngle)
g.TranslateTransform(-(Location.X + Size.Width / 2), -(Location.Y + Size.Height / 2))
' 原绘制逻辑
If outlinePen IsNot Nothing Then outlinePen.Dispose()
outlinePen = New Pen(foreColor, penThickness)
Dim surfaceBrush As New SolidBrush(backColor)
g.FillPath(surfaceBrush, Path)
g.DrawPath(outlinePen, Path)
If Selected Then
Dim rect As Rectangle = Rectangle.Round(Path.GetBounds())
rect.Inflate(New Size(focusBorderSpace, focusBorderSpace))
ControlPaint.DrawFocusRectangle(g, rect)
End If
surfaceBrush.Dispose()
g.ResetTransform()
End Sub
End Class
5.3 用户交互优化
-
快捷键支持
:为常用操作添加快捷键,如复制、粘贴、删除等,提高用户操作效率。可以在窗体的
KeyDown事件中处理快捷键逻辑。
Private Sub DrawingSurface_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs) Handles MyBase.KeyDown
If e.Control AndAlso e.KeyCode = Keys.C Then
' 复制形状逻辑
ElseIf e.Control AndAlso e.KeyCode = Keys.V Then
' 粘贴形状逻辑
ElseIf e.KeyCode = Keys.Delete Then
' 删除形状逻辑
End If
End Sub
-
鼠标交互增强
:优化鼠标交互体验,如添加鼠标悬停提示、拖动时显示辅助线等功能。可以在
MouseMove事件中实现鼠标悬停提示逻辑。
Private Sub DrawingSurface_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles MyBase.MouseMove
If currentShape IsNot Nothing AndAlso currentShape.HitTest(New Point(e.X, e.Y)) Then
ToolTip1.SetToolTip(Me, "当前选中形状:" & currentShape.GetType().Name)
Else
ToolTip1.RemoveAll()
End If
' 原鼠标移动逻辑
' ...
End Sub
6. 总结与展望
6.1 总结
本文详细介绍了两种不同的绘图程序开发方法:基于控件的绘图程序和基于形状对象的绘图程序。基于控件的方法利用控件的内置功能,开发简单但存在一定局限性;基于形状对象的方法虽然开发难度较高,但具有更好的渲染质量、更强的功能扩展性和灵活性。
6.2 展望
随着技术的不断发展,绘图应用的需求也在不断增加。未来可以进一步探索以下方向:
-
跨平台支持
:将绘图应用扩展到不同的操作系统和设备上,如移动平台,提供更广泛的用户覆盖。
-
人工智能辅助绘图
:引入人工智能技术,如自动识别图形、智能填充颜色等,提高绘图效率和质量。
-
云端协作绘图
:实现多人在线协作绘图功能,用户可以实时共享和编辑绘图内容,提升团队协作效率。
通过不断优化和扩展绘图应用的功能,能够满足更多用户的需求,为不同领域的绘图工作提供更强大的支持。
超级会员免费看

被折叠的 条评论
为什么被折叠?



