WPF Textbox/PasswordBox设置Placeholder

前言

将一个与占位符绑定的TextBlock放入VisualBrush内,在TextBox的Text为空时使用VisualBrush绘制背景,不为空时背景设为Null。

正因为如此,如果文本框设置了背景,使用此方法就会覆盖原有的背景。但一般不会设置TextBox的背景。

TextBox实现Placeholder

方式1 使用附加属性

添加引用

1
xmlns:local="clr-namespace:ZView"

使用方式

1
2
3
4
5
6
7
8
<TextBox
Padding="6"
VerticalContentAlignment="Center"
FontSize="18"
Foreground="gray"
local:PlaceholderManager.Placeholder="请输入密钥"
Text=""
VerticalScrollBarVisibility="Disabled" />

添加类PlaceholderManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace ZView
{
/// <summary>
/// 占位符的管理类
/// </summary>
public class PlaceholderManager
{
#region Fields

/// <summary>
/// 文本框和Visual画刷对应的字典
/// </summary>
private static readonly Dictionary<TextBox, VisualBrush> TxtBrushes = new Dictionary<TextBox, VisualBrush>();

#endregion Fields

#region Attached DependencyProperty

/// <summary>
/// 占位符的附加依赖属性
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
"Placeholder", typeof(string), typeof(PlaceholderManager),
new PropertyMetadata("请在此处输入", OnPlaceholderChanged));

/// <summary>
/// 获取占位符
/// </summary>
/// <param name="obj">占位符所在的对象</param>
/// <returns>占位符</returns>
public static string GetPlaceholder(DependencyObject obj)
{
return (string)obj.GetValue(PlaceholderProperty);
}

/// <summary>
/// 设置占位符
/// </summary>
/// <param name="obj">占位符所在的对象</param>
/// <param name="value">占位符</param>
public static void SetPlaceholder(DependencyObject obj, string value)
{
obj.SetValue(PlaceholderProperty, value);
}

#endregion Attached DependencyProperty

#region Events Handling

/// <summary>
/// 占位符改变的响应
/// </summary>
/// <param name="d">来源</param>
/// <param name="e">改变信息</param>
public static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var txt = d as TextBox;
if ((txt != null) && (!TxtBrushes.ContainsKey(txt)))
{
var placeholderTextBlock = new TextBox();
var binding = new Binding
{
Source = txt,
//绑定到附加属性
Path = new PropertyPath("(0)", PlaceholderProperty)
};
placeholderTextBlock.SetBinding(TextBox.TextProperty, binding);
//placeholderTextBlock.FontStyle = FontStyles.Italic;
placeholderTextBlock.Opacity = 0.8;
placeholderTextBlock.Padding = new Thickness(10, 0, 0, 0);
placeholderTextBlock.BorderThickness = new Thickness(0, 0, 0, 0);
placeholderTextBlock.Foreground = Brushes.Gray;

var placeholderVisualBrush = new VisualBrush
{
AlignmentX = AlignmentX.Left,
Stretch = Stretch.None,
Visual = placeholderTextBlock
};

txt.Background = placeholderVisualBrush;
txt.TextChanged += PlaceholderTextBox_TextChanged;
txt.Unloaded += PlaceholderTextBox_Unloaded;

TxtBrushes.Add(txt, placeholderVisualBrush);
}
}

/// <summary>
/// 文本变化的响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void PlaceholderTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var tb = sender as TextBox;
if ((tb != null) && (TxtBrushes.ContainsKey(tb)))
{
var placeholderVisualBrush = TxtBrushes[tb];
tb.Background = string.IsNullOrEmpty(tb.Text) ? placeholderVisualBrush : null;
}
}

/// <summary>
/// 文本框卸载的响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void PlaceholderTextBox_Unloaded(object sender, RoutedEventArgs e)
{
var tb = sender as TextBox;
if ((tb != null) && (TxtBrushes.ContainsKey(tb)))
{
TxtBrushes.Remove(tb);

tb.TextChanged -= PlaceholderTextBox_TextChanged;
tb.Unloaded -= PlaceholderTextBox_Unloaded;
}
}

#endregion Events Handling
}
}

方式2 使用自定义组件

使用方式

1
2
3
4
5
6
7
8
9
<local:PlaceholderTextBox
Padding="6"
VerticalContentAlignment="Center"
FontSize="18"
Foreground="gray"
Placeholder="请输入密钥"
Text=""
TextWrapping="Wrap"
VerticalScrollBarVisibility="Disabled" />

自定义组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace ZView
{
/// <summary>
/// 带点位符的文本输入控件
/// </summary>
public class PlaceholderTextBox : TextBox
{
#region Fields

/// <summary>
/// 占位符的文本框
/// </summary>
private readonly TextBox _placeholderTextBlock = new TextBox();

/// <summary>
/// 占位符的画刷
/// </summary>
private readonly VisualBrush _placeholderVisualBrush = new VisualBrush();

#endregion Fields

#region Properties

/// <summary>
/// 占位符的依赖属性
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(
"Placeholder", typeof(string), typeof(PlaceholderTextBox),
new FrameworkPropertyMetadata("请在此输入", FrameworkPropertyMetadataOptions.AffectsRender));

/// <summary>
/// 占位符
/// </summary>
public string Placeholder
{
get { return (string)GetValue(PlaceholderProperty); }
set { SetValue(PlaceholderProperty, value); }
}

#endregion Properties

#region Public Methods

public PlaceholderTextBox()
{
var binding = new Binding
{
Source = this,
Path = new PropertyPath("Placeholder")
};
_placeholderTextBlock.SetBinding(TextBox.TextProperty, binding);
//_placeholderTextBlock.FontStyle = FontStyles.Italic;
_placeholderTextBlock.Padding = new Thickness(10, 0, 0, 0);
_placeholderTextBlock.BorderThickness = new Thickness(0, 0, 0, 0);
_placeholderTextBlock.Foreground = Brushes.Gray;

_placeholderVisualBrush.AlignmentX = AlignmentX.Left;
_placeholderVisualBrush.Stretch = Stretch.None;
_placeholderVisualBrush.Visual = _placeholderTextBlock;

Background = _placeholderVisualBrush;
TextChanged += PlaceholderTextBox_TextChanged;
}

#endregion Public Methods

#region Events Handling

/// <summary>
/// 文本变化的响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PlaceholderTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
Background = string.IsNullOrEmpty(Text) ? _placeholderVisualBrush : null;
}

#endregion Events Handling
}
}

PasswordBox双向绑定

WPF 的 PasswordBox 控件因为安全原因,默认不支持MVVM的双向数据绑定,那么我们在MVVM开发的过程中如何解决这个问题呢?

添加附加属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
using System.Windows;
using System.Windows.Controls;

namespace ZView
{
public class PasswordBoxHelper
{
public static readonly DependencyProperty PasswordProperty = DependencyProperty.RegisterAttached(
"Password", typeof(string), typeof(PasswordBoxHelper), new PropertyMetadata(string.Empty,OnPasswordPropertyChanged));

public static readonly DependencyProperty AttachProperty = DependencyProperty.RegisterAttached(
"Attach", typeof(bool), typeof(PasswordBoxHelper), new PropertyMetadata(false,Attach));

public static readonly DependencyProperty IsUpdatingProperty = DependencyProperty.RegisterAttached(
"IsUpdating", typeof(bool), typeof(PasswordBoxHelper), new PropertyMetadata(default(bool)));

private static void OnPasswordPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PasswordBox passwordBox=d as PasswordBox;
passwordBox.PasswordChanged -= PasswordChanged;
if (!GetIsUpdating(passwordBox))
{
/*从Password往控件方向更新绑定值*/
passwordBox.Password = e.NewValue.ToString();
}

passwordBox.PasswordChanged += PasswordChanged;
}

public static void SetPassword(DependencyObject element, string value)
{
element.SetValue(PasswordProperty, value);
}

public static string GetPassword(DependencyObject element)
{
return (string) element.GetValue(PasswordProperty);
}


private static void Attach(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is PasswordBox passwordBox))
{
return;
}

if ((bool)e.OldValue)
{
passwordBox.PasswordChanged -= PasswordChanged;
}

if ((bool)e.NewValue)
{
/*当控件的值发生变化的时候,更新Password的值*/
passwordBox.PasswordChanged += PasswordChanged;
}
}

private static void PasswordChanged(object sender, RoutedEventArgs e)
{
PasswordBox passwordBox=sender as PasswordBox;
/*IsUpdating的作用类似一把互斥锁,因涉及到双向绑定更新*/
SetIsUpdating(passwordBox,true);
SetPassword(passwordBox,passwordBox.Password);
SetIsUpdating(passwordBox,false);
}

public static void SetAttach(DependencyObject element, bool value)
{
element.SetValue(AttachProperty, value);
}

public static bool GetAttach(DependencyObject element)
{
return (bool) element.GetValue(AttachProperty);
}

public static void SetIsUpdating(DependencyObject element, bool value)
{
element.SetValue(IsUpdatingProperty, value);
}

public static bool GetIsUpdating(DependencyObject element)
{
return (bool) element.GetValue(IsUpdatingProperty);
}
}
}

在需要用的PasswordBox中添加上面写的附加属性Password即可:

1
2
<PasswordBox Helper:PasswordBoxHelper.Attach="True"
Helper:PasswordBoxHelper.Password="{Binding Password,Mode=TwoWay}"/>

PasswordBox实现Placeholder

PasswordBox实现Placeholder不能跟TextBox使用同样方法实现的原因有两个

  1. PasswordBox的Password属性不支持绑定,这个可以自己添加属性解决。
  2. 设置背景的TextBlock中的文字会变成加密状态。

第二点就导致了TextBox实现Placeholder的方式在这里行不通。

添加属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/*
*┌────────────────────────────────────────────────┐
*│ 描 述:PasswordBoxHelper
*│ 作 者:剑行者
*│ 版 本:1.0
*│ 创建时间:2023/2/23 14:22:13
*└────────────────────────────────────────────────┘
*/

using System.Windows.Controls;
using System.Windows;

namespace ZView
{
public class PasswordBoxHelper
{
static bool isInistialised;

public static string GetPlaceholder(DependencyObject obj)
{
return (string)obj.GetValue(PlaceholderProperty);
}

public static void SetPlaceholder(DependencyObject obj, string value)
{
obj.SetValue(PlaceholderProperty, value);
}

public static readonly DependencyProperty PlaceholderProperty =
DependencyProperty.RegisterAttached("Placeholder", typeof(string), typeof(PasswordBoxHelper),
new UIPropertyMetadata(null, PlaceholderChanged));


public static bool GetShowPlaceholder(DependencyObject obj)
{
return (bool)obj.GetValue(ShowPlaceholderProperty);
}

public static void SetShowPlaceholder(DependencyObject obj, bool value)
{
obj.SetValue(ShowPlaceholderProperty, value);
}

public static readonly DependencyProperty ShowPlaceholderProperty =
DependencyProperty.RegisterAttached("ShowPlaceholder", typeof(bool), typeof(PasswordBoxHelper),
new UIPropertyMetadata(false));


static void PlaceholderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var pwd = obj as PasswordBox;

CheckShowPlaceholder(pwd);

if (!isInistialised)
{
pwd.PasswordChanged += new RoutedEventHandler(pwd_PasswordChanged);
pwd.Unloaded += new RoutedEventHandler(pwd_Unloaded);
isInistialised = true;
}
}

private static void CheckShowPlaceholder(PasswordBox pwd)
{
pwd.SetValue(PasswordBoxHelper.ShowPlaceholderProperty, pwd.Password == string.Empty);
}

static void pwd_PasswordChanged(object sender, RoutedEventArgs e)
{
var pwd = sender as PasswordBox;
CheckShowPlaceholder(pwd);
}

static void pwd_Unloaded(object sender, RoutedEventArgs e)
{
var pwd = sender as PasswordBox;
pwd.PasswordChanged -= new RoutedEventHandler(pwd_PasswordChanged);
}
}
}

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<ControlTemplate x:Key="ZPlaceholderPasswordBoxTemplate" TargetType="{x:Type PasswordBox}">
<Grid>
<TextBlock
Padding="4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Background="Transparent"
FontSize="14"
Foreground="#999999"
Text="{Binding Path=(local:PasswordBoxHelper.Placeholder), RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{Binding (local:PasswordBoxHelper.ShowPlaceholder), Converter={StaticResource BooleanToVisibilityConverter}, RelativeSource={RelativeSource TemplatedParent}}" />
<ScrollViewer
x:Name="PART_ContentHost"
HorizontalAlignment="Stretch"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
<PasswordBox
x:Name="PobPassword"
Width="222"
Margin="40,0,0,0"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
BorderBrush="{x:Null}"
BorderThickness="0"
FontSize="15"
Foreground="#333333"
Password=""
local:PasswordBoxHelper.Placeholder="请输入密码"
Template="{StaticResource ZPlaceholderPasswordBoxTemplate}" />

其中主要是添加以下两行

1
2
local:PasswordBoxHelper.Placeholder="请输入密码"
Template="{StaticResource ZPlaceholderPasswordBoxTemplate}"

PasswordBox实现Placeholder(失败)

当输入框中没有密码的时候显示如下,并没有显示请输入密码

image-20230223144606130

这是按照TextBox实现Placeholder的方式来实现的,最终不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace ZView
{
/// <summary>
/// 占位符的管理类
/// </summary>
public class PasswordPlaceholderManager
{
#region Fields

/// <summary>
/// 文本框和Visual画刷对应的字典
/// </summary>
private static readonly Dictionary<PasswordBox, VisualBrush> TxtBrushes =
new Dictionary<PasswordBox, VisualBrush>();

#endregion Fields

#region Attached DependencyProperty

/// <summary>
/// 占位符的附加依赖属性
/// </summary>
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
"Placeholder",
typeof(string),
typeof(PasswordPlaceholderManager),
new PropertyMetadata("请在此处输入", OnPlaceholderChanged)
);

public static readonly DependencyProperty PasswordProperty = DependencyProperty.RegisterAttached(
"Password", typeof(string), typeof(PasswordPlaceholderManager),
new PropertyMetadata(string.Empty, OnPasswordPropertyChanged));

public static readonly DependencyProperty IsUpdatingProperty = DependencyProperty.RegisterAttached(
"IsUpdating", typeof(bool), typeof(PasswordPlaceholderManager), new PropertyMetadata(default(bool)));

public static void SetIsUpdating(DependencyObject element, bool value)
{
element.SetValue(IsUpdatingProperty, value);
}

public static bool GetIsUpdating(DependencyObject element)
{
return (bool)element.GetValue(IsUpdatingProperty);
}

private static void OnPasswordPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PasswordBox passwordBox = d as PasswordBox;
if (passwordBox != null)
{
passwordBox.PasswordChanged -= Txt_PasswordChanged;
if (!GetIsUpdating(passwordBox))
{
/*从Password往控件方向更新绑定值*/
passwordBox.Password = e.NewValue.ToString();
}

passwordBox.PasswordChanged += Txt_PasswordChanged;
}
}

/// <summary>
/// 获取占位符
/// </summary>
/// <param name="obj">占位符所在的对象</param>
/// <returns>占位符</returns>
public static string GetPlaceholder(DependencyObject obj)
{
return (string)obj.GetValue(PlaceholderProperty);
}

/// <summary>
/// 设置占位符
/// </summary>
/// <param name="obj">占位符所在的对象</param>
/// <param name="value">占位符</param>
public static void SetPlaceholder(DependencyObject obj, string value)
{
obj.SetValue(PlaceholderProperty, value);
}

#endregion Attached DependencyProperty

#region Events Handling

/// <summary>
/// 占位符改变的响应
/// </summary>
/// <param name="d">来源</param>
/// <param name="e">改变信息</param>
public static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((d is PasswordBox pb) && (!TxtBrushes.ContainsKey(pb)))
{
var placeholderTextBlock = new PasswordBox();
var binding = new Binding
{
Source = pb,
//绑定到附加属性
Path = new PropertyPath("(0)", PlaceholderProperty)
};
placeholderTextBlock.SetBinding(PasswordProperty, binding);

placeholderTextBlock.Opacity = 0.8;
placeholderTextBlock.Padding = new Thickness(2, 0, 0, 0);
placeholderTextBlock.BorderThickness = new Thickness(0, 0, 0, 0);
placeholderTextBlock.Foreground = Brushes.Gray;

var placeholderVisualBrush = new VisualBrush
{
AlignmentX = AlignmentX.Left, Stretch = Stretch.None, Visual = placeholderTextBlock
};

pb.Background = placeholderVisualBrush;
pb.PasswordChanged += Txt_PasswordChanged;

pb.Unloaded += PlaceholderPasswordBox_Unloaded;

TxtBrushes.Add(pb, placeholderVisualBrush);
}
}

public static void SetPassword(DependencyObject element, string value)
{
element.SetValue(PasswordProperty, value);
}

private static void Txt_PasswordChanged(object sender, RoutedEventArgs e)
{
Console.WriteLine(sender is PasswordBox);
if ((sender is PasswordBox pb) && (TxtBrushes.ContainsKey(pb)))
{
/*IsUpdating的作用类似一把互斥锁,因涉及到双向绑定更新*/
SetIsUpdating(pb, true);
SetPassword(pb, pb.Password);
SetIsUpdating(pb, false);
var placeholderVisualBrush = TxtBrushes[pb];
Console.WriteLine(placeholderVisualBrush);
pb.Background = string.IsNullOrEmpty(pb.Password) ? placeholderVisualBrush : null;
}
}


/// <summary>
/// 文本框卸载的响应
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void PlaceholderPasswordBox_Unloaded(object sender, RoutedEventArgs e)
{
if ((sender is PasswordBox pb) && (TxtBrushes.ContainsKey(pb)))
{
TxtBrushes.Remove(pb);

pb.PasswordChanged -= Txt_PasswordChanged;
pb.Unloaded -= PlaceholderPasswordBox_Unloaded;
}
}

#endregion Events Handling
}
}

调用方式

1
2
3
4
5
6
7
8
9
10
11
12
<PasswordBox
x:Name="PobPassword"
Width="222"
Margin="40,0,0,0"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
local:PasswordPlaceholderManager.Placeholder="请输入密码"
BorderBrush="{x:Null}"
BorderThickness="0"
FontSize="15"
Foreground="#333333"
Password="" />