WPF RichTextBox Syntax Highlighting Issue

2020-02-04 21:53发布

问题:

Hello everyone I've been working on a WPF application that has a text editor this text editor should apply some styling or colorizing over some tokens (keywords) to highlight it and make it obvious,,, the problem is i tried very very hard but i still get the same result which is when the user enters one of the keywords the whole text after that keyword is being styled ! just imagine if you types the " string " keyword in " C# " the whole text after it will be colored blue.

this was the code i used:

    static List<string> tags = new List<string>();
    static List<char> specials = new List<char>();
    static string text;
    #region ctor
    static MainWindow()
    {
        string[] specialWords = { "string", "char", "null" };              
        tags = new List<string>(specialWords);     
        // We also want to know all possible delimiters so adding this stuff.     
        char[] chrs = {
            '.',
            ')',
            '(',
            '[',
            ']',
            '>',
            '<',
            ':',
            ';',
            '\n',
            '\t',
            '\r'
        };
        specials = new List<char>(chrs);
    }
    public MainWindow()
    {
        InitializeComponent();
    }
        #endregion
    //Now I should check statically if the string I passed is legal and constants in my dictionary
    public static bool IsKnownTag(string tag)
    {
        return tags.Exists(delegate(string s) { return s.ToLower().Equals(tag.ToLower()); });
    }            
    private static bool GetSpecials(char i)
    {
        foreach (var item in specials)
        {
            if (item.Equals(i))
            {
                return true;
            }
        }
        return false;
    }
    // Wow. Great. Now I should separate words, that equals to my tags. For this propose we'll create new internal structure named Tag. This will help us to save words and its' positions.
    new struct Tag
    {
        public TextPointer StartPosition;
        public TextPointer EndPosition;
        public string Word;     
    }              
    internal void CheckWordsInRun(Run theRun){
        //How, let's go through our text and save all tags we have to save.               
        int sIndex = 0;
        int eIndex = 0;
        List<Tag> m_tags = new List<Tag>();
        for (int i = 0; i < text.Length; i++)
        {
            if (Char.IsWhiteSpace(text[i]) | GetSpecials(text[i]))
            {
                if (i > 0 && !(Char.IsWhiteSpace(text[i - 1]) | GetSpecials(text[i - 1])))
                {
                    eIndex = i - 1;
                    string word = text.Substring(sIndex, eIndex - sIndex + 1);     
                    if (IsKnownTag(word))
                    {
                        Tag t = new Tag();
                        t.StartPosition = theRun.ContentStart.GetPositionAtOffset(sIndex, LogicalDirection.Forward);
                        t.EndPosition = theRun.ContentStart.GetPositionAtOffset(eIndex + 1, LogicalDirection.Backward);
                        t.Word = word;
                        m_tags.Add(t);
                    }
                }
                sIndex = i + 1;
            }
        }
        //How this works. But wait. If the word is last word in my text I'll never hightlight it, due I'm looking for separators. Let's add some fix for this case
        string lastWord = text.Substring(sIndex, text.Length - sIndex);
        if (IsKnownTag(lastWord))
        {
            Tag t = new Tag();
            t.StartPosition = theRun.ContentStart.GetPositionAtOffset(sIndex, LogicalDirection.Forward);
            t.EndPosition = theRun.ContentStart.GetPositionAtOffset(eIndex + 1, LogicalDirection.Backward);
            t.Word = lastWord;
            m_tags.Add(t);
        }
        //How I have all my words and its' positions in list. Let's color it! Dont forget to unsubscribe! text styling fires TextChanged event.
        txtStatus.TextChanged -= txtStatus_TextChanged;
        for (int i = 0; i < m_tags.Count; i++)
        {
            try
            {
                TextRange range = new TextRange(m_tags[i].StartPosition, m_tags[i].EndPosition);
                range.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Blue));
                range.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);
            }
            catch { }
        }
        m_tags.Clear();
        txtStatus.TextChanged += txtStatus_TextChanged;
    }

and here is the text changed event handler

    private void txtStatus_TextChanged(object sender, TextChangedEventArgs e)
    {           
        if (txtStatus.Document == null)
            return;
        TextRange documentRange = new TextRange(txtStatus.Document.ContentStart, txtStatus.Document.ContentEnd);
        //documentRange.ClearAllProperties();
        text = documentRange.Text;
        //Now let's create navigator to go though the text and hightlight it
        TextPointer navigator = txtStatus.Document.ContentStart;
        while (navigator.CompareTo(txtStatus.Document.ContentEnd) < 0)
        {
            TextPointerContext context = navigator.GetPointerContext(LogicalDirection.Backward);
            if (context == TextPointerContext.ElementStart && navigator.Parent is Run)
            {
                CheckWordsInRun((Run)navigator.Parent);
            }
            navigator = navigator.GetNextContextPosition(LogicalDirection.Forward);
        }
    }

Any advice or hand will be greatly appreciated,, thanks in advance.

回答1:

You should highlight the keywords until all the text is parsed, highlighting the keywords in each Run will affect the call to navigator.GetNextContextPosition, causing unexpected errors like firing textchanged event repeatedly.

And after you hightlight a keyword, the text you type after that keyword INHERITS the style of that keyword. One workaround is calling ClearAllProperties on the whole text before you hight light the keywords.

Below is the updated txtStatus_TextChanged and CheckWordsInRun method.

List<Tag> m_tags = new List<Tag>(); 
internal void CheckWordsInRun(Run theRun) //do not hightlight keywords in this method
{
    //How, let's go through our text and save all tags we have to save.               
    int sIndex = 0;
    int eIndex = 0;

    for (int i = 0; i < text.Length; i++)
    {
        if (Char.IsWhiteSpace(text[i]) | GetSpecials(text[i]))
        {
            if (i > 0 && !(Char.IsWhiteSpace(text[i - 1]) | GetSpecials(text[i - 1])))
            {
                eIndex = i - 1;
                string word = text.Substring(sIndex, eIndex - sIndex + 1);
                if (IsKnownTag(word))
                {
                    Tag t = new Tag();
                    t.StartPosition = theRun.ContentStart.GetPositionAtOffset(sIndex, LogicalDirection.Forward);
                    t.EndPosition = theRun.ContentStart.GetPositionAtOffset(eIndex + 1, LogicalDirection.Backward);
                    t.Word = word;
                    m_tags.Add(t);
                }
            }
            sIndex = i + 1;
        }
    }
    //How this works. But wait. If the word is last word in my text I'll never hightlight it, due I'm looking for separators. Let's add some fix for this case
    string lastWord = text.Substring(sIndex, text.Length - sIndex);
    if (IsKnownTag(lastWord))
    {
        Tag t = new Tag();
        t.StartPosition = theRun.ContentStart.GetPositionAtOffset(sIndex, LogicalDirection.Forward);
        t.EndPosition = theRun.ContentStart.GetPositionAtOffset(text.Length, LogicalDirection.Backward); //fix 1
        t.Word = lastWord;
        m_tags.Add(t);
    }
}

private void txtStatus_TextChanged(object sender, TextChangedEventArgs e)
{
    if (txtStatus.Document == null)
        return;
    txtStatus.TextChanged -= txtStatus_TextChanged;

    m_tags.Clear();

    //first clear all the formats
    TextRange documentRange = new TextRange(txtStatus.Document.ContentStart, txtStatus.Document.ContentEnd);
    documentRange.ClearAllProperties();
    //text = documentRange.Text; //fix 2

    //Now let's create navigator to go though the text, find all the keywords but do not hightlight
    TextPointer navigator = txtStatus.Document.ContentStart;
    while (navigator.CompareTo(txtStatus.Document.ContentEnd) < 0)
    {
        TextPointerContext context = navigator.GetPointerContext(LogicalDirection.Backward);
        if (context == TextPointerContext.ElementStart && navigator.Parent is Run)
        {
            text = ((Run)navigator.Parent).Text; //fix 2
                                 if (text != "")
                CheckWordsInRun((Run)navigator.Parent);
        }
        navigator = navigator.GetNextContextPosition(LogicalDirection.Forward);
    }

    //only after all keywords are found, then we highlight them
    for (int i = 0; i < m_tags.Count; i++)
    {
        try
        {
            TextRange range = new TextRange(m_tags[i].StartPosition, m_tags[i].EndPosition);
            range.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.Blue));
            range.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold);
        }
        catch { }
    }
    txtStatus.TextChanged += txtStatus_TextChanged;
}