Есть какой-нибудь способ сделать текстовый блок WPF выбираемым?
Вопрос
Я хочу, чтобы текст отображался в Остроумный, клиент Twitter с открытым исходным кодом, выбираемый.В данный момент он отображается с помощью пользовательского текстового блока.Мне нужно использовать текстовый блок, потому что я работаю со встроенными строками текстового блока для отображения и форматирования @username и ссылок в виде гиперссылок.Частая просьба заключается в том, чтобы иметь возможность копировать и вставлять текст.Чтобы сделать это, мне нужно сделать TextBlock выбираемым.
Я попытался заставить это работать, отображая текст с помощью текстового поля только для чтения, стилизованного под текстовый блок, но в моем случае это не сработает, потому что в текстовом поле нет встроенных строк.Другими словами, я не могу стилизовать или отформатировать текст внутри текстового поля по отдельности, как я могу с текстовым блоком.
Есть какие-нибудь идеи?
Решение
<TextBox Background="Transparent"
BorderThickness="0"
Text="{Binding Text, Mode=OneWay}"
IsReadOnly="True"
TextWrapping="Wrap" />
Другие советы
Все ответы здесь просто основаны на использовании TextBox
или попытка реализовать выделение текста вручную, что приводит к низкой производительности или нестандартному поведению (мигающий курсор в TextBox
, отсутствие поддержки клавиатуры в ручных реализациях и т.д.)
После нескольких часов копания и чтения Исходный код WPF, Вместо этого я обнаружил способ включить собственный выбор текста WPF для TextBlock
элементы управления (или действительно любые другие элементы управления).Большая часть функциональных возможностей, связанных с выделением текста, реализована в 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
используя эквивалент отражения этого кода:
_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
Я не смог найти ни одного примера реального ответа на этот вопрос.Во всех ответах использовалось текстовое поле или RichTextBox.Мне нужно было решение, которое позволяло бы мне использовать текстовый блок, и это решение я создал.
Я считаю, что правильный способ сделать это - расширить класс 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);
}
Примените этот стиль к вашему текстовому полю, и все (вдохновлено эта статья):
<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>
Создайте ControlTemplate для текстового блока и поместите внутрь текстовое поле с установленным свойством только для чтения.Или просто используйте TextBox и сделайте его доступным только для чтения, тогда вы можете изменить текстовое поле.Стиль, чтобы оно выглядело как TextBlock.
Я не уверен, можете ли вы сделать TextBlock выбираемым, но другим вариантом было бы использовать RichTextBox - это похоже на текстовое поле, как вы предложили, но поддерживает желаемое форматирование.
Согласно Центр разработки для Windows:
Текстовый блок.Свойство IsTextSelectionEnabled
[Обновлено для приложений UWP в Windows 10.Для Windows 8.х изделий, см. в Архив ]
Возвращает или задает значение, указывающее, включено ли выделение текста в Текстовый блок, либо с помощью действий пользователя, либо с помощью вызова API, связанного с выбором.
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>
Хотя в вопросе действительно сказано "Выбираемый", я полагаю, что намеренный результат заключается в том, чтобы поместить текст в буфер обмена.Этого можно легко и элегантно достичь, добавив контекстное меню и пункт меню под названием копировать, который помещает значение текстового свойства Textblock в буфер обмена.В любом случае, это просто идея.
Существует альтернативное решение, которое может быть адаптировано к 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
}
};
Я реализовал Выбираемый текстовый блок в моей библиотеке элементов управления с открытым исходным кодом.Вы можете использовать его следующим образом:
<jc:SelectableTextBlock Text="Some text" />
public MainPage()
{
this.InitializeComponent();
...
...
...
//Make Start result text copiable
TextBlockStatusStart.IsTextSelectionEnabled = true;
}
Добавление к ответу @torvin и, как упоминал @Dave Huang в комментариях, если у вас есть TextTrimming="CharacterEllipsis"
включено, приложение вылетает при наведении курсора мыши на многоточие.
Я пробовал другие варианты, упомянутые в теме об использовании текстового поля, но это действительно не похоже на решение, поскольку оно также не показывает "многоточие", а также, если текст слишком длинный, чтобы поместиться в контейнер, выбирающий содержимое текстового поля, "прокручивается" внутри, что не является поведением текстового блока.
Я думаю, что лучшее решение - это ответ @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;
}
}
}
}
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;
}
}