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