1. 程式人生 > >工作記錄--WPF自定義控制元件,實現一個可設定編輯模式的TextBox

工作記錄--WPF自定義控制元件,實現一個可設定編輯模式的TextBox

1. 背景

  因為最近在使用wpf開發桌面端應用,在檢視頁面需要把TextBox和Combox等控制元件設定為只讀的。原本是個很簡單的事,設定屬性IsReadOnly="True"或IsEnabled="False"就可以解決了,可是產品覺得樣式不是他想要的(背景是灰色的),想要實現的效果是和編輯時的樣式一致,僅僅是不可編輯而已。我想這也簡單啊,強制修改背景色和字型就完事了,結果發現TextBox修改背景色是可行的,但是修改後字型是灰色的,改也改不了,Combox更是連背景色都改不了。。。wtf

  於是開始了一段Google之路,發現別人說的都好複雜,大概明白了修改自帶的不可編輯或只讀的樣式不是一件簡單的事。於是我準備用一種變通的方法來解決這個問題,寫個自定義控制元件,通過設定自定義控制元件的屬性來顯示/隱藏內部控制元件的方式。

2. 實現

  首先新建一個使用者控制元件(UserControl),命名為LabelRenderTextBox,在LabelRenderTextBox.xaml檔案編寫以下內容 

   <!--DataContext="{Binding RelativeSource={RelativeSource self}} datacontext繫結自身會破壞binding鏈,而通過第一個child來binding則不會破壞"-->
  <StackPanel x:Name="panel" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:LabelRenderTextBox}}">
        <Border BorderThickness="1" BorderBrush="#ccc" x:Name="label" Width="{Binding ElementName=panel, Path=ActualWidth}" Height="24" Visibility="Collapsed" Padding="5,3,0,0">
            <TextBlock Text="{Binding Text}" ></TextBlock>
        </Border>
        <TextBox x:Name="textbox" Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="{Binding ElementName=panel, Path=ActualWidth}" Height="24"
                 base:CustomizeProperty.Placeholder="{Binding Placeholder}" TextChanged="Textbox_TextChanged" ></TextBox>
    </StackPanel>

  如圖,panel是UserControl內部最外層的元素,把這裡的DataContext設為自身,就可以把內部控制元件的屬性和cs檔案中的屬性進行繫結。label是預設隱藏的元素,textbox是預設顯示的控制元件。label和textbox的Text屬性都是繫結的LabelRenderTextBox的Text屬性,寬度都是和最外層的panel的寬度進行繫結的,以便於使用者自己設定寬度。

  那麼當我們使用LabelRenderTextBox控制元件進行編輯的時候,實際上還是通過TextBox來實現了,因為設定了Mode=TwoWay進行了雙向繫結,UpdateSourceTrigger=PropertyChanged,所以當TextBox的Text改變時,LabelRenderTextBox的Text值也會跟著改變,所以我們可以在LabelRenderTextBox.cs檔案裡新增一個屬性 public string Text { get; set; } ,可是當我們在外部直接修改LabelRenderTextBox的Text值如何使內部的TextBox值也跟著改變呢?這時候就需要用到wpf的依賴屬性了,首先通過檢視UserControl的繼承關係可以發現UserControl是繼承自DependencyObject的,意味著它是一個依賴物件,可以設定依賴屬性。所以我們可以在LabelRenderTextBox.cs檔案裡新增以下程式碼:

       public string Text { get
            {
                return (string)this.GetValue(TextProperty);
            }
            set
            {
                this.SetValue(TextProperty, value);
            }
        }

        public static readonly DependencyProperty TextProperty =
            DependencyProperty.RegisterAttached("Text", typeof(string), typeof(LabelRenderTextBox), new PropertyMetadata("", OnTextChanged));

        private static void OnTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.Text = e.NewValue as string;
            }
        }

  通過DependencyProperty.RegisterAttached方法註冊一個Text屬性,string型別的,型別為LabelRenderTextBox,PropertyMetadata的第一個引數是預設值,第二個引數是一個回撥函式(當屬性改變時進行回撥),回撥時通過Text的set方法來呼叫SetValue屬性來設定依賴屬性的值並通知與其繫結的屬性(想知道如何實現的可以參考:WPF依賴物件(DependencyObject) 實現原始碼,理解WPF原理必讀)。

  接下來,定義一個列舉類來列舉出需要的編輯模式,我這裡列舉出4種模式,可根據自己的需求進行調整。

    public enum EditModes
    {
        //編輯模式
        Editable = 0,
        //只讀模式
        ReadOnly = 1,      
        //只讀模式展示,單擊進入編輯模式,失去焦點後恢復只讀模式
        Click = 2,
        //只讀模式展示,雙擊進入編輯模式,失去焦點後恢復只讀模式
        DoubleCLick = 3
    }

  定義了列舉之後就要使用了,和設定Text屬性的方式來註冊一個EditMode依賴屬性,SetEditable方法傳入true/false來設定textbox(編輯)和label(只讀)的顯示和隱藏。這樣只要通過設定EditMode屬性就可以完美實現產品的需求了,我真棒!

       #region EditModeProperty 
        public EditModes EditMode {
            get { return (EditModes)this.GetValue(EditModeProperty); }
            set {
                this.SetValue(EditModeProperty, value);
                if (value == EditModes.Editable)
                {
                    SetEditable(true);
                }
                else
                {
                    SetEditable(false);
                }
            }
        }

        public static readonly DependencyProperty EditModeProperty =
            DependencyProperty.RegisterAttached("EditMode", typeof(EditModes), typeof(LabelRenderTextBox), new PropertyMetadata(EditModes.Editable, OnEditModeChanged));

        private static void OnEditModeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.EditMode = (EditModes)e.NewValue;
            }
        }
        #endregion

        private void SetEditable(bool editable)
        {
            if (editable)
            {
                this.label.Visibility = Visibility.Collapsed;
                this.textbox.Visibility = Visibility.Visible;
            }
            else
            {
                this.label.Visibility = Visibility.Visible;
                this.textbox.Visibility = Visibility.Collapsed;
            }
        }

  可是按照產品的需求只要Editable和ReadOnly兩個屬性就夠了,其他兩個是用來擴充套件的,可以在合適的場景下帶來更好的使用者體驗,預設展示只讀模式,單擊或者雙擊變為編輯模式,失去焦點後恢復只讀模式。實現方法也很簡單,直接在UserControl定義幾個事件,在對應的事件函式進行如下處理。

       private void Click(object sender, MouseButtonEventArgs e)
        {
            if (EditMode == EditModes.Click)
                SetEditable(true);
        }

        private void DoubleClick(object sender, MouseButtonEventArgs e)
        {
            if (EditMode == EditModes.DoubleCLick)
                SetEditable(true);
        }

        private void Lost_Focus(object sender, RoutedEventArgs e)
        {
            if(EditMode == EditModes.DoubleCLick || EditMode == EditModes.Click)
                SetEditable(false);
        }

  你可能會想,如果想監聽TextBox的事件該怎麼做呢,也很簡單,在cs檔案裡定義相同的事件,然後在cs檔案裡監聽對應事件,觸發時呼叫定義的事件即可。

        public event TextChangedEventHandler TextChanged;
       
        private void Textbox_TextChanged(object sender, TextChangedEventArgs e)
        {           
            this.TextChanged?.Invoke(this, e);
        }

  是不是很簡單呢?希望可以幫到遇到相同問題的朋友,完整程式碼如下:

<UserControl x:Class="YZ.HIS.UserControls.LabelRenders.LabelRenderTextBox"
             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:base="clr-namespace:APP_YFT.Base;assembly=APP-YFT.Base"
             xmlns:local="clr-namespace:YZ.HIS.UserControls.LabelRenders"
             mc:Ignorable="d" BorderThickness="0" 
             Height="24" d:DesignWidth="80" PreviewMouseLeftButtonDown="Click" PreviewMouseDoubleClick="DoubleClick"
             LostFocus="Lost_Focus">
    <UserControl.Resources>
        <Style x:Key="mySearchTextBoxStyle" TargetType="{x:Type TextBox}">
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Padding" Value="6,0,0,0"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type TextBox}">
                        <Border x:Name="bdRoot"  
                        BorderBrush="{TemplateBinding BorderBrush}"   
                        BorderThickness="{TemplateBinding BorderThickness}"   
                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"   
                        Background="{TemplateBinding Background}">
                            <DockPanel LastChildFill="True">
                                <Button x:Name="ClearButton"  
                                DockPanel.Dock="Right"   
                                Focusable="False"  Click="ExecCleanSearchText"
                                Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"
                                FontSize="{TemplateBinding FontSize}">
                                    <Button.Template>
                                        <ControlTemplate>
                                            <Image x:Name="Image" Source="/YZ.HIS;component/Images/tbClean_icon.png" Stretch="None"/>
                                        </ControlTemplate>
                                    </Button.Template>
                                </Button>
                                <ScrollViewer x:Name="PART_ContentHost" DockPanel.Dock="Left" Background="{TemplateBinding Background}"/>
                            </DockPanel>
                        </Border>
                        <ControlTemplate.Triggers>
                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text}" Value="">
                                <Setter TargetName="ClearButton" Property="Visibility" Value="Collapsed" />
                            </DataTrigger>
                            <Trigger Property="IsFocused" Value="True">
                                <Setter TargetName="bdRoot" Property="BorderBrush" Value="#569DE5"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter TargetName="bdRoot" Property="BorderBrush" Value="#7EB4EA"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    <!--DataContext="{Binding RelativeSource={RelativeSource self}} datacontext繫結自身會破壞binding鏈,而通過第一個child來binding則不會破壞"-->
    <StackPanel x:Name="panel" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:LabelRenderTextBox}}">
        <Border BorderThickness="1" BorderBrush="#ccc" x:Name="label" Width="{Binding ElementName=panel, Path=ActualWidth}" Height="24" Visibility="Collapsed" Padding="5,3,0,0">
            <TextBlock Text="{Binding Text}" ></TextBlock>
        </Border>
        <TextBox x:Name="textbox" Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="{Binding ElementName=panel, Path=ActualWidth}" Height="24"
                 base:CustomizeProperty.Placeholder="{Binding Placeholder}" TextChanged="Textbox_TextChanged" ></TextBox>
    </StackPanel>
</UserControl>
xaml
using APP_YFT.Base;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
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 YZ.HIS.UserControls.LabelRenders
{
    /// <summary>
    /// LabelRenderTextBox.xaml 的互動邏輯
    /// </summary>
    public partial class LabelRenderTextBox : UserControl
    {
        public LabelRenderTextBox()
        {
            InitializeComponent();
        }

        #region events
        public event TextChangedEventHandler TextChanged;
       
        private void Textbox_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (RequiredValue == RequiredValues.Number)
            {
                TextBox textBox = sender as TextBox;
                string text = textBox.Text;
                Regex reg = new Regex(@"^[0-9]*$");
                if (text == null || (text.Count() == 0) || !reg.IsMatch(text))
                {
                    textBox.Text = "";
                    return;
                }
            }
            this.TextChanged?.Invoke(this, e);
        }

        //清空輸入框事件
        private void ExecCleanSearchText(object sender, RoutedEventArgs e)
        {
            Button btn = sender as Button;
            if (btn == null)
                return;
            TextBox tb = btn.TemplatedParent as TextBox;
            if (tb != null)
            {
                tb.Text = "";
            }
        }

        private void Click(object sender, MouseButtonEventArgs e)
        {
            if (EditMode == EditModes.Click)
                SetEditable(true);
        }

        private void DoubleClick(object sender, MouseButtonEventArgs e)
        {
            if (EditMode == EditModes.DoubleCLick)
                SetEditable(true);
        }

        private void Lost_Focus(object sender, RoutedEventArgs e)
        {
            if(EditMode == EditModes.DoubleCLick || EditMode == EditModes.Click)
                SetEditable(false);
        }
        #endregion

        #region TextProperty 
        //public string Text { get; set; }
        public string Text { get
            {
                return (string)this.GetValue(TextProperty);
            }
            set
            {
                this.SetValue(TextProperty, value);
            }
        }

        public static readonly DependencyProperty TextProperty =
            DependencyProperty.RegisterAttached("Text", typeof(string), typeof(LabelRenderTextBox), new PropertyMetadata("", OnTextChanged));

        private static void OnTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.Text = e.NewValue as string;
            }
        }
        #endregion

        #region PlaceholderProperty 
        public string Placeholder { get
            {
                return (string)this.GetValue(PlaceholderProperty);
            }
            set
            {
                this.SetValue(PlaceholderProperty, value);
            }
        }

        public static readonly DependencyProperty PlaceholderProperty =
            DependencyProperty.RegisterAttached("Placeholder", typeof(string), typeof(LabelRenderTextBox), new PropertyMetadata("", OnPlaceholderChanged));

        private static void OnPlaceholderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.Placeholder = e.NewValue as string;
            }
        }
        #endregion

        #region RequiredValueProperty 
        public RequiredValues RequiredValue
        {
            get { return (RequiredValues)this.GetValue(RequiredValueProperty); }
            set
            {
                this.SetValue(RequiredValueProperty, value);
            }
        }

        public static readonly DependencyProperty RequiredValueProperty =
            DependencyProperty.RegisterAttached("RequiredValue", typeof(RequiredValues), typeof(LabelRenderTextBox), new PropertyMetadata(RequiredValues.String, OnRequiredValueChanged));

        private static void OnRequiredValueChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.RequiredValue = (RequiredValues)e.NewValue;
            }
        }
        #endregion

        #region UseSearchProperty 
        public bool UseSearch
        {
            get { return (bool)this.GetValue(UseSearchProperty); }
            set
            {
                this.SetValue(UseSearchProperty, value);
                if (value)
                    this.textbox.Style = this.FindResource("mySearchTextBoxStyle") as Style;

            }
        }

        public static readonly DependencyProperty UseSearchProperty =
            DependencyProperty.RegisterAttached("UseSearch", typeof(bool), typeof(LabelRenderTextBox), new PropertyMetadata(false, OnUseSearchChanged));

        private static void OnUseSearchChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.UseSearch = (bool)e.NewValue;
            }
        }
        #endregion

        #region CaretIndexProperty 
        public int CaretIndex {
            get { return (int)this.GetValue(CaretIndexProperty); }
            set
            {
                this.SetValue(CaretIndexProperty, value);
                this.textbox.CaretIndex = value;
            }
        }

        public static readonly DependencyProperty CaretIndexProperty =
            DependencyProperty.RegisterAttached("CaretIndex", typeof(int), typeof(LabelRenderTextBox), new PropertyMetadata(0, OnCaretIndexChanged));

        private static void OnCaretIndexChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.CaretIndex = (int)e.NewValue;
            }
        }
        #endregion

        #region SelectionStartProperty 
        public int SelectionStart
        {
            get { return (int)this.GetValue(SelectionStartProperty); }
            set
            {
                this.SetValue(SelectionStartProperty, value);
                this.textbox.SelectionStart = value;
            }
        }

        public static readonly DependencyProperty SelectionStartProperty =
            DependencyProperty.RegisterAttached("SelectionStart", typeof(int), typeof(LabelRenderTextBox), new PropertyMetadata(0, OnSelectionStartChanged));

        private static void OnSelectionStartChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.SelectionStart = (int)e.NewValue;
            }
        }
        #endregion

        #region EditModeProperty 
        public EditModes EditMode {
            get { return (EditModes)this.GetValue(EditModeProperty); }
            set {
                this.SetValue(EditModeProperty, value);
                if (value == EditModes.Editable)
                {
                    SetEditable(true);
                }
                else
                {
                    SetEditable(false);
                }
            }
        }

        public static readonly DependencyProperty EditModeProperty =
            DependencyProperty.RegisterAttached("EditMode", typeof(EditModes), typeof(LabelRenderTextBox), new PropertyMetadata(EditModes.Editable, OnEditModeChanged));

        private static void OnEditModeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var element = obj as LabelRenderTextBox;
            if (element != null)
            {
                element.EditMode = (EditModes)e.NewValue;
            }
        }
        #endregion

        private void SetEditable(bool editable)
        {
            if (editable)
            {
                this.label.Visibility = Visibility.Collapsed;
                this.textbox.Visibility = Visibility.Visible;
            }
            else
            {
                this.label.Visibility = Visibility.Visible;
                this.textbox.Visibility = Visibility.Collapsed;
            }
        }

       
    }

    public enum EditModes
    {
        //編輯模式
        Editable = 0,
        //只讀模式
        ReadOnly = 1,      
        //只讀模式展示,單擊進入編輯模式,失去焦點後恢復只讀模式
        Click = 2,
        //只讀模式展示,雙擊進入編輯模式,失去焦點後恢復只讀模式
        DoubleCLick = 3
    }

    public enum RequiredValues
    {
        String = 0,
        Number = 1
    }
}
C#