Search code examples
c#wpf.net-6.0richtextbox

I need to get cursor position of the end of a WPF .NET 6 RichTextBox in order to select and change the font of each appended line


In my application, I have a simple window with a read-only RichTextBox. I'm using WPF, .NET 6. The purpose of the Window is simply to display formatted information for the pluggable files used by the application.

When the window launches, the RichTextBox is programmatically populated with category headers in bold Font for the various pluggable file types, followed by the list of files of the type in a smaller normal font. The way I'm accomplising this is by appending text to the RichTextBox, then selecting the appended text, then setting the Selection Font dependency properties, which works fine. The probem I'm having is selecting each line of text after I append it. I've tried two approaches:

The problem with both methods is that the selection gets of more and more as more lines are added. I'm assuming this is because my methods use the raw text rather than the markup in the RichTextBox. Below is a screenshot of what happens: Image showing wrong font offsets

I've done every permutation of search I can think of to no avail. Any help would be appreciated.

I have tried to implement my own version of WinForms RichTextBox.GetFirstCharIndexOfCurrentLine() as follows:

public int GetFirstCharIndexOfCurrentLine()
{
    int index = 0;

    TextRange textRange = new TextRange(
        Document.ContentStart,
        Document.ContentEnd
    );

    var alltext = textRange.Text;

    string[] lines = alltext.Replace("\n", "").Split('\r');

    int lineIndex = lines.Length - 1;

    if (lineIndex > lines.Count())
    {
        throw new ArgumentOutOfRangeException("lineIndex");
    }

    for (int i = 0; i < lineIndex; i++)
    {
        index += lines[i].Length;
    }

     return index;
}

AND, I have attempted to select the text of the line as follows:

public void Select(string text)
{
    TextRange textRange = new TextRange(Document.ContentStart, Document.ContentEnd);
    string alltext = textRange.Text;

    int index = alltext.IndexOf(text);

    if (index < 0)
    {
        return;
    }

    Select(index, text.Length);
}

Solution

  • The text analyzing in the WPF RichTextBox control is totally different from the RichTextBox in the WinForms. You should use the context related methods of the TextPointer class.

    But I would recommend to use more simple way to set the font characteristics during appending a line. See code example below:

    The MainWindow.xaml:

    <Window x:Class="AppentTextLine.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"        
            mc:Ignorable="d"
            Title="MainWindow" Height="350" Width="400"
            DataContext="{Binding RelativeSource={RelativeSource Self}}">
        <Grid>
            <DockPanel>
                <ToolBar DockPanel.Dock="Top">
                    <ComboBox SelectedItem="{Binding MyFontFamily}"
                              ItemsSource="{Binding Source={x:Static Fonts.SystemFontFamilies}}" MinWidth="150" SelectedIndex="0" />
                    <ComboBox SelectedItem="{Binding SelectedFontSize}"
                              ItemsSource="{Binding FontSizeList}" MinWidth="40" SelectedIndex="0" />
                </ToolBar>
                <RichTextBox x:Name="rtb" Margin="5" VerticalScrollBarVisibility="Auto" />
            </DockPanel>
        </Grid>
    </Window>
    

    The MainWindow.xaml.cs:

    using System;
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    using System.Windows.Media;
    using System.Windows.Threading;
    
    namespace AppentTextLine
    {
        public partial class MainWindow : Window
        {
            public FontFamily MyFontFamily
            {
                get => (FontFamily)GetValue(MyFontFamilyProperty);
                set => SetValue(MyFontFamilyProperty, value);
            }
            public static readonly DependencyProperty MyFontFamilyProperty =
                DependencyProperty.Register("MyFontFamily", typeof(FontFamily), typeof(MainWindow), new UIPropertyMetadata(new FontFamily("Arial")));
    
            public List<double> FontSizeList
            {
                get => (List<double>)GetValue(FontSizeProperty);
                set => SetValue(FontSizeProperty, value);
            }
            public static readonly DependencyProperty FontSizeListProperty =
                DependencyProperty.Register("FontSizeList", typeof(List<double>), typeof(MainWindow),
                    new PropertyMetadata(new List<double>() { 8, 10, 12, 14, 16, 18, 20, 22 }));
    
            public double SelectedFontSize
            {
                get => (double)GetValue(SelectedFontSizeProperty);
                set => SetValue(SelectedFontSizeProperty, value);
            }
            public static readonly DependencyProperty SelectedFontSizeProperty =
                DependencyProperty.Register("SelectedFontSize", typeof(double), typeof(MainWindow), new PropertyMetadata(10.0));
    
            private readonly DispatcherTimer dispatcherTimer = new DispatcherTimer();
    
            public MainWindow()
            {
                InitializeComponent();
    
                dispatcherTimer.Tick += DispatcherTimer_Tick1;
                dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 1, 0);
                dispatcherTimer.Start();
            }
    
            private void DispatcherTimer_Tick1(object? sender, EventArgs e)
            {
               var tm = DateTime.Now.ToString("HH:mm:ss.fff");
                rtb.AppendLine(tm, MyFontFamily, FontWeights.Normal, SelectedFontSize);
                rtb.ScrollToEnd(); 
            }
        }
    
        public static class RichTextBoxExt
        {
            public static void AppendLine(this RichTextBox rtb, string text, FontFamily fontFamily, FontWeight fweight, double fsize = 10)
            {
                if (rtb.Document.Blocks.LastBlock is Paragraph par)
                {
                    par.Inlines.Add(new Run(text + Environment.NewLine) { FontFamily = fontFamily, FontSize = fsize, FontWeight = fweight });
                }
            }
        }
    }
    

    There is another way to append a new line and set text properties (font, color etc) - by using FlowDocument.ContentEnd text pointer that represent end of the document content:

    TextRange tr = new TextRange(rtb.Document.ContentEnd, rtb1.Document.ContentEnd)
        {
            Text = text
        };
    tr.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Red);
    

    But I found that this can affect performance. For example, when appending a new line instead of adding only Run object to the latest block in document a new Paragraph will be added that will include the new Run.