首先来讲讲创建这个控件的初衷,一个让我很郁闷的问题。
公司的客户端项目采用WPF+MVVM技术实现,在近期地推客户端的过程中遇到了一个很奇葩的问题:在登录界面点击密码框就会直接闪退,没有任何提示
密码框是WPF原生的PasswordBox,这似乎没有什么不对。出现这个情况的一般是在xp系统(ghost的雨林木风版本或番茄花园),某些xp系统又不会出现。
出现这个问题的原因是因为客户的系统缺少PasswordBox使用的某种默认的字体,网上有人说是times new roman,又或者其它某种字体。其实只要找到了这个字体,并在程序启动的时候安装这种字体可以解决。
但我觉得,这个解决方案太麻烦。与其依赖PasswordBox使用的默认字体,不如重写PasswordBox,避免密码框字体的依赖。
现在让我们来重写一个自己的带水印的文本(密码)框吧
1.首先创建一个类,继承TextBox
public class SJTextBox : TextBox
2.指定依赖属性的实例重写基类型的元数据
static SJTextBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SJTextBox), new FrameworkPropertyMetadata(typeof(SJTextBox))); }
3.定义依赖属性
public static DependencyProperty WaterRemarkProperty = DependencyProperty.Register("WaterRemark", typeof(string), typeof(SJTextBox)); /// <summary> /// 水印文字 /// </summary> public string WaterRemark { get { return GetValue(WaterRemarkProperty).ToString(); } set { SetValue(WaterRemarkProperty, value); } } public static DependencyProperty BorderCornerRadiusProperty = DependencyProperty.Register("BorderCornerRadius", typeof(CornerRadius), typeof(SJTextBox)); /// <summary> /// 边框角度 /// </summary> public CornerRadius BorderCornerRadius { get { return (CornerRadius)GetValue(BorderCornerRadiusProperty); } set { SetValue(BorderCornerRadiusProperty, value); } } public static DependencyProperty IsPasswordBoxProperty = DependencyProperty.Register("IsPasswordBox", typeof(bool), typeof(SJTextBox), new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsPasswordBoxChnage))); /// <summary> /// 是否为密码框 /// </summary> public bool IsPasswordBox { get { return (bool)GetValue(IsPasswordBoxProperty); } set { SetValue(IsPasswordBoxProperty, value); } } public static DependencyProperty PasswordCharProperty = DependencyProperty.Register("PasswordChar", typeof(char), typeof(SJTextBox), new FrameworkPropertyMetadata('●')); /// <summary> /// 替换明文的单个密码字符 /// </summary> public char PasswordChar { get { return (char)GetValue(PasswordCharProperty); } set { SetValue(PasswordCharProperty, value); } } public static DependencyProperty PasswordStrProperty = DependencyProperty.Register("PasswordStr", typeof(string), typeof(SJTextBox), new FrameworkPropertyMetadata(string.Empty)); /// <summary> /// 密码字符串 /// </summary> public string PasswordStr { get { return GetValue(PasswordStrProperty).ToString(); } set { SetValue(PasswordStrProperty, value); } }
4.当设置为密码框时,监听TextChange事件,处理Text的变化,这是密码框的核心功能
private static void OnIsPasswordBoxChnage(DependencyObject sender, DependencyPropertyChangedEventArgs e) { (sender as SJTextBox).SetEvent(); } /// <summary> /// 定义TextChange事件 /// </summary> private void SetEvent() { if (IsPasswordBox) this.TextChanged += SJTextBox_TextChanged; else this.TextChanged -= SJTextBox_TextChanged; }
5.在TextChange事件中,处理Text为密码文,并将原字符记录给PasswordStr予以存储
private void SJTextBox_TextChanged(object sender, TextChangedEventArgs e) { if (!IsResponseChange) //响应事件标识,替换字符时,不处理后续逻辑 return; Console.WriteLine(string.Format("------{0}------", e.Changes.Count)); foreach (TextChange c in e.Changes) { Console.WriteLine(string.Format("addLength:{0} removeLenth:{1} offSet:{2}", c.AddedLength, c.RemovedLength, c.Offset)); PasswordStr = PasswordStr.Remove(c.Offset, c.RemovedLength); //从密码文中根据本次Change对象的索引和长度删除对应个数的字符 PasswordStr = PasswordStr.Insert(c.Offset, Text.Substring(c.Offset, c.AddedLength)); //将Text新增的部分记录给密码文 lastOffset = c.Offset; } Console.WriteLine(PasswordStr); /*将文本转换为密码字符*/ IsResponseChange = false; //设置响应标识为不响应 this.Text = ConvertToPasswordChar(Text.Length); //将输入的字符替换为密码字符 IsResponseChange = true; //回复响应标识 this.SelectionStart = lastOffset + 1; //设置光标索引 Console.WriteLine(string.Format("SelectionStar:{0}", this.SelectionStart)); }
/// <summary> /// 按照指定的长度生成密码字符 /// </summary> /// <param name="length"></param> /// <returns></returns> private string ConvertToPasswordChar(int length) { if (PasswordBuilder != null) PasswordBuilder.Clear(); else PasswordBuilder = new StringBuilder(); for (var i = 0; i < length; i++) PasswordBuilder.Append(PasswordChar); return PasswordBuilder.ToString(); }
ConvertToPasswordChar()方法用于返回指定个数的密码字符,替换Text为密码文就是调用此方法传递Text的长度完成的
6.如果用户设置了记住密码,密码文(PasswordStr)一开始就有值的话,别忘了在Load事件里事先替换一次明文
private void SJTextBox_Loaded(object sender, RoutedEventArgs e) { if (IsPasswordBox) { IsResponseChange = false; this.Text = ConvertToPasswordChar(PasswordStr.Length); IsResponseChange = true; } }
7.代码逻辑部分已经完成,替换明文为密码字符的功能已经实现了。自定义边框角度,水印功能,需要借助Style来完成,让我们来为它写一个Style
<Style TargetType="{x:Type local:SJTextBox}"> <Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderBrush" Value="Gray"/> <Setter Property="Cursor" Value="IBeam"/> <Setter Property="Padding" Value="3,0,0,0"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:SJTextBox}"> <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="{TemplateBinding BorderCornerRadius}" <!--绑定自定义边框角度--> SnapsToDevicePixels="True"> <Grid> <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/> <TextBlock x:Name="txtRemark" Text="{TemplateBinding WaterRemark}" <!--绑定水印文字--> Foreground="Gray" VerticalAlignment="Center" Margin="{TemplateBinding Padding}" Visibility="Collapsed"/> <!--默认水印文字隐藏--> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="Text" Value=""> <!--使用触发器来控制水印的隐藏显示:当文本框没有字符时显示水印文字--> <Setter Property="Visibility" Value="Visible" TargetName="txtRemark"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
8.自定义水印文本(密码)框的调用
<local:SJTextBox Height="30" BorderCornerRadius="3" Margin="10,10" Background="White" WaterRemark="This is a TextBox"/> <!--水印文本框--> <local:SJTextBox Height="30" BorderCornerRadius="3" Margin="10,0" Background="White" WaterRemark="This is a PassworBox" IsPasswordBox="True" PasswordStr="{Binding Password}"/> <!--水印密码框-->
自定义的水印文本(密码)框已经完成了,它避免了对密码字体的依赖,同时密码文属性PasswordStr也支持数据绑定,非常方便。
效果图: