我有一个TextBlock,其中动态添加了Inlines(基本上是一堆Run对象,它们可以是斜体或加粗)。
在我的应用程序中,我有搜索功能。
我想能够突出显示正在搜索的TextBlock文本。
通过突出显示,我的意思是更改TextBlock文本的某些部分的颜色(请记住,它可能同时突出显示几个不同的Run对象)。
但它似乎非常不稳定:(
有没有简单的方法来解决这个问题?
我有一个TextBlock,其中动态添加了Inlines(基本上是一堆Run对象,它们可以是斜体或加粗)。
在我的应用程序中,我有搜索功能。
我想能够突出显示正在搜索的TextBlock文本。
通过突出显示,我的意思是更改TextBlock文本的某些部分的颜色(请记住,它可能同时突出显示几个不同的Run对象)。
但它似乎非常不稳定:(
有没有简单的方法来解决这个问题?
这个问题与如何在WPF项控件中显示带有高亮查询词的搜索结果类似。
回答该问题时,我提出了一种使用IValueConverter的方法。转换器将文本片段格式化为有效的XAML标记,并使用XamlReader将标记实例化为框架对象。
完整的解释比较长,所以我把它发布到了我的博客上:在WPF TextBlock中突出显示查询词
我采用了dthrasers的答案并去掉了XML解析器的需求。他在博客中非常好地解释了每个部分,但这并不需要我添加任何额外的库,以下是我的实现方法。
第一步,创建一个转换器类:
class StringToXamlConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string input = value as string;
if (input != null)
{
var textBlock = new TextBlock();
textBlock.TextWrapping = TextWrapping.Wrap;
string escapedXml = SecurityElement.Escape(input);
while (escapedXml.IndexOf("|~S~|") != -1) {
//up to |~S~| is normal
textBlock.Inlines.Add(new Run(escapedXml.Substring(0, escapedXml.IndexOf("|~S~|"))));
//between |~S~| and |~E~| is highlighted
textBlock.Inlines.Add(new Run(escapedXml.Substring(escapedXml.IndexOf("|~S~|") + 5,
escapedXml.IndexOf("|~E~|") - (escapedXml.IndexOf("|~S~|") + 5)))
{ FontWeight = FontWeights.Bold, Background= Brushes.Yellow });
//the rest of the string (after the |~E~|)
escapedXml = escapedXml.Substring(escapedXml.IndexOf("|~E~|") + 5);
}
if (escapedXml.Length > 0)
{
textBlock.Inlines.Add(new Run(escapedXml));
}
return textBlock;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException("This converter cannot be used in two-way binding.");
}
}
第二步: 使用ContentBlock而不是TextBlock。将字符串(您用于textBlock的字符串)传递给content block,如下所示:
步骤二: 使用ContentBlock替代TextBlock。将串(您用于textBlock的字符串)传入content block中,就像这样:
<ContentControl Margin="7,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{Binding Description, Converter={StaticResource CONVERTERS_StringToXaml}, Mode=OneTime}">
</ContentControl>
步骤三:
确保您传递的文本在您想要突出显示的文本部分之前包括|~S~|
,并在之后包括|~E~|
。例如,在此字符串中"my text |~S~|is|~E~| good"
中,会将is
突出显示为黄色。
注意事项:
您可以更改运行时的样式以确定您的文本被突出显示的方式和效果。
确保将您的转换器类添加到您的命名空间和资源中。这可能需要重新构建才能使其正常工作。
适用于.NET Framework 4.7-4.8。可能需要进行调整才能在新的.NET版本(如.NET 6)中使用,请参见注释。
Text="blabla"
属性。相反,将您的文本绑定到HighlightTermBehavior.Text="blabla"
。<TextBlock local:HighlightTermBehavior.TermToBeHighlighted="{Binding MyTerm}"
local:HighlightTermBehavior.Text="{Binding MyText}" />
或硬编码
<TextBlock local:HighlightTermBehavior.TermToBeHighlighted="highlight this"
local:HighlightTermBehavior.Text="bla highlight this bla" />
AddPartToTextBlock()
用于非高亮文本AddHighlightedPartToTextBlock()
用于高亮文本。FontWeights.ExtraBold
,非高亮文本为 FontWeights.Light
。public static class HighlightTermBehavior
{
public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
"Text",
typeof(string),
typeof(HighlightTermBehavior),
new FrameworkPropertyMetadata("", OnTextChanged));
public static string GetText(FrameworkElement frameworkElement) => (string) frameworkElement.GetValue(TextProperty);
public static void SetText(FrameworkElement frameworkElement, string value) => frameworkElement.SetValue(TextProperty, value);
public static readonly DependencyProperty TermToBeHighlightedProperty = DependencyProperty.RegisterAttached(
"TermToBeHighlighted",
typeof(string),
typeof(HighlightTermBehavior),
new FrameworkPropertyMetadata("", OnTextChanged));
public static string GetTermToBeHighlighted(FrameworkElement frameworkElement)
{
return (string) frameworkElement.GetValue(TermToBeHighlightedProperty);
}
public static void SetTermToBeHighlighted(FrameworkElement frameworkElement, string value)
{
frameworkElement.SetValue(TermToBeHighlightedProperty, value);
}
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBlock textBlock)
SetTextBlockTextAndHighlightTerm(textBlock, GetText(textBlock), GetTermToBeHighlighted(textBlock));
}
private static void SetTextBlockTextAndHighlightTerm(TextBlock textBlock, string text, string termToBeHighlighted)
{
textBlock.Text = string.Empty;
if (TextIsEmpty(text))
return;
if (TextIsNotContainingTermToBeHighlighted(text, termToBeHighlighted))
{
AddPartToTextBlock(textBlock, text);
return;
}
var textParts = SplitTextIntoTermAndNotTermParts(text, termToBeHighlighted);
foreach (var textPart in textParts)
AddPartToTextBlockAndHighlightIfNecessary(textBlock, termToBeHighlighted, textPart);
}
private static bool TextIsEmpty(string text)
{
return text.Length == 0;
}
private static bool TextIsNotContainingTermToBeHighlighted(string text, string termToBeHighlighted)
{
return text.Contains(termToBeHighlighted, StringComparison.Ordinal) == false;
}
private static void AddPartToTextBlockAndHighlightIfNecessary(TextBlock textBlock, string termToBeHighlighted, string textPart)
{
if (textPart == termToBeHighlighted)
AddHighlightedPartToTextBlock(textBlock, textPart);
else
AddPartToTextBlock(textBlock, textPart);
}
private static void AddPartToTextBlock(TextBlock textBlock, string part)
{
textBlock.Inlines.Add(new Run {Text = part, FontWeight = FontWeights.Light});
}
private static void AddHighlightedPartToTextBlock(TextBlock textBlock, string part)
{
textBlock.Inlines.Add(new Run {Text = part, FontWeight = FontWeights.ExtraBold});
}
public static List<string> SplitTextIntoTermAndNotTermParts(string text, string term)
{
if (text.IsNullOrEmpty())
return new List<string>() {string.Empty};
return Regex.Split(text, $@"({Regex.Escape(term)})")
.Where(p => p != string.Empty)
.ToList();
}
}
恰巧的是,我最近写了一篇文章解决了同样的问题。这是一个自定义控件,具有与相同的属性(因此您可以在需要时将其替换为),并且它有一个额外的属性,您可以绑定到称为的属性上,在主
创建这个控件相当简单,您可以在这里找到完整的代码解决方案:
public class SearchHightlightTextBlock : TextBlock
{
public SearchHightlightTextBlock() : base() { }
public String SearchText { get { return (String)GetValue(SearchTextProperty); }
set { SetValue(SearchTextProperty, value); } }
private static void OnDataChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
TextBlock tb = (TextBlock)source;
if (tb.Text.Length == 0)
return;
string textUpper = tb.Text.ToUpper();
String toFind = ((String) e.NewValue).ToUpper();
int firstIndex = textUpper.IndexOf(toFind);
String firstStr = tb.Text.Substring(0, firstIndex);
String foundStr = tb.Text.Substring(firstIndex, toFind.Length);
String endStr = tb.Text.Substring(firstIndex + toFind.Length,
tb.Text.Length - (firstIndex + toFind.Length));
tb.Inlines.Clear();
var run = new Run();
run.Text = firstStr;
tb.Inlines.Add(run);
run = new Run();
run.Background = Brushes.Yellow;
run.Text = foundStr;
tb.Inlines.Add(run);
run = new Run();
run.Text = endStr;
tb.Inlines.Add(run);
}
public static readonly DependencyProperty SearchTextProperty =
DependencyProperty.Register("SearchText",
typeof(String),
typeof(SearchHightlightTextBlock),
new FrameworkPropertyMetadata(null, OnDataChanged));
}
在您看来,这个:
<view:SearchHightlightTextBlock SearchText="{Binding TextPropertyContainingTextToSearch}"
Text="{Binding YourTextProperty}"/>
在这里,我提供另一种突出文本的方法。我有一个使用案例,需要在WPF中装饰一堆C#代码,但是我不想使用textBlock.Inlines.Add类型的语法,相反,我想动态生成高亮XAML,然后将其动态添加到WPF中的Canvas或其他容器中。
假设您想对以下代码片段进行着色并突出显示其中一部分:
public static void TestLoop(int count)
{
for(int i=0;i<count;i++)
Console.WriteLine(i);
}
using System.IO;
using System.Text;
public enum HighLightType
{
Type = 0,
Keyword = 1,
CustomTerm = 2
}
public class CodeHighlighter
{
public static string[] KeyWords = { "public", "static", "void", "return", "while", "for", "if" };
public static string[] Types = { "string", "int", "double", "long" };
private string FormatCodeInXaml(string code, bool withLineBreak)
{
string[] mapAr = { "<","<" , //Replace less than sign
">",">" }; //Replace greater than sign
StringBuilder sb = new StringBuilder();
using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(code))))
{
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
line = line.Replace("\t", "    "); //Replace tabs
line = line.Replace(" ", " "); //Replace spaces
for (int i = 0; i < mapAr.Length; i += 2)
line = line.Replace(mapAr[i], mapAr[i + 1]);
if (withLineBreak)
sb.AppendLine(line + "<LineBreak/>"); //Replace line breaks
else
sb.AppendLine(line);
}
}
return sb.ToString();
}
private string BuildForegroundTag(string highlightText, string color)
{
return "<Span Foreground=\"" + color + "\">" + highlightText + "</Span>";
}
private string BuildBackgroundTag(string highlightText, string color)
{
return "<Span Background=\"" + color + "\">" + highlightText + "</Span>";
}
private string HighlightTerm(HighLightType type, string term, string line)
{
if (term == string.Empty)
return line;
string keywordColor = "Blue";
string typeColor = "Blue";
string statementColor = "Yellow";
if (type == HighLightType.Type)
return line.Replace(term, BuildForegroundTag(term, typeColor));
if (type == HighLightType.Keyword)
return line.Replace(term, BuildForegroundTag(term, keywordColor));
if (type == HighLightType.CustomTerm)
return line.Replace(term, BuildBackgroundTag(term, statementColor));
return line;
}
public string ApplyHighlights(string code, string customTerm)
{
code = FormatCodeInXaml(code, true);
customTerm = FormatCodeInXaml(customTerm, false).Trim();
StringBuilder sb = new StringBuilder();
using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(code))))
{
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
line = HighlightTerm(HighLightType.CustomTerm, customTerm, line);
foreach (string keyWord in KeyWords)
line = HighlightTerm(HighLightType.Keyword, keyWord, line);
foreach (string type in Types)
line = HighlightTerm(HighLightType.Type, type, line);
sb.AppendLine(line);
}
}
return sb.ToString();
}
}
步骤2. 在您的MainWindow.xaml中添加一个Canvas XAML标记
<Window x:Class="TestCodeVisualizer.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:TestCodeVisualizer"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Canvas Name="canvas" />
</Window>
步骤三:在您的WPF应用程序中添加以下代码(确保test.txt位于正确的位置):
using System.Text;
using System.IO;
using System.Windows;
using System.Windows.Markup;
namespace TestCodeVisualizer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
string testText = File.ReadAllText("Test.txt");
FrameworkElement fe = GenerateHighlightedTextBlock(testText, "Console.WriteLine");
this.canvas.Children.Add(fe);
}
private FrameworkElement GenerateHighlightedTextBlock(string code, string term)
{
CodeHighlighter ch = new CodeHighlighter();
string uc = "<UserControl xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>[CONTENT]</UserControl>";
string content = "<TextBlock>" + ch.ApplyHighlights(code, term) + "</TextBlock>";
uc = uc.Replace("[CONTENT]", content);
FrameworkElement fe = XamlReader.Load(new System.IO.MemoryStream(Encoding.UTF8.GetBytes(uc))) as FrameworkElement;
return fe;
}
}
}
我曾遇到类似的问题——尝试在代表报告的一堆演讲者中实现文本搜索。该报告最初是写成一个字符串的形式,我们利用FlowDocumentViewer内置的ctrl-F进行搜索。它不是很好用,有些奇怪的选项,但足够使用。
如果你只想要类似的功能,可以按照以下步骤操作:
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph FontFamily="Lucida Console" FontSize="12">
<Run Text="{Binding Content, Mode=OneWay}"/>
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
public class HighlightingTextBlock : TextBlock
{
public static readonly DependencyProperty HighlightingProperty =
DependencyProperty.Register("Highlighting", typeof (HighlightingInformation), typeof (HighlightingTextBlock));
public HighlightingInformation Highlighting
{
get { return (HighlightingInformation)GetValue(HighlightingProperty); }
set { SetValue(HighlightingProperty, value); }
}
public HighlightingTextBlock()
{
AddValueChangedCallBackTo(HighlightingProperty, UpdateText);
}
private void AddValueChangedCallBackTo(DependencyProperty property, Action updateAction)
{
var descriptor = DescriptorFor(property);
descriptor.AddValueChanged(this, (src, args) => updateAction());
}
private DependencyPropertyDescriptor DescriptorFor(DependencyProperty property)
{
return DependencyPropertyDescriptor.FromProperty(property, GetType());
}
private void UpdateText()
{
var highlighting = Highlighting;
if (highlighting == null)
return;
highlighting.SetUpdateMethod(UpdateText);
var runs = highlighting.Runs;
Inlines.Clear();
Inlines.AddRange(runs);
}
}
这个类可以绑定到的类型在文本和高亮列表改变时使用update方法来更新Runs列表。高亮本身看起来像这样:
public class Highlight
{
private readonly int _length;
private readonly Brush _colour;
public int Start { get; private set; }
public Highlight(int start, int length,Brush colour)
{
Start = start;
_length = length;
_colour = colour;
}
private string TextFrom(string currentText)
{
return currentText.Substring(Start, _length);
}
public Run RunFrom(string currentText)
{
return new Run(TextFrom(currentText)){Background = _colour};
}
}
我没有在这里使用INotifyPropertyChanged或CollectionChanged,因为我们不需要将更改多播(例如,一个演示者具有多个视图)。最初,我尝试通过为文本添加一个事件更改通知和一个列表的事件更改通知来实现这一点(您还必须手动订阅INotifyCollectionChanged事件)。但是,我担心事件订阅会导致内存泄漏,并且文本和突出显示的更新不同时会导致问题。
这种方法的一个缺点是人们不应该绑定到此控件的Text属性。在真正的版本中,我添加了一些检查+异常抛出来阻止人们这样做,但是出于清晰起见,我从示例中省略了它!
我所需求的是,高亮必须能够完全可样式化,而不仅仅是一些预定义选项:
public partial class HighlightTextBlock : UserControl
{
public HighlightTextBlock()
{
InitializeComponent();
}
public static readonly DependencyProperty TextBlockStyleProperty = DependencyProperty.Register(
nameof(TextBlockStyle), typeof(Style), typeof(HighlightTextBlock), new PropertyMetadata(default(Style)));
public Style TextBlockStyle
{
get { return (Style)GetValue(TextBlockStyleProperty); }
set { SetValue(TextBlockStyleProperty, value); }
}
public static readonly DependencyProperty HighlightTextElementStyleProperty = DependencyProperty.Register(
nameof(HighlightTextElementStyle), typeof(Style), typeof(HighlightTextBlock), new PropertyMetadata(default(Style)));
public Style HighlightTextElementStyle
{
get { return (Style)GetValue(HighlightTextElementStyleProperty); }
set { SetValue(HighlightTextElementStyleProperty, value); }
}
public static readonly DependencyProperty NormalTextElementStyleProperty = DependencyProperty.Register(
nameof(NormalTextElementStyle), typeof(Style), typeof(HighlightTextBlock), new PropertyMetadata(default(Style)));
public Style NormalTextElementStyle
{
get { return (Style)GetValue(NormalTextElementStyleProperty); }
set { SetValue(NormalTextElementStyleProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
nameof(Text), typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(default(string), PropertyChangedCallback));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty HighlightProperty = DependencyProperty.Register(
nameof(Highlight), typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(default(string), PropertyChangedCallback));
public string Highlight
{
get { return (string)GetValue(HighlightProperty); }
set { SetValue(HighlightProperty, value); }
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.EnsureType<HighlightTextBlock>().update();
}
private void update()
{
var highlightLength = this.Highlight?.Length ?? 0;
if (highlightLength > 0)
{
var highlightOffset = this.Text?.IndexOf(this.Highlight, StringComparison.InvariantCultureIgnoreCase) ?? -1;
if (highlightOffset > -1)
{
PrefixRun.Text = this.Text.Substring(0, highlightOffset);
HighlightRun.Text = this.Text.Substring(highlightOffset, highlightLength);
SuffixRun.Text = this.Text.Substring(highlightOffset + highlightLength);
return;
}
}
PrefixRun.Text = this.Text;
HighlightRun.Text = null;
SuffixRun.Text = null;
}
}
使用PropertyChangedCallback
,它被HighlightProperty
和TextProperty
调用。
XAML:
<UserControl x:Class="Example.HighlightTextBlock"
x:Name="self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008">
<Grid>
<TextBlock DataContext="{Binding ElementName=self}" Style="{Binding TextBlockStyle}">
<!-- NOTE: TO avoid whitespaces when rendering Inlines, avoid them in markup (e.g. between Run tags)-->
<TextBlock.Inlines><Run
x:Name="PrefixRun" x:FieldModifier="private" Style="{Binding NormalTextElementStyle}"/><Run
x:Name="HighlightRun" x:FieldModifier="private" Style="{Binding HighlightTextElementStyle}"/><Run
x:Name="SuffixRun" x:FieldModifier="private" Style="{Binding NormalTextElementStyle}"/></TextBlock.Inlines>
</TextBlock>
</Grid>
</UserControl>
数据模板:
<DataTemplate x:Key="ExampleDataTemplate">
<DataTemplate.Resources>
<Style x:Key="HighlightTextElementStyle" TargetType="{x:Type Inline}">
<Setter Property="Foreground" Value="DarkGray"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
<Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="TextAlignment" Value="Left"/>
</Style>
</DataTemplate.Resources>
<controls1:HighlightTextBlock Text="{Binding ExampleText}"
Highlight="{Binding ExampleHighlight}"
TextBlockStyle="{StaticResource TextBlockStyle}"
HighlightTextElementStyle="{StaticResource HighlightTextElementStyle}"/>
</DataTemplate>
private TextBlock HighlightSearch(TextBlock textBlock, string searchTerm)
{
string[] words = textBlock.Text.Split(' ');
textBlock.Text = string.Empty;
foreach (string word in words)
{
if (!string.IsNullOrEmpty(searchTerm) &&
word.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0)
{
textBlock.Inlines.Add(new Run($"{word} ") { Foreground = Brushes.Purple, FontWeight = FontWeights.DemiBold });
}
else
{
textBlock.Inlines.Add($"{word} ");
}
}
return textBlock;
}
`
最终编写了以下代码
目前还有一些错误,但解决了问题
if (Main.IsFullTextSearch)
{
for (int i = 0; i < runs.Count; i++)
{
if (runs[i] is Run)
{
Run originalRun = (Run)runs[i];
if (Main.SearchCondition != null && originalRun.Text.ToLower()
.Contains(Main.SearchCondition.ToLower()))
{
int pos = originalRun.Text.ToLower()
.IndexOf(Main.SearchCondition.ToLower());
if (pos > 0)
{
Run preRun = CloneRun(originalRun);
Run postRun = CloneRun(originalRun);
preRun.Text = originalRun.Text.Substring(0, pos);
postRun.Text = originalRun.Text
.Substring(pos + Main.SearchCondition.Length);
runs.Insert(i - 1 < 0 ? 0 : i - 1, preRun);
runs.Insert(i + 1, new Run(" "));
runs.Insert(i + 2, postRun);
originalRun.Text = originalRun.Text
.Substring(pos, Main.SearchCondition.Length);
SolidColorBrush brush = new SolidColorBrush(Colors.Yellow);
originalRun.Background = brush;
i += 3;
}
}
}
}
}
TextIsNotContainingTermToBeHighlighted
和AddPartToTextBlockAndHighlightIfNecessary
以进行不区分大小写的比较。 - Mark Jansen