25、动态绘图应用开发:从控件到对象的实现

动态绘图应用开发:从控件到对象的实现

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 展望

随着技术的不断发展,绘图应用的需求也在不断增加。未来可以进一步探索以下方向:
- 跨平台支持 :将绘图应用扩展到不同的操作系统和设备上,如移动平台,提供更广泛的用户覆盖。
- 人工智能辅助绘图 :引入人工智能技术,如自动识别图形、智能填充颜色等,提高绘图效率和质量。
- 云端协作绘图 :实现多人在线协作绘图功能,用户可以实时共享和编辑绘图内容,提升团队协作效率。

通过不断优化和扩展绘图应用的功能,能够满足更多用户的需求,为不同领域的绘图工作提供更强大的支持。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值