如何允许TextBlock
选择文本?
我试图通过使用样式看起来像文本块的只读文本框显示文本来使其工作,但这在我的情况下不起作用,因为文本框没有内联。换句话说,如何使它可选择?
将 aTextBox
与这些设置一起使用以使其只读并看起来像一个TextBlock
控件。
<TextBox Background="Transparent"
BorderThickness="0"
Text="{Binding Text, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap" />
这里的所有答案都只是使用TextBox
或尝试手动实现文本选择,这会导致性能不佳或非本机行为(在 中闪烁插入符号TextBox
,手动实现中不支持键盘等)
经过数小时的挖掘和阅读WPF 源代码TextBlock
后,我发现了一种为控件(或实际上是任何其他控件)启用本机 WPF 文本选择的方法。围绕文本选择的大部分功能都是在System.Windows.Documents.TextEditor
系统类中实现的。
要为您的控件启用文本选择,您需要做两件事:
调用TextEditor.RegisterCommandHandlers()
一次以注册类事件处理程序
TextEditor
为您的类的每个实例创建一个实例并将您的基础实例传递System.Windows.Documents.ITextContainer
给它
还需要将控件的Focusable
属性设置为True
.
就是这个!听起来很简单,但不幸的是,TextEditor
类被标记为内部。所以我不得不在它周围写一个反射包装器:
class TextEditorWrapper
{
private static readonly Type TextEditorType = Type.GetType("System.Windows.Documents.TextEditor, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
private static readonly PropertyInfo IsReadOnlyProp = TextEditorType.GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly PropertyInfo TextViewProp = TextEditorType.GetProperty("TextView", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly MethodInfo RegisterMethod = TextEditorType.GetMethod("RegisterCommandHandlers",
BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(Type), typeof(bool), typeof(bool), typeof(bool) }, null);
private static readonly Type TextContainerType = Type.GetType("System.Windows.Documents.ITextContainer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");
private static readonly PropertyInfo TextContainerTextViewProp = TextContainerType.GetProperty("TextView");
private static readonly PropertyInfo TextContainerProp = typeof(TextBlock).GetProperty("TextContainer", BindingFlags.Instance | BindingFlags.NonPublic);
public static void RegisterCommandHandlers(Type controlType, bool acceptsRichContent, bool readOnly, bool registerEventListeners)
{
RegisterMethod.Invoke(null, new object[] { controlType, acceptsRichContent, readOnly, registerEventListeners });
}
public static TextEditorWrapper CreateFor(TextBlock tb)
{
var textContainer = TextContainerProp.GetValue(tb);
var editor = new TextEditorWrapper(textContainer, tb, false);
IsReadOnlyProp.SetValue(editor._editor, true);
TextViewProp.SetValue(editor._editor, TextContainerTextViewProp.GetValue(textContainer));
return editor;
}
private readonly object _editor;
public TextEditorWrapper(object textContainer, FrameworkElement uiScope, bool isUndoEnabled)
{
_editor = Activator.CreateInstance(TextEditorType, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, new[] { textContainer, uiScope, isUndoEnabled }, null);
}
}
我还创建了一个SelectableTextBlock
派生的TextBlock
,采用上述步骤:
public class SelectableTextBlock : TextBlock
{
static SelectableTextBlock()
{
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
TextEditorWrapper.RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata((object)null));
}
private readonly TextEditorWrapper _editor;
public SelectableTextBlock()
{
_editor = TextEditorWrapper.CreateFor(this);
}
}
另一种选择是创建一个附加属性,TextBlock
以便按需启用文本选择。在这种情况下,要再次禁用选择,需要TextEditor
使用以下代码的反射等效项来分离 a:
_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
我一直找不到任何真正回答这个问题的例子。所有答案都使用了 Textbox 或 RichTextbox。我需要一个允许我使用 TextBlock 的解决方案,这就是我创建的解决方案。
我相信正确的方法是扩展 TextBlock 类。这是我用来扩展 TextBlock 类以允许我选择文本并将其复制到剪贴板的代码。“sdo”是我在 WPF 中使用的命名空间引用。
WPF 使用扩展类:
xmlns:sdo="clr-namespace:iFaceCaseMain"
<sdo:TextBlockMoo x:Name="txtResults" Background="Black" Margin="5,5,5,5"
Foreground="GreenYellow" FontSize="14" FontFamily="Courier New"></TextBlockMoo>
扩展类的代码隐藏:
public partial class TextBlockMoo : TextBlock
{
TextPointer StartSelectPosition;
TextPointer EndSelectPosition;
public String SelectedText = "";
public delegate void TextSelectedHandler(string SelectedText);
public event TextSelectedHandler TextSelected;
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
Point mouseDownPoint = e.GetPosition(this);
StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Point mouseUpPoint = e.GetPosition(this);
EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true);
TextRange otr = new TextRange(this.ContentStart, this.ContentEnd);
otr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.GreenYellow));
TextRange ntr = new TextRange(StartSelectPosition, EndSelectPosition);
ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.White));
SelectedText = ntr.Text;
if (!(TextSelected == null))
{
TextSelected(SelectedText);
}
}
}
示例窗口代码:
public ucExample(IInstanceHost host, ref String WindowTitle, String ApplicationID, String Parameters)
{
InitializeComponent();
/*Used to add selected text to clipboard*/
this.txtResults.TextSelected += txtResults_TextSelected;
}
void txtResults_TextSelected(string SelectedText)
{
Clipboard.SetText(SelectedText);
}
为 TextBlock 创建 ControlTemplate 并在其中放置一个带有只读属性集的 TextBox。或者只是使用 TextBox 并将其设置为只读,然后您可以更改 TextBox.Style 使其看起来像 TextBlock。
将此样式应用于您的 TextBox 就是这样(灵感来自这篇文章):
<Style x:Key="SelectableTextBlockLikeStyle" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="-2,0,0,0"/>
<!-- The Padding -2,0,0,0 is required because the TextBox
seems to have an inherent "Padding" of about 2 pixels.
Without the Padding property,
the text seems to be 2 pixels to the left
compared to a TextBlock
-->
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="False" />
<Condition Property="IsFocused" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<TextBlock Text="{TemplateBinding Text}"
FontSize="{TemplateBinding FontSize}"
FontStyle="{TemplateBinding FontStyle}"
FontFamily="{TemplateBinding FontFamily}"
FontWeight="{TemplateBinding FontWeight}"
TextWrapping="{TemplateBinding TextWrapping}"
Foreground="{DynamicResource NormalText}"
Padding="0,0,0,0"
/>
</ControlTemplate>
</Setter.Value>
</Setter>
</MultiTrigger>
</Style.Triggers>
</Style>
我不确定您是否可以选择 TextBlock,但另一种选择是使用 RichTextBox - 它就像您建议的 TextBox,但支持您想要的格式。
根据Windows 开发中心:
TextBlock.IsTextSelectionEnabled 属性
[针对 Windows 10 上的 UWP 应用进行了更新。有关 Windows 8.x 文章,请参阅存档]
获取或设置一个值,该值指示是否通过用户操作或调用与选择相关的 API在TextBlock中启用文本选择。
虽然问题确实说“可选择”,但我相信有意的结果是将文本放到剪贴板。这可以通过添加上下文菜单和名为副本的菜单项来轻松优雅地实现,该副本将 Textblock Text 属性值放入剪贴板。反正只是一个想法。
TextBlock 没有模板。所以为了实现这一点,我们需要使用一个TextBox,它的样式被改变为一个textBlock。
<Style x:Key="TextBlockUsingTextBoxStyle" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<TextBox BorderThickness="{TemplateBinding BorderThickness}" IsReadOnly="True" Text="{TemplateBinding Text}" Background="{x:Null}" BorderBrush="{x:Null}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
有一种替代解决方案可能适用于本博客文章中的 RichTextBox - 当用户悬停在控件上时,它使用触发器来换出控件模板 - 应该有助于提高性能
new TextBox
{
Text = text,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
IsReadOnly = true,
Background = Brushes.Transparent,
BorderThickness = new Thickness()
{
Top = 0,
Bottom = 0,
Left = 0,
Right = 0
}
};
我已经在我的开源控件库中实现了SelectableTextBlock 。你可以像这样使用它:
<jc:SelectableTextBlock Text="Some text" />
Really nice and easy solution, exactly what I wanted !
我带来了一些小的修改
public class TextBlockMoo : TextBlock
{
public String SelectedText = "";
public delegate void TextSelectedHandler(string SelectedText);
public event TextSelectedHandler OnTextSelected;
protected void RaiseEvent()
{
if (OnTextSelected != null){OnTextSelected(SelectedText);}
}
TextPointer StartSelectPosition;
TextPointer EndSelectPosition;
Brush _saveForeGroundBrush;
Brush _saveBackGroundBrush;
TextRange _ntr = null;
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (_ntr!=null) {
_ntr.ApplyPropertyValue(TextElement.ForegroundProperty, _saveForeGroundBrush);
_ntr.ApplyPropertyValue(TextElement.BackgroundProperty, _saveBackGroundBrush);
}
Point mouseDownPoint = e.GetPosition(this);
StartSelectPosition = this.GetPositionFromPoint(mouseDownPoint, true);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
Point mouseUpPoint = e.GetPosition(this);
EndSelectPosition = this.GetPositionFromPoint(mouseUpPoint, true);
_ntr = new TextRange(StartSelectPosition, EndSelectPosition);
// keep saved
_saveForeGroundBrush = (Brush)_ntr.GetPropertyValue(TextElement.ForegroundProperty);
_saveBackGroundBrush = (Brush)_ntr.GetPropertyValue(TextElement.BackgroundProperty);
// change style
_ntr.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Colors.Yellow));
_ntr.ApplyPropertyValue(TextElement.ForegroundProperty, new SolidColorBrush(Colors.DarkBlue));
SelectedText = _ntr.Text;
}
}
public MainPage()
{
this.InitializeComponent();
...
...
...
//Make Start result text copiable
TextBlockStatusStart.IsTextSelectionEnabled = true;
}
添加到@torvin 的答案和@Dave Huang 在评论中提到的如果您TextTrimming="CharacterEllipsis"
启用了应用程序在您将鼠标悬停在省略号上时崩溃。
我尝试了线程中提到的有关使用 TextBox 的其他选项,但它确实似乎不是解决方案,因为它没有显示“省略号”,而且如果文本太长而无法容纳选择内容的容器文本框在内部“滚动”,这不是 TextBlock 行为。
我认为最好的解决方案是@torvin 的答案,但是在将鼠标悬停在省略号上时会发生严重的崩溃。
我知道这并不漂亮,但是在内部订阅/取消订阅未处理的异常并处理异常是我发现解决此问题的唯一方法,如果有人有更好的解决方案,请分享:)
public class SelectableTextBlock : TextBlock
{
static SelectableTextBlock()
{
FocusableProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata(true));
TextEditorWrapper.RegisterCommandHandlers(typeof(SelectableTextBlock), true, true, true);
// remove the focus rectangle around the control
FocusVisualStyleProperty.OverrideMetadata(typeof(SelectableTextBlock), new FrameworkPropertyMetadata((object)null));
}
private readonly TextEditorWrapper _editor;
public SelectableTextBlock()
{
_editor = TextEditorWrapper.CreateFor(this);
this.Loaded += (sender, args) => {
this.Dispatcher.UnhandledException -= Dispatcher_UnhandledException;
this.Dispatcher.UnhandledException += Dispatcher_UnhandledException;
};
this.Unloaded += (sender, args) => {
this.Dispatcher.UnhandledException -= Dispatcher_UnhandledException;
};
}
private void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
if (!string.IsNullOrEmpty(e?.Exception?.StackTrace))
{
if (e.Exception.StackTrace.Contains("System.Windows.Controls.TextBlock.GetTextPositionFromDistance"))
{
e.Handled = true;
}
}
}
}
只需使用 a FlowDocument
inside a FlowDocumentScrollViewer
,将您的内联传递给元素。您可以控制元素的样式,在我的例子中,我添加了一个小边框。
<FlowDocumentScrollViewer Grid.Row="2" Margin="5,3" BorderThickness="1"
BorderBrush="{DynamicResource Element.Border}"
VerticalScrollBarVisibility="Auto">
<FlowDocument>
<Paragraph>
<Bold>Some bold text in the paragraph.</Bold>
Some text that is not bold.
</Paragraph>
<List>
<ListItem>
<Paragraph>ListItem 1</Paragraph>
</ListItem>
<ListItem>
<Paragraph>ListItem 2</Paragraph>
</ListItem>
<ListItem>
<Paragraph>ListItem 3</Paragraph>
</ListItem>
</List>
</FlowDocument>
</FlowDocumentScrollViewer>
这对我有用。我创建了一个派生自 TextBox 并设置为只读的类 TextBlockEx,并在构造函数中自动换行。
public class TextBlockEx : TextBox
{
public TextBlockEx()
{
base.BorderThickness = new Thickness(0);
IsReadOnly = true;
TextWrapping = TextWrapping.Wrap;
//Background = Brushes.Transparent; // Uncomment to get parent's background color
}
}
我同意这里的大多数答案都不会创建可选择的 Text Block。@Billy Willoughby 运行良好,但没有明显的选择提示。我想扩展他的扩展,它可以在选中文本时突出显示文本。它还包含双击和三次单击选择。如果需要,您可以添加带有“复制”的上下文菜单。它使用该Background
属性来“突出显示”选择,因此它受到限制,因为它会覆盖Run.Background