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>
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 } }