8

I have a custom wpf control. It is basically a textblock which has an ability to apply fill and stroke to the text. It is already inherited by a class. The problem is that it does not have some textblock properties like fontfamily. I want to inherit this control with textblock so it can use its all properties. The custom control code is given below

namespace CustomXaml
{
public class OutlinedText : FrameworkElement, IAddChild
{
#region Private Fields

private Geometry _textGeometry;

#endregion

#region Private Methods

/// <summary>
/// Invoked when a dependency property has changed. Generate a new FormattedText object to display.
/// </summary>
/// <param name="d">OutlineText object whose property was updated.</param>
/// <param name="e">Event arguments for the dependency property.</param>
private static void OnOutlineTextInvalidated(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((OutlinedText)d).CreateText();
}

#endregion


#region FrameworkElement Overrides

/// <summary>
/// OnRender override draws the geometry of the text and optional highlight.
/// </summary>
/// <param name="drawingContext">Drawing context of the OutlineText control.</param>
protected override void OnRender(DrawingContext drawingContext)
{
    CreateText();
    // Draw the outline based on the properties that are set.
    drawingContext.DrawGeometry(Fill, new Pen(Stroke, StrokeThickness), _textGeometry);

}

/// <summary>
/// Create the outline geometry based on the formatted text.
/// </summary>
public void CreateText()
{
    FontStyle fontStyle = FontStyles.Normal;
    FontWeight fontWeight = FontWeights.Medium;

    if (Bold == true) fontWeight = FontWeights.Bold;
    if (Italic == true) fontStyle = FontStyles.Italic;

    // Create the formatted text based on the properties set.
    FormattedText formattedText = new FormattedText(
        Text,
        CultureInfo.GetCultureInfo("en-us"),                
        FlowDirection.LeftToRight,
        new Typeface(Font, fontStyle, fontWeight, FontStretches.Normal),                
        FontSize,
        Brushes.Black // This brush does not matter since we use the geometry of the text. 
        );

    // Build the geometry object that represents the text.
    _textGeometry = formattedText.BuildGeometry(new Point(0, 0));




    //set the size of the custome control based on the size of the text
    this.MinWidth = formattedText.Width;
    this.MinHeight = formattedText.Height;

}

#endregion

#region DependencyProperties

/// <summary>
/// Specifies whether the font should display Bold font weight.
/// </summary>
public bool Bold
{
    get
    {
        return (bool)GetValue(BoldProperty);
    }

    set
    {
        SetValue(BoldProperty, value);
    }
}

/// <summary>
/// Identifies the Bold dependency property.
/// </summary>
public static readonly DependencyProperty BoldProperty = DependencyProperty.Register(
    "Bold",
    typeof(bool),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
        false,
        FrameworkPropertyMetadataOptions.AffectsRender,
        new PropertyChangedCallback(OnOutlineTextInvalidated),
        null
        )
    );

/// <summary>
/// Specifies the brush to use for the fill of the formatted text.
/// </summary>
public Brush Fill
{
    get
    {
        return (Brush)GetValue(FillProperty);
    }

    set
    {
        SetValue(FillProperty, value);
    }
}

/// <summary>
/// Identifies the Fill dependency property.
/// </summary>
public static readonly DependencyProperty FillProperty = DependencyProperty.Register(
    "Fill",
    typeof(Brush),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
        new SolidColorBrush(Colors.LightSteelBlue),
        FrameworkPropertyMetadataOptions.AffectsRender,
        new PropertyChangedCallback(OnOutlineTextInvalidated),
        null
        )
    );

/// <summary>
/// The font to use for the displayed formatted text.
/// </summary>
public FontFamily Font
{
    get
    {
        return (FontFamily)GetValue(FontProperty);
    }

    set
    {
        SetValue(FontProperty, value);
    }
}

/// <summary>
/// Identifies the Font dependency property.
/// </summary>
public static readonly DependencyProperty FontProperty = DependencyProperty.Register(
    "Font",
    typeof(FontFamily),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
        new FontFamily("Arial"),
        FrameworkPropertyMetadataOptions.AffectsRender,
        new PropertyChangedCallback(OnOutlineTextInvalidated),
        null
        )
    );

/// <summary>
/// The current font size.
/// </summary>
public double FontSize
{
    get
    {
        return (double)GetValue(FontSizeProperty);
    }

    set
    {
        SetValue(FontSizeProperty, value);
    }
}

/// <summary>
/// Identifies the FontSize dependency property.
/// </summary>
public static readonly DependencyProperty FontSizeProperty = DependencyProperty.Register(
    "FontSize",
    typeof(double),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
         (double)48.0,
         FrameworkPropertyMetadataOptions.AffectsRender,
         new PropertyChangedCallback(OnOutlineTextInvalidated),
         null
         )
    );


/// <summary>
/// Specifies whether the font should display Italic font style.
/// </summary>
public bool Italic
{
    get
    {
        return (bool)GetValue(ItalicProperty);
    }

    set
    {
        SetValue(ItalicProperty, value);
    }
}

/// <summary>
/// Identifies the Italic dependency property.
/// </summary>
public static readonly DependencyProperty ItalicProperty = DependencyProperty.Register(
    "Italic",
    typeof(bool),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
         false,
         FrameworkPropertyMetadataOptions.AffectsRender,
         new PropertyChangedCallback(OnOutlineTextInvalidated),
         null
         )
    );

/// <summary>
/// Specifies the brush to use for the stroke and optional hightlight of the formatted text.
/// </summary>
public Brush Stroke
{
    get
    {
        return (Brush)GetValue(StrokeProperty);
    }

    set
    {
        SetValue(StrokeProperty, value);
    }
}

/// <summary>
/// Identifies the Stroke dependency property.
/// </summary>
public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(
    "Stroke",
    typeof(Brush),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
         new SolidColorBrush(Colors.Teal),
         FrameworkPropertyMetadataOptions.AffectsRender,
         new PropertyChangedCallback(OnOutlineTextInvalidated),
         null
         )
    );

/// <summary>
///     The stroke thickness of the font.
/// </summary>
public ushort StrokeThickness
{
    get
    {
        return (ushort)GetValue(StrokeThicknessProperty);
    }

    set
    {
        SetValue(StrokeThicknessProperty, value);
    }
}

/// <summary>
/// Identifies the StrokeThickness dependency property.
/// </summary>
public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(
    "StrokeThickness",
    typeof(ushort),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
         (ushort)0,
         FrameworkPropertyMetadataOptions.AffectsRender,
         new PropertyChangedCallback(OnOutlineTextInvalidated),
         null
         )
    );

/// <summary>
/// Specifies the text string to display.
/// </summary>
public string Text
{
    get
    {
        return (string)GetValue(TextProperty);
    }

    set
    {
        SetValue(TextProperty, value);
    }
}

/// <summary>
/// Identifies the Text dependency property.
/// </summary>
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
    "Text",
    typeof(string),
    typeof(OutlinedText),
    new FrameworkPropertyMetadata(
         "",
         FrameworkPropertyMetadataOptions.AffectsRender,
         new PropertyChangedCallback(OnOutlineTextInvalidated),
         null
         )
    );

public void AddChild(Object value)
{

}

public void AddText(string value)
{
    Text = value;
}

#endregion
}
}
Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
  • Glad to see that I'm not the only person with this question. Also glad to see it's been asked by someone other than me within the past year. Getting pretty ticked off that it seems to be being so actively ignored... – Will Mar 16 '16 at 22:53
  • How is this different from http://stackoverflow.com/a/9887123/548304 ? – zastrowm Mar 17 '16 at 02:15
  • @MackieChan Because that's creating a single-purpose control. The poster wants to know how to inherit/extend a TextBlock. The answer given in that question is completely different because you get basically a bare-bones control which lacks the flexibility and utility of a TextBlock. That's why this question is different. – Will Mar 17 '16 at 04:39
  • @developer group Almost often when things get complicated, I realize that I'm approaching it the wrong way. Can you aggregate and decorate the TextBlock instead of deriving from it? – M312V Mar 17 '16 at 07:40
  • @Will Can you clarify what the question is? I can provide much better ways to create the desired control, but if you insist on using this odd approach that will result in a much different answer. Or is the question "how to override OnRender?" – Glen Thomas Mar 17 '16 at 09:44

7 Answers7

4

First we need to understand the requirements and, from the question as well as from various answers and comments, I list several:

1) I want to have an outline around my textblock text, drawn with my desired stroke thickness and color. That has been answered here: How can I extend a TextBlock to display Outlined Text? . Use DropShadowEffect on the textblock.

2) I want to control the outline up to the distance from the text and the brush that I am going to use, not just a simple color, etc. I basically want to draw anything I want on my textblock, while getting all of its functionality. So you need to adorn the TextBlock with your own graphics. Then use an Adorner.

3) The most complex requirement seems to be "a control that does everything a TextBlock does, but with a stroke that I can control completely". For this there have been several attempts: trying to recreate TextBlock from FrameworkElement, trying to inherit from TextBlock, I even copied all the miriad of internal sealed classes that are used in TextBlock and tried to rewrite it as an open control. Just inherit from TextBlock and add the Adorner code inside.

As a solution for 3), here is the code that I made to replicate the original code, which may now be changed as desired, and using a TextBlock:

public class StrokeAdorner : Adorner
{
    private TextBlock _textBlock;

    private Brush _stroke;
    private ushort _strokeThickness;

    public Brush Stroke
    {
        get
        {
            return _stroke;
        }

        set
        {
            _stroke = value;
            _textBlock.InvalidateVisual();
            InvalidateVisual();
        }
    }

    public ushort StrokeThickness
    {
        get
        {
            return _strokeThickness;
        }

        set
        {
            _strokeThickness = value;
            _textBlock.InvalidateVisual();
            InvalidateVisual();
        }
    }

    public StrokeAdorner(UIElement adornedElement) : base(adornedElement)
    {
        _textBlock = adornedElement as TextBlock;
        ensureTextBlock();
        foreach (var property in TypeDescriptor.GetProperties(_textBlock).OfType<PropertyDescriptor>())
        {
            var dp = DependencyPropertyDescriptor.FromProperty(property);
            if (dp == null) continue;
            var metadata = dp.Metadata as FrameworkPropertyMetadata;
            if (metadata == null) continue;
            if (!metadata.AffectsRender) continue;
            dp.AddValueChanged(_textBlock, (s, e) => this.InvalidateVisual());
        }
    }

    private void ensureTextBlock()
    {
        if (_textBlock == null) throw new Exception("This adorner works on TextBlocks only");
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        ensureTextBlock();
        base.OnRender(drawingContext);
        var formattedText = new FormattedText(
            _textBlock.Text,
            CultureInfo.CurrentUICulture,
            _textBlock.FlowDirection,
            new Typeface(_textBlock.FontFamily, _textBlock.FontStyle, _textBlock.FontWeight, _textBlock.FontStretch),
            _textBlock.FontSize,
             Brushes.Black // This brush does not matter since we use the geometry of the text. 
        );

        formattedText.TextAlignment = _textBlock.TextAlignment;
        formattedText.Trimming = _textBlock.TextTrimming;
        formattedText.LineHeight = _textBlock.LineHeight;
        formattedText.MaxTextWidth = _textBlock.ActualWidth - _textBlock.Padding.Left - _textBlock.Padding.Right;
        formattedText.MaxTextHeight = _textBlock.ActualHeight - _textBlock.Padding.Top;// - _textBlock.Padding.Bottom;
        while (formattedText.Extent==double.NegativeInfinity)
        {
            formattedText.MaxTextHeight++;
        }

        // Build the geometry object that represents the text.
        var _textGeometry = formattedText.BuildGeometry(new Point(_textBlock.Padding.Left, _textBlock.Padding.Top));
        var textPen = new Pen(Stroke, StrokeThickness);
        drawingContext.DrawGeometry(Brushes.Transparent, textPen, _textGeometry);
    }

}


    public class StrokeTextBlock:TextBlock
    {
        private StrokeAdorner _adorner;
        private bool _adorned=false;

        public StrokeTextBlock()
        {
            _adorner = new StrokeAdorner(this);
            this.LayoutUpdated += StrokeTextBlock_LayoutUpdated;
        }

        private void StrokeTextBlock_LayoutUpdated(object sender, EventArgs e)
        {
            if (_adorned) return;
            _adorned = true;
            var adornerLayer = AdornerLayer.GetAdornerLayer(this);
            adornerLayer.Add(_adorner);
            this.LayoutUpdated -= StrokeTextBlock_LayoutUpdated;
        }

        private static void strokeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var stb = (StrokeTextBlock)d;
            stb._adorner.Stroke = e.NewValue as Brush;
        }

        private static void strokeThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var stb = (StrokeTextBlock)d;
            stb._adorner.StrokeThickness = DependencyProperty.UnsetValue.Equals(e.NewValue)?(ushort)0:(ushort)e.NewValue;
        }

        /// <summary>
        /// Specifies the brush to use for the stroke and optional hightlight of the formatted text.
        /// </summary>
        public Brush Stroke
        {
            get
            {
                return (Brush)GetValue(StrokeProperty);
            }

            set
            {
                SetValue(StrokeProperty, value);
            }
        }

        /// <summary>
        /// Identifies the Stroke dependency property.
        /// </summary>
        public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register(
            "Stroke",
            typeof(Brush),
            typeof(StrokeTextBlock),
            new FrameworkPropertyMetadata(
                 new SolidColorBrush(Colors.Teal),
                 FrameworkPropertyMetadataOptions.AffectsRender,
                 new PropertyChangedCallback(strokeChanged),
                 null
                 )
            );

        /// <summary>
        ///     The stroke thickness of the font.
        /// </summary>
        public ushort StrokeThickness
        {
            get
            {
                return (ushort)GetValue(StrokeThicknessProperty);
            }

            set
            {
                SetValue(StrokeThicknessProperty, value);
            }
        }

        /// <summary>
        /// Identifies the StrokeThickness dependency property.
        /// </summary>
        public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register(
            "StrokeThickness",
            typeof(ushort),
            typeof(StrokeTextBlock),
            new FrameworkPropertyMetadata(
                 (ushort)0,
                 FrameworkPropertyMetadataOptions.AffectsRender,
                 new PropertyChangedCallback(strokeThicknessChanged),
                 null
                 )
            );
    }

I hope it helps people.

Also, my advice is not to use a control that inherits from TextBlock, but instead find a way to adorn TextBlocks from XAML. For that, take a look at this: http://www.codeproject.com/Articles/54472/Defining-WPF-Adorners-in-XAML If one can encapsulate it into an attached property, then one can add the strokedtext as a Style on whatever textblocks you want. Here is how I did it:

public static class Adorning
{
    public static Brush GetStroke(DependencyObject obj)
    {
        return (Brush)obj.GetValue(StrokeProperty);
    }
    public static void SetStroke(DependencyObject obj, Brush value)
    {
        obj.SetValue(StrokeProperty, value);
    }
    // Using a DependencyProperty as the backing store for Stroke. This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty StrokeProperty =
    DependencyProperty.RegisterAttached("Stroke", typeof(Brush), typeof(Adorning), new PropertyMetadata(Brushes.Transparent, strokeChanged));

    private static void strokeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var stroke= e.NewValue as Brush;
        ensureAdorner(d,a=>a.Stroke=stroke);
    }

    private static void ensureAdorner(DependencyObject d, Action<StrokeAdorner> action)
    {
        var tb = d as TextBlock;
        if (tb == null) throw new Exception("StrokeAdorner only works on TextBlocks");
        EventHandler f = null;
        f = new EventHandler((o, e) =>
          {
              var adornerLayer = AdornerLayer.GetAdornerLayer(tb);
              if (adornerLayer == null) throw new Exception("AdornerLayer should not be empty");
              var adorners = adornerLayer.GetAdorners(tb);
              var adorner = adorners == null ? null : adorners.OfType<StrokeAdorner>().FirstOrDefault();
              if (adorner == null)
              {
                  adorner = new StrokeAdorner(tb);
                  adornerLayer.Add(adorner);
              }
              tb.LayoutUpdated -= f;
              action(adorner);
          });
        tb.LayoutUpdated += f;
    }

    public static double GetStrokeThickness(DependencyObject obj)
    {
        return (double)obj.GetValue(StrokeThicknessProperty);
    }
    public static void SetStrokeThickness(DependencyObject obj, double value)
    {
        obj.SetValue(StrokeThicknessProperty, value);
    }
    // Using a DependencyProperty as the backing store for StrokeThickness. This enables animation, styling, binding, etc...  
    public static readonly DependencyProperty StrokeThicknessProperty =
    DependencyProperty.RegisterAttached("StrokeThickness", typeof(double), typeof(Adorning), new PropertyMetadata(0.0, strokeThicknessChanged));

    private static void strokeThicknessChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ensureAdorner(d, a =>
        {
            if (DependencyProperty.UnsetValue.Equals(e.NewValue)) return;
            a.StrokeThickness = (ushort)(double)e.NewValue;
        });
    }
}

An example of use:

<TextBlock Text="Some text that needs to be outlined" Grid.Row="2"
                    local:Adorning.Stroke="Aquamarine" local:Adorning.StrokeThickness="2"
                     FontSize="30">
    <TextBlock.Foreground>
        <LinearGradientBrush EndPoint="0.504,1.5" StartPoint="0.504,0.03">
            <GradientStop Color="#FFFFC934" Offset="0"/>
            <GradientStop Color="#FFFFFFFF" Offset="0.567"/>
        </LinearGradientBrush>
    </TextBlock.Foreground>
</TextBlock>

So this is what I made for this particular SO question. It is not production ready, but it should steer you on the right path towards your particular project. Good luck!

Community
  • 1
  • 1
Siderite Zackwehdex
  • 6,293
  • 3
  • 30
  • 46
  • This looks good. I will take a look at it. It's definitely more than everything else I've seen. – Will Mar 17 '16 at 14:45
  • This is really, really close... unfortunately the stroke doesn't follow the text. When the text changes positions you're left with an empty stroke and the text just appears where ever... – Will Mar 17 '16 at 16:04
  • Don't understand. What you do mean, move? – Siderite Zackwehdex Mar 17 '16 at 16:52
  • I tried adding an element changing height above the textblocks, then I added the textbox to a canvas and changed its position... the adorner followed suit. You need to explain how to reproduce the problem, although it can only be a refresh issue, not a blocking problem. – Siderite Zackwehdex Mar 17 '16 at 16:59
  • If you set the text alignment to center and expand the control you will see what I mean. Also, if you add padding. I'm working on it right now. Another problem is that changes to the text don't reflect in the Stroke ( Adorner ). If you set the text to "Bar", set the Adorner, and then set the text to "Foo" it breaks... I think this can be handled by calling the `InvalidateVisual( )` on the adorner when it is called on the TextBlock ( you'd have to declare a new InvalidateVisual on the TextBlock, call the `base.InvalidateVisual( )` from it in addition to calling the Adorner InvalidateVisual( ). – Will Mar 17 '16 at 17:10
  • 1
    I understand what you are saying, it's the box model, so to speak. And I also need to refresh the text. I don't know if I have time for it today, but I will probably work on it anyway. – Siderite Zackwehdex Mar 17 '16 at 18:18
  • I started dynamically calculating the origin based on text alignment and padding. It's not there yet but it's close ( It's been forever since I had to use even the most rudimentary of mathematical formulae ). – Will Mar 17 '16 at 18:24
  • see : http://stackoverflow.com/questions/703167/how-to-detect-a-change-in-the-text-property-of-a-textblock – Will Mar 17 '16 at 18:34
  • Adding a pile of descriptors for each property which could affect the stroke seems to be the ticket. Unfortunately it doesn't help with text wrapping... but this is as close as I've ever seen it to being exactly right. – Will Mar 17 '16 at 18:47
  • Alright I have it figured. I'm posting an answer to build on this one. Awesome job. – Will Mar 17 '16 at 18:59
  • OK, fixed padding and margin. Now it works in most cases. Special attention to cases when the text can't fit into the box. Wrapping and Trimming sometimes fails, so best not to use trimming. It's the best that I can do. As I said, not production ready. Test it thoroughly. – Siderite Zackwehdex Mar 17 '16 at 21:51
  • This is pure genius. Works perfectly. Thanks for sharing it. My app uses code generated TextBlocks. For completeness, anyone who needs to use this in code, you get an AdornerLayer from the TextBlock. Then set the properties on the StrokeAdorner, then add the StrokeAdorner to the AdornerLayer. Just a few lines of code. It begs the question why don't the WPF developers just drop this code into the WPF library. Big hole in WPF not having a native text outline. – LT Dan Mar 25 '18 at 22:13
2

TextBlock, TextBox and Label does not have common base, but all of them have the same properties: TextElement.FontSize, TextElement.FontFamily, etc...

TextElement properties are attached properties. It's simple as that.

Look at source codes of TextBlock for example. This is how they use TextElement properties:

    /// <summary> 
    /// DependencyProperty for <see cref="FontFamily" /> property.
    /// </summary>
    [CommonDependencyProperty]
    public static readonly DependencyProperty FontFamilyProperty = 
            TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));

    /// <summary> 
    /// The FontFamily property specifies the name of font family.
    /// </summary> 
    [Localizability(LocalizationCategory.Font)]
    public FontFamily FontFamily
    {
        get { return (FontFamily) GetValue(FontFamilyProperty); } 
        set { SetValue(FontFamilyProperty, value); }
    } 

    /// <summary>
    /// DependencyProperty setter for <see cref="FontFamily" /> property. 
    /// </summary>
    /// <param name="element">The element to which to write the attached property.</param>
    /// <param name="value">The property value to set</param>
    public static void SetFontFamily(DependencyObject element, FontFamily value) 
    {
        if (element == null) 
        { 
            throw new ArgumentNullException("element");
        } 

        element.SetValue(FontFamilyProperty, value);
    }

    /// <summary>
    /// DependencyProperty getter for <see cref="FontFamily" /> property. 
    /// </summary> 
    /// <param name="element">The element from which to read the attached property.</param>
    public static FontFamily GetFontFamily(DependencyObject element) 
    {
        if (element == null)
        {
            throw new ArgumentNullException("element"); 
        }

        return (FontFamily)element.GetValue(FontFamilyProperty); 
    }
Liero
  • 25,216
  • 29
  • 151
  • 297
1

The issue here is that the OnRender method in TextBlock is sealed. It sucks, but there must be a good reason for it. One which I am not aware of.

An alternative would be to subscribe to the LayoutUpdated event and call your CreateText() method when the layout is updated. Here's an example:

public class OutlinedText : TextBlock
{
    public OutlinedText()
    {
        LayoutUpdated += OutlinedText_LayoutUpdated;
    }

    void OutlinedText_LayoutUpdated(object sender, EventArgs e)
    {
        CreateText();

        //...
    }

This is by no means the golden ticket, however LayoutUpdated gets called often and should be able to handle your text rendering requirements.

Oh, and here is some documentation for it.

Mike Eason
  • 9,525
  • 2
  • 38
  • 63
0

Inherit from TextBlock:

public class OutlinedText : TextBlock, IAddChild
Glen Thomas
  • 10,190
  • 5
  • 33
  • 65
  • @developergroup textblock is inherited from Frameworkelement, So you can use FrameworkElement also If you Inherit from TextBlock – Dinesh balan Aug 05 '15 at 10:25
  • 2
    After replacing Textblock it gives me an error on an override function that: You cannot override OnRender because it is sealed – developer group Aug 05 '15 at 10:33
  • Why do you need to override OnRender? – Glen Thomas Mar 17 '16 at 09:39
  • He would need to override OnRender because that is where the string is rendered as text on the screen. if the text isn't rendered, you can make your geometry all day long and it's useless. – Will Mar 17 '16 at 14:48
0

Came across this neat toolkit years ago and they have a StrokeTextBlock. I used it for my silverlight projects for more than 5 years now. They also have a WPF version. The code is a lot to post here so here is the link: (which i was surprised to see still existed on codeplex.

Blacklight Toolkit: StrokeTextBlock.cs

It inherits from System.Windows.Controls.Control Which allows it to have the properties you were looking for like FontFamily,FontSize,FontWeight...etc

Here is the generic.xaml for the control

<!-- StrokeTextBlock style -->
<Style TargetType="local:StrokeTextBlock">
    <Setter Property="Text" Value="StrokeTextBlock" />
    <Setter Property="StrokeOpacity" Value="1" />
    <Setter Property="Stroke" Value="#ffffffff" />
    <Setter Property="StrokeThickness" Value="1" />
    <Setter Property="Foreground" Value="#ff000000" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:StrokeTextBlock">
                <Grid>
                    <ItemsControl x:Name="PART_ItemsControl" 
                          VerticalAlignment="Top" HorizontalAlignment="Left" 
                          Opacity="{TemplateBinding StrokeOpacity}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <Grid  />
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>

                    <TextBlock x:Name="PART_TextBlock"
                               TextWrapping="{TemplateBinding TextWrapping}" 
                               Foreground="{TemplateBinding Foreground}"
                               FontSize="{TemplateBinding FontSize}" 
                               FontFamily="{TemplateBinding FontFamily}" 
                               FontWeight="{TemplateBinding FontWeight}"
                               VerticalAlignment="Top" HorizontalAlignment="Left"
                               UseLayoutRounding="False"
                               LineHeight="{TemplateBinding LineHeight}"
                               Text="{TemplateBinding Text}" />

                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

Inherit from TextBox, and apply your own Template. Don't make it editable to mimick it as a TextBlock.

Your Template would be a Geometry. Construct this Geometry in the constructor of your control or Loaded event. For example, you can call CreateText() from your ctor. There are various Geometry derived classes available like LineGeometry, PathGeometry etc.

** EDIT after working on a workable sample **

Inherit from Label.

Change your CreateText() to :

   public void CreateText()
    {
        FontStyle fontStyle = FontStyles.Normal;
        FontWeight fontWeight = FontWeights.Medium;

        //if (FontWeight == FontWeights.Bold) fontWeight = FontWeights.Bold;
        // if (FontStyle == FontStyles.Italic) fontStyle = FontStyles.Italic;

        // Create the formatted text based on the properties set.
        FormattedText formattedText = new FormattedText(
            Text,
            CultureInfo.GetCultureInfo("en-us"),
            FlowDirection.LeftToRight,
            new Typeface(FontFamily, FontStyle, FontWeight, FontStretches.Normal, new FontFamily("Arial")),
            FontSize,
            Brushes.Black // This brush does not matter since we use the geometry of the text. 
            );

        // Build the geometry object that represents the text.
        _textGeometry = formattedText.BuildGeometry(new Point(4, 4));

        //set the size of the custome control based on the size of the text
        this.MaxWidth = formattedText.Width + 100;
        this.MaxHeight = formattedText.Height + 10;
    }

And you can consider to completely remove the ControlTemplate of the parent Label. This is very easy by right-clicking the control and edit-template > create-empty.

AnjumSKhan
  • 9,647
  • 1
  • 26
  • 38
  • How does this help you when you need to draw it? – Will Mar 17 '16 at 04:38
  • @Will Thats why use Geometry. – AnjumSKhan Mar 17 '16 at 04:46
  • Okay that doesn't answer my question - my experience with drawing and geometries is pretty limited but don't you need a drawing context to draw? And isn't the only way to access a drawing context through the controls OnRender override? And doesn't the TextBlock control seal the OnRender override so you can't override it? – Will Mar 17 '16 at 05:08
  • Text box does not have a template, as far as I know – Liero Mar 17 '16 at 06:06
  • @Liero TextBlock doesnt have a template, but Label and TextBox has. You can use TextBox as a TextBlock by disabling its editing support. – AnjumSKhan Mar 17 '16 at 06:59
0

You can inherit from TextBlock, but when you do this, you don't need to implement IAddChild, because TextBlock already does this, as mentioned here: in this MSDN reference page.

What I would recommend is to create WPF UserControl and change it's inheritance from UserControl to TextBlock, then you can just extend the functionality in your class, I have personally tested this and it works fine. If you need to add any visual customisation, this can usually be done through a Itemtemplate / ControlTemplate.

Here is an article with another method that shows how to extend a WPF control which shows that it is possible: Basics of extending WPF control

Alternatively this is another method using a user control that transforms its content to different color.

If you are looking to display a custom stroke fill with the TextBlock, then here is a solution to that specific problem.

At least one of these methods or a combination should be able to get what you want done.

Community
  • 1
  • 1
MikeDub
  • 5,143
  • 3
  • 27
  • 44