3

I would like to format specific words in TextBlock dynamically, because it is binded to my object. I was thinking about using Converter but using following solution add only tags directly to the text (instead of showing it formatted).

public class TextBlockFormatter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        string regexp = @"\p{L}+#\d{4}";

        if (value != null) {
            return Regex.Replace(value as string, regexp, m => string.Format("<Bold>{0}</Bold>", m.Value));
        } 

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        return null;
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • You'll either need to use the complicated [`RichTextBox` class](http://msdn.microsoft.com/en-us/library/system.windows.controls.richtextbox(v=vs.110).aspx), or create some custom way of generating separate [`Run` elements](http://msdn.microsoft.com/en-us/library/system.windows.documents.run(v=vs.110).aspx) within a `TextBlock`. – Sheridan Jan 16 '14 at 15:14
  • @Sheridan pretty sure RichTextBox does not support binding and if there is a converter I suspect there is binding. – paparazzo Jan 16 '14 at 15:42
  • If you don't use a converter are you able to format individual words in a TextBlock? – paparazzo Jan 16 '14 at 16:05
  • I used this and it worked. And it is fast. Since InLines is readonly need to fool it. http://stackoverflow.com/questions/3728584/how-to-display-search-results-in-a-wpf-items-control-with-highlighted-query-term – paparazzo Jan 30 '14 at 14:21

3 Answers3

4

This is not an attempt to answer this question... it is a demonstration in response to a question in the question comments.

Yes @Blam, you can format individual words, or even characters in a TextBlock... you need to use the Run (or you could replace Run with TextBlock) class. Either way, you can also data bind to the Text property on either of them:

enter image description here

<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
    <Run Text="This" FontWeight="Bold" Foreground="Red" />
    <Run Text="text" FontSize="18" />
    <Run Text="is" FontStyle="Italic" />
    <Run Text="in" FontWeight="SemiBold" Background="LightGreen" />
    <Run Text="one" FontFamily="Candara" FontSize="20" />
    <Run Text="TextBlock" FontWeight="Bold" Foreground="Blue" />
</TextBlock>

UPDATE >>>

Regarding this question now, I suppose that it would be possible to use these Run elements in a DataTemplate and have them generated from the data... the data in this case would have to be classes with (obviously) a Text property, but also formatting properties that you could use to data bind to the Run style properties.

It would be awkward though because the TextBlock has no ItemsSource property that you could bind your collection of word classes to... maybe you could use a Converter for that part... just thinking aloud here... I'm going to stop now.


UPDATE >>>

@KrzysztofBzowski, unfortunately the TextBlock.Inlines property is not a DependencyProperty, so you won't be able to data bind to it. However, it got me thinking and I did another search and found the Binding text containing tags to TextBlock inlines using attached property in Silverlight for Windows Phone 7 article on Jevgeni Tsaikin's .NET laboratory.

It would involve you declaring an Attached Property and a Converter, but it looks promising... give it a go. And don't worry that it's for Silverlight... if it works in Silverlight, then it'll work in WPF.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • 1
    Thanks, I did not know you could use Run directly in TextBlock. I only knew of them in a FlowDocument. – paparazzo Jan 16 '14 at 16:28
  • Yeah... you could even use `TextBlock` elements inside other `TextBlock` elements, eg. replace the `Run` elements here with `TextBlock`s. – Sheridan Jan 16 '14 at 16:33
  • I need both formatting and copy paste (but read only). It appears a TextBox does not support InLines and hence does not support highlighting. Any thoughts. Right now I use a FlowDocumentScrollViewer but it is slow. – paparazzo Jan 16 '14 at 17:30
  • Meanwhile I have found: http://stackoverflow.com/questions/2891736/wpf-can-i-dynamically-make-part-of-the-textblock-text-to-different-colour but I would prefer something consistent with MVVM approach. – Krzysztof Bzowski Jan 16 '14 at 18:39
2

I recently had to solve this problem which I was able to do by writing a Blend behaviour for TextBlocks.

It can be declared in XAML with a list of Highlight elements where you specify the text to highlight, the colour you want that text to be and it's font weight (can easily add more formatting properties as required).

It works by looping though the desired highlights, scanning the TextBlock for each phrase starting at the TextBlock.ContentStart TextPointer. Once the phrase is found it can build a TextRange which can have the formatting options applied to it.

It should work if the TextBlock Text property is data bound too because I attach to the bindings Target updated event.

See below for the behaviour code and an example in XAML

public class TextBlockHighlightBehaviour : Behavior<TextBlock>
{
    private EventHandler<DataTransferEventArgs> targetUpdatedHandler;
    public List<Highlight> Highlights { get; set; }

    public TextBlockHighlightBehaviour()
    {
        this.Highlights = new List<Highlight>();
    }

    #region Behaviour Overrides

    protected override void OnAttached()
    {
        base.OnAttached();
        targetUpdatedHandler = new EventHandler<DataTransferEventArgs>(TextBlockBindingUpdated);
        Binding.AddTargetUpdatedHandler(this.AssociatedObject, targetUpdatedHandler);

        // Run the initial behaviour logic
        HighlightTextBlock(this.AssociatedObject);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        Binding.RemoveTargetUpdatedHandler(this.AssociatedObject, targetUpdatedHandler);
    }

    #endregion

    #region Private Methods

    private void TextBlockBindingUpdated(object sender, DataTransferEventArgs e)
    {
        var textBlock = e.TargetObject as TextBlock;
        if (textBlock == null)
            return;

        if(e.Property.Name == "Text")
            HighlightTextBlock(textBlock);
    }

    private void HighlightTextBlock(TextBlock textBlock)
    {
        foreach (var highlight in this.Highlights)
        {
            foreach (var range in FindAllPhrases(textBlock, highlight.Text))
            {
                if (highlight.Foreground != null)
                    range.ApplyPropertyValue(TextElement.ForegroundProperty, highlight.Foreground);

                if(highlight.FontWeight != null)
                    range.ApplyPropertyValue(TextElement.FontWeightProperty, highlight.FontWeight);
            }
        }
    }

    private List<TextRange> FindAllPhrases(TextBlock textBlock, string phrase)
    {
        var result = new List<TextRange>();
        var position = textBlock.ContentStart;

        while (position != null)
        {
            var range = FindPhrase(position, phrase);
            if (range != null)
            {
                result.Add(range);
                position = range.End;
            }
            else
                position = null;
        }

        return result;
    }

    // This method will search for a specified phrase (string) starting at a specified position.
    private TextRange FindPhrase(TextPointer position, string phrase)
    {
        while (position != null)
        {
            if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
            {
                string textRun = position.GetTextInRun(LogicalDirection.Forward);

                // Find the starting index of any substring that matches "phrase".
                int indexInRun = textRun.IndexOf(phrase);
                if (indexInRun >= 0)
                {
                    TextPointer start = position.GetPositionAtOffset(indexInRun);
                    TextPointer end = start.GetPositionAtOffset(phrase.Length);
                    return new TextRange(start, end);
                }
            }

            position = position.GetNextContextPosition(LogicalDirection.Forward);
        }

        // position will be null if "phrase" is not found.
        return null;
    }

    #endregion
}

public class Highlight
{
    public string Text { get; set; }
    public Brush Foreground { get; set; }
    public FontWeight FontWeight { get; set; }
}

Example usage in XAML:

<TextBlock Text="Here is some text">
   <i:Interaction.Behaviors>
      <behaviours:TextBlockHighlightBehaviour>
         <behaviours:TextBlockHighlightBehaviour.Highlights>
            <behaviours:Highlight Text="some" Foreground="{StaticResource GreenBrush}" FontWeight="Bold" />
            </behaviours:TextBlockHighlightBehaviour.Highlights>
         </behaviours:TextBlockHighlightBehaviour>
   </i:Interaction.Behaviors>
</TextBlock>

You'll need to import the Blend interactivity namespace and your behaviour's namespace:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behaviours="clr-namespace:YourProject.Behviours"
  • And just for further explanation, by applying formatting to the TextRange objects in the code it actually adds a series of Runs to the TextBlock.Inlines collection, pretty cool huh? – Neil Alderson Nov 05 '14 at 11:28
0

I had a similar use case where I needed to build a text editor with RichTextBox. However, all text formatted changes: Font, Color, Italic, Bold must reflect on the TextBlock dynamically. I found few articles pointing me to Textblock.inlines.Add() which seemed helpful but only allow one change at a time or appending to the existing text.

However, Textblock.inlines.ElementAt(index of the existing text to format) can be utilized to apply the desired text format to the text located at that index. Below is my pseudo approach to resolving this issue. I hope this helps:

For RichTextBox:    
selectedText.ApplyPropertyValue(TextElement.FontFamilyProperty, cbFontFamily.SelectedValue.ToString());

However, for the Textblock formatting to work I had to use Run run = new Run() concept which allows and works with:

Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(SelectedColorText))
FontFamily = new FontFamily(cbFontFamily.SelectedValue.ToString())
FontStyle = FontStyles.Italic
TextDecorations = TextDecorations.Underline
TextDecorations = TextDecorations.Strikethrough
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(cbFontWeightBox.SelectedItem.ToString())

Additionally, I created a class with various fields and constructors. I also created a custom based class dictionary to capture all the changes made in the RichTextbbox. Finally, I applied all the captured information in the dictionary via a forloop.

TextBlock.Inlines.ElementAt(mItemIndex).Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(dictionaryItem.Value._applyColor.ToString()));

TextBlock.Inlines.ElementAt(mItemIndex).FontFamily = new FontFamily(ftItem.Value._applyFontFamily.ToString());

TextBlock.Inlines.ElementAt(mItemIndex).FontStyle = FontStyles.Italic;

TextBlock.Inlines.ElementAt(mItemIndex).TextDecorations = TextDecorations.Underline;

TextBlock.Inlines.ElementAt(mItemIndex).TextDecorations = TextDecorations.Strikethrough;

enter image description here

A. Lartey
  • 59
  • 2