WPF自定義控制元件ChambersOverview(附原始碼)
背景
在很多時候我們需要用到WPF中的自定義控制元件,即我們想將一整套不同的控制元件組合成一個獨立的控制元件並且定義在一個獨立的自定義控制元件庫中,這樣整個控制元件就能夠得到更好的封裝和更好的獨立性並且在定義的時候有更大的靈活性,在這篇文章中我已ItemsControl作為主體通過擴充套件其ItemsPanel和ItemContainerStyle以及ItemTemplate來實現一個能夠對整個ItemsControl內部的Item進行移動拖拽並且改變ZIndex的獨立的CustomControl,這個裡面涉及到很多的概念,後面會進行一步步剖析來分析整個過程。
整體預覽
過程
1 增加自定義控制元件庫
這個熟悉自定義控制元件庫的肯定非常熟悉,定義xaml樣式和具體控制元件邏輯,這裡有一點需要注意,就是定義的樣式檔案必須要加入到Generic.xaml中,否則樣式無法生效,就像下面的程式碼描述的一樣將樣式xaml定義到Generic.xaml中的資源字典下面。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:ACM.Wpf.Toolkit"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/ACM.Wpf.Toolkit;component/Themes/Controls/ChambersOverview.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
2 定義樣式
這個是整個CustomControl整體的樣式,這個非常關鍵,我們先來看具體的定義,然後再一步步進行分析。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:ACM.Wpf.Toolkit.Controls" xmlns:data="clr-namespace:ACM.Wpf.Toolkit.Data" xmlns:converter="clr-namespace:ACM.Wpf.Toolkit.Data.Converters"> <converter:BoolToVisibilityConverter x:Key="boolToVisibilityConverter"></converter:BoolToVisibilityConverter> <converter:ChambersLocationConverter x:Key="chambersLocationConverter"></converter:ChambersLocationConverter> <converter:ChamberPositionConverter x:Key="chamberPositionConverter"></converter:ChamberPositionConverter> <Style TargetType="{x:Type controls:ChambersOverview}"> <Setter Property="Background" Value="Transparent"></Setter> <Setter Property="Margin" Value="1"></Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate> <Grid> <ItemsControl x:Name="itemsControl" Grid.Column="0" Margin="2" ItemsSource="{Binding Chambers}" MinWidth="220" MinHeight="220"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas IsItemsHost="True" Background="Transparent" /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemContainerStyle> <Style TargetType="ContentPresenter"> <Setter Property="Canvas.Left"> <Setter.Value> <MultiBinding Converter="{StaticResource chambersLocationConverter}" ConverterParameter="Left"> <Binding Path="Id"></Binding> <Binding Path="ChambersLocation" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType={x:Type controls:ChambersOverview}}"></Binding> </MultiBinding> </Setter.Value> </Setter> <Setter Property="Canvas.Top"> <Setter.Value> <MultiBinding Converter="{StaticResource chambersLocationConverter}" ConverterParameter="Top"> <Binding Path="Id"></Binding> <Binding Path="ChambersLocation" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType={x:Type controls:ChambersOverview}}"></Binding> </MultiBinding> </Setter.Value> </Setter> <Setter Property="Canvas.ZIndex"> <Setter.Value> <MultiBinding Converter="{StaticResource chambersLocationConverter}" ConverterParameter="ZIndex"> <Binding Path="Id"></Binding> <Binding Path="ChambersLocation" RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType={x:Type controls:ChambersOverview}}"></Binding> </MultiBinding> </Setter.Value> </Setter> <Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type controls:ChambersOverview}}}"></Setter> <Setter Property="ContextMenu"> <Setter.Value> <ContextMenu Visibility="{Binding Path=PlacementTarget.Tag.IsEditable,RelativeSource={RelativeSource Self},Converter={StaticResource boolToVisibilityConverter}}"> <ContextMenu.DataContext> <MultiBinding Converter="{StaticResource chamberPositionConverter}"> <Binding Path="PlacementTarget.DataContext.Id" RelativeSource="{RelativeSource Self}"></Binding> <Binding Path="PlacementTarget.Tag.ChambersLocation" RelativeSource="{RelativeSource Self}"></Binding> </MultiBinding> </ContextMenu.DataContext> <MenuItem Header="Move Up" Command="{Binding MoveUpOneLayer}" CommandParameter="{Binding PlacementTarget, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ContextMenu}}"></MenuItem> <MenuItem Header="Move Down" Command="{Binding MoveDownOneLayer}" CommandParameter="{Binding PlacementTarget, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ContextMenu}}"></MenuItem> <MenuItem Header="Move Bottom" Command="{Binding MoveBottomLayer}" CommandParameter="{Binding PlacementTarget, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ContextMenu}}"></MenuItem> <MenuItem Header="Move Front" Command="{Binding MoveFrontLayer}" CommandParameter="{Binding PlacementTarget, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ContextMenu}}"></MenuItem> </ContextMenu> </Setter.Value> </Setter> </Style> </ItemsControl.ItemContainerStyle> <ItemsControl.ItemTemplate> <DataTemplate> <ContentPresenter ContentTemplate="{Binding ChamberControlDataTemplate,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type controls:ChambersOverview}}}"></ContentPresenter> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <CheckBox Content="Can Edit" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="8 5" ToolTip="Is current chamberOverview can be edit?" IsChecked="{Binding IsEditable,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type controls:ChambersOverview}},Mode=TwoWay}"></CheckBox> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
2.1 定義ItemsPanel
這裡第一個重要的地方就是重新定義ItemsControl的ItemsPanel,我們想要讓所有的Item都能夠進行拖拽和移動必須首先重寫這個ItemsPanel,ItemsControl預設的樣式我們來看看,通過編輯ItemsControl的模板,我們可以看到ItemsControl的預設樣式。
<Style x:Key="ItemsControlStyle1" TargetType="{x:Type ItemsControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ItemsControl}"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true"> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <ItemsPanelTemplate x:Key="ItemsPanelTemplate1"> <StackPanel IsItemsHost="True"/> </ItemsPanelTemplate>
這裡我們使用Canvas作為ItemsPanel的樣式,另外這裡的IsItemsHost必須設定為True,這個非常重要。
2.2 定義ItemsContainerStyle
這裡ItemsPanel直接的子物件是ContentPresenter,而後面的ItemTemplate的視覺內容是作為ContentPresenter的子物件,這個必須要重點來理解,所以我們定義的最重要的三個屬性Canvas.Left以及Canvas.Top和Canvas.ZIndex都是直接作用於ContentPresenter而不是後面的ItemTemplate中的元素,因為對於ItemsPanel來說,ContentPresenter才是其直接的子元素,如果這樣還不太好理解我們在來看看這個自定義ChambersOverview控制元件的視覺樹。
圖一 ItemsControl視覺樹
在上面的紅框中Canvas就是我們在2.1中定義的ItemsPanel,所以這裡第一個ContentPresenter才是我們定義位置和寫滑鼠事件的直接作用的元素,這裡必須有一個清晰的認識。
2.3 定義ItemTemplate
這個部分其內部的具體內容是由使用方來定義的,所以這裡我們使用了一個ContentPresenter然後其ContentTemplate繫結到我們ChambersOverview中定義的一個ChamberControlDateTemplate這個依賴項屬性上面,因為這個部分要通過外部進行傳入的,我們先來簡單看看。
public DataTemplate ChamberControlDataTemplate { get { return (DataTemplate)GetValue(ChamberControlDataTemplateProperty); } set { SetValue(ChamberControlDataTemplateProperty, value); } } // Using a DependencyProperty as the backing store for ChamberControl. This enables animation, styling, binding, etc... public static readonly DependencyProperty ChamberControlDataTemplateProperty = DependencyProperty.Register("ChamberControlDataTemplate", typeof(DataTemplate), typeof(ChambersOverview), new PropertyMetadata(null));
3 定義控制元件類
這個是整個自定義控制元件的重中之重,在這個裡面我們需要定義xaml中繫結的各種資料來源,並且外部使用這個自定義控制元件的地方也將會資料繫結到這些依賴項屬性上面,這些依賴項屬性就像一個橋樑溝通具體UI元素和最終的資料來源,另外整個ChamberControl的拖拽對應的事件都是在這個類裡面來完成的,我們先來看整體的程式碼,然後再進行一步步分析整個過程。
using ACM.Wpf.Toolkit.Data.Models.Components.Chambers; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace ACM.Wpf.Toolkit.Controls { public class ChambersOverview : Control { private ItemsControl _itemsControl; #region Constructor static ChambersOverview() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ChambersOverview), new FrameworkPropertyMetadata(typeof(ChambersOverview))); } #endregion #region Dependency Properties public bool IsEditable { get { return (bool)GetValue(IsEditableProperty); } set { SetValue(IsEditableProperty, value); } } // Using a DependencyProperty as the backing store for IsEditable. This enables animation, styling, binding, etc... public static readonly DependencyProperty IsEditableProperty = DependencyProperty.Register("IsEditable", typeof(bool), typeof(ChambersOverview), new PropertyMetadata(true)); public DataTemplate ChamberControlDataTemplate { get { return (DataTemplate)GetValue(ChamberControlDataTemplateProperty); } set { SetValue(ChamberControlDataTemplateProperty, value); } } // Using a DependencyProperty as the backing store for ChamberControl. This enables animation, styling, binding, etc... public static readonly DependencyProperty ChamberControlDataTemplateProperty = DependencyProperty.Register("ChamberControlDataTemplate", typeof(DataTemplate), typeof(ChambersOverview), new PropertyMetadata(null)); /// <summary> /// record chambers which is refer to itemsSource /// </summary> public IList Chambers { get { return (IList)GetValue(ChambersProperty); } set { SetValue(ChambersProperty, value); } } // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty ChambersProperty = DependencyProperty.Register("Chambers", typeof(IList), typeof(ChambersOverview), new PropertyMetadata(null, (s, e) => { var chambersOverview = s as ChambersOverview; var contentPresenters = Utils.ControlsUtil.FindVisualChildren<ContentPresenter>(chambersOverview._itemsControl) .Where(d => d.DataContext != null).ToList(); chambersOverview._parent = Utils.ControlsUtil.FindVisualChildren<Canvas>(chambersOverview._itemsControl)?.Single(); if (contentPresenters.Any()) { var tempContentPresents = new List<ContentPresenter>(); foreach (var contentPresenter in contentPresenters) { dynamic currentDataContext = contentPresenter.DataContext; bool hasExist = false; foreach (var cpt in tempContentPresents) { dynamic cptDataContext = cpt.DataContext; if (currentDataContext.Id == cptDataContext.Id) { hasExist = true; break; } } if (hasExist) continue; //Unregister events contentPresenter.MouseLeftButtonDown -= chambersOverview.Container_MouseLeftButtonDown; contentPresenter.MouseLeftButtonUp -= chambersOverview.Container_MouseLeftButtonUp; contentPresenter.MouseMove -= chambersOverview.Container_MouseMove; contentPresenter.MouseLeave -= chambersOverview.Container_MouseLeave; //register events contentPresenter.MouseLeftButtonDown += chambersOverview.Container_MouseLeftButtonDown; contentPresenter.MouseLeftButtonUp += chambersOverview.Container_MouseLeftButtonUp; contentPresenter.MouseMove += chambersOverview.Container_MouseMove; contentPresenter.MouseLeave += chambersOverview.Container_MouseLeave; tempContentPresents.Add(contentPresenter); } } })); /// <summary> /// record chambers location in parent canvas container /// </summary> public ObservableCollection<ChamberPosition> ChambersLocation { get { return (ObservableCollection<ChamberPosition>)GetValue(ChambersLocationProperty); } set { SetValue(ChambersLocationProperty, value); } } // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty ChambersLocationProperty = DependencyProperty.Register("ChambersLocation", typeof(ObservableCollection<ChamberPosition>), typeof(ChambersOverview), new PropertyMetadata(new ObservableCollection<ChamberPosition>())); #endregion #region Overrides public override void OnApplyTemplate() { base.OnApplyTemplate(); _itemsControl = this.Template.FindName("itemsControl", this) as ItemsControl; } #endregion #region Events private Canvas _parent; private bool _isDown; private Point _prePosition = new Point(); private Point _currentPosition = new Point(); private Point GetPosition(MouseEventArgs e) { return e.GetPosition(_parent); } private void Container_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { //only can drag in editable state if (IsEditable == false) return; if (_isDown) return; _isDown = true; _prePosition = GetPosition(e); (sender as ContentPresenter).CaptureMouse(); } private void Container_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { //only can drag in editable state if (IsEditable == false) return; _isDown = false; var associatedObject = sender as ContentPresenter; //refresh ChambersLocation dynamic dataContext = associatedObject.DataContext; var chamberPosition = ChambersLocation.SingleOrDefault(d => d.Id == dataContext.Id); if (null != chamberPosition) { chamberPosition.CanvasLeft = _currentPosition.X; chamberPosition.CanvasTop = _currentPosition.Y; } associatedObject.ReleaseMouseCapture(); } private void Container_MouseMove(object sender, MouseEventArgs e) { //only can drag in editable state if (IsEditable == false) return; var associatedObject = sender as ContentPresenter; associatedObject.Cursor = Cursors.SizeAll; if (!_isDown) return; Point currentPosition = GetPosition(e); double offSetX = currentPosition.X - _prePosition.X; double offSetY = currentPosition.Y - _prePosition.Y; double left = Canvas.GetLeft(associatedObject); double top = Canvas.GetTop(associatedObject); double newLeft = double.IsNaN(left) ? 0 : left + offSetX; double newTop = double.IsNaN(top) ? 0 : top + offSetY; if (newLeft == 0 && newTop == 0) return; Canvas.SetLeft(associatedObject, newLeft); Canvas.SetTop(associatedObject, newTop); _currentPosition = new Point(newLeft, newTop); _prePosition = currentPosition; } private void Container_MouseLeave(object sender, MouseEventArgs e) { //only can drag in editable state if (IsEditable == false) return; _isDown = false; var associatedObject = sender as ContentPresenter; associatedObject.Cursor = Cursors.Arrow; associatedObject.ReleaseMouseCapture(); } #endregion } }
3.1 定義資料來源
這裡面主要包括這幾個內容:IsEdiable這個依賴項屬性主要定義當前的ItemsControl中具體項是否可以進行拖拽的一個開關,Chambers這個是定義在xaml中ItemsControl繫結的ItemsSource這個是非常重要的,這個是由外部進行傳入的,在具體的自定義控制元件庫中並不知道具體的集合物件是什麼,所以這裡定義的型別是IList,這個是非常關鍵的一點,這裡的型別定義我們可以參考ItemsControl中ItemsSource屬性的定義,ItemsSource可以繫結到各種型別的集合物件,但是在ItemsControl的內部並不知道外部將會傳入什麼樣的物件,所以我們來看看ItemsControl中是怎樣定義ItemsSource的。
// // 摘要: // 獲取或設定用於生成 System.Windows.Controls.ItemsControl 的內容的集合。 // // 返回結果: // 用於生成 System.Windows.Controls.ItemsControl 的內容的集合。 預設值為 null。 [Bindable(true)] [CustomCategoryAttribute("Content")] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IEnumerable ItemsSource { get; set; }
這個部分定義需要我們好好去理解,ChambersLocation是我們定義的每一個Chamber在Canvas中具體的位置以及ZIndex的資訊,這個完全可以被外部繫結然後進行儲存到檔案中用於保留現在的位置的資訊。ChamberControlDataTemplate在之前的程式碼中也簡單說過,我們每一個ItemTemplate是根據外部傳入的,這個DataTemplate是用於使用方進行繫結的。
3.2 定義拖動事件
這個在ChamberOverview中是一個非常獨立的過程,前面的分析中我們說過每個Chamber拖動事件必須作用於ItemsPanel下面的直接子元素ContentPresenter,那麼在這裡我們怎麼獲取這些具體的ContentPresenter呢?這裡我們首先Override基類的OnApplyTemplate()方法,在這個方法中我們能夠獲取到具體的ItemsControl物件,然後在Chambers依賴項屬性觸發回撥的過程中通過一個ControlsUtil工具類找到子類的ContentPresenter,然後分別訂閱Mouse事件,然後在裡面更改每一個ChambersPosition位置,到此這個更改位置的過程全部結束。
3.3 定義右鍵ContextMenu
這個部分主要是改變每一個Chamber的ZIndex,這裡面涉及到4個事件,這個都是定義在ChambersLocation這個集合中每一個具體的物件ChamberPosition中,這裡我們來看看這個資料型別的具體定義。
public class ChamberPosition : ModelBase, IIdentifier { public Guid Id { get; set; } private string _groupName; public string GroupName { get { return _groupName; } set { _groupName = value; } } private string _name; public string Name { get { return _name; } set { _name = value; } } /// <summary> /// left position in canvas /// </summary> private double _canvasLeft; public double CanvasLeft { get { return _canvasLeft; } set { if (null != _canvasLeft) { _canvasLeft = value; RaisePropertyChanged(nameof(CanvasLeft)); } } } /// <summary> /// top position in canvas /// </summary> private double _canvasTop; public double CanvasTop { get { return _canvasTop; } set { if (null != _canvasTop) { _canvasTop = value; RaisePropertyChanged(nameof(CanvasTop)); } } } private int _zIndex; public int ZIndex { get { return _zIndex; } set { if (value != _zIndex) { _zIndex = value; RaisePropertyChanged(nameof(ZIndex)); } } } private ObservableCollection<ChamberPosition> _parent; [XmlIgnore] public ObservableCollection<ChamberPosition> Parent { get { return _parent; } set { if (value != _parent) { _parent = value; RaisePropertyChanged(nameof(Parent)); } } } public ChamberPosition() { MoveUpOneLayer = new DelegatedCommand(DoMoveUpOneLayer, (r) => true); MoveDownOneLayer = new DelegatedCommand(DoMoveDownOneLayer, (r) => true); MoveBottomLayer = new DelegatedCommand(DoMoveBottomLayer, (r) => true); MoveFrontLayer = new DelegatedCommand(DoMoveFrontLayer, (r) => true); } #region Commands /// <summary> /// 上移一層 /// </summary> [XmlIgnore] public ICommand MoveUpOneLayer { get; set; } /// <summary> /// 下移一層 /// </summary> [XmlIgnore] public ICommand MoveDownOneLayer { get; set; } /// <summary> /// 置於頂層 /// </summary> [XmlIgnore] public ICommand MoveFrontLayer { get; set; } /// <summary> /// 置於底層 /// </summary> [XmlIgnore] public ICommand MoveBottomLayer { get; set; } private void DoMoveFrontLayer(object obj) { if (null == Parent || Parent.Count == 0) return; var maxIndex = Parent[0].ZIndex; foreach (var item in Parent) { if (item.ZIndex > maxIndex) { maxIndex = item.ZIndex; } } var placementTarget = obj as ContentPresenter; ZIndex = ++maxIndex; Panel.SetZIndex(placementTarget, ZIndex); } private void DoMoveBottomLayer(object obj) { if (null == Parent || Parent.Count == 0) return; var minIndex = Parent[0].ZIndex; foreach (var item in Parent) { if (item.ZIndex < minIndex) { minIndex = item.ZIndex; } } var placementTarget = obj as ContentPresenter; ZIndex = --minIndex; Panel.SetZIndex(placementTarget, ZIndex); } private void DoMoveDownOneLayer(object obj) { var placementTarget = obj as ContentPresenter; ZIndex--; Panel.SetZIndex(placementTarget, ZIndex); } private void DoMoveUpOneLayer(object obj) { var placementTarget = obj as ContentPresenter; ZIndex++; Panel.SetZIndex(placementTarget, ZIndex); } #endregion }
這個裡面定義了四個事件:上移一層、下移一層、置於頂層、置於底層四個命令用於繫結ContextMenu的具體命令,這裡需要注意這裡最終都是通過Panel.SetZIndex方法來設定每個Item在Canvas中的ZIndex的,另外關於ContextMenu如何繫結到事件是非常有技巧的,因為ContextMenu是不屬於視覺樹上面的元素的,但是ContextMenu的PlacementTarget是屬於視覺樹上面的,所以我們可以充分利用這個屬性來進行繫結,另外在繫結的時候合理利用Tag屬性也是非常重要的一個技巧,這個需要自己在平時的編碼過程中進行不斷積累和總結。
4 外部使用自定義控制元件
這裡在自定義控制元件庫中定義好了以後是要供外部進行呼叫的,我們來看看在外部我們該如何進行使用的,這裡我們直接來看示例程式碼。
<Border x:Name="borderChamberView" Canvas.Left="80" Canvas.Top="10" BorderThickness="1" BorderBrush="White" CornerRadius="10"> <acmToolkit:ChambersOverview Grid.Column="0" Width="{Binding ActualWidth,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type UserControl},AncestorLevel=1}, Converter={StaticResource convMath}, ConverterParameter=x-90}" Height="{Binding ChambersLocation,Converter={StaticResource chambersOverviewHeightConverter},ConverterParameter=80}" local:TransferableView.WaferWillPlace="ChamberView_AnyWafer_WillBePlaced" local:TransferableView.WaferWillPick ="ChamberView_AnyWafer_WillBePicked" ChambersLocation="{Binding ChambersLocation}" Chambers="{Binding Chambers}"> <acmToolkit:ChambersOverview.ChamberControlDataTemplate> <DataTemplate> <local:ProcessModuleView Width="60" Height="60" Chamber="{Binding}" InEditMode="{Binding IsEditable,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type acmToolkit:ChambersOverview}}}" Margin="5 15" Tag="{Binding ElementName=canvas}" Background="Transparent"> </local:ProcessModuleView> </DataTemplate> </acmToolkit:ChambersOverview.ChamberControlDataTemplate> </acmToolkit:ChambersOverview> </Border>
總結
這裡將整個如何建立自定義控制元件庫、定義控制元件樣式、重寫模板、新增事件以及到最終使用全部分析了一遍,當然整個過程理解整個思路是最關鍵的部分,當然按這篇文章中略過了一些不太重要的細節例如這些轉換器實現等等,如果還需要了解完整的過程,請點選這裡下載原始碼。