2016-10-19 2 views
0

Ich habe eine C# WPF RichTextBox bekam, die über eine Slider für ScaleX und ScaleYLayoutTransform Anpassungen erlaubt. Leider kann diese Skalierung dazu führen, dass das Caret nicht mehr rendert, ein Fehler, der mit dem Code at this SO post here behoben werden kann. Leider verursacht die Einstellung RenderTransform des Carets, dass die roten Linien während der Eingabe nicht mehr angezeigt werden. Es scheint so, als würde man die RichTextBox nicht fokussieren und durch erneutes Klicken auf Slider wieder fokussieren, so dass alle roten Linien wieder auftauchen. Sie können eine Demo dieses Fehlers auf meinem GitHub here anzeigen.WPF RichTextBox - Einstellung Caret.RenderTransform Breaks Spell Check

GIF of bug

Frage: Wie kann ich die rote Wellenrechtschreibprüfung Linien führen die Benutzertypen zu zeigen, wie während immer noch für RichTextBox Skalierung ermöglicht und eine komplett gerenderte-at-all-Scale-Ebenen caret? Ich habe versucht, manuell GetSpellingError(TextPointer) aufrufen, und das funktioniert ... Art von. Es ist nicht vollständig zuverlässig, es sei denn, ich rufe GetSpellingError auf alle Wort der RichTextBox, die sehr langsam ist zu berechnen, wenn es eine große Menge an Inhalt ist. Ich habe auch versucht, die Reflexion und solche auf Artikel innerhalb der Speller und verwandte interne Klassen, wie Highlights, SpellerStatusTable und SpellerHighlightLayer zu verwenden. Wenn man sich die Laufliste von SpellerStatusTable anschaut (die Informationen darüber zu haben scheint, ob Läufe sauber oder schmutzig sind), werden die Läufe nicht so lange aktualisiert, bis der Schieberegler angeklickt wird, was bedeutet, dass die RichTextBox nicht erneut nach Rechtschreibfehlern sucht .

Kommentieren caretSubElement.RenderTransform = scaleTransform; in CustomRichTextBox.cs "behebt" das Problem, aber bricht dann das Caret-Rendering wieder.

-Code -

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

Hoffen wir, dass [Jon Skeet] (https://stackoverflow.com/users/22656/jon-skeet) sieht dies. – cnsumner

+0

Das ist ein ziemlich typischer WPF-Bug. Die Rechtschreibprüfung und das erneute Rendern ist verzögert und es ist einfach genug, den Auslöser nicht zu bekommen, wenn es Ihnen gefällt. Jede .NET-Version hat viele WPF-Fehlerbehebungen, aber sie sind nur aktiviert, wenn Sie diese Version in Ihrem Projekt als Ziel haben. Stellen Sie zuerst sicher, dass Sie 4.6.2 anvisieren. Wenn Sie es immer noch sehen, dann lassen Sie sie an 4.6.3 arbeiten, das funktioniert nur, wenn Sie ihnen über connect.microsoft.com davon erzählen. –

+0

@HansPassant Vielen Dank für Ihren Vorschlag. Ich habe ein Update auf 4.6.2 versucht, und der Bug ist leider immer noch vorhanden. Ich würde mir vorstellen, dass irgendwo ein manueller Neu-Render-Anruf oder "bitte überprüfe meinen Text" -Ruf, aber ich habe noch keinen gefunden, der funktioniert. Ein Freund hat vorgeschlagen, dass ich versuche, mehr mit manuellen 'GetSpellingError'-Anrufen zu spielen, also werde ich mir das genauer ansehen. – Deadpikle

Antwort

0

ich es geschafft, die Dinge zum Laufen zu bringen, zumindest nach Einsätzen. Der tl; dr-Fix soll manuelle GetSpellingError Aufrufe des vorherigen/nächsten Wortes sowie des ersten und letzten Wortes des vorherigen/nächsten Paragraphs (Blocks) machen. Das Überprüfen der umgebenden Wörter funktionierte nicht, da aus irgendeinem seltsamen Grund, wenn ich am Ende der Zeile 'Enter/Return' drücke UND das letzte Wort dieses Absatzes falsch geschrieben wurde, trat die Rechtschreibprüfung nicht ein das erste Wort des vorherigen Absatzes wurde falsch geschrieben, nachdem 'Enter/Return' gedrückt wurde, würde der rote Squiggle verschwinden! In jedem Fall scheint die Überprüfung der Wörter, aber nicht der Wörter zu überprüfen, OK zu funktionieren.

Mein persönliches Projekt hat einige zusätzliche "Bitte überprüfen Sie die Rechtschreibung auf umliegende Wörter" fordert für einige OnPreviewKeyDown Instanzen für den Fall, dass UpdateAdorner nicht rechtzeitig aufgerufen wurde, aber ich überlasse es als eine Übung für den Leser. :)

Ich vermute, es gibt bessere Antworten irgendwo.

-Code (leicht auf Github betrachtet 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 (Aus 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 bleibt gleich.