Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to inherit a textblock properties to a custom control in c#

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
}
}
like image 777
developer group Avatar asked Aug 05 '15 09:08

developer group


2 Answers

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!

like image 117
Siderite Zackwehdex Avatar answered Nov 16 '22 00:11

Siderite Zackwehdex


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); 
    }
like image 2
Liero Avatar answered Nov 16 '22 01:11

Liero