Маскирование ввода в TextBox

Задача — «создать поле ввода пароля с возможностью показать или скрыть пароль». Требований к безопасности нет, поэтому работу с паролем можно вести в строковом виде.

На данный момент эту проблему можно решить довольно просто, используя шрифт с одним символом маски в TextBox. При срабатывании обычного CheckBox шрифт заменяется маскирующим шрифтом или наоборот. Но такой маскирующий шрифт необходимо подготовить заранее.

Но недавно дали дополнительное условие «возможность смены маскирующего символа». Самый простой способ изменить символ — в PasswordBox. Но у него нет возможности отключить маскирование и для привязки пароля необходимо использовать дополнительный код. На данный момент нам приходится использовать решение, в котором TextBox используется в режиме «без маскировки», а PasswordBox — в режиме «с маскированием».

Можете ли вы посоветовать что-то более оптимальное?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
65
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, что хорошее и простое решение — использовать декоратор для визуализации маскирующих символов как наложение исходного ввода.

Покажите декоратору, чтобы он отображал маскирующие символы, скрывая пароль, установив кисть переднего плана на кисть фона.

В следующем примере показано, как использовать 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 null)
    {
      throw new InvalidOperationException("No AdornerDecorator found in parent visual tree");
    }

    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);
      ShowText();
    }
    else
    {
      HideText();
      this.adornerLayer?.Add(this.maskingAdorner);
    }

    this.isChangeInternal = false;
  }

  private void ShowText()
    => SetCurrentValue(ForegroundProperty, this.foregroundInternal);
  private void HideText()
  {
    this.foregroundInternal = this.Foreground;
    SetCurrentValue(ForegroundProperty, this.Background);
  }

  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 DefaultSystemTextPadding = 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);

      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;

      ReadOnlySpan<char> maskedInput = MaskInput(this.unsecurePasswodBox.Text);
      var maskedText = new FormattedText(
      maskedInput.ToString(),
      CultureInfo.CurrentCulture,
      this.unsecurePasswodBox.FlowDirection,
      typeface,
      this.unsecurePasswodBox.FontSize,
      this.Foreground,
      pixelsPerDip)
      {
        MaxTextWidth = ((FrameworkElement)this.AdornedElement).ActualWidth + UnsecurePasswordBoxAdorner.DefaultSystemTextPadding,
        Trimming = TextTrimming.None
      };

      var textOrigin = new Point(0, 0);
      textOrigin.Offset(this.unsecurePasswodBox.Padding.Left + UnsecurePasswordBoxAdorner.DefaultSystemTextPadding, 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);
    }
  }
}

Общий.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 = "&#xE890;"
                            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>

К сожалению, ваш код содержит ошибки в классе UnsecurePasswordBoxAdorner. Скорее всего они произошли при копировании кода в сообщение. Буду очень признателен, если исправите эти ошибки.

EldHasp 01.04.2024 19:27

Вы можете попробовать еще раз.

BionicCode 01.04.2024 20:59

Я предлагаю использовать UserControl, который использует исходный PasswordBox и альтернативный TextBox, управляемый ToggleButton. Пользовательский элемент управления XAML:

<UserControl x:Class = "Problem300324A.DualPasswordBox"
             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:local = "clr-namespace:Problem300324A"
             mc:Ignorable = "d" 
             d:DesignHeight = "450" d:DesignWidth = "800" Name = "Parent">
    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key = "BolleanToVisibilityConverter" />
        <local:InvertBooleanToVisibilityConverter x:Key = "InvertBooleanToVisibilityConverter"/>
        <local:ShowHideConverter x:Key = "ShowHideConverter"/>
    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <PasswordBox Name = "password"
                     PasswordChanged = "PasswordBox_PasswordChanged" 
                     Visibility = "{Binding ElementName=Parent,Path=ShowPassword,Converter = {StaticResource InvertBooleanToVisibilityConverter}}"
                    />
        
        <TextBox Name = "text"  Grid.Column = "0"
                 TextChanged = "text_TextChanged"
                 Text = "{Binding ElementName=Parent,Path=Password}" 
                 Visibility = "{Binding ElementName=Parent,Path=ShowPassword,Converter = {StaticResource BolleanToVisibilityConverter}}" 
                
                 />
        <ToggleButton Name = "toggle" Grid.Column = "1" 
                      IsChecked = "{Binding ElementName=Parent,Path=ShowPassword,Mode=TwoWay}" Width = "40"
                      Content = "{Binding ElementName=Parent,Path=ShowPassword,Converter = {StaticResource ShowHideConverter}}" 
                      />
    </Grid>
</UserControl>

Код позади:

public partial class DualPasswordBox : UserControl
    {
        public DualPasswordBox()
        {
            InitializeComponent();
        }
        public static readonly DependencyProperty ShowPasswordProperty =
         DependencyProperty.Register("ShowPassword", typeof(bool), typeof(DualPasswordBox));

        public bool ShowPassword
        {
            get { return (bool)GetValue(ShowPasswordProperty); }
            set { SetValue(ShowPasswordProperty, value); }
            
        }
        private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
        {
            if (text.Text != password.Password)
            {
                text.Text = password.Password;
                text.CaretIndex = text.Text.Length;
            }
            
        }
        private void text_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (text.Text != password.Password)
            {
                password.Password = text.Text;
                password
                    .GetType().
                    GetMethod("Select", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(password, new object[] { text.Text.Length, 0 });


            }
        }
    }

Используя пример:

<local:DualPasswordBox ShowPassword = "True" Height = "40" />

Естественно, код представляет собой нарушение безопасности, которое необходимо оценить.

На данный момент мы также используем подобное решение. Я об этом написал в своем вопросе. Я считаю это решение неоптимальным и «сложным». Но другой лучшей альтернативы мы пока не нашли.

EldHasp 31.03.2024 15:59

Другие вопросы по теме