主頁 > .NET開發 > WPF教程十二:了解自定義控制元件的基礎和自定義無外觀控制元件

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

2021-04-30 07:18:11 .NET開發

這一篇本來想先寫風格主題,主題切換、自定義配套的樣式,但是最近加班、搬家、新租的房子打掃衛生,我家寶寶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:
             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="https://www.cnblogs.com/duwenlong/p/{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}}, Path=Alpha}"/>
        <Slider Name="sliderRed" Grid.Row="1" Minimum="0" Maximum="255" Value="https://www.cnblogs.com/duwenlong/p/{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}},Path=Red}"/>
        <Slider Name="sliderGreen" Grid.Row="2" Minimum="0" Maximum="255" Value="https://www.cnblogs.com/duwenlong/p/{Binding RelativeSource={RelativeSource Findancestor, AncestorType={x:Type  UserControl}},Path=Green}"/>
        <Slider Name="sliderBlue" Grid.Row="3" Minimum="0" Maximum="255" Value="https://www.cnblogs.com/duwenlong/p/{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:
        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="https://www.cnblogs.com/duwenlong/p/{Binding Path=Red, RelativeSource={RelativeSource TemplatedParent}}"/>
                            <Slider Name="PART_GreenSlider" Grid.Row="1" Minimum="0" Maximum="255" Value="https://www.cnblogs.com/duwenlong/p/{Binding RelativeSource={RelativeSource templatedParent},Path=Green}"/> 
                            <Slider Name="PART_BlueSlider" Grid.Row="2" Minimum="0" Maximum="255" Value="https://www.cnblogs.com/duwenlong/p/{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:
        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="https://www.cnblogs.com/duwenlong/p/Vertical"/>
            <Setter Property="Minimum" Value="https://www.cnblogs.com/duwenlong/p/0"/>
            <Setter Property="Maximum" Value="https://www.cnblogs.com/duwenlong/p/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來管理我們的模板,

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/281564.html

標籤:WPF

上一篇:推薦一個不得不知道的 Visual Studio 快捷鍵

下一篇:.NET RulesEngine(規則引擎)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more