WPF控件开发思路,以视觉软件ROI绘制为例

WPF控件开发思路,以视觉软件ROI绘制为例

  此原则不仅仅引用于ROI绘制,而是WPF控件开发一般性原则,也可用于流程图编辑器等控件的开发中;

原则:

  1.使用数据驱动、属性驱动,而不是事件驱动

  2.ViewModel不依赖与WPF概念或相关类

  3.交互行为,如事件响应,图形渲染,应该被封装到自定义控件中

 

为什么:

  1. WPF的MVVM模式,就是要将编写业务和复杂的UI交互分离,最好能独立开,将编写业务的人从复杂的UI交互中解放出来;
  2. 如果在ViewModels中包含着UI概念和UI交互,如对鼠标,键盘,UI的显示控制,那就和Winform没什么分别,发挥不出MVVM的优势。
  3. 有些人可以写业务,对数据读写后端比较擅长,有些人对前端图形化比较擅长,通过MVVM可以职责划分和分工协作;

 

自定义控件与用户控件的区别:

  1. 技术上,二者都继承Control类,区别在于自定义空间可以自定义一个模板,已改变其外观,但其本质仍有其后台代码定义的,如Button,即使改变了外观,其后台代码不变,本质仍然是一个Button。
  2. 应用上,自定义控件应该封装了交互行为和绘制渲染行为,并通过暴露Command传递给ViewModel,并通过暴露的依赖属性与ViewModel中的数据同步,或根据ViewModel的数据来呈现。
  3. 用户控件,一般带有数据上下文信息,并组合一个或多个自定义控件,可以理解为ViewModel的视图映射,一般情况下,一个用户控件对应一个ViewModel,
  4. 二者区别:自定义控件更加偏向于通用的组件,用户控件更加偏向于面向特定的业务逻辑(这是由于它和ViewModel是对应的)。自定义控件一般不绑定一个数据上下文信息,用户控件一般对应着ViewModel的数据上下文

 

具体实操:

   现以ROIs绘制Demo为例,来解释一般强交互业务的开发思路:

数据模型,ROIs应该是一组对区域的描述的数据结构,以ArcROI为例,包含圆心及半径的像素坐标表示,及包含起始角度、终止角度,位置像素坐标

1 public class ArcROIDModel:
2 {
   Point CirclePosition,
4   Double Radius,
5   Double StartAngle,
6   Double EndAngle,
7 }

 

视图

  初步想到应该可以对应一个Shape,该Shape具有Radius, StartAngle, EndAngle,三个依赖属性,来渲染自身,至于为什么没有对应上述模型类中的 CirclePosition、Radius

的依赖属性,很简单,因为CirclePosition不属于Shape控件本身,而是由其与包含该元素的布局容器共同决定的;而Radius我们可以借助于控件自身的Width/Height依赖属性来实现(通过Radius可以计算出Width/Height依赖属性)

  第一步,我们先设计一个这样的Shape图形类,命名为RingShape,继承与WPF框架提供的Shape基类,我们很容易就设计出这样一个类,以下是代码片段:

 1 public sealed class RingShape : Shape
 2 {
 5         public static readonly DependencyProperty StartAngleProperty =
 6           DependencyProperty.Register("StartAngle", typeof(double), typeof(RingShape),
 7             new FrameworkPropertyMetadata(360d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
 8               new PropertyChangedCallback(OnStartAngleChanged)));
 9 
10         public double StartAngle
11         {
12             get
13             {
14                 return (double)GetValue(StartAngleProperty);
15             }
16             set
17             {
18                 SetValue(StartAngleProperty, value);
19             }
20         }
21 
22 
23     public static readonly DependencyProperty EndAngleProperty =
24           DependencyProperty.Register("EndAngle", typeof(double), typeof(RingShape),
25             new FrameworkPropertyMetadata(360d, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
26               new PropertyChangedCallback(OnEndAngleChanged), new CoerceValueCallback(CoerceEndAngleValue)));
27 
28         public double EndAngle
29         {
30             get
31             {
32                 return (double)GetValue(EndAngleProperty);
33             }
34             set
35             {
36                 SetValue(EndAngleProperty, value);
37             }
38         }
39 
40     ...
41
42 }

 

图形的渲染功能已经完成。

现在要考虑用户可以对图形通过拖拽来控制其Radius,StartAngle,EndAngle等,上述RingShape类不满足这种需要,而且从单一职责的角度,Shape也不应该具有这些功能;

所以考虑开发一个自定义控件来组合RingShape,然后增加拖拽式的交互,并将交互行为以Command的方式传递给ViewModel,并通过Binding的方式根据ViewModel中属性的变化来重绘控件

增加新的CustomControl,代码如下:

Xaml:

    <converters:AngleToSliceConverter x:Key="AngleToSliceConverter"/>
    <converters:StartAngleConverter x:Key="StartAngleConverter"/>
    <Style x:Key="ROIThumbStyle" TargetType="{x:Type Thumb}">
        <Setter Property="Stylus.IsPressAndHoldEnabled" Value="false"/>
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Foreground" Value="Red"/>
        <Setter Property="Width" Value="10"/>
        <Setter Property="Height" Value="10"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Thumb}">
                    <Rectangle Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Background}" StrokeThickness="2" Cursor="SizeAll"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="{x:Type shapeControl:ArcControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type shapeControl:ArcControl}">
                    <Grid>
                        <Thumb x:Name="MoveTracker" Style="{StaticResource ROIThumbStyle}"/>
                        <Ellipse StrokeDashArray="2 2" StrokeThickness="{TemplateBinding StrokeThickness}" Stroke="Yellow" IsHitTestVisible="False"/>
                        <shapeControl:RingShape SweepDirection="Counterclockwise" StartAngle="{TemplateBinding StartAngle,Converter={StaticResource StartAngleConverter}}" Slice="{TemplateBinding ArcAngle,Converter={StaticResource AngleToSliceConverter}}" Mode="Slice" Stroke="Red" StrokeThickness="{TemplateBinding StrokeThickness}" IsHitTestVisible="False"/>
                        <Thumb x:Name="StartAngleTracker" Style="{StaticResource ROIThumbStyle}">
                            <Thumb.RenderTransform>
                                <MatrixTransform/>
                            </Thumb.RenderTransform>
                        </Thumb>
                        <Thumb x:Name="EndAngleTracker" Style="{StaticResource ROIThumbStyle}">
                            <Thumb.RenderTransform>
                                <MatrixTransform/>
                            </Thumb.RenderTransform>
                        </Thumb>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Code behind:

  1    public class ArcControl : Control
  2     {
  3         private Thumb _StartAngleTracker;
  4         private Thumb _EndAngleTracker;
  5         private Thumb _MoveTracker;
  6         static ArcControl()
  7         {
  8             DefaultStyleKeyProperty.OverrideMetadata(typeof(ArcControl), new FrameworkPropertyMetadata(typeof(ArcControl)));
  9         }
 10 
 11         public ArcControl()
 12         {
 13             Loaded += ArcControl_Loaded;
 14         }
 15 
 16         public override void OnApplyTemplate()
 17         {
 18             base.OnApplyTemplate();
 19             _MoveTracker = GetTemplateChild("MoveTracker") as Thumb;
 20             _StartAngleTracker = GetTemplateChild("StartAngleTracker") as Thumb;
 21             _EndAngleTracker = GetTemplateChild("EndAngleTracker") as Thumb;
 22 
 23             _MoveTracker.DragDelta += _MoveTracker_DragDelta;//movetracker抽象出来做成组件
 24             _StartAngleTracker.DragDelta += _StartAngleTracker_DragDelta;
 25             _EndAngleTracker.DragDelta += _EndAngleTracker_DragDelta;
 26         }
 27 
 28         private void _EndAngleTracker_DragDelta(object sender, DragDeltaEventArgs e)
 29         {
 30             var center = new Point(RenderSize.Width / 2, RenderSize.Height / 2);
 31 
 32             var startVector = GetPointByAngle(StartAngle) - center;
 33             var endVector = (GetPointByAngle(StartAngle + ArcAngle) - center) + new Vector(e.HorizontalChange, e.VerticalChange);
 34             var angleDelta = Vector.AngleBetween(endVector,startVector);
 35 
 36             ArcAngle = angleDelta > 0 ? angleDelta : angleDelta + 360;//for test 需要command 发出
 37         }
 38 
 39         private void _StartAngleTracker_DragDelta(object sender, DragDeltaEventArgs e)
 40         {
 41             //var center = new Point(RenderSize.Width / 2, RenderSize.Height / 2);
 42             //// var endVector = (GetPointByAngle(StartAngle + ArcAngle) - center) ;
 43 
 44             //var startVector = (GetPointByAngle(StartAngle) - center) + new Vector(e.HorizontalChange, e.VerticalChange);
 45             //var angleDelta = Vector.AngleBetween(startVector, GetPointByAngle(StartAngle) - center);
 46             ////angleDelta = angleDelta >= 0 ? angleDelta : angleDelta + 360;
 47 
 48             //StartAngle = angleDelta;
 49             //ArcAngle -= angleDelta;
 50         }
 51 
 52         private void _MoveTracker_DragDelta(object sender, DragDeltaEventArgs e)
 53         {
 54             var newLeft = double.IsNaN(Canvas.GetLeft(this)) ? 0 : Canvas.GetLeft(this) + e.HorizontalChange;
 55             SetCurrentValue(Canvas.LeftProperty, newLeft);
 56 
 57             var newTop = double.IsNaN(Canvas.GetTop(this)) ? 0 : Canvas.GetTop(this) + e.VerticalChange;
 58             SetCurrentValue(Canvas.TopProperty, newTop);
 59         }
 60 
 61   
 62 
 63         private void ArcControl_Loaded(object sender, RoutedEventArgs e)
 64         {
 65             LocateDragTracker();
 66         }
 67 
 68         #region DP
 69         public static readonly DependencyProperty StartAngleProperty = DependencyProperty.Register(
 70             nameof(StartAngle), typeof(double), typeof(ArcControl), new PropertyMetadata(default(double), OnPropertyChanged));
 71 
 72 
 73 
 74         public double StartAngle
 75         {
 76             get { return (double)GetValue(StartAngleProperty); }
 77             set { SetValue(StartAngleProperty, value); }
 78         }
 79 
 80         public static readonly DependencyProperty ArcAngleProperty = DependencyProperty.Register(
 81             nameof(ArcAngle), typeof(double), typeof(ArcControl), new PropertyMetadata(90d, OnPropertyChanged));
 82 
 83         public double ArcAngle
 84         {
 85             get { return (double)GetValue(ArcAngleProperty); }
 86             set { SetValue(ArcAngleProperty, value); }
 87         }
 88 
 89         public static readonly DependencyProperty ShowTrackerProperty = DependencyProperty.Register(
 90             nameof(ShowTracker), typeof(bool), typeof(ArcControl), new PropertyMetadata(default(bool), OnPropertyChanged));
 91 
 92         public bool ShowTracker
 93         {
 94             get { return (bool)GetValue(ShowTrackerProperty); }
 95             set { SetValue(ShowTrackerProperty, value); }
 96         }
 97 
 98 
 99         public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(
100             nameof(StrokeThickness), typeof(double), typeof(ArcControl), new PropertyMetadata(2d, OnPropertyChanged));
101         //复用其他dp才用override dp,此处不用
102 
103         public double StrokeThickness
104         {
105             get { return (double)GetValue(StrokeThicknessProperty); }
106             set { SetValue(StrokeThicknessProperty, value); }
107         }
108 
109         #endregion
110 
111         private Point GetPointByAngle(double angle)
112         {
113             var ellipseRect = new Rect(StrokeThickness / 2, StrokeThickness / 2, RenderSize.Width - StrokeThickness, RenderSize.Height - StrokeThickness);
114             var position = EllipseHelper.PointOfRadialIntersection(ellipseRect, angle);
115 
116             //坐标变换
117             var matrix = Matrix.Identity;
118             matrix.Translate(0, -RenderSize.Height);
119             matrix.Scale(1, -1);
120             position = matrix.Transform(position);
121             return position;
122         }
123 
124 
125 
126 
127         private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
128         {
129             (d as ArcControl).LocateDragTracker();
130         }
131 
132         private void LocateDragTracker()
133         {
134             if (_StartAngleTracker == null || _EndAngleTracker == null)
135             {
136                 return;
137             }
138 
139             var startAngleTrackerTargetPosition = GetPointByAngle(StartAngle); //EllipseHelper.PointOfRadialIntersection(ellipseRect, StartAngle);
140                                                                                //物体仿射变换
141             var matrix = Matrix.Identity;
142             matrix.Translate(startAngleTrackerTargetPosition.X - RenderSize.Width / 2, startAngleTrackerTargetPosition.Y - RenderSize.Height / 2);
143             _StartAngleTracker.RenderTransform = new MatrixTransform(matrix);
144 
145             var endAngleTrackerTargetPosition = GetPointByAngle(StartAngle + ArcAngle);
146             matrix = Matrix.Identity;
147             matrix.Translate(endAngleTrackerTargetPosition.X - RenderSize.Width / 2, endAngleTrackerTargetPosition.Y - RenderSize.Height / 2);
148             _EndAngleTracker.RenderTransform = new MatrixTransform(matrix);
149         }
150 
151 
152 
153         #region Commands
154         public static readonly DependencyProperty ChangeAngleCommandProperty = DependencyProperty.Register(
155             nameof(ChangeAngleCommand), typeof(ICommand), typeof(ArcControl), new PropertyMetadata(null));
156 
157         public ICommand ChangeAngleCommand
158         {
159             get { return (ICommand)GetValue(ChangeAngleCommandProperty); }
160             set { SetValue(ChangeAngleCommandProperty, value); }
161         }
162 
163         public static readonly DependencyProperty MoveCommandProperty = DependencyProperty.Register(
164             nameof(MoveCommand), typeof(ICommand), typeof(ArcControl), new PropertyMetadata(null));
165 
166         public ICommand MoveCommand
167         {
168             get { return (ICommand)GetValue(MoveCommandProperty); }
169             set { SetValue(MoveCommandProperty, value); }
170         }
171 
172         #endregion
173 
174 
175     }
View Code

至此我们将ViewModel的相关通知属性和Command与该控件绑定,就可以自由的在ViewModel中控制改控件的显示方式,并可以自由地根据业务逻辑响应改变半径,改变StartAngle、EndAngle等命令了。

这样就可以其放置到一个Cavas上,或通过ListControl的数据模板的方式使用该控件了,有很高的复用性。

最终效果如下:

 

posted @ 2023-05-06 10:59  GARRYTSUI  阅读(1201)  评论(2)    收藏  举报