我有一个C#WPF RichTextBox
,允许通过ScaleX
进行ScaleY
和LayoutTransform
Slider
调整。不幸的是,这种缩放会导致插入符号停止渲染,这是一个可以根据代码at this SO post here修复的错误。更不幸的是,设置插入符RenderTransform
会导致拼写检查红色波浪线在您键入时停止显示。好像没有聚焦RichTextBox
并通过点击Slider
再次聚焦将导致所有红色波浪线重新出现。 您可以在我的GitHub here 上查看此错误的演示。
问题:如何在用户输入时显示红色波浪形拼写检查线,同时仍然允许RichTextBox
缩放和完全渲染的所有比例级别插入符号? 我尝试过手动调用GetSpellingError(TextPointer)
,这样做有点......它不是完全可靠的,除非我在 GetSpellingError
的每个字上调用RichTextBox
,这在内容很多时计算速度非常慢。我还尝试对Speller
及相关内部类中的项目使用反射等,例如Highlights
,SpellerStatusTable
和SpellerHighlightLayer
。查看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>
答案 0 :(得分:1)
我设法让事情变得有效,至少是出场了。 tl; dr fix是对上一个/下一个单词以及上一个/下一个GetSpellingError
(Paragraphs
)的第一个和最后一个单词进行手动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保持不变。