【问题标题】:WPF RichTextBox - Setting Caret.RenderTransform Breaks Spell CheckWPF RichTextBox - 设置 Caret.RenderTransform 会中断拼写检查
【发布时间】:2016-10-19 20:57:53
【问题描述】:

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

问题:如何在用户键入时显示红色波浪形拼写检查行,同时仍允许 RichTextBox 缩放和完全渲染的所有比例级别的插入符号? 我试过手动调用GetSpellingError(TextPointer),这很有效……有点。除非我在RichTextBox 的每个 单词上调用GetSpellingError,否则它并不完全可靠,当有大量内容时,计算速度非常慢。我还尝试对Speller 和相关内部类中的项目使用反射等,例如Highlights、SpellerStatusTable 和SpellerHighlightLayer。在查看SpellerStatusTable 的运行列表(其中似乎包含有关运行是干净还是脏的信息)时,在单击滑块之前不会更新运行以包含错误,这意味着RichTextBox 不会重新检查拼写错误。

在CustomRichTextBox.cs 中注释掉caretSubElement.RenderTransform = scaleTransform; “修复”了问题,但随后又中断了插入符号渲染。

代码--

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="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>

CustomRichTextBox.cs:

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

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

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

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

        // Fixing missing caret bug code adjusted from: https://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
            }), DispatcherPriority.ContextIdle);
        }

        public double ScaleX
        {
            get { return (double)GetValue(ScaleXProperty); }
            set { SetValue(ScaleXProperty, value); }
        }
        public static readonly DependencyProperty ScaleXProperty =
            DependencyProperty.Register("ScaleX", typeof(double), typeof(CustomRichTextBox), 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(CustomRichTextBox), new UIPropertyMetadata(1.0));

    }
}

【问题讨论】:

  • 希望Jon Skeet 看到这个。
  • 这是一个非常典型的 WPF 错误。拼写检查和重新渲染被延迟,当你喜欢它发生时很容易不触发它。每个 .NET 版本都有很多 WPF 错误修复,但仅当您在项目中定位该版本时才会打开它们。所以首先确保你的目标是 4.6.2。如果您仍然看到它,那么让他们在 4.6.3 上工作,这只有在您通过 connect.microsoft.com 告诉他们时才有效
  • @HansPassant 感谢您的建议。我确实尝试了对 4.6.2 的更新,但遗憾的是该错误仍然存​​在。我想在某个地方有一些手动重新渲染调用或“请重新检查我的文本”调用,但我还没有找到一个可行的方法。一位朋友建议我尝试更多地使用手动 GetSpellingError 调用,所以我会仔细研究一下。

标签: c# wpf richtextbox spell-checking


【解决方案1】:

我设法让事情顺利进行,至少从表面上看是这样。 tl;dr 修复方法是手动调用 GetSpellingError 上一个/下一个单词以及上一个/下一个单词的第一个和最后一个单词 Paragraphs (Blocks)。只是检查周围的单词不起作用,因为某些奇怪的原因,如果我在行尾点击“输入/返回”并且该段落的最后一个单词拼写错误,则拼写检查器不会启动。如果上一段的第一个单词在点击“enter/return”后拼写错误,红色的波浪线会消失!在任何情况下,手动检查单词,而不是检查所有的单词,似乎都可以正常工作。

我的个人项目有一些额外的“请检查周围单词的拼写”调用一些OnPreviewKeyDown 实例,以防 UpdateAdorner 未被及时调用,但我将把它作为练习留给读者。 :)

我猜在某个地方会有更好的答案。

代码(可在 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 保持不变。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多