¿Hay alguna manera de hacer que un bloque de texto WPF sea seleccionable?

Quiero hacer que el texto que se muestra en el Witty , un cliente de Twitter de código abierto, pueda seleccionarse. Actualmente se muestra con un bloque de texto personalizado. Necesito usar un TextBlock porque estoy trabajando con las líneas del bloque de texto para visualizar y formatear el @nombre de usuario y los enlaces como hipervínculos. Una solicitud frecuente es poder copiar y pegar el texto. Para hacer eso, necesito hacer que TextBlock sea seleccionable.

Traté de hacerlo funcionar al mostrar el texto usando un TextBox de solo lectura diseñado para que parezca un bloque de texto, pero esto no funcionará en mi caso porque un TextBox no tiene líneas. En otras palabras, no puedo aplicar estilos o formatear el texto dentro de un TextBox individualmente como puedo con un TextBlock.

¿Algunas ideas?

 

Todas las respuestas aquí son simplemente usar un TextBox o tratar de implementar la selección de texto manualmente, lo que conduce a un mal rendimiento o comportamiento no nativo (parpadear en el TextBox de TextBox , sin compatibilidad con el teclado en implementaciones manuales, etc.)

Después de horas buscando y leyendo el código fuente de WPF , en su lugar descubrí una forma de habilitar la selección nativa de texto WPF para los controles TextBlock (o realmente cualquier otro control). La mayor parte de la funcionalidad en torno a la selección de texto se implementa en la clase de sistema System.Windows.Documents.TextEditor .

Para habilitar la selección de texto para su control, debe hacer dos cosas:

  1. Llame a TextEditor.RegisterCommandHandlers() una vez para registrar los controladores de eventos de clase

  2. Cree una instancia de TextEditor para cada instancia de su clase y pase la instancia subyacente de System.Windows.Documents.ITextContainer a ella

También es necesario que la propiedad Focusable su control esté configurada en True .

¡Eso es todo! Suena fácil, pero desafortunadamente la clase TextEditor está marcada como interna. Así que tuve que escribir una envoltura de reflexión al respecto:

 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); } } 

También creé un SelectableTextBlock derivado de TextBlock que sigue los pasos indicados anteriormente:

 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); } } 

Otra opción sería crear una propiedad adjunta para TextBlock para habilitar la selección de texto a pedido. En este caso, para deshabilitar la selección nuevamente, se necesita separar un TextEditor usando el reflection equivalente de este código:

 _editor.TextContainer.TextView = null; _editor.OnDetach(); _editor = null; 

No he podido encontrar ningún ejemplo de realmente responder la pregunta. Todas las respuestas usaron un Textbox o RichTextbox. Necesitaba una solución que me permitiera usar TextBlock, y esta es la solución que creé.

Creo que la forma correcta de hacerlo es ampliar la clase TextBlock. Este es el código que utilicé para extender la clase TextBlock para permitirme seleccionar el texto y copiarlo en el portapapeles. “sdo” es la referencia de espacio de nombres que utilicé en WPF.

WPF usando clase extendida:

 xmlns:sdo="clr-namespace:iFaceCaseMain"  

Código detrás para la clase extendida:

 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); } } } 

Ejemplo de código de ventana:

  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); } 

Cree ControlTemplate para TextBlock y coloque un TextBox dentro con el conjunto de propiedades readonly. O simplemente use TextBox y haga que sea de solo lectura, luego puede cambiar TextBox.Style para que se vea como TextBlock.

Aplica este estilo a tu TextBox y listo (inspirado en este artículo ):

  

No estoy seguro si puede hacer que un TextBlock sea seleccionable, pero otra opción sería usar un RichTextBox, es como un TextBox como sugirió, pero admite el formato que desea.

De acuerdo con Windows Dev Center :

Propiedad TextBlock.IsTextSelectionEnabled

[Actualizado para aplicaciones UWP en Windows 10. Para artículos sobre Windows 8.x, consulte el archivo ]

Obtiene o establece un valor que indica si la selección de texto está habilitada en el Bloque de texto , ya sea a través de la acción del usuario o de la API relacionada con la selección de llamadas.

TextBlock no tiene una plantilla. Por lo tanto, para lograr esto, necesitamos usar un TextBox cuyo estilo se cambie para comportarse como un bloque de texto.

  

Hay una solución alternativa que podría ser adaptable al RichTextBox que se muestra en esta publicación del blog : utilizó un activador para cambiar la plantilla de control cuando el uso pasa el control por encima, lo que debería ayudar a mejorar el rendimiento.

Si bien la pregunta dice ‘Seleccionable’, creo que los resultados intencionales consisten en llevar el texto al portapapeles. Esto se puede lograr fácil y elegantemente agregando un menú de contexto y un elemento de menú llamado copy que pone el valor de la propiedad Textblock Text en el portapapeles. Solo una idea de todos modos.

 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 } };
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 } }; 

Implementé SelectableTextBlock en mi biblioteca de controles de código abierto. Puedes usarlo así:

  
 Really nice and easy solution, exactly what I wanted ! 

Traigo algunas pequeñas modificaciones

 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; } } 
    Intereting Posts