WPF RichTextBox - 设置Caret.RenderTransform中断拼写检查

时间:2016-10-19 20:57:53

标签: c# wpf richtextbox spell-checking

我有一个C#WPF RichTextBox,允许通过ScaleX进行ScaleYLayoutTransform Slider调整。不幸的是,这种缩放会导致插入符号停止渲染,这是一个可以根据代码at this SO post here修复的错误。更不幸的是,设置插入符RenderTransform会导致拼写检查红色波浪线在您键入时停止显示。好像没有聚焦RichTextBox并通过点击Slider再次聚焦将导致所有红色波浪线重新出现。 您可以在我的GitHub here 上查看此错误的演示。

GIF of bug

问题:如何在用户输入时显示红色波浪形拼写检查线,同时仍然允许RichTextBox缩放和完全渲染的所有比例级别插入符号? 我尝试过手动调用GetSpellingError(TextPointer),这样做有点......它不是完全可靠的,除非我在 GetSpellingError的每个字上调用RichTextBox,这在内容很多时计算速度非常慢。我还尝试对Speller及相关内部类中的项目使用反射等,例如HighlightsSpellerStatusTableSpellerHighlightLayer。查看SpellerStatusTable的运行列表(似乎有关于运行是干净还是脏的信息)时,运行不会更新以包含错误,直到单击滑块,这意味着{{ 1}}没有重新检查拼写错误。

RichTextBox"修正"中评论caretSubElement.RenderTransform = scaleTransform;问题,但然后再次打破插入符号渲染。

代码 -

MainWindow.xaml

CustomRichTextBox.cs

CustomRichTextBox.cs

<Window x:Class="BrokenRichTextBox.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"
        xmlns:local="clr-namespace:BrokenRichTextBox"
        mc:Ignorable="d"
        Title="Rich Text Box Testing" Height="350" Width="525">
    <Grid Background="LightGray">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Slider Name="FontZoomSlider" Grid.Row="0" Width="150" Value="2" Minimum="0.3" Maximum="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
        <local:CustomRichTextBox x:Name="richTextBox" 
                                 Grid.Row="1" 
                                 SpellCheck.IsEnabled="True"
                                 ScaleX="{Binding ElementName=FontZoomSlider, Path=Value}" 
                                 ScaleY="{Binding ElementName=FontZoomSlider, Path=Value}"
                                 AcceptsTab="True">
            <local:CustomRichTextBox.LayoutTransform>
                <ScaleTransform ScaleX="{Binding ElementName=richTextBox, Path=ScaleX, Mode=TwoWay}" 
                                ScaleY="{Binding ElementName=richTextBox, Path=ScaleY, Mode=TwoWay}"/>
            </local:CustomRichTextBox.LayoutTransform>
            <FlowDocument>
                <Paragraph>
                    <Run>I am some sample text withhh typooos</Run>
                </Paragraph>
                <Paragraph>
                    <Run FontStyle="Italic">I am some more sample text in italic</Run>
                </Paragraph>
            </FlowDocument>
        </local:CustomRichTextBox>
    </Grid>
</Window>

1 个答案:

答案 0 :(得分:1)

我设法让事情变得有效,至少是出场了。 tl; dr fix是对上一个/下一个单词以及上一个/下一个GetSpellingErrorParagraphs)的第一个和最后一个单词进行手动Blocks调用。如果我点击进入/返回&#39;在该行的结尾处,该段落的最后一个单词拼写错误,拼写检查器没有启动。如果上一段中的第一个单词在点击后输入/返回&#39后拼写错误;,红色波浪将消失!在任何情况下,手动检查单词,但不检查所有单词,似乎工作正常。

我的个人项目有一些额外的&#34;请检查周围单词的拼写&#34;如果没有及时调用UpdateAdorner,则会调用一些OnPreviewKeyDown个实例,但我会将其作为练习留给读者。 :)

我猜测某处有更好的答案。

代码(在Github here上轻松查看):

MainWindow.xaml

<Window x:Class="BrokenRichTextBox.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"
        xmlns:local="clr-namespace:BrokenRichTextBox"
        mc:Ignorable="d"
        Title="Rich Text Box Testing" Height="480" Width="640">
    <Grid Background="LightGray">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <!--CheckBox Content="Enable Extra" Grid.Row="0" VerticalAlignment="Center"/-->
        <Label Content="Broken RichTextBox" Grid.Row="0"/>
        <Slider Name="FontZoomSlider" Grid.Row="0" Width="150" Value="2" Minimum="0.3" Maximum="10" HorizontalAlignment="Right" VerticalAlignment="Center"/>
        <local:CustomRichTextBox x:Name="RichTextBox" 
                                 Grid.Row="1" 
                                 SpellCheck.IsEnabled="True"
                                 ScaleX="{Binding ElementName=FontZoomSlider, Path=Value}" 
                                 ScaleY="{Binding ElementName=FontZoomSlider, Path=Value}"
                                 AcceptsTab="True">
            <local:CustomRichTextBox.LayoutTransform>
                <ScaleTransform ScaleX="{Binding ElementName=RichTextBox, Path=ScaleX, Mode=TwoWay}" 
                                ScaleY="{Binding ElementName=RichTextBox, Path=ScaleY, Mode=TwoWay}"/>
            </local:CustomRichTextBox.LayoutTransform>
            <FlowDocument>
                <Paragraph>
                    <Run>I am some sample text withhh typooos</Run>
                </Paragraph>
                <Paragraph>
                    <Run FontStyle="Italic">I am some more sample text in italic</Run>
                </Paragraph>
            </FlowDocument>
        </local:CustomRichTextBox>
        <Label Content="Better/Fixed RichTextBox" Grid.Row="2"/>
        <local:FixedCustomRichTextBox x:Name="FixedRichTextBox" 
                                 Grid.Row="3" 
                                 SpellCheck.IsEnabled="True"
                                 ScaleX="{Binding ElementName=FontZoomSlider, Path=Value}" 
                                 ScaleY="{Binding ElementName=FontZoomSlider, Path=Value}"
                                 AcceptsTab="True">
            <local:FixedCustomRichTextBox.LayoutTransform>
                <ScaleTransform ScaleX="{Binding ElementName=FixedRichTextBox, Path=ScaleX, Mode=TwoWay}" 
                                ScaleY="{Binding ElementName=FixedRichTextBox, Path=ScaleY, Mode=TwoWay}"/>
            </local:FixedCustomRichTextBox.LayoutTransform>
            <FlowDocument>
                <Paragraph>
                    <Run>I am some sample text withhh typooos</Run>
                </Paragraph>
                <Paragraph>
                    <Run FontStyle="Italic">I am some more sample text in italic</Run>
                </Paragraph>
            </FlowDocument>
        </local:FixedCustomRichTextBox>
    </Grid>
</Window>

FixedCustomRichTextBox.cs

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Threading;

namespace BrokenRichTextBox
{
    class FixedCustomRichTextBox : RichTextBox
    {
        private bool _didAddLayoutUpdatedEvent = false;

        public FixedCustomRichTextBox() : base()
        {
            UpdateAdorner();
            if (!_didAddLayoutUpdatedEvent)
            {
                _didAddLayoutUpdatedEvent = true;
                LayoutUpdated += updateAdorner;
            }
        }

        public void UpdateAdorner()
        {
            updateAdorner(null, null);
        }

        // Fixing missing caret bug code adjusted from: http://stackoverflow.com/questions/5180585/viewbox-makes-richtextbox-lose-its-caret
        private void updateAdorner(object sender, EventArgs e)
        {
            Dispatcher.BeginInvoke(new Action(() =>
            {
                Selection.GetType().GetMethod("System.Windows.Documents.ITextSelection.UpdateCaretAndHighlight", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(
                    Selection, null);
                var caretElement = Selection.GetType().GetProperty("CaretElement", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Selection, null);
                if (caretElement == null)
                    return;
                var caretSubElement = caretElement.GetType().GetField("_caretElement", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(caretElement) as UIElement;
                if (caretSubElement == null) return;
                // Scale slightly differently if in italic just so it looks a little bit nicer
                bool isItalic = (bool)caretElement.GetType().GetField("_italic", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(caretElement);
                double scaleX = 1;
                if (!isItalic)
                    scaleX = (1 / ScaleX);
                else
                    scaleX = 0.685;// output;
                double scaleY = 1;
                var scaleTransform = new ScaleTransform(scaleX, scaleY);
                caretSubElement.RenderTransform = scaleTransform; // The line of trouble
                updateSpellingErrors(CaretPosition);
            }), DispatcherPriority.ContextIdle);
        }

        private void checkSpelling(TextPointer pointer, string currentWord)
        {
            if (pointer != null)
            {
                string otherText = WordBreaker.GetWordRange(pointer).Text;
                if (currentWord != otherText || currentWord == "" || otherText == "")
                {
                    GetSpellingError(pointer);
                }
            }
        }

        private void checkSpelling(Paragraph paragraph, string currentWord)
        {
            if (paragraph != null)
            {
                checkSpelling(paragraph.ContentStart.GetPositionAtOffset(3, LogicalDirection.Forward), currentWord);
                checkSpelling(paragraph.ContentEnd.GetPositionAtOffset(-3, LogicalDirection.Backward), currentWord);
            }
        }

        private void updateSpellingErrors(TextPointer position)
        {
            string currentWord = GetCurrentWord();

            // Update first and last words of previous and next paragraphs
            var previousParagraph = position.Paragraph?.PreviousBlock as Paragraph;
            checkSpelling(previousParagraph, currentWord);
            var nextParagraph = position.Paragraph?.NextBlock as Paragraph;
            checkSpelling(nextParagraph, currentWord);

            // Update surrounding words next to current caret
            checkSpelling(position.GetPositionAtOffset(-3), currentWord);
            checkSpelling(position.GetPositionAtOffset(3), currentWord);
        }

        // Modified from: http://stackoverflow.com/a/26689916/3938401
        private string GetCurrentWord()
        {
            TextPointer start = CaretPosition;  // this is the variable we will advance to the left until a non-letter character is found
            TextPointer end = CaretPosition;    // this is the variable we will advance to the right until a non-letter character is found
            string stringBeforeCaret = start.GetTextInRun(LogicalDirection.Backward);   // extract the text in the current run from the caret to the left
            string stringAfterCaret = start.GetTextInRun(LogicalDirection.Forward);     // extract the text in the current run from the caret to the left
            int countToMoveLeft = 0;  // we record how many positions we move to the left until a non-letter character is found
            int countToMoveRight = 0; // we record how many positions we move to the right until a non-letter character is found
            for (int i = stringBeforeCaret.Length - 1; i >= 0; --i)
            {
                // if the character at the location CaretPosition-LeftOffset is a letter, we move more to the left
                if (!char.IsWhiteSpace(stringBeforeCaret[i]))
                    ++countToMoveLeft;
                else break; // otherwise we have found the beginning of the word
            }
            for (int i = 0; i < stringAfterCaret.Length; ++i)
            {
                // if the character at the location CaretPosition+RightOffset is a letter, we move more to the right
                if (!char.IsWhiteSpace(stringAfterCaret[i]))
                    ++countToMoveRight;
                else break; // otherwise we have found the end of the word
            }
            start = start.GetPositionAtOffset(-countToMoveLeft);    // modify the start pointer by the offset we have calculated
            end = end.GetPositionAtOffset(countToMoveRight);        // modify the end pointer by the offset we have calculated
            // extract the text between those two pointers
            TextRange r = new TextRange(start, end);
            string text = r.Text;
            // check the result
            return text;
        }

        public double ScaleX
        {
            get { return (double)GetValue(ScaleXProperty); }
            set { SetValue(ScaleXProperty, value); }
        }
        public static readonly DependencyProperty ScaleXProperty =
            DependencyProperty.Register("ScaleX", typeof(double), typeof(FixedCustomRichTextBox), new UIPropertyMetadata(1.0));

        public double ScaleY
        {
            get { return (double)GetValue(ScaleYProperty); }
            set { SetValue(ScaleYProperty, value); }
        }
        public static readonly DependencyProperty ScaleYProperty =
            DependencyProperty.Register("ScaleY", typeof(double), typeof(FixedCustomRichTextBox), new UIPropertyMetadata(1.0));

    }
}

WordBreaker.cs (来自MSDN):

using System.Windows.Documents;

namespace BrokenRichTextBox
{
    // https://blogs.msdn.microsoft.com/prajakta/2006/11/01/navigate-words-in-richtextbox/
    public static class WordBreaker
    {
        /// <summary>
        /// Returns a TextRange covering a word containing or following this TextPointer.
        /// </summary>
        /// <remarks>
        /// If this TextPointer is within a word or at start of word, the containing word range is returned.
        /// If this TextPointer is between two words, the following word range is returned.
        /// If this TextPointer is at trailing word boundary, the following word range is returned.
        /// </remarks>
        public static TextRange GetWordRange(TextPointer position)
        {
            TextRange wordRange = null;
            TextPointer wordStartPosition = null;
            TextPointer wordEndPosition = null;
            // Go forward first, to find word end position.
            wordEndPosition = GetPositionAtWordBoundary(position, /*wordBreakDirection*/LogicalDirection.Forward);
            if (wordEndPosition != null)
            {
                // Then travel backwards, to find word start position.
                wordStartPosition = GetPositionAtWordBoundary(wordEndPosition, /*wordBreakDirection*/LogicalDirection.Backward);
            }
            if (wordStartPosition != null && wordEndPosition != null)
            {
                wordRange = new TextRange(wordStartPosition, wordEndPosition);
            }
            return wordRange;
        }

        /// <summary>
        /// 1.  When wordBreakDirection = Forward, returns a position at the end of the word,
        ///     i.e. a position with a wordBreak character (space) following it.
        /// 2.  When wordBreakDirection = Backward, returns a position at the start of the word,
        ///     i.e. a position with a wordBreak character (space) preceeding it.
        /// 3.  Returns null when there is no workbreak in the requested direction.
        /// </summary>
        private static TextPointer GetPositionAtWordBoundary(TextPointer position, LogicalDirection wordBreakDirection)
        {
            if (!position.IsAtInsertionPosition)
            {
                position = position.GetInsertionPosition(wordBreakDirection);
            }
            TextPointer navigator = position;
            while (navigator != null && !IsPositionNextToWordBreak(navigator, wordBreakDirection))
            {
                navigator = navigator.GetNextInsertionPosition(wordBreakDirection);
            }
            return navigator;
        }
        // Helper for GetPositionAtWordBoundary.
        // Returns true when passed TextPointer is next to a wordBreak in requested direction.
        private static bool IsPositionNextToWordBreak(TextPointer position, LogicalDirection wordBreakDirection)
        {
            bool isAtWordBoundary = false;
            // Skip over any formatting.
            if (position.GetPointerContext(wordBreakDirection) != TextPointerContext.Text)
            {
                position = position.GetInsertionPosition(wordBreakDirection);
            }
            if (position.GetPointerContext(wordBreakDirection) == TextPointerContext.Text)
            {
                LogicalDirection oppositeDirection = (wordBreakDirection == LogicalDirection.Forward) ?
                    LogicalDirection.Backward : LogicalDirection.Forward;
                char[] runBuffer = new char[1];
                char[] oppositeRunBuffer = new char[1];
                position.GetTextInRun(wordBreakDirection, runBuffer, /*startIndex*/0, /*count*/1);
                position.GetTextInRun(oppositeDirection, oppositeRunBuffer, /*startIndex*/0, /*count*/1);
                if (runBuffer[0] == ' ' && !(oppositeRunBuffer[0] == ' '))
                {
                    isAtWordBoundary = true;
                }
            }
            else
            {
                // If we’re not adjacent to text then we always want to consider this position a “word break”. 
                // In practice, we’re most likely next to an embedded object or a block boundary.
                isAtWordBoundary = true;
            }
            return isAtWordBoundary;
        }
    }
}

CustomRichTextBox.cs保持不变。