【WPF學習】第六十四章 構建基本的用戶控件


  創建一個簡單用戶控件是開始自定義控件的好方法。本章主要介紹創建一個基本的顏色拾取器。接下來分析如何將這個控件分解成功能更強大的基於模板的控件。

  創建基本的顏色拾取器很容易。然而,創建自定義顏色拾取器仍是有價值的聯系,因為這不僅演示了構建控件的各種重要概念,而且提供了一個實用的功能。

  可為顏色拾取器創建自定義對話框。但如果希望創建能集成進不同窗口的顏色拾取器,使用自定義控件是更好的選擇。最簡單的自定義控件類型是用戶控件,當設計窗口或頁面時通過用戶控件可以使用相同的方式組裝多個元素。因為僅通過直接組合現有控件並添加功能並不能實現顏色拾取器,所以用戶控件看起來是更合理的選擇。

  典型的顏色拾取器允許用戶通過單擊顏色梯度中的某個位置或分別指定紅、綠和藍三元色成分來選擇顏色。下圖顯示了創建的基本顏色拾取器。該顏色拾取器包含三個Slider控件,這些控件用於調節顏色成分,同時使用Rectangle元素預覽選擇的顏色。

一、定義依賴性屬性

  創建顏色拾取器的第一步是為自定義控件庫項目添加用戶控件。當添加用戶控件后,Visual Studio會創建XAML標記文件和相應的包含初始化代碼即事件處理代碼的自定義類。這與創建新的窗口或也賣弄是相同的——唯一的區別在與頂級容器是UserControl類:

<UserControl x:Class="CustomControls.ColorPickerUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d"  Name="colorPicker">
</UserControl>

  最簡單的起點是設計用戶控件對外界公開的公共接口。換句話說,就是設計控件使用者使用的魚顏色拾取器進行交互的屬性、方法和事件。

  最基本的細節是Color屬性——畢竟,顏色拾取器不過是用於顯示和選擇顏色的特定工具。為支持WPF特性,如數據綁定、樣式以及動畫,控件的可寫屬性幾乎都是依賴項屬性。

  在前面章節中學習過,創建依賴項屬性的第一步是為之定義靜態字段,並在屬性名稱的后面加上單詞Property:

public static DependencyProperty ColorProperty;

  Color屬性將允許控件使用者通過代碼設置或檢索顏色值。然而,顏色拾取器中的滑動條控件也允許用戶修改當前顏色的一個方面。為實現這一設計,當滑動條額值發生變化時,需要使用事件處理程序進行響應,並且響應地更新Color屬性。但使用數據綁定關聯滑動條會更加清晰。為使用數據綁定,需要將每個顏色成分定義為單獨的依賴項屬性:

public static DependencyProperty RedProperty;
public static DependencyProperty GreenProperty;
public static DependencyProperty BlueProperty;

  盡管Color屬性存儲了System.Windows.Media.Color對象,但Red、Green以及Blue屬性將存儲表示每個顏色成分的單個字節值。

  為屬性定義靜態字段只有第一步。還需要有靜態構造函數,用於在用戶控件中注冊這些依賴性屬性,指定屬性的名稱、數據類型以及擁有屬性的控件類。可通過傳遞具有正確標記設置的FrameworkPropertyMetadata對象,在靜態構造函數中指定選擇的特定屬性特性(如值繼承)。還可指出在什么地方為驗證、數據強制以及屬性更改通知關聯回調函數。

  在顏色拾取器中,只需要考慮一個因素——當各種屬性變化時需要關聯回調函數進行響應。因為Red、Green和Blue屬性實際上時Color屬性的不同表示,並且如果一個屬性發生變化,就需要確保其他屬性保持同步。

  下面是注冊顏色拾取器的4個依賴性屬性的靜態構造函數的代碼:

static ColorPickerUserControl()
        {
            ColorProperty = DependencyProperty.Register("Color", typeof(Color),
                typeof(ColorPickerUserControl),
                new FrameworkPropertyMetadata(Colors.Black, new PropertyChangedCallback(OnColorChanged)));

            RedProperty = DependencyProperty.Register("Red", typeof(byte),
                typeof(ColorPickerUserControl),
                new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));

            GreenProperty = DependencyProperty.Register("Green", typeof(byte),
                typeof(ColorPickerUserControl),
                new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));

            BlueProperty = DependencyProperty.Register("Blue", typeof(byte),
                typeof(ColorPickerUserControl),
                new FrameworkPropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));
        }

  現在已經定義了依賴性屬性,可添加標准的屬性封裝器,使范文它們變得更加容易,並可在XAML中使用它們:

public Color Color
        {
            get { return (Color)GetValue(ColorProperty); }
            set { SetValue(ColorProperty, value); }
        }
public byte Red
        {
            get { return (byte)GetValue(RedProperty); }
            set { SetValue(RedProperty, value); }
        }

public byte Green
        {
            get{return (byte)GetValue(GreenProperty);}
            set{SetValue(GreenProperty,value);}
        }
public byte Blue
        {
            get { return (byte)GetValue(BlueProperty); }
            set { SetValue(BlueProperty, value); }
        }

  請記住,屬性封裝器不能包含任何邏輯,因為可直接使用DependencyObject基類的SetValue()和GetValue()方法設置和檢索屬性。例如,在這個示例中的屬性同步邏輯是使用回調函數實現的,當屬性發生變化時通過屬性封裝器或者直接調用SetValue()方法引發回調函數。

  屬性變化回調函數負責使Color屬性與Red、Green以及Blue屬性保持一致。無論何時Red、Green以及Blue屬性發生變化,都會相應地調整Color屬性:

private static void OnColorRGBChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender;
            Color color = colorPicker.Color;
            if (e.Property == RedProperty)
                color.R = (byte)e.NewValue;
            else if (e.Property == GreenProperty)
                color.G = (byte)e.NewValue;
            else if (e.Property == BlueProperty)
                color.B = (byte)e.NewValue;

            colorPicker.Color = color;
        }

  當設置Color屬性時,也會更新Red、Green和Blue值:

 private static void OnColorChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender;
            Color oldColor = (Color)e.OldValue;
            Color newColor = (Color)e.NewValue;
            colorPicker.Red = newColor.R;
            colorPicker.Green = newColor.G;
            colorPicker.Blue = newColor.B;
        }

  盡管很明顯,但當各個屬性試圖改變其他屬性時,上面的代碼不會引起一系列無休止的調用。因為WPF不允許重新進入屬性變化回調函數。例如,如果改變Color順序,就會觸發OnColorChanged()方法。OnColorChanged()方法會修改Red、Green以及Blue屬性,從而觸發OnColorRGBChanged()回調方法三次(每個屬性觸發一次)。然而,OnColorRGBChanged()方法不會再次觸發OnColorChanged()方法。

二、定義路由事件

  通過添加路由事件,當發生一些事情時用於通知控件使用者。在顏色拾取器示例中,當顏色發生變化后,觸發一個事件是很有用處的。盡管可將這個事件定義為普通的.NET事件,但使用路由事件可提供冒泡和隧道特性,從而可在更高層次的父元素中處理事件。

  與依賴項屬性一樣,定義路由事件的一個步驟是為值創建靜態屬性,並在時間名稱的后面添加單詞Event:

public static readonly RoutedEvent ColorChangedEvent;

  然后可在靜態構造函數中注冊事件。在靜態構造函數中指定事件的名稱、路由策略、簽名以及擁有事件的類:

ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble,
                typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPickerUserControl));

  不一定要為事件簽名創建新的委托,有時可重用已經存在的委托。兩個有用的委托是RoutedEventHandler(用於不帶額外信息的路由事件)和RoutedPropertyChangedEventHandler(用於提供屬性發生變化之后的舊值和新值得路由事件)。上例中使用RoutedPropertyChangedEventHandler委托,是被類型參數化了的泛型委托。所以,可為任何屬性數據類型使用該委托,而不會犧牲類型安全功能。

  定義並注冊事件后,需要創建標准的.NET事件封裝器來公開事件。事件封裝器可用於關聯和刪除事件監聽程序:

public event RoutedPropertyChangedEventHandler<Color> ColorChanged
        {
            add { AddHandler(ColorChangedEvent, value); }
            remove { RemoveHandler(ColorChangedEvent, value); }
        }

  最后的細節是在適當時候引發事件的代碼。該代碼必須調用繼承自DependencyObject基類的RaiseEvent()方法。

  在顏色拾取器示例中,只需要在OnColorChanged()方法之后添加如下代碼即可:

RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>(oldColor, newColor);
args.RoutedEvent = ColorChangedEvent;
colorPicker.RaiseEvent(args);

  請記住,無論何時修改Color屬性,不管是直接修改還是通過修改Red、Green以及Blue成分,都會觸發OnColorChanged()回調函數。

三、添加標記

  現在已經定義好用戶控件的公有接口,需要做的所有工作就是創建控件外觀的標記。在這個示例中,需要使用一個基本Grid控件將三個Slider控件和預覽顏色的Rectangle元素組合在一起。技巧是使用數據綁定表達式,將這些控件連接到合適的屬性,而不需要使用事件處理代碼。

  總之,顏色拾取器中總共使用4個數據綁定表達式。三個滑動條被綁定到Red、Green和Blue屬性。而且屬性值得允許范圍是0~255(一個字節可以接受的數值)。Rectangle.Fill屬性使用SolidColorBrush畫刷進行設置。畫刷的Color屬性被綁定到用戶控件的Color屬性。

  下面是完整的標記:

<UserControl x:Class="CustomControls.ColorPickerUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d"  Name="colorPicker">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Slider Name="sliderRed" Minimum="0" Maximum="255"
                Margin="{Binding ElementName=colorPicker,Path=Padding}"
                Value="{Binding ElementName=colorPicker,Path=Red}"></Slider>
        <Slider Grid.Row="1" Name="sliderGreen" Minimum="0" Maximum="255"
                Margin="{Binding ElementName=colorPicker,Path=Padding}"
                Value="{Binding ElementName=colorPicker,Path=Green}"></Slider>
        <Slider Grid.Row="2" Name="sliderBlue" Minimum="0" Maximum="255"
                Margin="{Binding ElementName=colorPicker,Path=Padding}"
                Value="{Binding ElementName=colorPicker,Path=Blue}"></Slider>
        <Rectangle Grid.Column="1" Grid.RowSpan="3" 
                   Margin="{Binding ElementName=colorPicker,Path=Padding}"
                   Width="50" Stroke="Black" StrokeThickness="1">
            <Rectangle.Fill>
                <SolidColorBrush Color="{Binding ElementName=colorPicker,Path=Color}"></SolidColorBrush>
            </Rectangle.Fill>
        </Rectangle>
    </Grid>
</UserControl>

  用於用戶控件的標記和無外觀控件的控件模板扮演相同的角色。如果希望使標記中的一些細節是可配置的,可使用將他們連接到控件屬性的綁定表達式。例如,目前Rectangle元素的寬度被固定為50個單位。然而,可使用數據綁定表達式從用戶控件的依賴性屬性中提取數值來代替這些細節。這樣,控件使用者可通過修改屬性來選擇不同的寬度。同樣,可使筆畫顏色和寬度也是可變的。然而,如果希望使控件具有真正的靈活性,最好的創建無外觀的控件,並在模板中定義標記。

  偶爾可選用數據綁定表達式,重用已在控件中定義過的核心屬性。例如,UserControl類使用Padding屬性在外側邊緣和用戶定義的內部內容之間添加空間(這一細節是通過UserControl控件的控件模板實現的)。然而,也可以使用Padding屬性在每個滑動條的周圍設置空間,如下所示:

 <Slider Name="sliderRed" Minimum="0" Maximum="255"
                Margin="{Binding ElementName=colorPicker,Path=Padding}"
                Value="{Binding ElementName=colorPicker,Path=Red}"></Slider>

  類似地,也可從UserControl類的BorderThickness和BorderBrush屬性為Rectan元素獲取邊框設置。同樣,這樣快捷方式對於創建簡單的控件是非常合理的,但可通過引入額外的屬性(如SliderMargin、PreviewBorderBrush以及PreviewBorderThickness)或創建功能完備的基於模板的控件加以改進。

四、使用控件

  現在完成了控件,使用該控件很容易。為在另一個窗口中使用顏色拾取器,首先需要將程序集合.NET名稱控件映射到XAML名稱空間,如下所示:

<Window x:Class="CustomControlsClient.ColorPickerUserControlTest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls"  ...>

  使用定義的XML名稱控件和用戶控件類名,在XAML標記中可像創建其他類型的對象那樣創建自定義的用戶控件。還可在控件標記中設置它的屬性,以及直接關聯事件處理程序,如下所示:

<lib:ColorPickerUserControl 
        Name="colorPicker" Margin="2" Padding="3" ColorChanged="colorPicker_ColorChanged"  Color="Yellow"></lib:ColorPickerUserControl>

  因為Color屬性使用Color數據類型,並且Color數據類型使用TypeConverter特性進行了修飾,所以在設置Color屬性之前,WPF知道使用ColorConverter轉換器將顏色名稱字符串轉換成相應的Color對象。

  處理ColorChanged事件的代碼很簡單:

private void colorPicker_ColorChanged(object sender, RoutedPropertyChangedEventArgs<Color> e)
        {
            if (lblColor != null) lblColor.Text = "The new color is " + e.NewValue.ToString();
        }

  現在已經完成了自定義控件。

五、命令支持

  許多控件具有命令支持。可使用以下兩種方法為自定義控件添加命令支持:

  •   添加將控件鏈接到特定命令的命令綁定。通過這種方法,控件可以相應命令,而且不需要借助於任何外部代碼。
  •   為命令創建新的RoutedUICommand對象,作為自定義控件的靜態字段。然后為這個命令對象添加命令綁定。這種方法可使自定義控件自動支持沒有在基本命令類集合中定義的命令。

  接下來的將使用第一種方法為ApplicationCommands.Undo命令添加支持。

  在顏色拾取器中為了支持Undo功能,需要使用成員字段跟蹤以前選擇的顏色:

private Color? previousColor;

  將該字段設置為可空是合理的,因為當第一次創建控件時,還沒有設置以前選擇的顏色。

  當顏色發生變化時,只需要記錄舊值。可通過在OnColorChanged()方法的最后添加以下代碼行來達到該目的:

colorPicker.previousColor = oldColor;

  現在已經具備了支持Undo命令需要的基礎框架。剩余的工作是創建將控件鏈接到命令以及處理CanExecute和Executed事件的命令綁定。

  第一次創建控時是創建命令綁定的最佳時機。例如,下面的代碼使用顏色拾取器的構造函數為ApplicationCommands.Undo命令添加命令綁定:

 public ColorPickerUserControl()
        {
            InitializeComponent();
            SetUpCommands();
        }

        private void SetUpCommands()
        {
            CommandBinding binding = new CommandBinding(ApplicationCommands.Undo,
                UndoCommand_Executed, UndoCommand_CanExecute);
            this.CommandBindings.Add(binding);
        }

  為使命令奏效,需要處理CanExecute事件,並且只要有以前的顏色值就允許執行命令:

private void UndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = previousColor.HasValue;
        }

  最后,當執行命令后,可交換新的顏色:

private void UndoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            this.Color = (Color)previousColor;
        }

  可通過兩種不同方式觸發Undo命令。當用戶控件中的某個元素具有焦點時,可以使用默認的Ctrl+Z組合鍵綁定,也可為客戶添加用於觸發命令的按鈕,如下所示:

<Button Command="Undo" CommandTarget="{Binding ElementName=colorPicker}"  Margin="5,0,5,0" Padding="2">Undo</Button>

  這兩種方法都會丟棄當前顏色並應用以前的顏色。

 

  更可靠的命令

  前面描述的技術是將命令鏈接到控件的相當合理的方法,但這不是在WPF元素和專業控件中使用的技術。這些元素使用更可靠的方法,並使用CommandManager.RegisterClassCommandBinding()方法關聯靜態的命令處理程序。

  上一個示例中演示的實現存在問題:使用公用CommandBindings集合。這使得命令比較脆弱,因為客戶可自由修改CommandBindings集合。而使用RegisterClassCommandBinding()方法無法做到這一點。WPF控件使用的就是這種方法。例如,如果查看TextBox的CommandBindings集合,不會發現任何用於硬編碼命令的綁定,例如Undo、Redo、Cut、Copy以及Paste等命令,因為他們被注冊為類綁定。

  這種技術非常簡單。不在實例構造函數中創建命令綁定,而必須在靜態構造函數中創建命令綁定,使用如下所示的代碼:

CommandManager.RegisterClassCommandBinding(typeof(ColorPickerUserControl),
                new CommandBinding(ApplicationCommands.Undo, UndoCommand_Executed, UndoCommand_CanExecute));

  盡管上面的代碼變化不大,但有一個重要變化。因為 UndoCommand_Executed()和UndoCommand_CanExecute()方法是在構造函數中引用的,所以必須是靜態方法。為檢索實例數據(例如當前顏色和以前顏色的信息),需要將事件發送者轉換為ColorPickerUserControl對象,並使用該對象。

  下面是修改之后的命令處理代碼:

private static void UndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender;
            e.CanExecute =colorPicker.previousColor.HasValue;
        }

private static void UndoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ColorPickerUserControl colorPicker = (ColorPickerUserControl)sender;
            colorPicker.Color = (Color)colorPicker.previousColor.Value;
        }

  此外,這種技術不局限於命令。如果希望將事件處理邏輯硬編碼到自定義控件,可通過EventManager.RegisterClassHandler()方法使用類事件處理程序。類事件處理程序總在實例事件處理程序之前調用,從而允許開發人員很容易地抑制事件。

六、深入分析用戶控件

  用戶控件提供了一種非常簡單的,但是有一定限制的創建自定義控件的方法。為理解其中的原因,深入分析用戶控件的工作原理是很有幫助的。

  在后台,UserControl類的工作方式和其父類ContentControl非常類似。實際上,只有幾個重要的區別:

  •   UserControl類改變了一些默認值。即該類將IsTabStop和Focusable屬性設置為false(從而在Tab順序中沒有占據某個單獨的額位置),並將HorizontalAlignment和VerticalAlignment屬性設置為Stretch(而非Left或Top),從而可以填充可用空間。
  •   UserControl類應用了一個新的控件模板,該模板由包含ContentPresenter元素的Border元素組成。ContentPresenter元素包含了用標記添加的內容。
  •   UserControl類改變了路由事件的源。當事件從用戶控件內的控件向用戶控件外的元素冒泡或隧道路由時,事件源變為指向用戶控件而不是原始元素。這提供了更好的封裝性。

  用戶控件和其他類型的自定義控件之間最重的區別是設計用戶控件的方法。與所有控件一樣,用戶控件有控件模板。然而,很少改變控件模板——反而,將作為自定義用戶控件類的一部分提供標記,並且當創建了控件后,會使用InitializeComponet()方法處理這個標記。另一個方面,無外觀控件是沒有標記——需要的所有內容都在模板中。

  普通的ContentControl控件具有下面的簡單模板:

<ControlTemplate TargetType="ContentControl">
    <ContentPresenter
        ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
        Content="{TemplateBinding ContentControl.Content}"/>
</ControlTemplate>

  這個模板僅填充所提供的內容並應用可選的內容模板。Padding、Background、HorizontalAlignment以及VerticalAlignment等熟悉沒有任何影響(除非顯示綁定屬性)。

  UserControl類有一個類似的模板,並又更多的細節。最明顯的是,它添加了一個Border元素並將其屬性綁定到用戶控件的BorderBrush、BorderThickness、Background以及Padding屬性,以確保它們具有相同的含義。此外,內部的ContentPresenter元素已綁定到對齊屬性。

<ControlTempalte TargetType="UserControl">
    <Border BorderBrush="{TemplateBinding Border.BorderBrush}"
       BorderThickness="{TemplateBinding Border.BorderThickness}"
       Background="{TemplateBinding Border.Background}"
       Padding="{TemplateBinding Border.Padding}"
       SnapsToDevicePixels="True">
        <ContentPresenter
            HorizontalAlignment="{TemplateBinding Control.HorizontalAlignment}"
            VerticalAlignment="{TemplateBinding Control.VerticalAlignment}"
            SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
            Contenttemplate="{TemplateBinding ContentControl.ContentTemplate}"
            Content="{TemplateBinding ContentControl.Content}"/>
    </Border>
</ControlTemplate>

  從技術角度看,可改變用戶控件的模板。實際上,只需要進行很少的調整,就可以將所有標記移到模板中。但卻是沒有理由采取該方法——如果希望得到更靈活的控件,時可視化外觀和由自定義控件類定義的借款分開,創建無外觀的自定義控件可能會更好一些。

  本章程序源代碼:CustomControl.zip


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM