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

WPF绘图画布控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (13投票s)

2012 年 9 月 25 日

CPOL

13分钟阅读

viewsIcon

129770

downloadIcon

8180

一个绘图工具程序,可以创建简化的XAML代码。

引言

我花了不少时间研究一个自定义的绘图画布,它可以实现一些简单但实用的功能,一个拥有正常绘图程序部分功能的程序,并且可以用来生成一些 XAML 内容。为了编写对他人来说有趣且有用的代码,我们经常希望使用“真实世界”的数据作为输入。这意味着,我们可能有一张图片,比如一个赛道,但仅凭这张图片构建 XAML 代码来编写其他花哨的 WPF 内容会很麻烦。

一如既往,我做的第一件事就是在 CodeProject 网站和其他地方进行搜索,并且我在本网站上找到了一个 WPF 绘图工具。我找到的文章是 WPF-DrawTools, 它涵盖了一个绘图程序的所有基本需求。我有一些不同的想法来实现一些功能,比如缩放和平移、加载图像和其他东西,这些都需要对程序进行大量重新设计,而我需要挑战,所以我决定自己编写绘图程序。

当我在网上寻找有趣的想法时,我发现本网站上有几篇文章给了我一些灵感;特别是以下两篇给我留下了深刻印象:

  • WPF: A* search 作者:Sacha Barber。这篇文章中的代码结构非常 nice,而且易于阅读。可以轻松维护和扩展代码,所以我尝试效仿,但这并不容易。
  • WPF Diagram Designer: Part 1 作者:sukram。他使用 Adorner 实现的调整大小的方式非常巧妙,我几乎没有做任何修改就使用了他的代码。

我将在此描述的控件将继承两个自定义控件以及框架本身的一个控件。唯一完全自定义的控件是我的绘图画布,它继承自 Canvas 控件,并进行了一些简单的修改。

背景

在我以前工作的地方,我曾花费大量时间编辑数字地图,并亲眼目睹了一个优秀的数字绘图程序编辑工具是多么重要。我也曾经历过缺乏这些工具的困难,它们几乎可以决定是痛苦还是不痛苦。不幸的是,一个真正优秀的通用绘图程序非常庞大,并且通常需要一个完整的编程团队才能以易于使用的方式实现。所以这个程序最终会有不足之处,但我希望它是一个好的开始。

基于我能找到的其他程序,最简单的方法似乎是创建一个继承自 Canvas 的自定义用户控件。这个画布将容纳所有绘制的元素,并且有必要覆盖一些默认属性。标准的画布也有一些问题,因为它本身并不支持滚动或限制。这需要通过使用一些外部控件来解决,以弥补这种情况。

我还必须创建一个用于调整绘图区域大小的控件,因为我不希望它无限大。这可能是最容易的部分,因为我实现了 WPF Diagram Designer 的项目,稍微调整了设计,然后就完成了。

最后,我还将需要创建一个继承自 Scroll View 控件的自定义控件。这个控件将负责平移和缩放主绘图区域,它也必须是最外层的控件,这意味着它将包含 Adorner 调整大小和自定义画布。

关于冒泡和隧道事件

像这样的绘图程序将不得不处理大量非常复杂的事件。事件需要根据将要执行的操作进行停止、隧道化和路由。我将从缩放和平移事件开始。经过一些思考,我最终得出结论,平移必须由滚动控件处理,缩放也必须从同一个控件发起,因为下面两个控件都将依赖于此。

我还需要部分地排除调整大小控件的缩放事件,因为如果我足够缩小,它就会变得不可见。它的尺寸,即高度和宽度,应该根据画布的高度和宽度的变化而变化。

然而,实际绘制图元的拖动和调整大小必须在画布控件本身上进行,因为它将拥有我创建的自定义类中所有必需的自定义框架元素信息。这意味着我的主自定义控件将由三个独立的部分组成:

  • 自定义设计的 Scroll viewer
  • 自定义调整大小控件,源自 CodeProject 网站上的这个项目。
  • 继承 Canvas 并添加缩放功能的自定义控件。

然而,这也意味着我必须设计所有进行实际绘图的控件,使其能够找出您是点击了一个点还是线,这意味着它们必须通过继承 FrameworkElement 并使用 DrawingContext 来保存显示在 Canvas 控件上的视觉对象。我将所有这些控件称为基本控件,因为它们构成了要绘制的元素的基础,而外部控件只是它们的容器块和组织者。坏消息是,一些信息必须跨越多个控件路由,这使得程序更难阅读和重用,但我真的不知道如何以其他方式实现。

创建基本绘图控件

该程序总共有 5 个实现绘图功能的控件。然而,它们都使用了相同的逻辑实现,尽管 Canvas Point 只是其他控件使用的元素,因为我需要在线和多边形上的点进行命中测试,所以它只被间接使用。这是我将展示完整代码的唯一控件,因为它最小,并且具有以下结构:

Imports System.Globalization
 
Public Class CanvasPoint
    Inherits FrameworkElement
 
    Implements System.ComponentModel.INotifyPropertyChanged
 
    ' Create a collection of child visual objects.
    Private _children As VisualCollection
 
#Region "Constructors"
    Public Sub New()
        _children = New VisualCollection(Me)
        _children.Add(CreateDrawingVisualCircle())
 
    End Sub
 
    Public Sub New(p_position As Point)
        PositionOnCanvas = p_position
 
        _children = New VisualCollection(Me)
        _children.Add(CreateDrawingVisualCircle())
    End Sub
#End Region
 
#Region "Properties"
 
    Private p_PositionOnCanvas As New Point
    Public Property PositionOnCanvas() As Point
        Get
            Return p_PositionOnCanvas
        End Get
        Set(ByVal value As Point)
            p_PositionOnCanvas = value
            INotifyChange("PositionOnCanvas")
        End Set
    End Property
#End Region
 
#Region "Overided properties"
 
    ' Provide a required override for the VisualChildrenCount property.
    Protected Overrides ReadOnly Property VisualChildrenCount() As Integer
        Get
            Return _children.Count
        End Get
    End Property
 
    ' Provide a required override for the GetVisualChild method.
    Protected Overrides Function GetVisualChild(ByVal index As Integer) As Visual
        If index < 0 OrElse index >= _children.Count Then
            Throw New ArgumentOutOfRangeException()
        End If
 
        Return _children(index)
    End Function
 
#End Region
 
#Region "Drawing"
    ' Create a DrawingVisual that contains a rectangle.
    Private Function CreateDrawingVisualCircle() As DrawingVisual
        Dim drawingVisual As New DrawingVisual()
 
        ' Retrieve the DrawingContext in order to create new drawing content.
        Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()
 
        ' Create a circle and draw it in the DrawingContext.
        drawingContext.DrawEllipse(Brushes.Red, New Pen(Brushes.Red, 1.0), PositionOnCanvas, 4, 4)
 
        ' Persist the drawing content.
        drawingContext.Close()
 
        Return drawingVisual
    End Function
 
#End Region
 
#Region "Events"
    Public Sub INotifyChange(ByVal info As String)
        RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(info))
    End Sub
 
    Public Event PropertyChanged(sender As Object, _
           e As System.ComponentModel.PropertyChangedEventArgs) _
           Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
#End Region
 
End Class

下面给出了实现的另外四个控件

  • CustomLine
  • CustomPolygon
  • Measurement (带文本的自定义点)
  • CustomPictureControl
它们包含更复杂的处理,因为它们不可避免地会有更多的元素需要容纳。您还应该注意到,我可以创建一组属性来控制设计的各个方面。为简单起见,这些属性并未在此程序中实现。

DrawingCanvas 控件

这是程序中最复杂的控件,因为它处理大部分绘图功能,并停止和发起几乎所有功能。

DrawingCanvas 的开头包含一组私有变量,用于预览绘图。我允许预览绘图的方式是,在名为 MouseLeftButtonDownOnCanvas 的 PointCollection 中注册整个鼠标左键按下事件。当发生鼠标移动事件时,我只需克隆鼠标左键按下 PointCollection,并将 MouseMove 当前鼠标位置添加为 DottedLineForPreviewDrawing 的最后一个元素。当我点击鼠标右键时,预览绘图和 PolyLines 的实线绘图将被清除。

Public Class DrawingCanvas
    Inherits Canvas
    Implements ComponentModel.INotifyPropertyChanged

#Region "Private variables and properties"

    'Get the Zoom area 
    Private RectZoom As New Rectangle

    'Temporary lines used while drawing 
    Private SolidLineForDrawing, DottedLineForPreviewDrawing As New Polyline

    'Stores all the mouse button Left button clicks, used in mouse move
    Private MouseLeftButtonDownOnDrawingCanvas As New PointCollection

    'Variables used for dragging elements
    Private startPoint As Point
    Private selectedElementOrigins As Point
    Private isDragging As Boolean
#End Region

#Region "Constructor"
    Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(DrawingCanvas), _
               New FrameworkPropertyMetadata(GetType(DrawingCanvas)))

        'Create and add an empty polyline to the DrawingCanvas
        SolidLineForDrawing.StrokeThickness = 2
        SolidLineForDrawing.Stroke = Brushes.Black
        Me.Children.Add(SolidLineForDrawing)

        'Create and add an empty polyline to the DrawingCanvas
        DottedLineForPreviewDrawing.StrokeThickness = 1
        DottedLineForPreviewDrawing.Stroke = Brushes.Black
        Dim d As New DoubleCollection
        d.Add(3.5)
        d.Add(1.5)
        DottedLineForPreviewDrawing.StrokeDashArray = d
        Me.Children.Add(DottedLineForPreviewDrawing)

        'Enables a rectangle of the zoom in on DrawingCanvas
        RectZoom.Fill = Brushes.Transparent
        RectZoom.Stroke = Brushes.Black
        RectZoom.StrokeThickness = 2
        RectZoom.StrokeDashArray = d
        RectZoom.Height = 0
        RectZoom.Width = 0
        Me.Children.Add(RectZoom)

        'Set clip to bounds true, as elements outside
        'the Decorator should not be visible unless you increas the size
        Me.ClipToBounds = True
    End Sub
#End Region

接下来是一些依赖属性,以及 SelectedDrawingEvent 的类型,如下面的枚举所示

Public Enum SelectedDrawingEvent
    SelectCursor
    Hand
    ZoomInRect
    PlacePoint
    DrawLine
    DrawClosedPolygon
    AddPoints
End Enum

如果按住 Ctrl 键并转动鼠标滚轮,您可以调整选定元素的缩放比例。

''' <summary>
''' Resize the selected element only
''' </summary>
''' <param name="e"></param>
''' <remarks>Should be called from Parant control</remarks>
Public Sub ChangeSizeOfElement(ByVal e As MouseWheelEventArgs)

    'Ued to translate mouse wheel argument to zoom factor
    Dim scalefactor As Double

    'Creating And Storing The scalefactor
    Dim scale As Double = 1
    'Transform the MouseWheel to the scalefactor
    If e.Delta > 0 Then
        scalefactor = 0.1
    ElseIf e.Delta < 0 Then
        scalefactor = -0.1
    End If

    'The scale cannot be negative
    If scale + scalefactor < 0 Then
        Exit Sub
    End If

    'Adjusting the old scale
    scale = scale + scalefactor

    If TypeOf (SelectedElement) Is CustomPolygon Then
        Dim NewPointCollection As New PointCollection
        Dim OldCenterPoint As New Point
        Dim NewCenterPoint As New Point
        Dim PointerCustomPolygon As CustomPolygon = DirectCast(SelectedElement, CustomPolygon)
        For Each p As Point In PointerCustomPolygon.Points
            OldCenterPoint.X += p.X
            OldCenterPoint.Y += p.Y
        Next

        OldCenterPoint.X /= PointerCustomPolygon.Points.Count
        OldCenterPoint.Y /= PointerCustomPolygon.Points.Count

        NewCenterPoint.X = OldCenterPoint.X * scale
        NewCenterPoint.Y = OldCenterPoint.Y * scale

        Dim correction As New Point
        correction.X = OldCenterPoint.X - NewCenterPoint.X
        correction.Y = OldCenterPoint.Y - NewCenterPoint.Y

        For Each p As Point In PointerCustomPolygon.Points
            NewPointCollection.Add(New Point(p.X * scale + correction.X, p.Y * scale + correction.Y))
        Next

        PointerCustomPolygon.Points = NewPointCollection
        PointerCustomPolygon.ReDraw()

    ElseIf TypeOf (SelectedElement) Is CustomLine Then
        Dim PointerCustomLine As CustomLine = DirectCast(SelectedElement, CustomLine)

        Dim NewPointCollection As New PointCollection
        Dim OldCenterPoint As New Point
        Dim NewCenterPoint As New Point

        For Each CustomLinePoint As Point In PointerCustomLine.Points
            OldCenterPoint.X += CustomLinePoint.X
            OldCenterPoint.Y += CustomLinePoint.Y
        Next

        OldCenterPoint.X /= PointerCustomLine.Points.Count
        OldCenterPoint.Y /= PointerCustomLine.Points.Count

        NewCenterPoint.X = OldCenterPoint.X * scale
        NewCenterPoint.Y = OldCenterPoint.Y * scale

        Dim MassCenterCorrection As New Point
        MassCenterCorrection.X = OldCenterPoint.X - NewCenterPoint.X
        MassCenterCorrection.Y = OldCenterPoint.Y - NewCenterPoint.Y

        For Each p As Point In PointerCustomLine.Points
            NewPointCollection.Add(New Point(p.X * scale + MassCenterCorrection.X, _
                                   p.Y * scale + MassCenterCorrection.Y))
        Next

        PointerCustomLine.Points = NewPointCollection
        PointerCustomLine.ReDraw()
    End If

End Sub

您也可以通过按住鼠标在直线上来移动整个 LinePolygon。如果您右键单击其中一个点,您只会移动选定的点而不是整个元素。

DrawingCanvas 上的鼠标左键按下事件在下面的代码中给出。由于复杂的拖动事件应用于点和线,我必须同时控制预览鼠标按下和鼠标按下事件。

Private Sub CanvasDraw_MouseDown(sender As System.Object, _
           e As System.Windows.Input.MouseButtonEventArgs) Handles Me.PreviewMouseDown
    Dim Actual_position, Modified_position As New Point
    Actual_position = Mouse.GetPosition(Me)
    Modified_position = Mouse.GetPosition(Me)

    MousePosition = "X: " & CInt(Actual_position.X) & " Y: " & CInt(Actual_position.Y)

    If CanvasEvent = SelectedDrawingEvent.SelectCursor Then
        If Mouse.RightButton = MouseButtonState.Pressed Then
            CreateContextMenu()
        End If
    ElseIf CanvasEvent = SelectedDrawingEvent.AddPoints Then
        AddPointToCustomDrawingObject(Actual_position)
    ElseIf CanvasEvent = SelectedDrawingEvent.ZoomInRect Then
        RectangleZoom(Actual_position, e)
    Else
        If Mouse.LeftButton = MouseButtonState.Pressed Or Mouse.MiddleButton = MouseButtonState.Pressed Then
            If Not CanvasEvent = SelectedDrawingEvent.PlacePoint Then
                DrawCustomObjectWithLines(Actual_position, Modified_position)
            Else
                Dim NewCustomPoint As New CustomPoint("FileAttr", Actual_position)
                Me.Children.Add(NewCustomPoint)
                ClearTempVariables()
            End If
        ElseIf Mouse.RightButton = MouseButtonState.Pressed Then
            'Check if the Lines or Polygons could be ended or closed:
            If MouseLeftButtonDownOnDrawingCanvas.Count > 1 Then
                If CanvasEvent = SelectedDrawingEvent.DrawClosedPolygon Then
                    'Add a new polygon
                    Dim NewCustomPolygon As New CustomPolygon(MouseLeftButtonDownOnDrawingCanvas.Clone)
                    Me.Children.Add(NewCustomPolygon)
                    ClearTempVariables()
                ElseIf CanvasEvent = SelectedDrawingEvent.DrawLine Then
                    'Add a new line
                    Dim NewCustomLine As New CustomLine(MouseLeftButtonDownOnDrawingCanvas.Clone)
                    Me.Children.Add(NewCustomLine)
                    ClearTempVariables()
                Else
                    'The program should not have come this far
                    ClearTempVariables()
                    MessageBox.Show("You have not selected type of drawing")
                End If
                e.Handled = True
            Else
                ClearTempVariables()
            End If
        End If
    End If
End Sub

我还注册了两个事件。一个 INotifyPropertyChanged 和一个 ChangeRectangelZoom。后者是为了将 Select 缩放发送到自定义 DrawingScrollbar。该控件处理 DrawingCanvas 上发生的所有缩放。

#Region "Events"
    Public Event PropertyChanged(sender As Object, e
 As System.ComponentModel.PropertyChangedEventArgs) Implements 
System.ComponentModel.INotifyPropertyChanged.PropertyChanged

    Private Sub NotifyPropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs(propertyName))
    End Sub

    Public Event NewRectangleZoom(ByVal TopLeft As Point, ByVal BottomRight As Point)

    Private Sub ChangeRectangleZoom(ByVal TopLeft As Point, ByVal BottomRight As Point)
        RaiseEvent NewRectangleZoom(TopLeft, BottomRight)
    End Sub
#End Region

正如您所注意到的,该控件具有一系列预览子程序,主要用于您需要检查事件类型并需要阻止其冒泡和穿过此点的场景。预览事件几乎总是后跟普通事件,其中所有必要的实现都不会在任何其他控件中发生。

还有一个我没有解释的功能:如果您按住 Shift 键,无论线条是 Polygon 还是 Line 绘图,都会出现 90 度弯曲。

画布上的缩放

当您进行缩放时,您通常希望将焦点保持在光标所在的位置,这意味着在使用鼠标滚轮放大后,画布上的位置保持不变。这需要对自定义 Scrollviewer 进行一些编码才能像这样工作,代码取自 MSDN WPF 论坛,并在 线程中给出。

所有需要做的就是利用 ScaleTransform 进行放大。然而,我能够设置渲染区域,这意味着我只需要对 DrawingCanas 应用 ScaleTransform,并计算实际绘图区域周围新的调整大小边框。这是通过在鼠标悬停在调整大小图标上时进行注册来完成的,如果您正在这样做,您就无法使用鼠标滚轮进行放大。DrawingScrollViewer 的完整类在下面给出。

Public Class DrawingScrollViewer
    Inherits ScrollViewer

#Region "Data"
    ' Used when manually scrolling
    Private scrollStartPoint As Point
    Private scrollStartOffset As Point

#End Region

    ' Since Im going to have clicable elements I want to turn off the possibility of moving the canvans and 
    ' stopping an event on it. I called e.Handle = True in these functions, as I wanted to move around freely 
    ' without thinking about getting hits from other elements.
    Public Shared ReadOnly HandProperty As DependencyProperty = _
       DependencyProperty.Register("Hand", _
       GetType([Boolean]), GetType(DrawingScrollViewer), New PropertyMetadata())

    Private _Hand As Boolean = False
    Public Property Hand() As Boolean
        Get
            Return _Hand
        End Get
        Set(ByVal value As Boolean)
            _Hand = value
        End Set
    End Property

#Region "Mouse Events"
    Protected Overrides Sub OnPreviewMouseDown(e As MouseButtonEventArgs)
        If Hand Then
            If IsMouseOver Then
                ' Save starting point, used later when determining how much to scroll.
                scrollStartPoint = e.GetPosition(Me)
                scrollStartOffset.X = HorizontalOffset
                scrollStartOffset.Y = VerticalOffset

                ' Update the cursor if can scroll or not.
                Me.Cursor = If((ExtentWidth > ViewportWidth) OrElse _
                    (ExtentHeight > ViewportHeight), Cursors.ScrollAll, Cursors.Arrow)

                Me.CaptureMouse()
            End If
            MyBase.OnPreviewMouseDown(e)
            e.Handled = True
        End If
    End Sub

    Protected Overrides Sub OnPreviewMouseMove(e As MouseEventArgs)
        If Hand Then
            If Me.IsMouseCaptured Then
                ' Get the new scroll position.
                Dim point As Point = e.GetPosition(Me)

                ' Determine the new amount to scroll.
                Dim delta As New Point(If((point.X > Me.scrollStartPoint.X), -(point.X - Me.scrollStartPoint.X), _
                   (Me.scrollStartPoint.X - point.X)), If((point.Y > Me.scrollStartPoint.Y),_
                    -(point.Y - Me.scrollStartPoint.Y), (Me.scrollStartPoint.Y - point.Y)))

                ' Scroll to the new position.
                ScrollToHorizontalOffset(Me.scrollStartOffset.X + delta.X)
                ScrollToVerticalOffset(Me.scrollStartOffset.Y + delta.Y)
            End If

            MyBase.OnPreviewMouseMove(e)
            e.Handled = True
        End If
    End Sub

    Protected Overrides Sub OnPreviewMouseUp(e As MouseButtonEventArgs)
        If Hand Then
            If Me.IsMouseCaptured Then
                Me.Cursor = Cursors.Arrow
                Me.ReleaseMouseCapture()
            End If
            MyBase.OnPreviewMouseUp(e)
            e.Handled = True
        End If
    End Sub

    Public Sub ScrollFromCode(ByVal CenterPoint As Point)
        ScrollFromRectangleZoom = True
        OldSenterPoint = CenterPoint
    End Sub

    Private OldSenterPoint As New Point
    Private ScrollFromRectangleZoom As Boolean = False

    ' This assunes that you want to keep the same center in your picture while zooming in
    Protected Overrides Sub OnScrollChanged(e As System.Windows.Controls.ScrollChangedEventArgs)
        MyBase.OnScrollChanged(e)

        'http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/c48379ca-a3e1-4930-a2a1-a7bf5d5f5cf9
        If e.Source Is Me Then
            If e.ExtentHeightChange <> 0 Or e.ExtentWidthChange <> 0 Then
                Dim mousePosition As Point
                If ScrollFromRectangleZoom Then
                    mousePosition = OldSenterPoint
                    ScrollFromRectangleZoom = False
                Else
                    mousePosition = Mouse.GetPosition(Me)
                End If


                Dim offsetx As Double = e.HorizontalOffset + mousePosition.X
                Dim offsety As Double = e.VerticalOffset + mousePosition.Y

                Dim oldExtentWidth As Double = e.ExtentWidth - e.ExtentWidthChange
                Dim oldExtentHeight As Double = e.ExtentHeight - e.ExtentHeightChange

                Dim relx As Double = offsetx / oldExtentWidth
                Dim rely As Double = offsety / oldExtentHeight

                offsetx = Math.Max(relx * e.ExtentWidth - mousePosition.X, 0)
                offsety = Math.Max(rely * e.ExtentHeight - mousePosition.Y, 0)

                Me.ScrollToHorizontalOffset(offsetx)
                Me.ScrollToVerticalOffset(offsety)

            End If
        End If
    End Sub
#End Region

End Class

所有缩放事件都托管在 DrawingCanvasControl 中,而它构成了您在主程序中实现的实际用户控件。然而,实现起来并不容易,因为我想附加一个特定的缩放功能,该功能执行以下操作:

仅更改 DrawingCanvas 的大小,这意味着我可以对该对象调用 LayoutTransfrom,但同时不更改 Resize 控件的布局。

关于样式

关于样式实现,尤其是用于选择绘图元素类型的单选按钮,我只说一点。它们被实现为一个自己的 MainViewModel 类,该类包含一个包含三个变量的 ObservableCollection。它如何连接到 Window 也在下面给出,您可以在 这里 阅读更多关于该方法的信息。

然而,有一件事您应该知道,关于将资源添加到您的项目。如果您像这样在主窗体中进行:

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Resources/AppStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
        <local:MainViewModel x:Key="ViewModel" />
        <ImageBrush x:Key="Ocean" ImageSource="Images/OceanWaves.jpg" Stretch="Fill"  />
    </ResourceDictionary> 
</Window.Resources>  

那么在您项目中存储的其他窗体将找不到这些样式。您也可以将它们作为资源添加到这些项目中,但这似乎有点麻烦。更简单的方法是将样式添加到主 Application.XAML 文件中,如下所示:

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/AppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources> 

这意味着您的所有样式都将在您的所有窗口中可用,无论它们存储在哪里。当然,您需要在主窗口中设置您的本地资源。

     <Window.Resources> 
            <local:MainViewModel x:Key="ViewModel" />
            <ImageBrush x:Key="Ocean" ImageSource="Images/OceanWaves.jpg" Stretch="Fill"  /> 
    </Window.Resources> 

主窗口中关于 Radiobuttons 的样式是这样做的:

<StackPanel Orientation="Horizontal" Background="Black" 
        x:Name="rbtn" DataContext="{StaticResource ViewModel}" Height="47">
    <ItemsControl  VerticalAlignment="Center" 
                Margin="5" ItemsSource="{Binding Intersections}">
        <ItemsControl.Template>
            <ControlTemplate>
                <WrapPanel  Width="{TemplateBinding Width}" 
                     Height="{TemplateBinding Height}" 
                     FlowDirection="LeftToRight" IsItemsHost="true"/>
            </ControlTemplate>
        </ItemsControl.Template>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <RadioButton GroupName="Intersections" Margin="0" 
                             Height="30" Style="{StaticResource toggleStyle}" 
                             IsChecked="{Binding IsChecked, Mode=TwoWay}">
                        <RadioButton.Content>
                            <TextBlock Text="{Binding Text}" VerticalAlignment="Center" 
                               FontSize="14" Foreground="White" Margin="5" />
                        </RadioButton.Content>
                    </RadioButton>
                </WrapPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

并且样式是由 Michael Sync 的博客文章 Michael Sync 从 Silverlight 模板转换而来的,它在 AppStyles.XAML 中实现并设置:

<!--Glass Styled Radiobutton-->
<Style x:Key="toggleStyle" 
         BasedOn="{StaticResource {x:Type ToggleButton}}" 
         TargetType="{x:Type RadioButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border BorderBrush="#FFFFFFFF" BorderThickness="1,1,1,1" >
                    <Border.Triggers>
                    <EventTrigger RoutedEvent="Border.MouseEnter">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                                Storyboard.TargetName="glow" 
                                                Storyboard.TargetProperty="(UIElement.Opacity)">
                                            <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="1"/>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                        <EventTrigger RoutedEvent="Border.MouseLeave">
                            <EventTrigger.Actions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                                Storyboard.TargetName="glow" 
                                                Storyboard.TargetProperty="(UIElement.Opacity)">
                                            <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger.Actions>
                        </EventTrigger>
                    </Border.Triggers>
                    <Border x:Name="border" Background="#7F000000" 
                          BorderBrush="#FF000000" BorderThickness="1,1,1,1" 
                          CornerRadius="4,4,4,4">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="0.507*"/>
                                <RowDefinition Height="0.493*"/>
                            </Grid.RowDefinitions>
                            <Border Opacity="0" HorizontalAlignment="Stretch" 
                                    x:Name="glow" Width="Auto" 
                                    Grid.RowSpan="2" CornerRadius="4,4,4,4">
                                <Border.Background>
                                    <RadialGradientBrush>
                                        <RadialGradientBrush.RelativeTransform>
                                            <TransformGroup>
                                                <ScaleTransform ScaleX="1.702" ScaleY="2.243"/>
                                                <SkewTransform AngleX="0" AngleY="0"/>
                                                <RotateTransform Angle="0"/>
                                                <TranslateTransform X="-0.368" Y="-0.152"/>
                                            </TransformGroup>
                                        </RadialGradientBrush.RelativeTransform>
                                        <GradientStop Color="#B28DBDFF" Offset="0"/>
                                        <GradientStop Color="#008DBDFF" Offset="1"/>
                                    </RadialGradientBrush>
                                </Border.Background>
                            </Border>
                            <ContentPresenter HorizontalAlignment="Center" 
                              VerticalAlignment="Center" Width="Auto" Grid.RowSpan="2"/>
                            <Border HorizontalAlignment="Stretch" 
                                  Margin="0,0,0,0" x:Name="shine" Width="Auto" >
                                <Border.Background>
                                    <LinearGradientBrush EndPoint="0.494,0.889" StartPoint="0.494,0.028">
                                        <GradientStop Color="#99FFFFFF" Offset="0"/>
                                        <GradientStop Color="#33FFFFFF" Offset="1"/>
                                    </LinearGradientBrush>
                                </Border.Background>
                            </Border>
                        </Grid>
                    </Border>
                </Border>
   
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="true">
                        <Setter Property="Background" 
                               TargetName="border" Value="Blue"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

如何使用

假设您想绘制一个赛车跑道,您可以获得完整跑道的卫星照片。您想将跑道集成到您程序中的 XAML 代码中。您只需这样做:

  1. 点击“添加图片”,然后添加跑道的概览图片。
  2. 通过鼠标指针为蓝色时使用鼠标滚轮放大。如果您想稍微平移图像,只需点击“抓手”图标进行拖动,然后将图像拖动到适当位置。
  3. 选择“绘制多边形”(假设跑道是圆形的)。单击代表跑道的点,然后用鼠标右键单击结束以关闭多边形。您不必非常精确,因为之后可以调整跑道。您可以通过选择“指针”并单击多边形来选中它。单击“添加点”按钮。然后将鼠标放在您想要添加点的位置。
  4. 这将完成赛车跑道,但您想放大跑道。您单击“指针”按钮,选择多边形。按住 Ctrl 键并开始使用鼠标滚轮,您将看到跑道被放大或缩小,现在跑道尺寸合适,但有些车道超出了边界。
  5. 您将鼠标移到左下角;确保“指针”按钮已单击。它位于灰色方块的外面。按住鼠标左键,然后简单地拖入空白的灰色区域,您会看到它会变大。现在您想移动整个赛道。
  6. 再次确保“指针”按钮是蓝色的,然后单击并按住(鼠标左键在其中一个线段上)。开始拖动它,将其移到正确的位置。
  7. 单击“导出到 XAML”,将弹出一个窗口显示组成赛车跑道电路的所有线条。

提示:您通过 RightMouseButtonDown 完成每个绘图或缩放事件。

是的,我曾想过为 Marcelo Ricardo de Oliveira 的 WPF Grand Prix 程序做一个编辑器。

那么,还缺少什么?

嗯……其实还不少。有些东西我没有包含,因为我认为程序代码已经足够满足我的需求。最迫切的需求可能是:

  • 一个绑定到选定元素的属性编辑器,您可以在其中设置画布上显示的各种形状的属性。这需要为每个自定义用户控件创建大量依赖属性。不过,我可以使用 CodePlex 网站上这个项目的属性编辑器。
  • 一个撤销历史记录,以便您可以返回。
  • 将绘图保存到文件,当然还有从文件加载。
  • 缺少一些基本元素:圆形、椭圆形、贝塞尔曲线段等。我没有包含这些,但它们在绘图程序中肯定很有用。

历史

好了,这就是全部内容。还有一些样式问题我没有提到,但我认为它们并不难找出,我将它们排除在文章之外,并将您引向源代码进行查看。

无论如何,希望您觉得这个工具很有用。我可能会在未来重新审视这个程序并进一步扩展其功能。

参考文献

在程序的创建过程中使用了一些 CodeProject 文章:

我还使用了我自己关于“向 Polyline 添加点”的文章。

GlassButton 样式取自 Michael Sync 的博客文章:

© . All rights reserved.