TextBoxUtils.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. using System.Linq;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Documents;
  5. using System.Windows.Media;
  6. namespace InABox.Wpf;
  7. public static class TextBoxUtils
  8. {
  9. public static string GetPlaceholder(DependencyObject obj) =>
  10. (string)obj.GetValue(PlaceholderProperty);
  11. public static void SetPlaceholder(DependencyObject obj, string value) =>
  12. obj.SetValue(PlaceholderProperty, value);
  13. public static readonly DependencyProperty PlaceholderProperty =
  14. DependencyProperty.RegisterAttached(
  15. "Placeholder",
  16. typeof(string),
  17. typeof(TextBoxUtils),
  18. new FrameworkPropertyMetadata(
  19. defaultValue: null,
  20. propertyChangedCallback: OnPlaceholderChanged)
  21. );
  22. private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  23. {
  24. if (d is TextBox textBoxControl)
  25. {
  26. if (!textBoxControl.IsLoaded)
  27. {
  28. // Ensure that the events are not added multiple times
  29. textBoxControl.Loaded -= TextBoxControl_Loaded;
  30. textBoxControl.Loaded += TextBoxControl_Loaded;
  31. }
  32. textBoxControl.TextChanged -= TextBoxControl_TextChanged;
  33. textBoxControl.TextChanged += TextBoxControl_TextChanged;
  34. // If the adorner exists, invalidate it to draw the current text
  35. if (GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner))
  36. adorner.InvalidateVisual();
  37. }
  38. }
  39. private static void TextBoxControl_Loaded(object sender, RoutedEventArgs e)
  40. {
  41. if (sender is TextBox textBoxControl)
  42. {
  43. textBoxControl.Loaded -= TextBoxControl_Loaded;
  44. GetOrCreateAdorner(textBoxControl, out _);
  45. }
  46. }
  47. private static void TextBoxControl_TextChanged(object sender, TextChangedEventArgs e)
  48. {
  49. if (sender is TextBox textBoxControl
  50. && GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner))
  51. {
  52. // Control has text. Hide the adorner.
  53. if (textBoxControl.Text.Length > 0)
  54. adorner.Visibility = Visibility.Hidden;
  55. // Control has no text. Show the adorner.
  56. else
  57. adorner.Visibility = Visibility.Visible;
  58. }
  59. }
  60. private static bool GetOrCreateAdorner(TextBox textBoxControl, out PlaceholderAdorner adorner)
  61. {
  62. // Get the adorner layer
  63. AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxControl);
  64. // If null, it doesn't exist or the control's template isn't loaded
  65. if (layer == null)
  66. {
  67. adorner = null;
  68. return false;
  69. }
  70. // Layer exists, try to find the adorner
  71. adorner = layer.GetAdorners(textBoxControl)?.OfType<PlaceholderAdorner>().FirstOrDefault();
  72. // Adorner never added to control, so add it
  73. if (adorner == null)
  74. {
  75. adorner = new PlaceholderAdorner(textBoxControl);
  76. layer.Add(adorner);
  77. }
  78. return true;
  79. }
  80. public class PlaceholderAdorner : Adorner
  81. {
  82. public PlaceholderAdorner(TextBox textBox) : base(textBox) { }
  83. protected override void OnRender(DrawingContext drawingContext)
  84. {
  85. TextBox textBoxControl = (TextBox)AdornedElement;
  86. string placeholderValue = TextBoxUtils.GetPlaceholder(textBoxControl);
  87. if (string.IsNullOrEmpty(placeholderValue))
  88. return;
  89. // Create the formatted text object
  90. FormattedText text = new FormattedText(
  91. placeholderValue,
  92. System.Globalization.CultureInfo.CurrentCulture,
  93. textBoxControl.FlowDirection,
  94. new Typeface(textBoxControl.FontFamily,
  95. textBoxControl.FontStyle,
  96. textBoxControl.FontWeight,
  97. textBoxControl.FontStretch),
  98. textBoxControl.FontSize,
  99. SystemColors.InactiveCaptionBrush,
  100. VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip);
  101. text.MaxTextWidth = System.Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10);
  102. text.MaxTextHeight = System.Math.Max(textBoxControl.ActualHeight, 10);
  103. // Render based on padding of the control, to try and match where the textbox places text
  104. Point renderingOffset = new Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top);
  105. // Template contains the content part; adjust sizes to try and align the text
  106. if (textBoxControl.Template.FindName("PART_ContentHost", textBoxControl) is FrameworkElement part)
  107. {
  108. Point partPosition = part.TransformToAncestor(textBoxControl).Transform(new Point(0, 0));
  109. renderingOffset.X += partPosition.X;
  110. renderingOffset.Y += partPosition.Y;
  111. text.MaxTextWidth = System.Math.Max(part.ActualWidth - renderingOffset.X, 10);
  112. text.MaxTextHeight = System.Math.Max(part.ActualHeight, 10);
  113. }
  114. // Draw the text
  115. drawingContext.DrawText(text, renderingOffset);
  116. }
  117. }
  118. }