How do you apply stroke (outline around text) to a textblock in xaml in WPF?
Below is my more idiomatically WPF, full-featured take on this. It supports pretty much everything you'd expect, including:
Here's a simple example of what can be achieved with it:
<local:OutlinedTextBlock FontFamily="Verdana" FontSize="20pt" FontWeight="ExtraBold" TextWrapping="Wrap" StrokeThickness="1" Stroke="{StaticResource TextStroke}" Fill="{StaticResource TextFill}"> Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit </local:OutlinedTextBlock>
Which results in:
Here's the code for the control:
using System; using System.ComponentModel; using System.Globalization; using System.Windows; using System.Windows.Documents; using System.Windows.Markup; using System.Windows.Media; [ContentProperty("Text")] public class OutlinedTextBlock : FrameworkElement { public static readonly DependencyProperty FillProperty = DependencyProperty.Register( "Fill", typeof(Brush), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register( "Stroke", typeof(Brush), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register( "StrokeThickness", typeof(double), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register( "Text", typeof(string), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextInvalidated)); public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register( "TextAlignment", typeof(TextAlignment), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextDecorationsProperty = DependencyProperty.Register( "TextDecorations", typeof(TextDecorationCollection), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextTrimmingProperty = DependencyProperty.Register( "TextTrimming", typeof(TextTrimming), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register( "TextWrapping", typeof(TextWrapping), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(TextWrapping.NoWrap, OnFormattedTextUpdated)); private FormattedText formattedText; private Geometry textGeometry; public OutlinedTextBlock() { this.TextDecorations = new TextDecorationCollection(); } public Brush Fill { get { return (Brush)GetValue(FillProperty); } set { SetValue(FillProperty, value); } } public FontFamily FontFamily { get { return (FontFamily)GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } [TypeConverter(typeof(FontSizeConverter))] public double FontSize { get { return (double)GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } public FontStretch FontStretch { get { return (FontStretch)GetValue(FontStretchProperty); } set { SetValue(FontStretchProperty, value); } } public FontStyle FontStyle { get { return (FontStyle)GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } public FontWeight FontWeight { get { return (FontWeight)GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } public Brush Stroke { get { return (Brush)GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } } public double StrokeThickness { get { return (double)GetValue(StrokeThicknessProperty); } set { SetValue(StrokeThicknessProperty, value); } } public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } public TextAlignment TextAlignment { get { return (TextAlignment)GetValue(TextAlignmentProperty); } set { SetValue(TextAlignmentProperty, value); } } public TextDecorationCollection TextDecorations { get { return (TextDecorationCollection)this.GetValue(TextDecorationsProperty); } set { this.SetValue(TextDecorationsProperty, value); } } public TextTrimming TextTrimming { get { return (TextTrimming)GetValue(TextTrimmingProperty); } set { SetValue(TextTrimmingProperty, value); } } public TextWrapping TextWrapping { get { return (TextWrapping)GetValue(TextWrappingProperty); } set { SetValue(TextWrappingProperty, value); } } protected override void OnRender(DrawingContext drawingContext) { this.EnsureGeometry(); drawingContext.DrawGeometry(this.Fill, new Pen(this.Stroke, this.StrokeThickness), this.textGeometry); } protected override Size MeasureOverride(Size availableSize) { this.EnsureFormattedText(); // constrain the formatted text according to the available size // the Math.Min call is important - without this constraint (which seems arbitrary, but is the maximum allowable text width), things blow up when availableSize is infinite in both directions // the Math.Max call is to ensure we don't hit zero, which will cause MaxTextHeight to throw this.formattedText.MaxTextWidth = Math.Min(3579139, availableSize.Width); this.formattedText.MaxTextHeight = Math.Max(0.0001d, availableSize.Height); // return the desired size return new Size(this.formattedText.Width, this.formattedText.Height); } protected override Size ArrangeOverride(Size finalSize) { this.EnsureFormattedText(); // update the formatted text with the final size this.formattedText.MaxTextWidth = finalSize.Width; this.formattedText.MaxTextHeight = finalSize.Height; // need to re-generate the geometry now that the dimensions have changed this.textGeometry = null; return finalSize; } private static void OnFormattedTextInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var outlinedTextBlock = (OutlinedTextBlock)dependencyObject; outlinedTextBlock.formattedText = null; outlinedTextBlock.textGeometry = null; outlinedTextBlock.InvalidateMeasure(); outlinedTextBlock.InvalidateVisual(); } private static void OnFormattedTextUpdated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var outlinedTextBlock = (OutlinedTextBlock)dependencyObject; outlinedTextBlock.UpdateFormattedText(); outlinedTextBlock.textGeometry = null; outlinedTextBlock.InvalidateMeasure(); outlinedTextBlock.InvalidateVisual(); } private void EnsureFormattedText() { if (this.formattedText != null || this.Text == null) { return; } this.formattedText = new FormattedText( this.Text, CultureInfo.CurrentUICulture, this.FlowDirection, new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, FontStretches.Normal), this.FontSize, Brushes.Black); this.UpdateFormattedText(); } private void UpdateFormattedText() { if (this.formattedText == null) { return; } this.formattedText.MaxLineCount = this.TextWrapping == TextWrapping.NoWrap ? 1 : int.MaxValue; this.formattedText.TextAlignment = this.TextAlignment; this.formattedText.Trimming = this.TextTrimming; this.formattedText.SetFontSize(this.FontSize); this.formattedText.SetFontStyle(this.FontStyle); this.formattedText.SetFontWeight(this.FontWeight); this.formattedText.SetFontFamily(this.FontFamily); this.formattedText.SetFontStretch(this.FontStretch); this.formattedText.SetTextDecorations(this.TextDecorations); } private void EnsureGeometry() { if (this.textGeometry != null) { return; } this.EnsureFormattedText(); this.textGeometry = this.formattedText.BuildGeometry(new Point(0, 0)); } }
I modified the most voted answer with several fixes, including:
Fix so texts with a single line would show when using UseLayoutRounding.
Outlines would show outside the text instead of in the middle of the border.
The pen is created only once instead of on each render.
Fix so it won't crash when the text is set to null.
Fix so outline uses proper round caps.
using System; using System.ComponentModel; using System.Globalization; using System.Windows; using System.Windows.Documents; using System.Windows.Markup; using System.Windows.Media; [ContentProperty("Text")] public class OutlinedTextBlock : FrameworkElement { private void UpdatePen() { _Pen = new Pen(Stroke, StrokeThickness) { DashCap = PenLineCap.Round, EndLineCap = PenLineCap.Round, LineJoin = PenLineJoin.Round, StartLineCap = PenLineCap.Round }; InvalidateVisual(); } public static readonly DependencyProperty FillProperty = DependencyProperty.Register( "Fill", typeof(Brush), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender)); public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register( "Stroke", typeof(Brush), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender, StrokePropertyChangedCallback)); private static void StrokePropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { (dependencyObject as OutlinedTextBlock)?.UpdatePen(); } public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register( "StrokeThickness", typeof(double), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsRender, StrokePropertyChangedCallback)); public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner( typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register( "Text", typeof(string), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextInvalidated)); public static readonly DependencyProperty TextAlignmentProperty = DependencyProperty.Register( "TextAlignment", typeof(TextAlignment), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextDecorationsProperty = DependencyProperty.Register( "TextDecorations", typeof(TextDecorationCollection), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextTrimmingProperty = DependencyProperty.Register( "TextTrimming", typeof(TextTrimming), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(OnFormattedTextUpdated)); public static readonly DependencyProperty TextWrappingProperty = DependencyProperty.Register( "TextWrapping", typeof(TextWrapping), typeof(OutlinedTextBlock), new FrameworkPropertyMetadata(TextWrapping.NoWrap, OnFormattedTextUpdated)); private FormattedText _FormattedText; private Geometry _TextGeometry; private Pen _Pen; public Brush Fill { get { return (Brush)GetValue(FillProperty); } set { SetValue(FillProperty, value); } } public FontFamily FontFamily { get { return (FontFamily)GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } [TypeConverter(typeof(FontSizeConverter))] public double FontSize { get { return (double)GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } public FontStretch FontStretch { get { return (FontStretch)GetValue(FontStretchProperty); } set { SetValue(FontStretchProperty, value); } } public FontStyle FontStyle { get { return (FontStyle)GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } public FontWeight FontWeight { get { return (FontWeight)GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } public Brush Stroke { get { return (Brush)GetValue(StrokeProperty); } set { SetValue(StrokeProperty, value); } } public double StrokeThickness { get { return (double)GetValue(StrokeThicknessProperty); } set { SetValue(StrokeThicknessProperty, value); } } public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } public TextAlignment TextAlignment { get { return (TextAlignment)GetValue(TextAlignmentProperty); } set { SetValue(TextAlignmentProperty, value); } } public TextDecorationCollection TextDecorations { get { return (TextDecorationCollection)GetValue(TextDecorationsProperty); } set { SetValue(TextDecorationsProperty, value); } } public TextTrimming TextTrimming { get { return (TextTrimming)GetValue(TextTrimmingProperty); } set { SetValue(TextTrimmingProperty, value); } } public TextWrapping TextWrapping { get { return (TextWrapping)GetValue(TextWrappingProperty); } set { SetValue(TextWrappingProperty, value); } } public OutlinedTextBlock() { UpdatePen(); TextDecorations = new TextDecorationCollection(); } protected override void OnRender(DrawingContext drawingContext) { EnsureGeometry(); drawingContext.DrawGeometry(null, _Pen, _TextGeometry); drawingContext.DrawGeometry(Fill, null, _TextGeometry); } protected override Size MeasureOverride(Size availableSize) { EnsureFormattedText(); // constrain the formatted text according to the available size double w = availableSize.Width; double h = availableSize.Height; // the Math.Min call is important - without this constraint (which seems arbitrary, but is the maximum allowable text width), things blow up when availableSize is infinite in both directions // the Math.Max call is to ensure we don't hit zero, which will cause MaxTextHeight to throw _FormattedText.MaxTextWidth = Math.Min(3579139, w); _FormattedText.MaxTextHeight = Math.Max(0.0001d, h); // return the desired size return new Size(Math.Ceiling(_FormattedText.Width), Math.Ceiling(_FormattedText.Height)); } protected override Size ArrangeOverride(Size finalSize) { EnsureFormattedText(); // update the formatted text with the final size _FormattedText.MaxTextWidth = finalSize.Width; _FormattedText.MaxTextHeight = Math.Max(0.0001d, finalSize.Height); // need to re-generate the geometry now that the dimensions have changed _TextGeometry = null; return finalSize; } private static void OnFormattedTextInvalidated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var outlinedTextBlock = (OutlinedTextBlock)dependencyObject; outlinedTextBlock._FormattedText = null; outlinedTextBlock._TextGeometry = null; outlinedTextBlock.InvalidateMeasure(); outlinedTextBlock.InvalidateVisual(); } private static void OnFormattedTextUpdated(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var outlinedTextBlock = (OutlinedTextBlock)dependencyObject; outlinedTextBlock.UpdateFormattedText(); outlinedTextBlock._TextGeometry = null; outlinedTextBlock.InvalidateMeasure(); outlinedTextBlock.InvalidateVisual(); } private void EnsureFormattedText() { if (_FormattedText != null) { return; } _FormattedText = new FormattedText( Text ?? "", CultureInfo.CurrentUICulture, FlowDirection, new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), FontSize, Brushes.Black); UpdateFormattedText(); } private void UpdateFormattedText() { if (_FormattedText == null) { return; } _FormattedText.MaxLineCount = TextWrapping == TextWrapping.NoWrap ? 1 : int.MaxValue; _FormattedText.TextAlignment = TextAlignment; _FormattedText.Trimming = TextTrimming; _FormattedText.SetFontSize(FontSize); _FormattedText.SetFontStyle(FontStyle); _FormattedText.SetFontWeight(FontWeight); _FormattedText.SetFontFamily(FontFamily); _FormattedText.SetFontStretch(FontStretch); _FormattedText.SetTextDecorations(TextDecorations); } private void EnsureGeometry() { if (_TextGeometry != null) { return; } EnsureFormattedText(); _TextGeometry = _FormattedText.BuildGeometry(new Point(0, 0)); } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With