65.9K
CodeProject 正在变化。 阅读更多。
Home

智能 WPF 登录叠加层(Windows 8 风格)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (23投票s)

2012年11月23日

CPOL

12分钟阅读

viewsIcon

145271

downloadIcon

18221

用于WPF应用程序的登录覆盖层,风格与Windows 8登录屏幕相似。

目录

  1. 新闻
  2. 引言
  3. 背景
  4. 要求
  5. 使用控件
  6. 所有类成员列表
  7. 用户控件的代码
  8. 关注点
  9. 最近更改
  10. 历史

新闻

在会员 IFFI向我询问是否应该有一个输入用户名的选项后,我决定实现此功能。我 认为,当 用户 能够 输入用户名和密码时, 这样做更有意义 只有这样 才能 确保 用户切换 就像它应该的那样容易,并且 该控件 可以在各种应用程序中使用 但是,我 也保留了旧的功能 (不提供输入用户名的选项 所有 代码 更改都可以在 此处 找到。此外,我还添加了第二个演示来演示此新功能(请参阅此处)。

引言

对于任何对轻量级用户验证控件(用于WPF应用程序)感兴趣的人,我都实现了一个Smart Login Overlay WPF控件。登录覆盖层——尤其是密码框——具有与Windows 8登录屏幕相同的视觉样式和行为。我拍摄了Windows 8登录屏幕的几张截图,并尽力模仿它。

以下是一些控件运行时的截图,以便您了解我的意思。

控件的行为

如果您尝试在没有凭据的情况下登录,您将看到此行为(文本是可自定义的)

Smart Login Overlay NO credentials - Demo Window

如果您尝试使用错误的凭据登录,您将看到此行为(文本是可自定义的)

Smart Login Overlay WRONG credentials - Demo Window

还有一个功能齐全的RevealButton(类似于Windows 8密码框中的按钮)

Smart Login Overlay reveal button - Demo Window

此外,还有一个Caps Lock激活时的提示(文本可自定义)

Smart Login Overlay CapsLock active - Demo Window

背景

几天前,我在我的博客上实现并撰写了一篇关于Smart Password Box的文章(Windows 8 PasswordBox风格的WPF)。完成此操作后,我决定让它更智能一些,并将其样式设置为类似于Windows 8登录屏幕。

我利用业余时间实现了其中大部分组件。因此,如果有什么不完美的地方,请您谅解。如果您需要任何帮助,请随时问我。

要求

要使用SmartLoginOverlay WPF控件,您需要

使用控件

使用控件非常简单。只需在项目中添加对“WPFSmartLibraryLight35.dll”的引用(右键单击References,然后选择“Add Reference”)。您的引用应如下所示

Reference to the WPFSmartlibraryLight

添加引用后,您应该声明一个XAML命名空间,该命名空间映射到各种WPFSmartLibrary CLR命名空间。将以下XAML命名空间添加到您的Window标签中

<... xmlns:wpfsl="http://schemas.softarcs.com/wpfsmartlibrary" ...>

完成此操作后,您就可以在XAML代码中使用该控件,如下所示

<wpfsl:SmartLoginOverlay FullSpan="On" Background="#FF16499A"
	               UserName="{Binding UserName}"
		       Password="{Binding Password}"
		       AccessPassword="{Binding UserPassword}"
		       UserImageSource="{Binding UserImageSource}"
		       AdditionalUserInfo="{Binding EMailAddress}"
		       AdditionalSystemInfo="Locked" />

件事非常重要 并且 应该被遵守:

  1. 控件应该Grid面板的子元素。如果控件的父元素不是Grid面板,Span属性将无法正常工作(有关Span属性的更多信息)。
  2. 为了保证控件是顶层元素,它必须是XAML中定义的最后一个控件,或者Panel.ZIndex属性必须设置为所有已定义控件中的最高值。

使用该控件有三种可能的方法

  1. 完整的MVVM,并由控件提供自动密码验证
    • 您可以在上面的XAML代码中看到此方法的示例。如果设置了AccessPassword属性,用户控件将自动执行密码验证。
  2. 完整的MVVM,在ViewModel中使用命令绑定进行自定义密码验证(推荐)
    • 此方法的XAML代码如下所示(您可以看到AccessPassword属性设置) 
    • <wpfsl:SmartLoginOverlay x:Name="SmartLoginOverlayControl"
                       FullSpan="On" Background="#FF16499A"
                       UserName="{Binding UserName}"
                       Password="{Binding Password}"
                       UserImageSource="{Binding UserImageSource}"
                       AdditionalUserInfo="{Binding EMailAddress}"
                       Command="{Binding SubmitCommand}"
                       CommandParameter="{Binding RelativeSource={RelativeSource Self} }" />
      		

      这是一个Command属性及其相应命令方法的示例实现

      // ---------------------------------------------------------------------------------
      // Put this in the constructor of your ViewModel class
      this.SubmitCommand = new ActionCommand( this.ExecuteSubmit, this.CanExecuteSubmit );
      // ---------------------------------------------------------------------------------
      
      public ICommand SubmitCommand { get; private set; }
      
      private void ExecuteSubmit(object commandParameter)
      {
        Debug.WriteLine( "Here you would implement the submission and a following validation of this data:\n" +
      		this.UserName + "\n" + this.EMailAddress + "\n" + this.Password );
      
        var accessControlSystem = commandParameter as SmartLoginOverlay;
      
        if (accessControlSystem != null)
        {
          if (this.Password.Equals( this.UserPassword ))
          {
            accessControlSystem.Unlock();
          }
          else
          {
            accessControlSystem.ShowWrongCredentialsMessage();
          }
        }
      }
      
      private bool CanExecuteSubmit(object commandParameter)
      {
        return !string.IsNullOrEmpty( this.Password );
      }		
      		
  3. 为属性分配固定值,让控件进行验证,或者在代码隐藏中验证密码(适用于不熟悉MVVM或不想使用它的任何人)
    • 此方法的XAML代码如下所示
    • <wpfsl:SmartLoginOverlay x:Name="SmartLoginOverlayControl"
                       FullSpan="On" Background="#FF16499A"
                       UserName="PSY" AccessPassword="gangnamstyle"
                       UserImageSource="E:\WPFSmartLibraryLight\LoginOverlayDemo\Images\PSY.png"
                       AdditionalUserInfo="psy@youtuberecord.com"
                       AdditionalSystemInfo="Locked" />
      	

      如果您想在代码隐藏中自行处理密码验证,只需将AccessPassword设置为未设置,并将事件处理程序分配给SubmitRequested事件。

演示应用程序中显示了所有三种方法。

所有类成员列表

SmartLoginOverlay类

命名空间           : SoftArcs.WPFSmartLibrary.UserControls
程序集              : WPFSmartLibraryLight35.dll
XAML的XMLNS : http://schemas.softarcs.com/wpfsmartlibrary

SmartLoginOverlay类型公开以下成员

构造函数

名称 描述
SmartLoginOverlay 初始化 SmartLoginOverlay 类的新实例。

属性

名称 描述 属性类型
AccessPassword 获取或设置由内部验证过程接受的密码。这是一个依赖属性(默认双向绑定)。 字符串
AdditionalSystemInfo 获取或设置将显示在AdditionalUserInfo下,字体大小更小,不透明度为0.6的附加系统信息。这是一个依赖属性。 字符串
AdditionalUserInfo 获取或设置将显示在UserName下,字体大小更小的附加用户信息。这是一个依赖属性。 字符串
CapsLockInfo 获取或设置当CapsLock激活时显示的提示。这是一个依赖属性。默认值为“Caps Lock is active”。 字符串
命令 获取或设置单击提交按钮或按下回车键时调用的命令。这是一个依赖属性。(继承自AdvancedUserControl ICommand
CommandParameter 获取或设置要传递给Command属性的参数。这是一个依赖属性。(继承自AdvancedUserControl ICommand
DisappearAnimation 获取或设置消失动画的类型。这是一个依赖属性。默认值为MoveAndFadeOutToRight。 DisappearAnimationType
FullColumnSpan 开启或关闭用户控件的整列跨度。这是一个依赖属性。默认值为Off。仅当父元素是Grid面板时适用。(继承自AdvancedUserControl SwitchState
FullRowSpan 开启或关闭用户控件的整行跨度。这是一个依赖属性。默认值为Off。仅当父元素是Grid面板时适用。(继承自AdvancedUserControl SwitchState
FullSpan 开启或关闭用户控件的整行和整列跨度。这是一个依赖属性。默认值为Off。仅当父元素是Grid面板时适用。(继承自AdvancedUserControl SwitchState
IsUserOptionAvailable 获取或设置输入用户名的选项。这是一个依赖属性。 布尔值
NoCredentialsInfo 获取或设置尝试使用无凭据登录时显示的提示。这是一个依赖属性。默认值为“Enter your credentials and try again.”。 字符串
密码 获取或设置用户输入的密码。这是一个依赖属性(默认双向绑定)。(由于它是一个依赖属性,因此它是可绑定的,与标准PasswordBox的不可绑定Password属性相反) 字符串
SubmitButtonTooltip 获取或设置提交按钮的工具提示。这是一个依赖属性。默认值为“Submit”。 字符串
UserImageSource 获取或设置将为最近使用的用户显示的图片的URI。这是一个依赖属性。 字符串
UserName 获取或设置将作为最近使用的用户显示的用户名。这是一个依赖属性(默认双向绑定且DefaultUpdateSourceTrigger=PropertyChanged)。 字符串
Watermark 获取或设置密码框中的水印,当密码框为空时显示。这是一个依赖属性。默认值为“Enter password”。 字符串
WrongCredentialsInfo 获取或设置尝试使用错误的凭据登录时显示的提示。这是一个依赖属性。默认值为“The password is incorrect. Make sure that you use the password for your account. You can reset the password at any time under 'myaccount.credentialserver.com/reset'。”。 字符串

方法

名称 描述
锁定 重置所有动画,如有必要,同化背景,并将焦点设置到PasswordBox控件。
ShowNoCredentialsMessage 显示NoCredentialsInfo并将焦点设置到OK按钮。
ShowWrongCredentialsMessage 显示WrongCredentialsInfo并将焦点设置到OK按钮。
Unlock 执行定义的(或默认的)DisappearAnimation和FadeOut动画,使覆盖层消失。

事件

名称 描述
SubmitRequested 当单击提交按钮或在控件获得焦点时按下回车键时发生。
DisappearAnimationType枚举
public enum DisappearAnimationType
{
	FadeOut,
	MoveAndFadeOutToRight,
	MoveAndFadeOutToTop,
	MoveAndFadeOutToRightSimultaneous,
	MoveAndFadeOutToTopSimultaneous
}
AdvancedUserControl基类

AdvancedUserControl公开了一些有趣的属性,这些属性在创建新的用户控件时非常有用。

首先是Span属性:FullSpan、FullRowSpan、FullColumnSpan。使用这些属性,可以使用户控件跨越父Grid的所有行或列。现在您可能会说:“好的,但是我可以做到这一点,通过附加属性Grid.Row或Grid.Column!”是的,这是真的,但是当您向父Grid添加行或列时,您总是需要调整附加属性以适应所有列或行。使用AdvancedUserControl的Span属性,您的优势在于它总是会跨越所有列或行。当您添加更多列或行时,您不必担心这一点。

Span属性本身也使用一个有趣的枚举:SwitchState。SwitchState枚举在某种程度上类似于布尔类型,但在某些用例中,我认为它更合适。SwitchState枚举仅包含“On”和“Off”两个值。在某些情况下,我并不完全满意内置的布尔“true/false”类型。我认为在某些情况下,使用SwitchState比使用布尔类型更合适。

AdvancedUserControl公开的其他属性是Command属性和相应的CommandParameter属性。如果您正在使用MVVM,您总是会尝试使用命令而不是代码隐藏事件处理程序。因此,如果您正在创建一个新的用户控件,您可以将最重要的事件连接到Command属性,以便您的用户控件的使用者能够在ViewModel中使用它。

SwitchState枚举
public enum SwitchState
{
	On,
	Off
}

用户控件的代码

现在让我们来看代码。首先,这是用户控件的XAML代码(请参阅最近的更改 此处) :

<ft:AdvancedUserControl x:Class="SoftArcs.WPFSmartLibrary.SmartUserControls.SmartLoginOverlay"
                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:sys="clr-namespace:System;assembly=mscorlib"
                xmlns:ap="clr-namespace:SoftArcs.WPFSmartLibrary.UIClassAttachedProperties"
                xmlns:ft="clr-namespace:SoftArcs.WPFSmartLibrary.FoundationTypes"
                mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="350"
                x:Name="VisualRoot" Loaded="SmartLoginOverlay_Loaded">

  <ft:AdvancedUserControl.RenderTransform>
    <TranslateTransform x:Name="VisualRootTranslateTransform" />
  </ft:AdvancedUserControl.RenderTransform>

  <ft:AdvancedUserControl.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="..\ResourceDictionaries\CommonRD\TextAndFontsRD.xaml" />
        <ResourceDictionary Source="..\ResourceDictionaries\SmartStyles\SmartPasswordBoxesRD.xaml" />
      </ResourceDictionary.MergedDictionaries>

      <Style TargetType="{x:Type Label}" BasedOn="{StaticResource {x:Type Label}}">
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Top" />
        <Setter Property="Foreground" Value="White" />
        <Setter Property="FontFamily" Value="Segoe UI Light" />
        <Setter Property="FontSize" Value="{StaticResource StandardFontSize}" />
        <!--<Setter Property="FontFamily" Value="Segoe WP SemiLight" />-->
      </Style>

      <Style TargetType="{x:Type PasswordBox}" BasedOn="{StaticResource Win8ExtendedPasswordBoxStyle}">
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Top" />
        <Setter Property="Height" Value="25" />
        <Setter Property="Width" Value="160" />
        <Setter Property="FontSize" Value="{StaticResource MediumFontSize}" />
      </Style>

      <KeyTime x:Key="FadeOutDurationSimultaneousKeyTime">0:0:0.5</KeyTime>
      <KeyTime x:Key="FadeOutDurationKeyTime">0:0:0.3</KeyTime>
      <KeyTime x:Key="FadeInDurationKeyTime">0:0:0.1</KeyTime>

      <Storyboard x:Key="MoveOutToTopStoryboard">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRootTranslateTransform"
                             Storyboard.TargetProperty="Y">
          <SplineDoubleKeyFrame KeyTime="0" Value="0"/>
          <SplineDoubleKeyFrame KeyTime="{StaticResource FadeOutDurationKeyTime}" Value="-300.0"/>
        </DoubleAnimationUsingKeyFrames>
      </Storyboard>

      <Storyboard x:Key="MoveOutToRightStoryboard">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRootTranslateTransform"
                             Storyboard.TargetProperty="X">
          <SplineDoubleKeyFrame KeyTime="0" Value="0"/>
          <SplineDoubleKeyFrame KeyTime="{StaticResource FadeOutDurationKeyTime}" Value="300.0"/>
        </DoubleAnimationUsingKeyFrames>
      </Storyboard>

      <Storyboard x:Key="FadeOutStoryboard">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot"
                             Storyboard.TargetProperty="Opacity">
          <SplineDoubleKeyFrame KeyTime="0" Value="1"/>
          <SplineDoubleKeyFrame KeyTime="{StaticResource FadeOutDurationKeyTime}" Value="0"/>
        </DoubleAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot"
                             Storyboard.TargetProperty="Visibility">
          <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
          <DiscreteObjectKeyFrame KeyTime="{StaticResource FadeOutDurationKeyTime}"
                          Value="{x:Static Visibility.Hidden}"/>
        </ObjectAnimationUsingKeyFrames>
      </Storyboard>

      <Storyboard x:Key="MoveOutToTopSimultaneousStoryboard">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="VisualRootTranslateTransform"
                             Storyboard.TargetProperty="Y">
          <SplineDoubleKeyFrame KeyTime="0" Value="0"/>
          <SplineDoubleKeyFrame KeyTime="{StaticResource FadeOutDurationSimultaneousKeyTime}"
                         Value="-300.0"/>
        </DoubleAnimationUsingKeyFrames>
      </Storyboard>

      <Storyboard x:Key="MoveOutToRightSimultaneousStoryboard">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="VisualRootTranslateTransform"
                             Storyboard.TargetProperty="X">
          <SplineDoubleKeyFrame KeyTime="0" Value="0"/>
          <SplineDoubleKeyFrame KeyTime="{StaticResource FadeOutDurationSimultaneousKeyTime}"
                         Value="300.0"/>
        </DoubleAnimationUsingKeyFrames>
      </Storyboard>

      <Storyboard x:Key="FadeOutSimultaneousStoryboard">
        <DoubleAnimationUsingKeyFrames Storyboard.TargetName="VisualRoot"
                             Storyboard.TargetProperty="Opacity">
          <SplineDoubleKeyFrame KeyTime="0" Value="1"/>
          <SplineDoubleKeyFrame KeyTime="{StaticResource FadeOutDurationSimultaneousKeyTime}"
                         Value="0"/>
        </DoubleAnimationUsingKeyFrames>
        <ColorAnimationUsingKeyFrames Storyboard.TargetName="VisualRoot"
                            Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)">
          <DiscreteColorKeyFrame KeyTime="0" Value="Transparent" />
        </ColorAnimationUsingKeyFrames>
        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="VisualRoot"
                             Storyboard.TargetProperty="Visibility">
          <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Visible}"/>
          <DiscreteObjectKeyFrame KeyTime="{StaticResource FadeOutDurationSimultaneousKeyTime}"
                          Value="{x:Static Visibility.Hidden}"/>
        </ObjectAnimationUsingKeyFrames>
      </Storyboard>
    </ResourceDictionary>
  </ft:AdvancedUserControl.Resources>

  <Grid x:Name="LayoutRoot">
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" MinHeight="170" />
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="Auto" />
      </Grid.ColumnDefinitions>
      <Grid.RenderTransform>
        <TranslateTransform x:Name="LayoutRootTranslateTransform" />
      </Grid.RenderTransform>

      <Border Grid.RowSpan="3" BorderThickness="1" VerticalAlignment="Top" Margin="0,20,0,0">
        <Image x:Name="imgUser" Source="../CommonImages/UserSilhouette.png" MaxHeight="150" />
      </Border>

      <StackPanel Grid.Row="0" Grid.Column="1" Margin="0,20,0,0">
        <Label x:Name="lblUserName" Margin="12,0" Padding="0" FontSize="22"
             Content="{Binding ElementName=VisualRoot, Path=UserName, Mode=TwoWay}" />

        <Label x:Name="lblAdditionalUserInfo" Margin="12,0" Padding="0" FontSize="12"
             Content="{Binding ElementName=VisualRoot, Path=AdditionalUserInfo, Mode=TwoWay}" />

        <Label x:Name="lblAdditionalSystemInfo" Margin="12,1" Padding="0" FontSize="12" Opacity="0.6"
             Content="{Binding ElementName=VisualRoot, Path=AdditionalSystemInfo, Mode=TwoWay}" />
      </StackPanel>

      <StackPanel Grid.Row="1" Grid.Column="1">
        <PasswordBox x:Name="PasswordBoxControl" Margin="12,18,12,5" Width="200"
                  KeyDown="PasswordBoxControl_OnKeyDown"
                  GotFocus="PasswordBoxControl_OnGotFocus"
                  LostFocus="PasswordBoxControl_OnLostFocus"
                  ap:PasswordBoxBinding.Password="{Binding ElementName=VisualRoot, Path=Password,
                                              Mode=TwoWay}"/>

        <Label x:Name="lblCapsLockInfo" Margin="12,1" Padding="0" FontSize="10" Foreground="#FFFD8B6C"
             Content="{Binding ElementName=VisualRoot, Path=CapsLockInfo, Mode=TwoWay}"
             Visibility="Hidden" />
      </StackPanel>

      <StackPanel x:Name="FaultMessagePanel" Grid.Row="2" Grid.Column="1" Visibility="Hidden">
        <TextBlock x:Name="tblNoCredentialsMessage" Margin="12,18,12,5" Padding="0" FontSize="10"
                Foreground="#FFFD8B6C" Width="200" TextWrapping="Wrap"
                Text="{Binding ElementName=VisualRoot, Path=NoCredentialsInfo, Mode=TwoWay}" />
        <TextBlock x:Name="tblWrongCredentialsMessage" Margin="12,18,12,5" Padding="0" FontSize="10"
                Foreground="#FFFD8B6C" Width="200" TextWrapping="Wrap"
                Text="{Binding ElementName=VisualRoot, Path=WrongCredentialsInfo, Mode=TwoWay}" />
        <Button x:Name="btnOK" Style="{StaticResource Win8_OKButtonStyle}" Margin="12,1"
              HorizontalAlignment="Left" FontFamily="Segoe UI" Click="btnOK_OnClick"/>
      </StackPanel>
    </Grid>
  </Grid>

</ft:AdvancedUserControl>

现在我将解释XAML代码中最重要的部分。

我想指出以下这行

<Style TargetType="{x:Type PasswordBox}" BasedOn="{StaticResource Win8ExtendedPasswordBoxStyle}">	

在这里,我们为PasswordBox类型定义了一个隐式样式,该样式基于 Win8ExtendedPasswordBoxStyle样式。这是用户控件最重要的部分,因为这个样式定义了PasswordBox的 Win8 外观和Win8行为。您可以在此处找到该样式的XAML代码。

接下来,我想向您展示PasswordBox的XAML代码

<PasswordBox x:Name="PasswordBoxControl" Margin="12,18,12,5" Width="200"
            KeyDown="PasswordBoxControl_OnKeyDown"
            GotFocus="PasswordBoxControl_OnGotFocus"
            LostFocus="PasswordBoxControl_OnLostFocus"
            ap:PasswordBoxBinding.Password="{Binding ElementName=VisualRoot, Path=Password,
                                            Mode=TwoWay}"/>
	

这里最重要的事情是PasswordBoxBinding类的Password附加属性。如果没有这个附加属性,就无法绑定PasswordBox的Password属性,因为它不是依赖属性。这是附加属性的代码:

public class PasswordBoxBinding
{
  private static bool IsUpdating;

  public static string GetPassword(DependencyObject dpo)
  {
    return (string)dpo.GetValue( PasswordProperty );
  }
  public static void SetPassword(DependencyObject dpo, string value)
  {
    dpo.SetValue( PasswordProperty, value );
  }
  /// <summary>
  /// Gets or sets the bindable Password property on the PasswordBox control. This is a dependency property.
  /// </summary>
  public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.RegisterAttached( "Password", typeof( string ),
                                 typeof( PasswordBoxBinding ),
                                 new FrameworkPropertyMetadata( String.Empty,
                                 new PropertyChangedCallback( OnPasswordPropertyChanged ) ) );
  /// <summary>
  /// Handles changes to the 'Password' attached property.
  /// </summary>
  private static void OnPasswordPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
  {
    PasswordBox targetPasswordBox = sender as PasswordBox;

    if (targetPasswordBox != null)
    {
      targetPasswordBox.PasswordChanged -= PasswordBox_PasswordChanged;

      if (IsUpdating == false)
      {
        targetPasswordBox.Password = (string)e.NewValue;
      }

      targetPasswordBox.PasswordChanged += PasswordBox_PasswordChanged;
    }
  }

  /// <summary>
  /// Handle the 'PasswordChanged'-event on the PasswordBox
  /// </summary>
  private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
  {
    PasswordBox passwordBox = sender as PasswordBox;

    IsUpdating = true;
    SetPassword( passwordBox, passwordBox.Password );
    IsUpdating = false;
  }
}	

最后,我想就XAML代码的这部分说几句话

<Button x:Name="btnOK" Style="{StaticResource Win8_OKButtonStyle}" Margin="12,1"
        HorizontalAlignment="Left" FontFamily="Segoe UI" Click="btnOK_OnClick"/>
	

这是提交按钮。按钮的外观定义在 Win8_OKButtonStyle中,该样式实现如下

<Style x:Key="Win8_OKButtonStyle" TargetType="{x:Type Button}">
  <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Button}">
        <Grid>
          <Border x:Name="OuterBorder" Background="White" Width="70" Height="25">
            <Border x:Name="ContentBorder" Background="#FF1FAEFF" Margin="0.7"
                  BorderThickness="{TemplateBinding BorderThickness}">
              <TextBlock x:Name="OK_TextBlock" Text="OK" HorizontalAlignment="Center" VerticalAlignment="Center"
                      Foreground="White" />
            </Border>
          </Border>
        </Grid>

        <ControlTemplate.Triggers>
          <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsMouseOver}" Value="True">
            <Setter TargetName="ContentBorder" Property="Background" Value="#FF3EB9FF" />
          </DataTrigger>

          <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsPressed}" Value="True">
            <Setter TargetName="ContentBorder" Property="Background" Value="Transparent" />
            <Setter TargetName="OK_TextBlock" Property="Foreground" Value="Black" />
          </DataTrigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style	

There is just one thing I want to point on. 正如您在用户控件的XAML代码中看到的,我为label类型定义了一个隐式样式,我首先使用了Segoe WP SemiLight字体,因为它最符合Windows 8字体(不,这被注释掉了)。不幸的是,这个字体在Windows 7中并非标准可用,所以我切换到了可用的Segoe UI Light字体(如果您安装了Windows Phone SDK 7.0、7.1——或许是8.0——那么上面提到的字体就会被安装)。

这是用于PasswordBox的 Win8ExtendedPasswordBoxStyle的代码:

<Style x:Key="Win8ExtendedPasswordBoxStyle" TargetType="{x:Type PasswordBox}"
     BasedOn="{StaticResource {x:Type PasswordBox}}">
  <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.ControlTextBrushKey}}"/>
  <Setter Property="Background" Value="{StaticResource {x:Static SystemColors.WindowBrushKey}}"/>
  <Setter Property="FontFamily" Value="Segoe UI"/>
  <Setter Property="PasswordChar" Value="?"/>
  <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
  <Setter Property="BorderThickness" Value="0"/>
  <Setter Property="HorizontalContentAlignment" Value="Left"/>
  <Setter Property="VerticalContentAlignment" Value="Center"/>
  <Setter Property="Padding" Value="4,1,1,2"/>
  <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
  <Setter Property="AllowDrop" Value="true"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type PasswordBox}">
        <Border x:Name="OuterBorder"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}"
              Background="{TemplateBinding Background}"
              SnapsToDevicePixels="true">

          <Grid VerticalAlignment="Center">
            <Grid.ColumnDefinitions>
              <ColumnDefinition />
              <ColumnDefinition Width="Auto" />
              <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <ScrollViewer x:Name="PART_ContentHost"
                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
            <TextBox x:Name="RevealedPassword"
                  Text="{TemplateBinding ap:PasswordBoxBinding.Password}"
                  VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                  VerticalContentAlignment="Center"
                  Padding="{TemplateBinding Padding}"
                  Margin="{TemplateBinding Padding}"
                  Background="{TemplateBinding Background}"
                  Visibility="Hidden" BorderThickness="0" IsReadOnly="True" FontFamily="Segoe UI" />
            <TextBlock x:Name="PART_TextBlockHint"
                    Padding="{TemplateBinding Padding}"
                    Background="Transparent"
                    Cursor="IBeam" Margin="2,0" FontFamily="Segoe UI Light" Foreground="#FF5D5F62"
                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                    HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                    Visibility="{Binding ElementName=RevealedPassword, Path=Text,
                            Converter={StaticResource StringToOppositeVisibilityConverter}}" />

            <Button x:Name="PART_RevealButton"
                  Grid.Column="1" SnapsToDevicePixels="True"
                  Style="{StaticResource RevealButtonExtendedStyle}"
                  Visibility="{Binding ElementName=RevealedPassword, Path=Text,
                                Converter={StaticResource StringToVisibilityConverter}}">
            </Button>

            <Button x:Name="PART_SubmitButton" FontSize="11"
                  Grid.Column="2" SnapsToDevicePixels="True"
                  Style="{StaticResource SubmitButtonStyle}">
            </Button>
          </Grid>
        </Border>

        <ControlTemplate.Triggers>
          <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Background" TargetName="OuterBorder"
                  Value="{StaticResource {x:Static SystemColors.ControlBrushKey}}" />
            <Setter Property="Foreground"
                  Value="{StaticResource {x:Static SystemColors.GrayTextBrushKey}}" />
            <Setter Property="Opacity" TargetName="PART_RevealButton" Value="0.5" />
            <Setter Property="Text" TargetName="PART_TextBlockHint" Value="{}{disabled} "/>
          </Trigger>
          <DataTrigger Binding="{Binding ElementName=PART_RevealButton, Path=IsPressed}" Value="True">
            <Setter TargetName="RevealedPassword" Property="Visibility" Value="Visible" />
          </DataTrigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

...然后是两个相应的Button样式。

首先是RevealButtonExtendedStyle

<Style x:Key="RevealButtonExtendedStyle" TargetType="{x:Type Button}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Button}">
        <Grid>
          <Border x:Name="PasswordRevealGlyphBorder" Background="Transparent" Margin="0,1"
                BorderThickness="{TemplateBinding BorderThickness}">
                    <TextBlock x:Name="GlyphElement" Foreground="Black"
                            VerticalAlignment="Center" HorizontalAlignment="Center" 
                            Text="" FontFamily="Segoe UI Symbol" Margin="3,0"
                            FontSize="{TemplateBinding FontSize}" />
          </Border>
        </Grid>

        <ControlTemplate.Triggers>
          <DataTrigger Binding="{Binding ElementName=GlyphElement, Path=IsMouseOver}" Value="True">
            <Setter TargetName="PasswordRevealGlyphBorder" Property="Background" 
                  Value="Gainsboro" />
          </DataTrigger>

          <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsPressed}"
                    Value="True">
            <Setter TargetName="PasswordRevealGlyphBorder" Property="Background" Value="Black" />
            <Setter TargetName="GlyphElement" Property="Foreground" Value="White" />
          </DataTrigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

...其次是SubmitButtonStyle

<Style x:Key="SubmitButtonStyle" TargetType="{x:Type Button}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Button}">
        <Grid>
          <Border x:Name="SignInGlyphBorder"
                Background="#FF1FAEFF" Margin="1"
                BorderThickness="{TemplateBinding BorderThickness}">
            <TextBlock x:Name="GlyphElement" Foreground="White" Padding="3,0"
                    HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                    Text="→" FontFamily="Calibri" Margin="3" SnapsToDevicePixels="True"
                    FontWeight="Bold" FontSize="{TemplateBinding FontSize}" />
          </Border>
        </Grid>

        <ControlTemplate.Triggers>
          <DataTrigger Binding="{Binding ElementName=GlyphElement, Path=IsMouseOver}" Value="True">
            <Setter TargetName="SignInGlyphBorder" Property="Background" Value="#FF3EB9FF" />
          </DataTrigger>

          <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsPressed}"
                    Value="True">
            <Setter TargetName="SignInGlyphBorder" Property="Background" Value="Transparent" />
            <Setter TargetName="GlyphElement" Property="Foreground" Value="Black" />
          </DataTrigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

结论

我希望您能从这篇文章中有所收获。建议或关于错误的反馈将是有益的。还有一个请求:如果您喜欢阅读这篇文章(或使用代码),请对其进行评分。谢谢。

值得关注的点

我使用.NET 3.5 Framework而不是.NET 4.0 Framework实现了该控件……为什么?因为外面仍然有许多应用程序运行在.NET 3.5上!如果我在Styles中使用.NET 4.0 Framework的某些类(例如VisualStateManager类),那么将代码适配到.NET 3.5应用程序将不会那么容易。我知道对于某些类(如上面提到的VisualStateManager),可以使用WPF Toolkit Library,但是那样就需要引用更多的库。As often as possible i try to write my libraries in .NET 3.5 because the possibilities for reusing them are more versatile.

如果您有一个.NET 4.0项目,您可以毫无障碍地使用该库。所有样式和类都可以正常工作。

很快就会有更多精彩的内容在CodeProject上发布,因为我决定撰写一些关于我近几年/几个月来创建的所有智能控件的文章。

最近的更改

Adding the input capability of the user name, has led to the following code changes.

首先,我在UserControl的XAML代码中添加了一个TextBox控件

<TextBox x:Name="tbUserName" Margin="12,5,12,0"
        FontSize="{StaticResource MediumFontSize}"
        Style="{StaticResource SmartWatermarkTextBoxStyle}"
        Height="{Binding ElementName=PasswordBoxControl, Path=ActualHeight}"
        Text="{Binding ElementName=VisualRoot, Path=UserName, Mode=TwoWay,
                  UpdateSourceTrigger=PropertyChanged}"
        Visibility="{Binding ElementName=VisualRoot, Path=IsUserOptionAvailable,
                      Converter={conv:BoolToVisibilityConverter}}" />
	

现在我来解释一下这个TextBox控件最重要的部分。

为了使其更智能一些,TextBox使用了SmartWatermarkTextBoxStyle进行样式设置。这确保了我们在TextBox中拥有水印。SmartWatermarkTextBoxStyle的XAML代码稍后会显示。第二个有趣的点是Height属性的绑定。它绑定到PasswordBoxControl的ActualHeight。这确保了控件的表示是平衡的。但最重要的是Visibility属性的绑定。它使用BoolToVisibilityConverter转换器绑定到IsUserOptionAvailable属性。所以,如果您想激活用户名输入功能,只需将此属性设置为true。

正如我承诺的,这是SmartWatermarkTextBoxStyle的XAML代码

<Style x:Key="SmartWatermarkTextBoxStyle" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
  <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
  <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
  <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>
  <Setter Property="FontFamily" Value="Segoe UI"/>
  <Setter Property="BorderThickness" Value="1"/>
  <Setter Property="HorizontalContentAlignment" Value="Left"/>
  <Setter Property="VerticalContentAlignment" Value="Center"/>
  <Setter Property="Padding" Value="4,1,1,1"/>
  <Setter Property="AllowDrop" Value="true"/>
  <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type TextBox}">
        <Border x:Name="OuterBorder" BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}"
              Background="{TemplateBinding Background}"
              SnapsToDevicePixels="true">
          <Grid>
            <ScrollViewer x:Name="PART_ContentHost"
                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            <TextBlock x:Name="PART_TextBlockHint"
                    Text="Enter Username" SnapsToDevicePixels="True"
                    Padding="{TemplateBinding Padding}" Background="Transparent"
                    Cursor="IBeam" Margin="2,0" FontFamily="Segoe UI Light" Foreground="#FF5D5F62"
                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                    HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                    Visibility="{TemplateBinding Text,
                            Converter={StaticResource StringToOppositeVisibilityConverter}}" />
          </Grid>
        </Border>

        <ControlTemplate.Triggers>
          <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Background" TargetName="OuterBorder"
                  Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
            <Setter Property="Foreground"
                  Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
	

历史

2012年12月4日 - 添加了输入用户名的选项,并在此描述了所有代码更改

2012年11月28日 - 改进了文章,并添加了更多关于用户控件的信息

2012年11月23日 - 首次发布

© . All rights reserved.