- UI组件
- 桌面应用
【免费下载链接】HandyControl
Contains some simple and commonly used WPF controls
PinBox 是 HandyControl 提供的一种"密码框的另一种形式"(见 pinBox 官方文档):它不再是一个整体输入框,而是将密码拆分成若干个独立的单字符单元,每个单元一个格子,适合 PIN 码、验证码、支付密码等逐位输入的场景。读完本文,你将掌握 PinBox 的全部属性、Completed 事件用法,并结合源码理解其"安全模式"下的密码存储机制与键盘交互实现。
控件本质:由 N 个单字符 PasswordBox 组成的组合控件
PinBox 在源码层面继承自System.Windows.Controls.Control,并通过[TemplatePart]约定模板中的命名部件(PinBox.cs):
[TemplatePart(Name = ElementPanel, Type = typeof(Panel))] public class PinBox : Control其中ElementPanel对应名为PART_Panel的 Panel。在默认样式中,这个 Panel 是一个UniformGrid,列数通过{TemplateBinding Length}动态绑定(PinBoxBaseStyle.xaml):
<ControlTemplate TargetType="hc:PinBox"> <UniformGrid Name="PART_Panel" Columns="{TemplateBinding Length}" Rows="1"/> </ControlTemplate>当模板应用(OnApplyTemplate)或Length变化时,控件会调用UpdateItems(),按长度向 Panel 中填充对应数量的PasswordBox(PinBox.cs)。每个内部 PasswordBox 的关键特征:
MaxLength = 1:强制每次只能输入一个字符;PasswordChar = PasswordChar:继承 PinBox 的掩码字符;- 水平、垂直内容均居中,且
Padding为默认值; SelectionBrush、SelectionTextBrush、SelectionOpacity、CaretBrush四个外观属性通过 Binding 从 PinBox 同步到每个单元(其中SelectionTextBrush仅在 NET40~NET472 之外的目标框架下编译)。
因此,PinBox 的实际交互单元就是 WPF 原生的PasswordBox,这保证了每位密码都具备标准的掩码与焦点行为。
属性详解
| 属性 | 描述 | 默认值 | 备注 | | - | - | - | - | | Password | 密码 | | 出于安全考虑,无法绑定 | | PasswordChar | 掩码字符 | ● | | | Length | 密码长度 | 4 | 最小值为 4 | | ItemMargin | 单元框间隔 | 4,0(默认样式) | 类型为 Thickness | | ItemWidth | 单元框宽度 | DefaultControlHeight(默认样式) | | | ItemHeight | 单元框高度 | DefaultControlHeight(默认样式) | |
Password:只读密码,不可绑定
Password是 PinBox 的核心数据属性。它的 getter 实时把所有内部 PasswordBox 的Password拼成字符串返回(PinBox.cs):
public string Password { get => _panel == null ? string.Empty : string.Join(string.Empty, _panel.Children.OfType<System.Windows.Controls.PasswordBox>().Select(item => item.Password)); ... }它并不是一个DependencyProperty,因此无法作为 Binding 的目标——这正是官方文档标注"出于安全考虑,无法绑定"的原因。你可以通过代码pinBox.Password读取,但无法在 XAML 中对其做{Binding ...}或{x:Bind}。
setter 的存在主要是为了支持 XAML 预置初始值(如<hc:PinBox Password="1234"/>)以及不安全模式下的回写。由于读取时机可能早于模板应用,setter 会先把密码逐字符存入List<SecureString>,待OnApplyTemplate时再通过Marshal.SecureStringToGlobalAllocUnicode转出并逐个填充到单元框,用完立即ZeroFreeGlobalAllocUnicode释放、password.Clear()清空(PinBox.cs、PinBox.cs)。从源码结构看,这一过程刻意使用SecureString避免明文字符串在托管堆中长期驻留。
Length:密码长度(最小 4,不足自动回钳)
Length是最重要的布局参数,注册时同时携带了校验与强制回调(PinBox.cs):
public static readonly DependencyProperty LengthProperty = DependencyProperty.Register( nameof(Length), typeof(int), typeof(PinBox), new PropertyMetadata(MinLength, OnLengthChanged, CoerceLength), ValidateHelper.IsInRangeOfPosInt); private static object CoerceLength(DependencyObject d, object basevalue) => (int) basevalue < 4 ? MinLength : basevalue;- 默认值
MinLength = 4; - 通过
ValidateHelper.IsInRangeOfPosInt校验必须为正整数,非法值会被拒绝; CoerceLength会把任何小于 4 的值钳制回 4,因此你无法得到 1~3 位的 PinBox;Length一旦变化,会触发UpdateItems()重建全部单元框,已输入的密码将丢失。
PasswordChar:掩码字符
PasswordChar通过AddOwner复用了 WPF 原生PasswordBox.PasswordCharProperty,默认值为'●'(PinBox.cs),因此它支持与原生 PasswordBox 相同的char赋值语法,例如PasswordChar="❤"。该值会被透传给每个内部单元框。
ItemMargin / ItemWidth / ItemHeight:单元外观
这三个属性分别控制每个单元框的间隔(Thickness)、宽度与高度(double)。需要注意它们只是普通属性,不参与PART_Panel的网格布局计算(面板列数由 Length 决定),实际效果由默认样式的默认值决定:
ItemMargin默认4,0(水平间距 4、垂直 0);ItemWidth与ItemHeight默认均为主题资源DefaultControlHeight,与控件标准高度一致(PinBoxBaseStyle.xaml)。
若你希望单元框更小、间距更密(例如验证码输入场景),可覆盖这三项。
事件:Completed
Completed是一个冒泡型(RoutingStrategy.Bubble)路由事件,注册于控件类型(PinBox.cs):
public static readonly RoutedEvent CompletedEvent = EventManager.RegisterRoutedEvent("Completed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PinBox)); public event RoutedEventHandler Completed { add => AddHandler(CompletedEvent, value); remove => RemoveHandler(CompletedEvent, value); }触发时机:当最后一个单元框输入了字符,且所有单元框均已非空时触发(PinBox.cs)。触发前会先清除键盘焦点:
if (++_inputIndex >= Length) { _inputIndex = Length - 1; if (_panel.Children.OfType<System.Windows.Controls.PasswordBox>() .All(item => item.Password.Any())) { FocusManager.SetFocusedElement(this, null); Keyboard.ClearFocus(); RaiseEvent(new RoutedEventArgs(CompletedEvent, this)); } return; }由于它是路由事件,既可以在 XAML 中直接挂接(hc:PinBox.Completed="..."),也可以通过AddHandler在代码中订阅。事件参数的OriginalSource即 PinBox 本身,可据此读取Password。官方演示(PinBoxDemo.xaml.cs)的做法是:
private void PinBox_OnCompleted(object sender, RoutedEventArgs e) { if (e.OriginalSource is PinBox pinBox) { Growl.Info(pinBox.Password); } }内置键盘交互逻辑
PinBox 没有自定义按键来接收输入,而是接管了单元之间的焦点流转,相关逻辑集中在三个重写方法里:
- 自动前进:
PasswordBoxsPasswordChanged中,当某位输入了字符(Password.Length > 0)且未到末位时,_inputIndex自增并把焦点移到下一个单元;反之当某位被清空时焦点回退到上一个单元(PinBox.cs)。 - 左右方向键:
OnPreviewKeyDown处理Key.Left/Key.Right,在单元间移动焦点并SelectAll()选中当前位便于覆盖输入(PinBox.cs)。 - 删除与退格:
OnPreviewKeyUp处理Key.Delete/Key.Back,借助_isInternalAction标志避免删除动作再次触发密码变化事件造成焦点错乱,随后把焦点退回前一个单元(PinBox.cs)。
此外,任一单元获得焦点时会记录当前索引并SelectAll()(PasswordBoxsGotFocus),保证用户直接点击任意格子后都能从该位开始输入。控件自身Focusable=False,初始聚焦由PinBox_Loaded中的FocusPasswordBox()完成——当 PinBox 自身处于焦点状态时,会把焦点转移给第一个单元(PinBox.cs)。
安全模式与 UnsafePassword
PinBox 从 HandyControl 的PasswordBox复用了两个附加在控件上的属性(PinBox.cs):
IsSafeEnabled:安全模式开关,默认True。为True时密码只存在于各单元 PasswordBox 内部,不对外暴露;为False时控件会把明文同步到UnsafePassword。UnsafePassword:不安全模式下的明文密码,注册时带BindsTwoWayByDefault,默认绑定模式为双向。仅在IsSafeEnabled == False时有效。
官方演示正好展示了这一用法(PinBoxDemo.xaml):
<hc:UniformSpacingPanel Spacing="16" Orientation="Vertical" Margin="32" VerticalAlignment="Center" hc:PinBox.Completed="PinBox_OnCompleted"> <hc:PinBox Length="4" Password="1234"/> <hc:PinBox Length="4" Password="1234" Name="pinBoxDemo" IsSafeEnabled="False"/> <TextBox Text="{Binding UnsafePassword, ElementName=pinBoxDemo, UpdateSourceTrigger=PropertyChanged}"/> <hc:PinBox Length="6" Password="123456" PasswordChar="❤"/> </hc:UniformSpacingPanel>当需要把 PinBox 的密码双向绑定到 ViewModel 时,标准做法是设置IsSafeEnabled="False",然后绑定UnsafePassword;如果保持默认安全模式,则只能通过代码读取Password,这正是"出于安全考虑,无法绑定"的完整含义。
实战:完整使用示例
综合官方文档与演示工程,一个可直接运行的典型用法如下(xmlns:hc 为 HandyControl 命名空间):
<StackPanel Margin="32" VerticalAlignment="Center" hc:PinBox.Completed="PinBox_OnCompleted"> <!-- 4 位 PIN,默认掩码 ●,默认样式自带 4,0 间隔与标准单元尺寸 --> <hc:PinBox Length="4" Password="1234"/> <!-- 6 位 PIN,自定义掩码字符,并设置单元间隔与尺寸 --> <hc:PinBox Length="6" Password="123456" Margin="0,16,0,0" PasswordChar="❤" ItemMargin="8,0" ItemWidth="48" ItemHeight="48"/> </StackPanel>要点回顾:
Length最小为 4,设小值会被自动钳制回 4,设非正整数会被校验拒绝;- 默认密码为
●,可换成任意char; - 输入完最后一位且所有格子非空时触发
Completed,此时焦点已被清除,可在处理函数中通过e.OriginalSource as PinBox读取Password; - 需要绑定时改用
IsSafeEnabled="False"+ 双向绑定UnsafePassword; - 单元框尺寸与间距由
ItemWidth/ItemHeight/ItemMargin控制,默认值定义在 PinBoxBaseStyle.xaml,整体样式通过 PinBox.xaml 以BasedOn方式挂接,并在 Theme.xaml 中统一注册为默认主题样式,引入 HandyControl 主题后即可直接使用。
总结
PinBox 用一组受控的单元 PasswordBox 实现了逐位输入的 PIN 码交互:Length决定格子数量(最小 4)、PasswordChar决定掩码、Completed在输满时通知业务代码;安全模式下密码以SecureString暂存且不可绑定,需要双向绑定则显式关闭IsSafeEnabled。从源码(PinBox.cs)与默认样式(PinBoxBaseStyle.xaml)可以看出,焦点自动流转、左右键移动与退格回退等细节均已内置,接入时只需关注属性和事件即可。
- UI组件
- 桌面应用
【免费下载链接】HandyControl
Contains some simple and commonly used WPF controls
相关推荐
HandyControl Pagination 页码条控件实战指南:属性、事件与源码级分页原理
HandyControl Pagination 页码条控件实战指南:属性、事件与源码级分页原理 当列表数据量过多时,使用分页拆分数据能显著提升浏览效率与界面整洁
UI组件桌面应用HandyControl Divider 分割线控件完全指南:属性详解、样式定制与源码实现
HandyControl Divider 分割线控件完全指南:属性详解、样式定制与源码实现 本指南围绕 HandyControl 中的 Divider(分割线)
UI组件桌面应用HandyControl StepBar 步骤条控件详解:属性、方法、事件与实战案例
HandyControl StepBar 步骤条控件详解:属性、方法、事件与实战案例 导读 StepBar(步骤条)是 HandyControl 中用于引导用户
UI组件桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考