¿Alguna forma de hacer que un bloque de texto WPF sea seleccionable?

224

Quiero que el texto que se muestra en Witty , un cliente de código abierto de Twitter, sea seleccionable. Actualmente se muestra usando un bloque de texto personalizado. Necesito usar un TextBlock porque estoy trabajando con las líneas del bloque de texto para mostrar 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.

Intenté que funcionara mostrando el texto usando un TextBox de solo lectura diseñado para parecerse a un bloque de texto, pero esto no funcionará en mi caso porque un TextBox no tiene líneas. En otras palabras, no puedo diseñar o formatear el texto dentro de un TextBox individualmente como puedo hacerlo con un TextBlock.

¿Algunas ideas?

Alan Le
fuente
1
Intentaré usar el control RichTextBox para ver si eso funciona. Pero a partir de la experiencia previa, trabajar con richtextbox es mucho más complicado.
Alan Le
¿Has pensado en usar un FlowDocumentScrollViewer, con un FlowDocument que contiene párrafos y ejecuciones? - Esto funciona bastante bien para mí cuando necesito texto seleccionable, y cada párrafo y ejecución se pueden diseñar por separado.
BrainSlugs83
Después de probar algunas de las soluciones a continuación, FlowDocumentScrollViewer fue el camino a seguir. Parece ocupar un punto medio útil entre RichTextBox y TextBlock.
Tom Makin
vote hacia abajo para aceptar una respuesta que no se ajuste a sus requisitos.
Blechdose el

Respuestas:

218
<TextBox Background="Transparent"
         BorderThickness="0"
         Text="{Binding Text, Mode=OneWay}"
         IsReadOnly="True"
         TextWrapping="Wrap" />
MSB
fuente
66
Tengo un proyecto que contiene muchos TextBlocks / Labels, realmente no puedo convertirlos en TextBoxes. Lo que sí quiero hacer es agregar un estilo mágico que se aplique a todos al recurso de nivel de aplicación para que afecte a todos los Label / TextBlock, y hacer que su presentador de texto interno sea un TextBox de solo lectura, ¿sabe de alguna manera? ¿para hacerlo?
Shimmy Weitzhandler
55
Es posible que desee agregar IsTabStop = "False" dependiendo de su situación
Karsten
1
+1 ¡Muy buena solución! Agregué un Padding = "0", ya que en mi proyecto la parte inferior del texto estaba cortada de ... Tal vez debido a un estilo en otro lugar.
REAPAWNed
123
-1 La pregunta específicamente pregunta cómo hacer que un bloque de texto sea seleccionable. Porque no quiere perder la propiedad "Inlines" (que los cuadros de texto no tienen). Esta 'respuesta' solo sugiere hacer que un cuadro de texto se vea como un bloque de texto.
00jt
19
@AlanLe ¿Por qué aceptó esta respuesta cuando es lo que dijo explícitamente que no quería? ¿Y por qué 147 personas despistadas lo votaron?
Jim Balter
66

Todas las respuestas aquí solo usan TextBoxo intentan implementar la selección de texto manualmente, lo que conduce a un bajo rendimiento o un comportamiento no nativo (parpadeo TextBox, no hay soporte de teclado en implementaciones manuales, etc.)

Después de horas de buscar y leer el código fuente de WPF , descubrí una forma de habilitar la selección de texto WPF nativo para los TextBlockcontroles (o realmente cualquier otro control). La mayor parte de la funcionalidad en torno a la selección de texto se implementa en la System.Windows.Documents.TextEditorclase del sistema.

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

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

  2. Crea una instancia de TextEditorpara cada instancia de tu clase y pasa la instancia subyacente de tu System.Windows.Documents.ITextContainera ella

También existe el requisito de que la Focusablepropiedad de su control esté establecida True.

¡Eso es todo! Suena fácil, pero desafortunadamente la TextEditorclase está marcada como interna. Así que tuve que escribir un envoltorio de reflexión a su alrededor:

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 SelectableTextBlockderivado de TextBlockque toma los pasos mencionados 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 TextBlockhabilitar la selección de texto a pedido. En este caso, para deshabilitar la selección nuevamente, uno necesita separar un TextEditorusando el equivalente de reflexión de este código:

_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
torvin
fuente
1
¿Cómo usarías la clase SelectableTextBlock dentro de otro xaml que debería contenerlo?
Yoav Feuerstein
1
de la misma manera que usarías cualquier otro control personalizado. ver stackoverflow.com/a/3768178/332528 por ejemplo
torvin
3
@BillyWilloughby su solución simplemente emula la selección. Carece de muchas funciones de selección nativas: soporte de teclado, menú contextual, etc. Mi solución habilita la función de selección nativa
torvin
3
Parece que esta solución hace el trabajo cuando la TextBlockha incrustado Hyperlinks, siempre que el Hyperlinkno es la última línea en el mismo. Agregar un vacío final Runal contenido corrige el problema subyacente que resulta en que ExecutionEngineExceptionse arroje.
Anton Tykhyy
2
¡Esto es genial! Excepto si tiene TextTrimming="CharacterEllipsis"en el TextBlocky la anchura disponible es insuficiente, si se mueve el puntero del ratón sobre la ..., se estrella con System.ArgumentException "distancia solicitada está fuera del contenido del documento asociado." en System.Windows.Documents.TextPointer.InitializeOffset (posición del puntero de texto, distancia Int32, dirección de la dirección lógica) :( No sé si existe una solución alternativa que no sea dejar TextTrimming en Ninguno.
Dave Huang
32

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

Creo que la forma correcta de hacer esto es extender la clase TextBlock. Este es el código que usé para extender la clase TextBlock para permitirme seleccionar el texto y copiarlo al portapapeles. "sdo" es la referencia de espacio de nombres que utilicé en el WPF.

WPF usando clase extendida:

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>

Código detrás de 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);
        }
    }
}

Código de ventana de ejemplo:

    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);
    }
Billy Willoughby
fuente
1
¡Esta debería ser la respuesta aceptada! Sin trucos de reflexión, sin usar un TextBox ... Y se puede refactorizar fácilmente en un comportamiento reutilizable. ¡Muy bien, gracias!
Thomas Levesque
19

Aplique este estilo a su TextBox y listo (inspirado en este artículo ):

<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>
sakito
fuente
1
Por cierto, a partir de hoy, el enlace al artículo parece estar muerto
superjos
2
Otra adición: el relleno debe ser -2,0, -2,0. Dentro de TextBox, se crea un control TextBoxView que tiene un Margen predeterminado de 2,0,2,0. Desafortunadamente, no puede redefinir su Estilo porque está marcado como interno.
fdub
11
Nadie parece capaz de leer. El OP necesita un TextBlock, no un TextBox con el estilo de un TextBlock.
Jim Balter
18

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

Jobi Joy
fuente
11
¿Cómo se configura el ControlTemplate para un TextBlock? No puedo encontrar la propiedad?
HaxElit
18
Este enfoque no funcionará si su TextBlock tiene elementos en línea dentro de él. ¿Qué sucede si tiene hipervínculos o corridas de texto en negrita o cursiva? TextBox no es compatible con estos.
dthrasher
1
No funciona si está utilizando ejecuciones en línea, y como preguntó HaxElit, no estoy seguro de qué quiere decir con plantilla de control.
Ritch Melton el
77
-1 TextBlock no tiene ControlTemplate porque es una subclase directa de FrameworkElement. TextBox, por otro lado, es una subclase de Control.
REAPAWNed
55
¿Por qué nadie puede leer? El OP dijo explícitamente que se necesita un TextBlock, no un TextBox, porque TextBlock admite el formato en línea y TextBox no. ¿Por qué las respuestas basura completamente incorrectas como esta obtienen numerosos votos a favor?
Jim Balter
10

No estoy seguro de si puede seleccionar un TextBlock, pero otra opción sería usar un RichTextBox; es como un TextBox como sugirió, pero admite el formato que desee.

Bruce
fuente
1
Intenté hacer esto, y en el proceso tuve que hacer que RichTextBox se vincule con una propiedad de dependencia. Desafortunadamente, los viejos documentos de flujo no se descartan correctamente y la memoria se pierde como una locura. Alan, me pregunto si encontraste una forma de evitar esto.
John Noonan el
@AlanLe De todas las respuestas aquí, esta es solo una de las dos que realmente responde a la pregunta que se hace ... todas las demás hablan sobre diseñar un TextBox para que parezca un TextBlock, sin tener en cuenta la necesidad de formatear. Es extraño y desafortunado que el OP haya aceptado una de esas no respuestas, en lugar de la respuesta correcta para usar RichTextBox en lugar de TextBox.
Jim Balter
9

Según el Centro de desarrollo de Windows :

Propiedad TextBlock.IsTextSelectionEnabled

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

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

Jack Pines
fuente
55
Desafortunadamente, no es compatible con Win7 (a veces es un requisito obligatorio)
Yury Schkatula
24
Amswer parece incorrecto. IsTextSelectionEnabled es solo para UWP, no para WPF: la pregunta original especificó WPF.
Puffin
6

Si bien la pregunta dice 'Seleccionable', creo que los resultados intencionales son llevar el texto al portapapeles. Esto se puede lograr de manera fácil y elegante agregando un menú contextual y un elemento de menú llamado copia que coloca el valor de la propiedad Textblock Text en el portapapeles. Solo una idea de todos modos.

SimperT
fuente
4

TextBlock no tiene una plantilla. Entonces, para lograr esto, necesitamos usar un cuadro de texto cuyo estilo se cambie para comportarse como un bloque de texto.

<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>
Saraf Talukder
fuente
¿Qué ventajas ofrece este enfoque en comparación con otras respuestas? No veo ninguno
surfen
Probé este estilo: TextBoxBorder no está definido. Si lo comenta, funciona bien
sthiers
Este código de ejemplo es excelente, muestra cómo obtener el color predeterminado para un TextBlock.
Contango
1
Esto es bastante confuso. Primero, la clave x: "TextBlockUsingTextBoxStyle" está al revés; debería ser "TextBoxUsingTextBlockStyle". En segundo lugar, el OP ya sabía cómo diseñar un TextBox como un TextBlock, pero dijo repetidamente que no podía usar eso porque necesitaba inlines para formatear.
Jim Balter
2

Hay una solución alternativa que podría ser adaptable al RichTextBox incluido en esta publicación de blog : utilizaba un disparador para cambiar la plantilla de control cuando el uso se desplaza sobre el control, debería ayudar con el rendimiento

Ricardo
fuente
1
Tu enlace está muerto. Incluya toda la información relevante dentro de una respuesta y use enlaces solo como citas.
Jim Balter
1

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

Lu55
fuente
1
Esto no es de ayuda. Lea la pregunta para ver qué quería realmente el OP.
Jim Balter
1

Agregando a la respuesta de @ torvin y como @Dave Huang mencionó en los comentarios si ha TextTrimming="CharacterEllipsis"habilitado la aplicación se bloquea cuando pasa el cursor sobre los puntos suspensivos.

Probé otras opciones mencionadas en el hilo sobre el uso de un TextBox, pero realmente no parece ser la solución, ya que no muestra los 'puntos suspensivos' y también si el texto es demasiado largo para caber en el contenedor seleccionando el contenido de el cuadro de texto 'se desplaza' internamente, lo que no es un comportamiento de TextBlock.

Creo que la mejor solución es la respuesta de @ torvin, pero tiene el desagradable accidente cuando se cierne sobre los puntos suspensivos.

Sé que no es bonito, pero suscribir / cancelar la suscripción internamente a excepciones no controladas y manejar la excepción fue la única forma que encontré de resolver este problema, por favor comparta si alguien tiene una mejor solución :)

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;
            }
        }
    }
}
rauland
fuente
0

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

<jc:SelectableTextBlock Text="Some text" />
Robert Važan
fuente
44
Esto solo usa un TextBox, como muchas otras respuestas de muchos años antes.
Chris
0
public MainPage()
{
    this.InitializeComponent();
    ...
    ...
    ...
    //Make Start result text copiable
    TextBlockStatusStart.IsTextSelectionEnabled = true;
}
Angel T
fuente
-1
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;
    }
}
Titwan
fuente
1
Debe explicar lo que ha cambiado de la respuesta a continuación, por favor. -1
Alex Hope O'Connor
La línea 51 da: System.ArgumentNullException: 'El valor no puede ser nulo. Nombre del parámetro: position1 '
lanza