☰
HandyControl PinBox PIN码框控件详解:属性、事件与源码实现
2026/10/3 8:15:08 网站建设 项目流程
  • UI组件
  • 桌面应用

【免费下载链接】HandyControl

Contains some simple and commonly used WPF controls

项目地址:https://gitcode.com/gh_mirrors/ha/HandyControl
点击查看免费下载

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>

要点回顾:

  1. Length最小为 4,设小值会被自动钳制回 4,设非正整数会被校验拒绝;
  2. 默认密码为●,可换成任意char;
  3. 输入完最后一位且所有格子非空时触发Completed,此时焦点已被清除,可在处理函数中通过e.OriginalSource as PinBox读取Password;
  4. 需要绑定时改用IsSafeEnabled="False"+ 双向绑定UnsafePassword;
  5. 单元框尺寸与间距由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

项目地址:https://gitcode.com/gh_mirrors/ha/HandyControl
点击查看免费下载

相关推荐

上一篇:如何在Shell中格式化并打印当前日期时间 - jbranchaud/til项目技巧
下一篇:如何快速部署Apache PredictionIO推荐引擎:Docker完整安装指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询