2016-10-19 4 views
0

У меня есть C# WPF RichTextBox, которая позволяет ScaleX и ScaleYLayoutTransform корректировки через Slider. К сожалению, это масштабирование может привести к остановке каретки, что может быть исправлено по коду at this SO post here. К сожалению, установка в карете заставляет заклинание проверять красные криволинейные линии, чтобы перестать отображаться по мере ввода. Кажется, что сфокусировавшись на RichTextBox и фокусируя его снова, нажав на Slider, все вновь появятся красные squiggly линии. Вы можете просмотреть демо-версию этой ошибки на моем GitHub here.WPF RichTextBox - Установка Caret.RenderTransform Перерывы Проверка орфографии

GIF of bug

Вопрос: Как я могу вызвать красную волнистую проверять орфографию линии, чтобы показать, как тип пользователей в то же время позволяя RichTextBox масштабирования и полностью оказанного-на-все-Scale уровни каретки? Я пробовал вручную звонить GetSpellingError(TextPointer), и это работает ... вроде. Он не является полностью надежным, если я не позвоню GetSpellingError по телефону каждый слово RichTextBox, которое очень медленно вычисляется при наличии большого количества контента. Я также попытался использовать отражение и т. Д. В пунктах Speller и связанных внутренних классах, таких как Highlights, SpellerStatusTable и SpellerHighlightLayer. При просмотре списка запусков SpellerStatusTable (который, как представляется, содержит информацию о том, являются ли прогоны чистыми или грязными), прогоны не обновляются, чтобы содержать ошибки до тех пор, пока не будет нажат ползунок, а это означает, что RichTextBox не перепроверяет орфографические ошибки ,

Замечание caretSubElement.RenderTransform = scaleTransform; в CustomRichTextBox.cs «исправляет» проблему, но затем снова разрывает визуализацию каретки.

код -

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)); 

    } 
} 
+0

Будем надеяться, что [Jon Skeet] (https://stackoverflow.com/users/22656/jon-skeet) видит это. – cnsumner

+0

Это довольно типичная ошибка WPF. Проверка орфографии и повторная рендеринг задерживаются, и достаточно легко не получить триггер, когда вам это нравится. В каждой версии .NET есть много исправлений ошибок WPF, но они включаются только тогда, когда вы настраиваете таргетинг на этот выпуск в своем проекте. Поэтому сначала убедитесь, что вы нацелились на 4.6.2. Если вы все еще видите это, то пусть они работают на 4.6.3, что работает только, когда вы расскажете им об этом через connect.microsoft.com. –

+0

@HansPassant Спасибо за ваше предложение. Я попробовал обновление до 4.6.2, и ошибка все еще присутствует печально. Я бы предположил, что где-то есть какой-то ручной вызов повторной обработки или вызов «пожалуйста, повторите проверку моего текста», но я еще не нашел тот, который работает. Друг предположил, что я больше пытаюсь поиграть с ручными вызовами «GetSpellingError», поэтому я больше посмотрю на это. – Deadpikle

ответ

0

мне удалось получить все работало, по крайней мере, по внешнему виду. Исправление tl; dr должно выполнять ручные GetSpellingError вызовы предыдущего/следующего слова, а также первое и последнее слова предыдущего/следующего Paragraphs (Blocks). Просто проверка окружающих слов не сработала, как по какой-то странной причине, если я нажму «enter/return» в конце строки И последнее слово этого абзаца написано неправильно, проверка орфографии не началась. первое слово предыдущего абзаца было написано неправильно после нажатия «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 остается неизменным.

Смежные вопросы