記事のカテゴリー: C#、.NET9、WPF
Webサイトの検索欄や入力フォームなどで見かける透かし文字(Placeholder)をWPFのTextBoxで実現する方法を考えます。
1. Adornerを作る
最初にTextBoxの上に透かし文字を表示するためのAdornerを作ります。Adornerはコントロールの手前のレイヤーを使ってコントロールに関連付いた装飾を表示するための手法です(詳しくは装飾の概要を参照してください)。テキストの描画は難しいことをしないで、シンプルにTextBlockを配置します。
Adornerの表示位置はTextBoxの内部で実際にテキストの表示を担当するTextBoxViewの位置を取得して設定しています。
C#:
// 透かし文字を TextBox の上に表示する Adorner
public class PlaceholderAdorner : Adorner
{
public PlaceholderAdorner(TextBox textBox) : this(textBox, string.Empty) {}
public PlaceholderAdorner(TextBox textBox, string text) : base(textBox)
{
if ((textBox.Template.FindName("PART_ContentHost", textBox) is ScrollViewer contentHost)
&& (contentHost.Content is UIElement textBoxView))
{
Offset = new(textBoxView.TranslatePoint(new(), textBox).X, 0.0);
}
else
{
Offset = new(textBox.BorderThickness.Left + textBox.Padding.Left, 0.0);
}
TextBlock textBlock = new();
textBlock.Foreground = SystemColors.GrayTextBrush;
textBlock.Text = placeholder;
visualChildren.Add(textBlock);
}
private List<Visual> visualChildren = [];
protected override int VisualChildrenCount
{
get { return visualChildren.Count; }
}
// 透かし文字のプロパティ
public string Placeholder
{
get { return ((TextBlock)visualChildren[0]).Text; }
set { ((TextBlock)visualChildren[0]).Text = value; }
}
// 以下、文字の色やフォントを調整するプロパティ (カスタマイズ用)
public Brush Foreground
{
get { return ((TextBlock)visualChildren[0]).Foreground; }
set { ((TextBlock)visualChildren[0]).Foreground = value; }
}
public FontFamily FontFamily
{
get { return ((TextBlock)visualChildren[0]).FontFamily; }
set { ((TextBlock)visualChildren[0]).FontFamily = value; }
}
public double FontSize
{
get { return ((TextBlock)visualChildren[0]).FontSize; }
set { ((TextBlock)visualChildren[0]).FontSize = value; }
}
public FontStretch FontStretch
{
get { return ((TextBlock)visualChildren[0]).FontStretch; }
set { ((TextBlock)visualChildren[0]).FontStretch = value; }
}
public FontStyle FontStyle
{
get { return ((TextBlock)visualChildren[0]).FontStyle; }
set { ((TextBlock)visualChildren[0]).FontStyle = value; }
}
public FontWeight FontWeight
{
get { return ((TextBlock)visualChildren[0]).FontWeight; }
set { ((TextBlock)visualChildren[0]).FontWeight = value; }
}
public Vector Offset { get; set; }
protected override Visual GetVisualChild(int index)
{
return visualChildren[index];
}
protected override Size ArrangeOverride(Size finalSize)
{
TextBlock textBlock = (TextBlock)visualChildren[0];
double y = (textBlock.DesiredSize.Height < finalSize.Height)
? (finalSize.Height - textBlock.DesiredSize.Height) / 2.0 : 0.0;
textBlock.Arrange(new Rect(Offset.X, y, textBlock.DesiredSize.Width, textBlock.DesiredSize.Height));
return finalSize;
}
}
2. 添付ビヘイビアを作る
次に添付ビヘイビアを作ります。Adornerを作成してTextBoxの上に表示するPlaceholder添付プロパティを定義します。TextBoxのTextChangedイベントを購読して、テキストが空のときにのみAdornerを表示します。
C#:
public static class PlaceholderBehavior
{
// 透かし文字を TextBox の上に表示する添付プロパティ
public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached(
"Placeholder", typeof(string), typeof(PlaceholderBehavior), new FrameworkPropertyMetadata(null, OnPlaceholderChanged));
public static string? GetPlaceholder(DependencyObject obj) { return (string)obj.GetValue(PlaceholderProperty); }
public static void SetPlaceholder(DependencyObject obj, string? value) { obj.SetValue(PlaceholderProperty, value); }
// イベント購読に使う内部用の添付プロパティ
private static readonly DependencyProperty IsAttachedProperty = DependencyProperty.RegisterAttached(
"IsAttached", typeof(bool), typeof(PlaceholderBehavior), new FrameworkPropertyMetadata(false, OnIsAttachedChanged));
private static bool GetIsAttached(DependencyObject obj) { return (bool)obj.GetValue(IsAttachedProperty); }
private static void SetIsAttached(DependencyObject obj, bool value) { obj.SetValue(IsAttachedProperty, value); }
// Adorner を保存する内部用の添付プロパティ
private static readonly DependencyProperty AdornerProperty = DependencyProperty.RegisterAttached(
"Adorner", typeof(PlaceholderAdorner), typeof(PlaceholderBehavior), new FrameworkPropertyMetadata(null));
private static PlaceholderAdorner? GetAdorner(DependencyObject obj) { return (PlaceholderAdorner)obj.GetValue(AdornerProperty); }
private static void SetAdorner(DependencyObject obj, PlaceholderAdorner? value) { obj.SetValue(AdornerProperty, value); }
private static void OnPlaceholderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is not TextBox textBox)
{
return;
}
if (e.NewValue is string { Length: > 0 })
{
UpdateAdorner(textBox);
SetIsAttached(textBox, true); // イベントを購読する
}
else
{
SetIsAttached(textBox, false); // イベントを購読解除する
}
}
private static void OnIsAttachedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is not TextBox textBox)
{
return;
}
if (e.NewValue is true)
{
WeakEventManager<TextBox, RoutedEventArgs>.AddHandler(textBox, nameof(textBox.Loaded), TextBox_Loaded);
WeakEventManager<TextBox, RoutedEventArgs>.AddHandler(textBox, nameof(textBox.Unloaded), TextBox_Unloaded);
WeakEventManager<TextBox, TextChangedEventArgs>.AddHandler(textBox, nameof(textBox.TextChanged), TextBox_TextChanged);
}
else
{
WeakEventManager<TextBox, RoutedEventArgs>.RemoveHandler(textBox, nameof(textBox.Loaded), TextBox_Loaded);
WeakEventManager<TextBox, RoutedEventArgs>.RemoveHandler(textBox, nameof(textBox.Unloaded), TextBox_Unloaded);
WeakEventManager<TextBox, TextChangedEventArgs>.RemoveHandler(textBox, nameof(textBox.TextChanged), TextBox_TextChanged);
DeleteAdorner(textBox, true);
}
}
// Adorner を削除する
private static void DeleteAdorner(TextBox textBox, bool completely)
{
if ((GetAdorner(textBox) is PlaceholderAdorner adorner)
&& (adorner.Parent is AdornerLayer adornerLayer))
{
adornerLayer.Remove(adorner);
}
if (completely)
{
SetAdorner(textBox, null);
}
}
// Adorner の表示を更新する
private static void UpdateAdorner(TextBox textBox)
{
if (!textBox.IsLoaded)
{
return;
}
string placeholder = (string)GetPlaceholder(textBox)!; // null のときは到達しない
// Adorner が未作成なら作成する
if (GetAdorner(textBox) is PlaceholderAdorner adorner)
{
adorner.Placeholder = placeholder;
}
else
{
adorner = new(textBox, placeholder) { FontSize = textBox.FontSize, IsClipEnabled = true };
SetAdorner(textBox, adorner);
}
// Adorner が AdornerLayer に追加されていないなら追加する
if (!adorner.IsLoaded)
{
if (AdornerLayer.GetAdornerLayer(textBox) is not AdornerLayer adornerLayer)
{
return;
}
adornerLayer.Add(adorner);
}
// TextBox の入力状態に応じて Adorner を表示、または非表示にする
adorner.Visibility = (textBox.Text.Length == 0) ? Visibility.Visible : Visibility.Hidden;
}
private static void TextBox_Loaded(object? sender, RoutedEventArgs e)
{
if (sender is TextBox textBox)
{
UpdateAdorner(textBox);
}
}
private static void TextBox_Unloaded(object? sender, RoutedEventArgs e)
{
if (sender is TextBox textBox)
{
DeleteAdorner(textBox, false);
}
}
private static void TextBox_TextChanged(object? sender, TextChangedEventArgs e)
{
if (sender is TextBox textBox)
{
UpdateAdorner(textBox);
}
}
}
3. TextBoxに添付プロパティを設定する
TextBoxにPlaceholder添付プロパティを設定します。
XAML:
<Window
...
xmlns:local="clr-namespace:WpfApp1"
...>
XAML:
<TextBox local:PlaceholderBehavior.Placeholder="設定を検索" />
更新履歴
- : 初稿
- : イベント購読を多重登録してしまう問題に対応しました。
PlaceholderBehaviorクラスにIsAttached添付プロパティを追加して再構成しています。 - :
TextBoxを破棄してもAdornerが表示されたままになってしまう問題に対応しました。PlaceholderBehaviorクラスでTextBoxのUnloadedイベントの購読を追加しています。

0 件のコメント:
コメントを投稿