[WPF] [AMindMap] 开发手记-4 (UI ANode的在代码中触发动画)
一路下来,ANode的大体结构基本要完工了,我之所以说这是个结构,因为,这是一次
渐进式开发,各个部分的处理方式,大体上已经确定下来了,如有后续内容,将依照现在
的模式去做。
关于 动画 的 触发 |
今天说一下代码中触发动画,这个话题其实很短,那就加上,ANode的一个简单测试环境
其实也是,后面我们UI主窗口的一个基本结构。
从代码中触发动画其实只有一句话,简单到不行。
BeginStoryboard(Me.Resources.Item("SelectedStatus"))
SelectedStatus就是我们当初在Xaml中定义的画板,用BeginStoryboard执行一下,就
Ok。
ANode的事件触发,目前的几种状态触发,我都是通过代码实现的,因为,在触发的同时
要抛出事件,还有设置状态标识,为了统一起见,我都放在代码中实现了。
还是列表的形式来说明各个状态是在什么情况下出现的吧
ANode的状态触发条件 | |||
状态 | 触发条件 | 附加条件 | 响应 |
选中状态 | 鼠标进入ANode | 非编辑状态 | 1.触发动画
2.设置当前ANode为活动 (关系到失去焦点的ANode,恢复正常状态) 3.抛出OnSelected事件 |
鼠标在NodeGrid上抬起 | 无 | 1.触发动画
2.抛出DragEnd事件 |
|
拖放状态 | 鼠标在NodeGrid上按下 | 无 | 1.触发动画
2.抛出DragStart事件 |
编辑状态 | 鼠标在NodeTextInput上抬起 | 无 | 1.触发动画
2.设置IsEdit标志位 |
正常状态 | 自定义方法SetNormal | 无 | 1.触发动画
2.设置IsEdit=False |
在这里要先说明一下,ANode在主界面上,会有很多很多,但是,每次选中的只有一个,
不会存在多选的形式(至少目前,我所预期的方式不会),那么当ANode被选中时,将取消
上一个被选中的ANode,在这里我用了一个Shared方法,来实现同一的管理。
1 Private Shared ActiveNode As ANode 2 3 Private Shared Sub SetActiveNode(ByVal n As ANode) 4 If Not ActiveNode Is Nothing Then 5 If Not ActiveNode.Equals(n) Then 6 ActiveNode.ZIndex = 0 7 ActiveNode.SetNormal() 8 End If 9 End If 10 ActiveNode = n 11 If Not n Is Nothing Then 12 n.ZIndex = 1 13 End If 14 End Sub 15 16 Public Shared Sub SetAllNormal() 17 SetActiveNode(Nothing) 18 End Sub
一个静态的变量标识这整个ANode群的活动单元,一个私有的SetActiveNode,
用来当当前单元被选中时将自己设置为活动单元,并取消前一个单元的活动状态。
并且,加了一个SetAllNormal方法,给外部提供的全部取消活动的法。
以下是各个状态相关的方法和变量的代码
1 2 #Region " 选择状态 " 3 Public Event OnSelected(ByVal sender As ANode) 4 5 Private Sub NodeGrid_MouseEnter(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles NodeGrid.MouseEnter 6 If IsEdit Then Exit Sub 7 SetActiveNode(Me) 8 BeginStoryboard(Me.Resources.Item("SelectedStatus")) 9 RaiseEvent OnSelected(Me) 10 End Sub 11 #End Region 12 13 #Region " 拖放状态 " 14 Public Event OnDragStart(ByVal sender As ANode, ByVal DragPoint As Point) 15 Public Event OnDragEnd(ByVal sender As ANode) 16 17 Private Sub NodeGrid_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles NodeGrid.MouseDown 18 BeginStoryboard(Me.Resources.Item("DragStatus")) 19 RaiseEvent OnDragStart(Me, e.GetPosition(sender)) 20 e.Handled = True 21 End Sub 22 23 Private Sub NodeGrid_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles NodeGrid.MouseUp 24 BeginStoryboard(Me.Resources.Item("SelectedStatus")) 25 RaiseEvent OnDragEnd(Me) 26 End Sub 27 #End Region 28 29 #Region " 编辑状态 " 30 Private IsEdit As Boolean = False 31 Private Sub NodeTextInput_PreviewMouseDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles NodeTextInput.PreviewMouseDown 32 BeginStoryboard(Me.Resources.Item("EditStatus")) 33 IsEdit = True 34 End Sub 35 #End Region 36 37 #Region " 普通状态 " 38 Public Sub SetNormal() 39 BeginStoryboard(Me.Resources.Item("NormalStatus")) 40 IsEdit = False 41 End Sub 42 #End Region 43 44 #Region " ActiveNode " 45 Private Shared ActiveNode As ANode 46 47 Private Shared Sub SetActiveNode(ByVal n As ANode) 48 If Not ActiveNode Is Nothing Then 49 If Not ActiveNode.Equals(n) Then 50 ActiveNode.ZIndex = 0 51 ActiveNode.SetNormal() 52 End If 53 End If 54 ActiveNode = n 55 If Not n Is Nothing Then 56 n.ZIndex = 1 57 End If 58 End Sub 59 60 Public Shared Sub SetAllNormal() 61 SetActiveNode(Nothing) 62 End Sub 63 #End Region
至此,ANode貌似就差不多介绍完了
哦,还有几个属性,Text,Background,Foreground,ZIndex
为了可以支持绑定,这些属性都是DependencyProperty。
代码也贴一下。
1 2 #Region " Text DependencyProperty " 3 ''' <summary> 4 ''' PropertyComment 5 ''' </summary> 6 ''' <remarks></remarks> 7 Public Shared ReadOnly TextProperty As DependencyProperty = _ 8 DependencyProperty.Register( 9 "Text", GetType(String), GetType(ANode), New PropertyMetadata( _ 10 "", New PropertyChangedCallback(AddressOf TextPropertyChanged_CallBack))) 11 12 Public Property Text() As String 13 Get 14 Return GetValue(TextProperty) 15 End Get 16 Set(ByVal Value As String) 17 SetValue(TextProperty, Value) 18 End Set 19 End Property 20 21 Public Shared Sub TextPropertyChanged_CallBack(ByVal dp As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs) 22 23 End Sub 24 #End Region 25 26 #Region " BackgroundColor DependencyProperty " 27 ''' <summary> 28 ''' PropertyComment 29 ''' </summary> 30 ''' <remarks></remarks> 31 Public Shared ReadOnly BackgroundColorProperty As DependencyProperty = _ 32 DependencyProperty.Register( 33 "BackgroundColor", GetType(Brush), GetType(ANode), New PropertyMetadata( _ 34 Brushes.LightBlue, New PropertyChangedCallback(AddressOf BackgroundColorPropertyChanged_CallBack))) 35 36 Public Property BackgroundColor() As Brush 37 Get 38 Return GetValue(BackgroundColorProperty) 39 End Get 40 Set(ByVal Value As Brush) 41 SetValue(BackgroundColorProperty, Value) 42 End Set 43 End Property 44 45 Public Shared Sub BackgroundColorPropertyChanged_CallBack(ByVal dp As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs) 46 Dim an As ANode = CType(dp, ANode) 47 an.NodeBackground.Background = e.NewValue 48 an.NodeBackground.BorderBrush = e.NewValue 49 With CType(e.NewValue, SolidColorBrush).Color 50 Dim brightness As Integer = Math.Max(.R, Math.Max(.G, .B)) 51 If brightness < 127 Then 52 an.ForegroundColor = Brushes.White 53 Else 54 an.ForegroundColor = Brushes.Black 55 End If 56 End With 57 End Sub 58 #End Region 59 60 #Region " ForegroundColor DependencyProperty " 61 ''' <summary> 62 ''' PropertyComment 63 ''' </summary> 64 ''' <remarks></remarks> 65 Public Shared ReadOnly ForegroundColorProperty As DependencyProperty = _ 66 DependencyProperty.Register( 67 "ForegroundColor", GetType(Brush), GetType(ANode), New PropertyMetadata( _ 68 Brushes.Black, New PropertyChangedCallback(AddressOf ForegroundColorPropertyChanged_CallBack))) 69 70 Public Property ForegroundColor() As Brush 71 Get 72 Return GetValue(ForegroundColorProperty) 73 End Get 74 Set(ByVal Value As Brush) 75 SetValue(ForegroundColorProperty, Value) 76 End Set 77 End Property 78 79 Public Shared Sub ForegroundColorPropertyChanged_CallBack(ByVal dp As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs) 80 Dim an As ANode = CType(dp, ANode) 81 an.NodeText.Foreground = e.NewValue 82 End Sub 83 #End Region 84 85 #Region " ZIndex DependencyProperty " 86 ''' <summary> 87 ''' PropertyComment 88 ''' </summary> 89 ''' <remarks></remarks> 90 Public Shared ReadOnly ZIndexProperty As DependencyProperty = _ 91 DependencyProperty.Register( 92 "ZIndex", GetType(Double), GetType(ANode), New PropertyMetadata( _ 93 0.0, New PropertyChangedCallback(AddressOf ZIndexPropertyChanged_CallBack))) 94 95 Public Property ZIndex() As Double 96 Get 97 Return GetValue(ZIndexProperty) 98 End Get 99 Set(ByVal Value As Double) 100 SetValue(ZIndexProperty, Value) 101 End Set 102 End Property 103 104 Public Shared Sub ZIndexPropertyChanged_CallBack(ByVal dp As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs) 105 106 End Sub 107 #End Region
Ok,ANode就完事儿了。
那么就搞一个对ANode的测试页面吧。
测试页 的 构建 |
建立一个Wpf应用程序工程
MainWindowViewModel
1 Imports System.Collections.ObjectModel 2 3 Public Class MainWindowViewModel 4 Inherits NotificationObject 5 ' 6 'NodeList As ObservableCollection(Of Node) 7 ' 8 Private mNodeList As ObservableCollection(Of Node) 9 Public Property NodeList() As ObservableCollection(Of Node) 10 Get 11 If mNodeList Is Nothing Then 12 mNodeList = New ObservableCollection(Of Node) 13 End If 14 Return mNodeList 15 End Get 16 Set(ByVal Value As ObservableCollection(Of Node)) 17 mNodeList = Value 18 RaisePropertyChanged("NodeList") 19 End Set 20 End Property 21 22 Public Sub New() 23 Dim newNode As New Node 24 newNode.Text = "新主题" 25 newNode.Top = 100 26 newNode.Left = 50 27 newNode.ZIndex = 0 28 newNode.Background = Brushes.LightCoral 29 NodeList.Add(newNode) 30 Dim node2 As New Node 31 node2.Text = "副本" 32 node2.Left = 200 33 node2.Top = 100 34 node2.ZIndex = 0 35 node2.Background = Brushes.Black 36 NodeList.Add(node2) 37 Dim node3 As New Node 38 With node3 39 .Text = "副本" 40 .Left = 300 41 .Top = 100 42 .ZIndex = 0 43 .Background = Brushes.Black 44 End With 45 46 NodeList.Add(node3) 47 End Sub 48 49 End Class 50
有一个用于绑定的NodeList属性。
为了简便起见,我自定义了NotificationObject类,而并没有引入Prism工具,
在ViewModel文件夹中,以后还是再建一个文件夹存放吧,现在稍显凌乱,也无伤大雅
关于NotificationObject,给不了解的同学补充一点东西吧,看代码。
1 Public Class NotificationObject 2 Implements ComponentModel.INotifyPropertyChanged 3 4 Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged 5 6 Public Sub RaisePropertyChanged(ByVal propertyName As String) 7 RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(propertyName)) 8 End Sub 9 End Class 10
MVVM中,为了让绑定的属性跟控件属性建立真正的联系也就是说,当被绑定属性(ViewModel
或Model中的属性)变化时,在控件上有所体现,Model或ViewModel必须实现
INotifyPropertyChanged接口,以便于通知控件属性变化,在ViewModel或Model中的属性
要这样写
1 2 Private mText As String 3 Public Property Text() As String 4 Get 5 Return mText 6 End Get 7 Set(ByVal Value As String) 8 mText = Value 9 RaisePropertyChanged("Text") 10 End Set 11 End Property 12
第9行,就是用来通知控件的。当然这里也要强调,控件属性必须是DependencyProperty才
能够被绑定。
Node类 对应与ANode,实现了几个需要被存储,和用于操作ANode在界面上的效果的几个属性。
这里,还有待于探讨,因为为了简便实现,Node参与了UI层的东西,而配合操作ANodeUI元素的
东西应该出现在ViewModel层。
1 2 Public Class Node 3 Inherits NotificationObject 4 5 ' 6 'Text As String 7 ' 8 Private mText As String 9 Public Property Text() As String 10 Get 11 Return mText 12 End Get 13 Set(ByVal Value As String) 14 mText = Value 15 RaisePropertyChanged("Text") 16 End Set 17 End Property 18 19 ' 20 'Left As Double 21 ' 22 Private mLeft As Double 23 Public Property Left() As Double 24 Get 25 Return mLeft 26 End Get 27 Set(ByVal Value As Double) 28 mLeft = Value 29 RaisePropertyChanged("Left") 30 End Set 31 End Property 32 33 ' 34 'Top As Double 35 ' 36 Private mTop As Double 37 Public Property Top() As Double 38 Get 39 Return mTop 40 End Get 41 Set(ByVal Value As Double) 42 mTop = Value 43 RaisePropertyChanged("Top") 44 End Set 45 End Property 46 47 ' 48 'ZIndex As integer 49 ' 50 Private mZIndex As Integer = 0 51 Public Property ZIndex() As Integer 52 Get 53 Return mZIndex 54 End Get 55 Set(ByVal Value As Integer) 56 mZIndex = Value 57 RaisePropertyChanged("ZIndex") 58 End Set 59 End Property 60 61 ' 62 'Background As Brush 63 ' 64 Private mBackground As Brush = Brushes.LightBlue 65 Public Property Background() As Brush 66 Get 67 Return mBackground 68 End Get 69 Set(ByVal Value As Brush) 70 mBackground = Value 71 RaisePropertyChanged("Background") 72 End Set 73 End Property 74 75 End Class
跨界的属性也只是ZIndex而已,如果实在没办法,就这样吧,唉唉。
MainWindowViewModel
1 Imports System.Collections.ObjectModel 2 3 Public Class MainWindowViewModel 4 Inherits NotificationObject 5 ' 6 'NodeList As ObservableCollection(Of Node) 7 ' 8 Private mNodeList As ObservableCollection(Of Node) 9 Public Property NodeList() As ObservableCollection(Of Node) 10 Get 11 If mNodeList Is Nothing Then 12 mNodeList = New ObservableCollection(Of Node) 13 End If 14 Return mNodeList 15 End Get 16 Set(ByVal Value As ObservableCollection(Of Node)) 17 mNodeList = Value 18 RaisePropertyChanged("NodeList") 19 End Set 20 End Property 21 22 Public Sub New() 23 Dim newNode As New Node 24 newNode.Text = "新主题" 25 newNode.Top = 100 26 newNode.Left = 50 27 newNode.ZIndex = 0 28 newNode.Background = Brushes.LightCoral 29 NodeList.Add(newNode) 30 Dim node2 As New Node 31 node2.Text = "副本" 32 node2.Left = 200 33 node2.Top = 100 34 node2.ZIndex = 0 35 node2.Background = Brushes.Black 36 NodeList.Add(node2) 37 Dim node3 As New Node 38 With node3 39 .Text = "副本" 40 .Left = 300 41 .Top = 100 42 .ZIndex = 0 43 .Background = Brushes.Black 44 End With 45 46 NodeList.Add(node3) 47 End Sub 48 49 End Class 50
看看MainWindow的代码吧
先是Xaml
1 <Window x:Class="MainWindow" x:Name="mainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:ct="clr-namespace:AMindMapControls;assembly=AMindMapControls" 5 Title="MainWindow" Height="350" Width="525"> 6 <Grid Background="Transparent"> 7 <ItemsControl Name="NodeLayer" ItemsSource="{Binding NodeList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"> 8 <ItemsControl.Template> 9 <ControlTemplate TargetType="{x:Type ItemsControl}"> 10 <ItemsPresenter/> 11 </ControlTemplate> 12 </ItemsControl.Template> 13 <ItemsControl.ItemContainerStyle> 14 <Style TargetType="{x:Type ContentPresenter}"> 15 <Setter Property="Canvas.Left" Value="{Binding Path=Left}"></Setter> 16 <Setter Property="Canvas.Top" Value="{Binding Path=Top}"></Setter> 17 <Setter Property="Canvas.ZIndex" Value="{Binding Path=ZIndex}"></Setter> 18 </Style> 19 </ItemsControl.ItemContainerStyle> 20 <ItemsControl.ItemTemplate> 21 <DataTemplate> 22 <ct:ANode x:Name="Node" Text="{Binding Text}" 23 OnDragStart="ANode_OnDragStart" 24 OnDragEnd="ANode_OnDragEnd" 25 ZIndex="{Binding ZIndex, Mode=OneWayToSource}" 26 BackgroundColor ="{Binding Background}" 27 > 28 29 </ct:ANode> 30 </DataTemplate> 31 </ItemsControl.ItemTemplate> 32 <ItemsControl.ItemsPanel> 33 <ItemsPanelTemplate> 34 <Canvas Name="NodeCanvas" Background="Transparent" 35 MouseMove="NodeCanvas_MouseMove" 36 MouseDown="NodeCanvas_MouseDown"> 37 </Canvas> 38 </ItemsPanelTemplate> 39 </ItemsControl.ItemsPanel> 40 </ItemsControl> 41 </Grid> 42 </Window> 43
Canvas是一个方便通过坐标来控制控件位置的容器,所以,像MindMap还是最好用Canvas
来实现,但是问题就来了,Canvas并不能绑定列表或表格形式的数据。而我们的Node是
列表形式的,也是为了方便存储。所以采用ItemsContorl来实现Canvas的列表数据绑定,
为什么要这么做呢,首先,从操作上看,ANode的个数不是固定的,而且会反复的增加和减少
如果自己管理ANode的增加和减少的话,将是不胜其烦的事情,而列表绑定,就变得非常轻松
了,只要对后台模型进行操作,即可实现UI层的对象的增添。也简化每个ANode对应后台的
绑定过程,像我这种懒人,简直是不二之选,哪怕打破UI和Model的松耦合,实际上,目前来看
处理有一个迂回的绑定(详见另一Post WPF 苦逼的迂回绑定),也没有其他的问题。
分解一下,MainWindow.Xaml
最外层Grid,背景被设置为Transparent,如果不设置任何背景色,将无法获得鼠标事件的支持。
我们还要拖动ANode。接下来是ItemsContorl,对于ItemsContorl和Canvas的关系,当然还有
ANode,请参考另三个Pos 将ObservableCollection(Of T) 数据 绑定到 Canvas (2)(3)
好吧,MainWindow.Xaml没什么好讲的了。
看看后台
1 Imports AMindMapControls 2 3 Class MainWindow 4 Private vm As New MainWindowViewModel 5 6 Public Sub New() 7 8 ' 此调用是设计器所必需的。 9 InitializeComponent() 10 11 ' 在 InitializeComponent() 调用之后添加任何初始化。 12 Me.DataContext = vm 13 End Sub 14 15 #Region " 拖放 " 16 Private DragPoint As Point 17 Private DragNode As ANode 18 19 Private Sub NodeCanvas_MouseMove(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseEventArgs) 20 If DragNode Is Nothing Then Exit Sub 21 With CType(DragNode.DataContext, Node) 22 .Left = e.GetPosition(sender).X - DragPoint.X 23 .Top = e.GetPosition(sender).Y - DragPoint.Y 24 End With 25 End Sub 26 27 Private Sub ANode_OnDragStart(ByVal sender As AMindMapControls.ANode, ByVal p As Point) 28 DragPoint = p 29 DragNode = sender 30 End Sub 31 32 Private Sub ANode_OnDragEnd(ByVal sender As AMindMapControls.ANode) 33 DragNode = Nothing 34 End Sub 35 #End Region 36 37 Private Sub NodeCanvas_MouseDown(ByVal sender As System.Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) 38 ANode.SetAllNormal() 39 End Sub 40 41 End Class 42
Wpf的事件传递 真是太复杂了,所以,用OnDragStart和OnDragEnd事件来标记拖拽的开始
和结束,并且在界面空白处按下鼠标,取消所有ANode的活动状态,测试完成,效果很理想
下期预告,为 脑图节点,建立后台模型和对模型进行基本管理。其实Node就是模型。