任务是“创建一个带有显示或隐藏密码选项的密码输入字段”。没有安全要求,因此可以使用字符串类型来处理密码。
目前,可以通过在文本框中使用带有单个掩码字符的字体来非常简单地解决这个问题。当由常规复选框触发时,字体将替换为掩码字体,反之亦然。但这样的遮蔽字体必须提前准备好。
但最近他们给出了一个额外的条件“能够改变屏蔽符号”。更改字符的简单方法是在PasswordBox 中。但它不具备禁用屏蔽功能,必须使用额外的代码来绑定密码。 目前,我们必须采用这样的解决方案:在“不屏蔽”模式下使用 TextBox,在“有屏蔽”模式下使用 PasswordBox。
你能推荐一些更优化的吗?
我认为一个好的且简单的解决方案是使用装饰器将屏蔽符号渲染为原始输入的覆盖。
通过将前景画笔设置为背景画笔,显示装饰器以渲染屏蔽字符,同时隐藏密码。
以下示例展示了如何使用
Adorner
来装饰 TextBox
。
我删除了一些代码以降低复杂性(原始库代码还包含输入字符和密码长度的验证、事件逻辑、路由命令、SecureString
支持、低级插入符定位以支持任何字体系列,其中字体不是等宽字体,密码字符和掩码字符将无法正确对齐等,并且默认值要复杂得多 ControlTemplate
)。因此,当前版本仅支持等宽字体。支持的字体必须在构造函数中注册。您可以改为实现等宽字体检测。
但是,这是一个完全有效的示例(复活节快乐!)。
UnsecurePasswodBox.cs
public class UnsecurePasswodBox : TextBox
{
public bool IsShowPasswordEnabled
{
get => (bool)GetValue(IsShowPasswordEnabledProperty);
set => SetValue(IsShowPasswordEnabledProperty, value);
}
public static readonly DependencyProperty IsShowPasswordEnabledProperty = DependencyProperty.Register(
"IsShowPasswordEnabled",
typeof(bool),
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(default(bool), OnIsShowPasswordEnabledChaged));
public char CharacterMaskSymbol
{
get => (char)GetValue(CharacterMaskSymbolProperty);
set => SetValue(CharacterMaskSymbolProperty, value);
}
public static readonly DependencyProperty CharacterMaskSymbolProperty = DependencyProperty.Register(
"CharacterMaskSymbol",
typeof(char),
typeof(UnsecurePasswodBox),
new PropertyMetadata('●', OnCharacterMaskSymbolChanged));
private FrameworkElement? part_ContentHost;
private AdornerLayer? adornerLayer;
private UnsecurePasswordBoxAdorner? maskingAdorner;
private Brush foregroundInternal;
private bool isChangeInternal;
private readonly HashSet<string> supportedMonospaceFontFamilies;
private FontFamily fallbackFont;
static UnsecurePasswodBox()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(typeof(UnsecurePasswodBox)));
TextProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(OnTextChanged));
ForegroundProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(propertyChangedCallback: null, coerceValueCallback: OnCoerceForeground));
FontFamilyProperty.OverrideMetadata(
typeof(UnsecurePasswodBox),
new FrameworkPropertyMetadata(propertyChangedCallback: null, coerceValueCallback: OnCoerceFontFamily));
}
public UnsecurePasswodBox()
{
this.Loaded += OnLoaded;
// Only use a monospaced font
this.supportedMonospaceFontFamilies = new HashSet<string>()
{
"Consolas",
"Courier New",
"Lucida Console",
"Cascadia Mono",
"Global Monospace",
"Cascadia Code",
};
this.fallbackFont = new FontFamily("Consolas");
this.FontFamily = fallbackFont;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
this.Loaded -= OnLoaded;
FrameworkElement adornerDecoratorChild = this.part_ContentHost ?? this;
this.adornerLayer = AdornerLayer.GetAdornerLayer(adornerDecoratorChild);
if (this.adornerLayer is not null)
{
Rect contentBounds = LayoutInformation.GetLayoutSlot(adornerDecoratorChild);
this.maskingAdorner = new UnsecurePasswordBoxAdorner(adornerDecoratorChild, this)
{
Foreground = Brushes.Black
};
HandleInputMask();
}
}
private static void OnIsShowPasswordEnabledChaged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var unsecurePasswordBox = (UnsecurePasswodBox)d;
unsecurePasswordBox.HandleInputMask();
Keyboard.Focus(unsecurePasswordBox);
}
private static void OnCharacterMaskSymbolChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((UnsecurePasswodBox)d).RefreshMask();
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> ((UnsecurePasswodBox)d).RefreshMask();
private static object OnCoerceForeground(DependencyObject d, object baseValue)
{
var unsecurePasswordBox = (UnsecurePasswodBox)d;
// Reject external font color change while in masking mode
// as this would reveal the password.
// But store new value and make it available when exiting masking mode.
if (!unsecurePasswordBox.isChangeInternal && !unsecurePasswordBox.IsShowPasswordEnabled)
{
unsecurePasswordBox.foregroundInternal = baseValue as Brush;
}
return unsecurePasswordBox.isChangeInternal
? baseValue
: unsecurePasswordBox.IsShowPasswordEnabled
? baseValue
: unsecurePasswordBox.Foreground;
}
private static object OnCoerceFontFamily(DependencyObject d, object baseValue)
{
var unsecurePasswordBox = (UnsecurePasswodBox)d;
var desiredFontFamily = baseValue as FontFamily;
return desiredFontFamily is not null
&& unsecurePasswordBox.supportedMonospaceFontFamilies.Contains(desiredFontFamily.Source)
? baseValue
: unsecurePasswordBox.FontFamily;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.part_ContentHost = GetTemplateChild("PART_ContentHost") as FrameworkElement;
}
private void HandleInputMask()
{
this.isChangeInternal = true;
if (this.IsShowPasswordEnabled)
{
this.adornerLayer?.Remove(this.maskingAdorner);
SetCurrentValue(ForegroundProperty, this.foregroundInternal);
}
else
{
this.foregroundInternal = this.Foreground;
SetCurrentValue(ForegroundProperty, this.Background);
this.adornerLayer?.Add(this.maskingAdorner);
}
this.isChangeInternal = false;
}
private void RefreshMask()
{
if (!this.IsShowPasswordEnabled)
{
this.maskingAdorner?.Update();
}
}
private class UnsecurePasswordBoxAdorner : Adorner
{
public Brush Foreground { get; set; }
private readonly UnsecurePasswodBox unsecurePasswodBox;
private const int DefaultTextPadding = 2;
public UnsecurePasswordBoxAdorner(UIElement adornedElement, UnsecurePasswodBox unsecurePasswodBox) : base(adornedElement)
{
this.IsHitTestVisible = false;
this.unsecurePasswodBox = unsecurePasswodBox;
}
public void Update()
=> InvalidateVisual();
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
ReadOnlySpan<char> maskedInput = MaskInput(this.unsecurePasswodBox.Text);
var typeface = new Typeface(
this.unsecurePasswodBox.FontFamily,
this.unsecurePasswodBox.FontStyle,
this.unsecurePasswodBox.FontWeight,
this.unsecurePasswodBox.FontStretch,
this.unsecurePasswodBox.fallbackFont);
double pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
var maskedText = new FormattedText(
maskedInput.ToString(),
CultureInfo.CurrentCulture,
this.unsecurePasswodBox.FlowDirection,
typeface,
this.unsecurePasswodBox.FontSize,
this.Foreground,
pixelsPerDip);
maskedText.MaxTextWidth = ((FrameworkElement)this.AdornedElement).ActualWidth + UnsecurePasswordBoxAdorner.DefaultTextPadding;
maskedText.Trimming = TextTrimming.None;
var textOrigin = new Point(0, 0);
textOrigin.Offset(this.unsecurePasswodBox.Padding.Left + UnsecurePasswordBoxAdorner.DefaultTextPadding, 0);
drawingContext.DrawText(maskedText, textOrigin);
}
private ReadOnlySpan<char> MaskInput(ReadOnlySpan<char> input)
{
if (input.Length == 0)
{
return input;
}
char[] textMask = new char[input.Length];
Array.Fill(textMask, this.unsecurePasswodBox.CharacterMaskSymbol);
return new ReadOnlySpan<char>(textMask);
}
}
}
Generic.xaml
<Style TargetType="local:UnsecurePasswodBox">
<Setter Property="BorderBrush"
Value="{x:Static SystemColors.ActiveBorderBrush}" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="Background"
Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:UnsecurePasswodBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<AdornerDecorator Grid.Column="0">
<ScrollViewer x:Name="PART_ContentHost" />
</AdornerDecorator>
<ToggleButton Grid.Column="1"
IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsShowPasswordEnabled}"
Background="Transparent"
VerticalContentAlignment="Center"
Padding="4,0">
<ToggleButton.Content>
<TextBlock Text=""
FontFamily="Segoe MDL2 Assets" />
</ToggleButton.Content>
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<ContentPresenter Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>