WPF教程十二:了解自定義控件的基礎和自定義無外觀控件


這一篇本來想先寫風格主題,主題切換、自定義配套的樣式。但是最近加班、搬家、新租的房子打掃衛生,我家寶寶6月中旬要出生協調各種的事情,導致了最近精神狀態不是很好,又沒有看到我比較喜歡的主題風格去模仿的,又不想降低教程的質量,所以就打算把風格的主題這一篇,放后面等我找到了我喜歡的主題,然后在開始仿寫。這一篇先入門自定義控件。

​ WPF支持樣式、內容控件和模板。因此不在刻意的強調自定義控件。這些特性為開發人員提供了多種方式來完善和擴展標准的控件,而不用派生新的控件類。通過以下幾種方式能實現大部分需求:

  • 樣式。可以使用樣式方便地重用控件屬性和觸發器的組合。

  • 內容控件。所有繼承自ContentControl類的控件都支持嵌套的內容。使用內容控件,可以快速創建聚集其他元素的符合控件(比如,可將按鈕變成圖像按鈕或將列表框變成圖象列表)。

  • 控件模板。所有WPF控件都是無外觀的,這意味着它們具有硬編碼的功能,但它們的外觀是通過控件模板單獨定義的。使用其他新的控件模板代替默認模板,可重新構建基本控件,例如重新構建按鈕、復選框、單選框和窗口。

  • 數據模板。所有派生自ItemsControl的類都支持數據模板,通過數據模板可創建某些數據對象類型的富列表標識。通過恰當的數據模板,可使用許多元素的組合顯示每個項,這些組合元素可以是文本、圖像甚至可以是可編輯控件(都在所選的布局容器中)。

    如果可以的話,在決定使用自定義控件或其他類型的自定義元素之前,可以繼續使用這些方法。因為這些解決方案更簡單,更容易實現,並且通常更容易重用。

    當微調元素外觀時不適用與自定義元素,但是當希望改變底層的功能時,自定義元素就十分有用了。例如,WPF為TextBox控件和PasswordBox控件使用不同的類是有原因的。它們使用不同的方法處理按鍵,以不同的方式在內部保存它們的數據,以不同的方式與其他組件(剪切板)進行交互,等等,如果希望設計一個具有不同屬性、方法和事件集合的控件,就需要構建自己的控件。

    這篇文章介紹如何創建自定義元素以及如何使用它們成為WPF中的重要成員。這意味着將使它們具備依賴項屬性和路由事件功能,以獲得對WPF重要服務的支持,如數據綁定、樣式以及動畫。還學習如何創建無外觀的控件——模板驅動的控件,允許控件的用戶提供不同的可視化外觀以獲得更大的靈活性。

    理解WPF中的自定義元素

    盡管可以在任意WPF項目中編寫自定義元素,但是通常希望在專門的類庫程序集(DLL)中放置自定義元素,用於在多個程序之前共享自定義元素。

    為確保具有正確得程序集引用和名稱空間導入,我們在創建項目時選擇Custom Control Library(WPF)項目類型。在類庫中,可創建任意數量的控件。

    想要寫好自定義控件,這個繼承關系必須要記着,這些基類工作在WPF的哪個層一定要搞清楚。

名稱 說明
FrameworkElement 當創建自定義元素時,這是常用的最低級的基類。通常只有當希望重寫OnRender()方法並使用System.Windows.media.DrawingContext從頭繪制內容時,才會使用這種方法。FrameworkElement類為哪些不打算與用戶進行交互的元素提供了一組基本的屬性和事件
Control 當從頭開始創建控件時,這是最常用的起點。該類時所有用戶交互小組件的基類。Control類添加了用於設置背景、前景、字體和內容對其方式的屬性。控件類還為自身設置了Tab順序(通過IsTabStop屬性),並且引入了鼠標雙擊功能(通過MouseDoubleClick和PreviewMouseDoubleClick事件)。但最重要的是,Control類定義了Template屬性,為了得到無限的靈活性,該屬性允許使用自定義元素樹替換其外觀
ContentControl 這是能夠顯示任意單一內容的控件的基類。顯示的內容可以是元素或集合使用模板的自定義對象(內容通過Content屬性設置,並且可以通過ContentTemplate屬性提供可選的模板)。許多控件都封裝了特定的類型在一定范圍內的內容(比如文本框中的文本字符串)。因為這些控件不支持所有元素,所以它們不是內容控件。
UserControl 這是可以使用設計視圖進行配置的內容控件。盡管用戶控件和普通的內容控件是不同的,但是希望在多個窗口中快速重用用戶界面中的不變模塊時(而不是創建真正的能在不同應用程序之間轉移的獨立控件),通常使用該基類。
ItemsControl或Selector ItemsControl 是封裝項列表的控件的基類,但不支持選擇,二Selector類是支持選擇的控件的更具體基類。創建自定義控件不經常使用這些類,因為ListBox、ListView以及TreeView控件的樹綁定特性提供了很大的靈活性
Panel 該類是具有布局邏輯控件的基類,布局空間能夠包含多個子元素,並根據特定的布局語義安排這些子元素。通常,面板提供了用於設置子元素的附加屬性,配置如何安排子元素。
Decorator 封裝其他元素的元素的基類,並且提供了一種圖形效果或特定的功能。兩個明顯的例子是Border和Viewbox,其中Border控件在元素的周圍繪制線條,Viewbox控件使用變換動態縮放其內容。其他裝飾元素包括為普通控件(如按鈕)提供熟悉邊框和背景色的修飾類。
特殊控件類 如果希望改進現有控件,可以直接繼承該控件。例如,可創建具有內置驗證邏輯的TextBox控件。然而,在采取這一步之前,應該首先分析是否可通過事件處理代碼或單獨的組件達到同一目的。這兩種方法都可以使自定義邏輯和控件相分離,從而可在其他控件中重用。

我們通過使用UserControl創建一個顏色拾取器,來分析如何將這個控件分解成為功能更強大的基於模板的控件。

我們的顏色拾取器包含4個Slider、一個Rectangle。slider用來控制Color的A、R、G、B4個通道,Rectangle用來顯示4個Slider值對應的ARGB顏色值。

然后再window中使用這個自定義控件。

我們再項目中創建UserControls文件夾,然后添加ColorPickerUserControls.xaml。

創建依賴項屬性我們使用的propdp=>2次Tab來實現的。添加的路由事件是我們自己寫的propurv=>2次Tab來實現的。

實現過程在這篇博客中:WPF技巧:通過代碼片段管理器編寫自己常用的代碼模板提示效率 - 杜文龍 - 博客園 (cnblogs.com)

好了,自定義控件的代碼如下:

<UserControl x:Class="CustomElement.UserControls.ColorPickerUserControls"
             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" 
             xmlns:local="clr-namespace:CustomElement.UserControls"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Slider Name="sliderAlpha" Grid.Row="0" Minimum="0" Maximum="100" Value="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}}, Path=Alpha}"/>
        <Slider Name="sliderRed" Grid.Row="1" Minimum="0" Maximum="255" Value="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}},Path=Red}"/>
        <Slider Name="sliderGreen" Grid.Row="2" Minimum="0" Maximum="255" Value="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}},Path=Green}"/>
        <Slider Name="sliderBlue" Grid.Row="3" Minimum="0" Maximum="255" Value="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}},Path=Blue}"/>
        <Rectangle Grid.Column="1" Grid.RowSpan="3" Width="50" Stroke="Black" StrokeThickness="1">
            <Rectangle.Fill>
                <SolidColorBrush Color="{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}},Path=Color}"/>
            </Rectangle.Fill>
        </Rectangle> 
    </Grid>
</UserControl>

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace CustomElement.UserControls
{
    /// <summary>
    /// ColorPicker.xaml 的交互邏輯
    /// </summary>
    public partial class ColorPickerUserControls : UserControl
    {
        public byte Alpha
        {
            get { return (byte)GetValue(AlphaProperty); }
            set { SetValue(AlphaProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Alpha.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AlphaProperty =
            DependencyProperty.Register("Alpha", typeof(byte), typeof(ColorPickerUserControls), new PropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));

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

        // Using a DependencyProperty as the backing store for Red.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RedProperty =
            DependencyProperty.Register("Red", typeof(byte), typeof(ColorPickerUserControls), new PropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));

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

        // Using a DependencyProperty as the backing store for Green.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GreenProperty =
            DependencyProperty.Register("Green", typeof(byte), typeof(ColorPickerUserControls), new PropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged))); 

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

        // Using a DependencyProperty as the backing store for Blue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BlueProperty =
            DependencyProperty.Register("Blue", typeof(byte), typeof(ColorPickerUserControls), new PropertyMetadata(new PropertyChangedCallback(OnColorRGBChanged)));

        private static void OnColorRGBChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ColorPickerUserControls colorPicker = (ColorPickerUserControls)d;
            Color color = colorPicker.Color;
            if (e.Property == AlphaProperty)
            {
                color.A = (byte)e.NewValue;
            }
            else 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;
        } 

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

        // Using a DependencyProperty as the backing store for Color.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColorProperty =
            DependencyProperty.Register("Color", typeof(Color), typeof(ColorPickerUserControls), new PropertyMetadata(new PropertyChangedCallback(OnColorChanged)));

        private static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ColorPickerUserControls colorPicker = (ColorPickerUserControls)d;
            Color oldColor = (Color)e.OldValue;
            Color newColor = (Color)e.NewValue;
            colorPicker.Alpha = newColor.A;
            colorPicker.Red = newColor.R;
            colorPicker.Green = newColor.G;
            colorPicker.Blue = newColor.B;

            if (!colorPicker.isUndo)
            {
                colorPicker.previousColors.Push((Color)e.OldValue);
                colorPicker.OnColorChanged(oldColor, newColor);
            }
            colorPicker.isUndo = false;
         
        }
        private bool isUndo = false;
        private Stack<Color> previousColors = new Stack<Color>(100);
        private void OnColorChanged(Color oldValue, Color newValue)
        {
            RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>(oldValue, newValue);
            args.RoutedEvent = ColorPickerUserControls.ColorChangedEvent;
            RaiseEvent(args);

        }

        public static readonly RoutedEvent ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble,
            typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPickerUserControls));

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

        static ColorPickerUserControls()
        { 
            CommandManager.RegisterClassCommandBinding(typeof(ColorPickerUserControls), new CommandBinding(ApplicationCommands.Undo, UndoCommand_Executed, UndoCommand_CanExecute));
        }
        public ColorPickerUserControls()
        {
            InitializeComponent();
        }  

        private static void UndoCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            ColorPickerUserControls colorPicker = (ColorPickerUserControls)sender;
            e.CanExecute = colorPicker.previousColors.Count > 0;
        }

        private static void UndoCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ColorPickerUserControls colorPicker = (ColorPickerUserControls)sender;
            colorPicker.isUndo = true;
            colorPicker.Color = (Color)colorPicker.previousColors.Pop();

        }
    }
}

完整代碼如上,我們主要創建了4個ARGB對應byte依賴項屬性,和OnColorRGBChanged變動的事件,如果ARGB值變動了,我們就去修改Color的值。

同時我們又創建了OnColorChanged事件用來更新ARGB。當各個屬性改變試圖改變其他屬性時,WPF不允許重新進入屬性變化回調函數。例如。如果改變Color屬性,就會觸發OnColorChanged()方法。OnColorChanged()方法會修改Alpha、Red、Green、Blue屬性,從而觸發OnColorRGBChanged()回調方法3次,每個屬性一次。

然而OnColorRGBChanged()方法不會再次觸發OnColorChanged()方法。

然后我們通過propurv=》2次tab實現了一個路由事件,當Color發生變化時會通知注冊了這個事件的控件調用者。而后我們在靜態構造函數通過RegisterClassCommandBinding注冊了一個撤銷命令,用於支持用戶撤銷他的操作。我們用了一個長度為100的Stack來保持用戶操作。

我們在Window下使用這個我們創建好的自定義控件,注意這一行代碼:

xmlns:usercontrols="clr-namespace:CustomElement.UserControls"

這是添加相關的引用。

其他完整代碼如下:

<Window x:Class="CustomElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CustomElement" xmlns:usercontrols="clr-namespace:CustomElement.UserControls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <usercontrols:ColorPickerUserControls Color="Beige" x:Name="colorPicker"  ColorChanged="ColorPicker_ColorChanged"/>
        <TextBlock  Text="{Binding ColorTxt}"/>
        <Button Width="120" Content="撤回" Command="Undo" CommandTarget="{Binding ElementName=colorPicker}"/>
    </StackPanel> 
</Window>

using System.Windows;
using System.Windows.Media;

namespace CustomElement
{
    /// <summary>
    /// MainWindow.xaml 的交互邏輯
    /// </summary>
    public partial class MainWindow : Window
    { 
        public string ColorTxt
        {
            get { return (string)GetValue(ColorTxtProperty); }
            set { SetValue(ColorTxtProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ColorTxt.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColorTxtProperty =
            DependencyProperty.Register("ColorTxt", typeof(string), typeof(MainWindow)); 

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        private void ColorPicker_ColorChanged(object sender, RoutedPropertyChangedEventArgs<Color> e)
        {
            ColorTxt = "The new color is " + e.NewValue;
        }
    }
}

這是Window下使用自定義控件的代碼。

用戶控件的目標是提供增補控件模板的設計表面,提供一種定義控件的快速方法,代價是時去了將來的靈活性,如果喜歡用戶控件的功能,但是需要修改其可視化外觀時,使用這種方法就有問題了。比如希望使用相同的顏色選擇器,但是希望使用不同的“皮膚”,將其更好地融合到已有地應用程序窗口中。可以通過樣式來改變用戶控件地某些方面,但是該控件地一些部分是在內部鎖定,並且硬編碼到標記中地,比如無法將預覽矩形移動到滑動條左邊。一般情況下,我們寫自定義控件也都是寫到了這一步。一個window下放入多個UserControl。然后編輯這些UserControl。各種邏輯代碼和狀態代碼都混到這里。

那么既然用自定義控件肯定是簡單地使用樣式、觸發器、模板無法滿足復雜要求然后才從新做的自定義控件,既然選擇了這個還是希望能實現到通用控件的程度,比如做一個播放器控件,做一個圖片瀏覽空間。等等。能夠通用和適配的東西,但是這樣就涉及到皮膚問題,就比如Button、ListBox等等。現在就開始梳理這個無外觀控件。

我們回到最開頭的表單中找到Control的描述:

Control:當從頭開始創建控件時,這是最常用的起點。該類時所有用戶交互小組件的基類。Control類添加了用於設置背景、前景、字體和內容對其方式的屬性。控件類還為自身設置了Tab順序(通過IsTabStop屬性),並且引入了鼠標雙擊功能(通過MouseDoubleClick和PreviewMouseDoubleClick事件)。但最重要的是,Control類定義了Template屬性,為了得到無限的靈活性,該屬性允許使用自定義元素樹替換其外觀

所以我們創建一個繼承自Control的類來實現無外觀控件。

創建一個名為CustomControls的WPF Custom Control Library工程,然后新建類改名為ColorPicker並繼承自Control

(不知道為什么在VS2017下有.Net Framework的WPF Custom Control Library工程,但是在VS2019下只有名字為.NET的WPF Custom Control Library工程,我創建的是NET Core3.1版本的,而我的CustomElement工程是.NET Framework 4.7.2的,沒法引用沒看清楚,導致這里出現了問題,添加引用后一直是黃色不可用狀態,浪費了我快半個小時寫的代碼,然后重新寫了)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace CustomControls
{
    /// <summary>
    /// 按照步驟 1a 或 1b 操作,然后執行步驟 2 以在 XAML 文件中使用此自定義控件。
    ///
    /// 步驟 1a) 在當前項目中存在的 XAML 文件中使用該自定義控件。
    /// 將此 XmlNamespace 特性添加到要使用該特性的標記文件的根 
    /// 元素中: 
    ///
    ///     xmlns:MyNamespace="clr-namespace:CustomControls"
    ///
    ///
    /// 步驟 1b) 在其他項目中存在的 XAML 文件中使用該自定義控件。
    /// 將此 XmlNamespace 特性添加到要使用該特性的標記文件的根 
    /// 元素中: 
    ///
    ///     xmlns:MyNamespace="clr-namespace:CustomControls;assembly=CustomControls"
    ///
    /// 您還需要添加一個從 XAML 文件所在的項目到此項目的項目引用,
    /// 並重新生成以避免編譯錯誤: 
    ///
    ///     在解決方案資源管理器中右擊目標項目,然后依次單擊
    ///     “添加引用”->“項目”->[選擇此項目]
    ///
    ///
    /// 步驟 2)
    /// 繼續操作並在 XAML 文件中使用控件。
    ///
    ///     <MyNamespace:CustomControl1/>
    ///
    /// </summary>
    public class ColorPicker : Control
    {
        public byte Blue
        {
            get { return (byte)GetValue(BlueProperty); }
            set { SetValue(BlueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Blue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BlueProperty =
            DependencyProperty.Register("Blue", typeof(byte), typeof(ColorPicker), new PropertyMetadata(new PropertyChangedCallback(OnRGBColorChanged)));

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

        // Using a DependencyProperty as the backing store for Green.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GreenProperty =
            DependencyProperty.Register("Green", typeof(byte), typeof(ColorPicker), new PropertyMetadata(new PropertyChangedCallback(OnRGBColorChanged)));

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

        // Using a DependencyProperty as the backing store for Red.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RedProperty =
            DependencyProperty.Register("Red", typeof(byte), typeof(ColorPicker), new PropertyMetadata(new PropertyChangedCallback(OnRGBColorChanged)));

        private static void OnRGBColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ColorPicker colorPicker = (ColorPicker)d;
            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;
        }

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

        // Using a DependencyProperty as the backing store for Color.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColorProperty =
            DependencyProperty.Register("Color", typeof(Color), typeof(ColorPicker), new PropertyMetadata(new PropertyChangedCallback(OnColorChanged)));

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

        private void OnColorChanged(Color oldValue, Color newValue)
        {
            RoutedPropertyChangedEventArgs<Color> args = new RoutedPropertyChangedEventArgs<Color>(oldValue, newValue);
            args.RoutedEvent = ColorPicker.ColorChangedEvent;
            RaiseEvent(args);
        }

        public static readonly RoutedEvent ColorChangedEvent = EventManager.RegisterRoutedEvent("ColorChanged", RoutingStrategy.Bubble,
            typeof(RoutedPropertyChangedEventHandler<Color>), typeof(ColorPicker));

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

        static ColorPicker()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker))); 
        }
    }
}

這是ColorPicker.cs的當前的全部代碼。注意ColorPicker.cs下的這段代碼。默認樣式在這里。

DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorPicker), new FrameworkPropertyMetadata(typeof(ColorPicker))); 

然后開始寫Style。我們在CustomControls下創建themes文件夾然后添加ColorPicker.xaml資源文件和generic.xaml(這應該是創建的時候自帶的)。

樣式在我們當前的情況下最大的作用就是應用新模板,新模板定義了控件的默認可視化外觀。

注意以下幾點:

1)當創建到連接到父控件類屬性的綁定表達式時,不能使用ElementName,而需要使用RelativeSource屬性指示需要希望綁定到的父控件,如果單向綁定完全能夠滿足要求,可以使用輕量級的TemplateBinding 標記表達式,而不需要使用功能完備的數據綁定。

2)不能再控件模板中關聯事件處理程序。相反,需要為元素提供能夠時別的名字,並再控件構造函數中通過代碼為它們關聯事件處理程序。

3)除非希望關聯事件處理程序或通過代碼與它進行交互,否則不要再控件模板中命名元素。當命名希望使用的元素時,使用"PART_元素名"的形式進行命名。

我們的Border下的Background使用的是TemplateBinding了,他是使用該控件的對象(並引用了該樣式和模板)傳入的Background。使用TemplateBinding能提取數據,但是如果需要雙向綁定,或者繼承自Freezable的類比如(SolidColorBrush)TemplateBinding就不工作了,就需要使用RelativeSource綁定的TemplateParent。

這樣就完成了控件模板的外觀,代碼如下:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:CustomControls">
    <Style TargetType="{x:Type local:ColorPicker}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ColorPicker}">
                    <Border 
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        BorderThickness="{TemplateBinding BorderThickness}" 
                        Background="{TemplateBinding Background}">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Slider Name="PART_RedSlider" Minimum="0" Maximum="255"  Value="{Binding Path=Red, RelativeSource={RelativeSource TemplatedParent}}"/>
                            <Slider Name="PART_GreenSlider" Grid.Row="1" Minimum="0" Maximum="255" Value="{Binding RelativeSource={RelativeSource templatedParent},Path=Green}"/> 
                            <Slider Name="PART_BlueSlider" Grid.Row="2" Minimum="0" Maximum="255" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=Blue}"/>
                            <Rectangle Width="50" Stroke="Black" Grid.RowSpan="3" Grid.Column="1" StrokeThickness="1">
                                <Rectangle.Fill>
                                    <SolidColorBrush Color="{Binding RelativeSource={RelativeSource TemplatedParent},Path=Color}"/>
                                </Rectangle.Fill>
                            </Rectangle>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value> 
        </Setter>
    </Style>
</ResourceDictionary>

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:CustomControls">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/CustomControls;component/themes/ColorPicker.xaml"/> 
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

但是這么寫的話,每一處模板樣式都需要寫很多這樣的綁定,我們可以把bangding關系放在模板的初始化階段。

我們再剛才定義了很多的以"PART_"開頭的Name。以PART_開頭,后面跟元素名稱,元素名稱的首字母大寫。我們現在把這些綁定關系放到一個專用的OnApplyTemplate()方法中,這樣就能最簡單的來使用模板。移出上面代碼中Slider和SolidColorBrush的綁定關系。關鍵部分代碼如下:

        <Slider Name="PART_RedSlider" Minimum="0" Maximum="255" />
                            <Slider Name="PART_GreenSlider" Grid.Row="1" Minimum="0" Maximum="255"/> 
                            <Slider Name="PART_BlueSlider" Grid.Row="2" Minimum="0" Maximum="255"/>
                            <Rectangle Width="50" Stroke="Black" Grid.RowSpan="3" Grid.Column="1" StrokeThickness="1">
                                <Rectangle.Fill>
                                    <SolidColorBrush x:Name="PART_PreviewBrush" Color="{Binding  RelativeSource={RelativeSource TemplatedParent},Path=Color}"/>
                                </Rectangle.Fill>
                            </Rectangle>

打開ColorPicker.cs找個合適的位置重寫OnApplyemplate()。

  public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            RangeBase slider = (RangeBase)GetTemplateChild("PART_RedSlider");
            if (slider != null)
            {
                Binding binding = new Binding("Red");
                binding.Source = this;
                binding.Mode = BindingMode.TwoWay;
                slider.SetBinding(RangeBase.ValueProperty, binding);
            }
            slider = (RangeBase)GetTemplateChild("PART_GreenSlider");
            if (slider != null)
            {
                Binding binding = new Binding("Green");
                binding.Source = this;
                binding.Mode = BindingMode.TwoWay;
                slider.SetBinding(RangeBase.ValueProperty, binding);
            }
            slider = (RangeBase)GetTemplateChild("PART_BlueSlider");
            {
                Binding binding = new Binding("Blue");
                binding.Source = this;
                binding.Mode = BindingMode.TwoWay;
                slider.SetBinding(RangeBase.ValueProperty, binding);
            }
            //Color="{Binding  RelativeSource={RelativeSource TemplatedParent},Path=Color}"
            //這里並沒有生效,2個小時了也沒有解決,所以這個算作一個問題先放着吧
            //后面單獨寫博客,解決這個問題,現在把這個相關的binding放到ColorPicker.xaml中。
            #region 這里沒有生效 ,先注釋掉吧,改用ColorPicker.xaml下使用TemplatedParent.
          //  SolidColorBrush brush =  GetTemplateChild("PART_PreviewBrush") as SolidColorBrush;
          //  if (brush != null)
          //  {
          //      Binding binding = new Binding("Color");
          //      binding.Source = brush;
          //      binding.Mode = BindingMode.OneWayToSource;
          //      this.SetBinding(ColorPicker.ColorProperty, binding);
          //  }
            #endregion
        }

修改ColorPicker.xaml這里直接再模板中綁定。這樣的話,每個模板都需要綁定。

 <SolidColorBrush x:Name="PART_PreviewBrush" Color="{Binding RelativeSource={RelativeSource TemplatedParent},Path=Color}"/>
                               

我們再上面使用的是RangeBase類,是Slider類的父類,使用這個是為了再其他模板下可以使用繼承自RangeBase的類。代替滑動條。

畫刷這里有問題,並且還沒有解決掉,問題是綁定了,但是沒有生效,很奇怪。還沒有查到問題。目前Color先使用模板綁定吧。

還有一個關鍵的內容。為控件添加TemplatePart特性,以記錄再控件模板中使用哪些部件名稱:

   [TemplatePart(Name="PART_RedSlider",Type =typeof(RangeBase))]
    [TemplatePart(Name ="PART_GreenSlider",Type =typeof(RangeBase))]
    [TemplatePart(Name ="PART_BlueSlider",Type =typeof(RangeBase))]
    public class ColorPicker : Control

我們再MainWindow下使用這個控件。

再MainWindow的工程下如果沒有引用這個項目,去添加引用。

然后再MainWindow下設置命名空間

xmlns:customControls="clr-namespace:CustomControls;assembly=CustomControls"
  <customControls:ColorPicker Color="Beige" />

這樣就可以正常使用拉。

我們再添加一個控件模板。

<Window x:Class="CustomElement.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CustomElement"  
        xmlns:usercontrols="clr-namespace:CustomElement.UserControls"
        xmlns:customControls="clr-namespace:CustomControls;assembly=CustomControls"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <Style x:Key="VerticalSliderStyle" TargetType="{x:Type Slider}">
            <Setter Property="Orientation" Value="Vertical"/>
            <Setter Property="Minimum" Value="0"/>
            <Setter Property="Maximum" Value="255"/>
            
        </Style>
        <ControlTemplate x:Key="FancyColorPickerTemplate">
            <Border Background="LightBlue"
                    BorderBrush="Black"
                    BorderThickness="1">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Ellipse  Margin="10" Width="100" Height="100" Stroke="LightGoldenrodYellow" StrokeThickness="5">
                        <Ellipse.Fill>
                            <SolidColorBrush Color="{Binding Path=Color,RelativeSource={RelativeSource TemplatedParent}}"></SolidColorBrush>
                        </Ellipse.Fill>
                    </Ellipse>
                    <Slider Style="{StaticResource VerticalSliderStyle}" Name="PART_RedSlider" Grid.Column="1" />
                    <TextBlock Grid.Row="1" Grid.Column="1">RED</TextBlock>
                    <Slider Style="{StaticResource VerticalSliderStyle}" Name="PART_GreenSlider" Grid.Column="2"/>
                    <TextBlock Grid.Row="1" Grid.Column="2">GREEN</TextBlock>
                    <Slider Style="{StaticResource VerticalSliderStyle}" Name="PART_BlueSlider" Grid.Column="3"/>
                    <TextBlock Grid.Row="1" Grid.Column="3">BLUE</TextBlock>
                </Grid>
            </Border>
        </ControlTemplate>
    </Window.Resources>
    <StackPanel>
        <usercontrols:ColorPickerUserControls Color="Beige" x:Name="colorPicker"  ColorChanged="ColorPicker_ColorChanged"/>
        <TextBlock  Text="{Binding ColorTxt}"/>
        <Button Width="120" Content="撤回" Command="Undo" CommandTarget="{Binding ElementName=colorPicker}"/>
        <customControls:ColorPicker Color="Beige" />
        <customControls:ColorPicker Color="Gold" Template="{StaticResource FancyColorPickerTemplate}"/>
    </StackPanel>
      
</Window>

這個效果就非常棒拉。我們從新定義了外觀。剛才哪個綁定失敗的問題,這樣來看,其實寫的過程中影響也不大。但是就是需要再模板下綁定一次。

這篇就寫到這里吧,不配圖了,靜下心自己寫出來跑一下更有感覺,這篇只是帶着入門以下,這個例子邏輯上非常的簡單,但是這篇博客也寫了三個晚上了。學習是一個持續的過程,如果這個知識點沒有理解,那就建議繼續花時間深入以下,如果你只是着急上項目,那么這篇你過一下了解了就行。如果是跟我一樣想系統的梳理,建議還是搞清楚你疑惑的這些技術點。這一篇遇到的問題確實比較多,下一篇會寫更復雜的通過VisualStateManager來管理我們的模板。


免責聲明!

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



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