記事のカテゴリー: 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 件のコメント:
コメントを投稿