web-dev-qa-db-fra.com

Est-il possible de sélectionner un bloc de texte WPF?

Je veux que le texte affiché dans Witty , un client Twitter open source, puisse être sélectionné. Il est actuellement affiché en utilisant un bloc de texte personnalisé. Je dois utiliser un TextBlock parce que je travaille avec les inlines du bloc de texte pour afficher et formater le @ nom d'utilisateur et les liens en tant que liens hypertexte. Une demande fréquente est de pouvoir copier-coller le texte. Pour ce faire, je dois rendre le TextBlock sélectionnable.

J'ai essayé de le faire fonctionner en affichant le texte à l'aide d'une zone de texte en lecture seule de style ressemblant à un bloc de texte, mais cela ne fonctionnera pas dans mon cas, car une zone de texte ne contient pas de texte en ligne. En d'autres termes, je ne peux pas styler ou mettre en forme le texte dans une zone de texte individuellement, comme je le peux avec un TextBlock.

Des idées?

194
Alan Le
<TextBox Background="Transparent"
         BorderThickness="0"
         Text="{Binding Text, Mode=OneWay}"
         IsReadOnly="True"
         TextWrapping="Wrap" />
197
MSB

Toutes les réponses ici utilisent simplement une variable TextBox ou tentent d'implémenter la sélection de texte manuellement, ce qui entraîne des performances médiocres ou un comportement non natif (clignotement du curseur dans la variable TextBox, absence de prise en charge du clavier dans les implémentations manuelles, etc.).

Après des heures consacrées à la lecture et à la lecture du code source WPF , j’ai plutôt découvert un moyen d’activer la sélection de texte WPF natif pour les contrôles TextBlock (ou réellement n’importe quel autre contrôle). La plupart des fonctionnalités relatives à la sélection de texte sont implémentées dans la classe système System.Windows.Documents.TextEditor

Pour activer la sélection de texte pour votre contrôle, vous devez faire deux choses:

  1. Appelez TextEditor.RegisterCommandHandlers() une fois pour enregistrer les gestionnaires d’événements de classe .__

  2. Créez une instance de TextEditor pour chaque instance de votre classe et transmettez-lui l'instance sous-jacente de votre System.Windows.Documents.ITextContainer

Il faut également que la propriété Focusable de votre contrôle soit définie sur True.

Ça y est ...! Cela semble facile, mais malheureusement, la classe TextEditor est marquée comme interne. J'ai donc dû écrire une feuille de réflexion autour de celle-ci:

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

J'ai également créé une SelectableTextBlock dérivée de TextBlock qui suit les étapes ci-dessus:

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

Une autre option serait de créer une propriété attachée pour TextBlock afin d'activer la sélection de texte à la demande. Dans ce cas, pour désactiver à nouveau la sélection, il est nécessaire de détacher une TextEditor en utilisant l'équivalent de réflexion de ce code:

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

Je suis incapable de trouver un exemple de réponse réelle à la question. Toutes les réponses utilisaient une zone de texte ou une zone de texte. J'avais besoin d'une solution qui me permettait d'utiliser un TextBlock et c'est la solution que j'ai créée. 

Je crois que la bonne façon de faire est d'étendre la classe TextBlock. C'est le code que j'ai utilisé pour étendre la classe TextBlock afin de me permettre de sélectionner le texte et de le copier dans le Presse-papiers. "sdo" est la référence de l'espace de noms que j'ai utilisée dans WPF. 

WPF utilisant la classe étendue:

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>

Code Behind pour Extended Class:

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

Exemple de code de fenêtre:

    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);
    }
27
Billy Willoughby

Créez ControlTemplate pour le TextBlock et insérez une zone de texte à l'intérieur avec la propriété en lecture seule ..... ou utilisez simplement TextBox et rendez-le en lecture seule, vous pouvez alors modifier TextBox.Style pour qu'il ressemble à TextBlock.

19
Jobi Joy

Appliquez ce style à votre TextBox et voilà (inspiré de cet article ):

<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>
19
juanjo.arana

Selon Centre de développement Windows :

TextBlock.IsTextSelectionEnabled propriété

[Mise à jour pour les applications UWP sous Windows 10. Pour les articles sur Windows 8.x, voir le archive ]

Obtient ou définit une valeur qui indique si la sélection de texte est activée dans le TextBlock , via l'action de l'utilisateur ou en appelant API liée à la sélection.

9
Jack Pines

Je ne sais pas si vous pouvez sélectionner TextBlock, mais une autre option consisterait à utiliser un RichTextBox - il ressemble à un TextBox comme vous l'avez suggéré, mais prend en charge le formatage souhaité.

9
Bruce

TextBlock n'a pas de modèle. Donc, pour cela, nous devons utiliser une zone de texte dont le style a été modifié pour se comporter comme un 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>
4
Saraf Talukder

Bien que la question dise «sélectionnable», je pense que le résultat intentionnel est de placer le texte dans le presse-papiers. Ceci peut être facilement et élégamment réalisé en ajoutant un menu contextuel et un élément de menu appelé copie qui place la valeur de la propriété Textblock dans le presse-papiers. Juste une idée quand même.

2
SimperT

Il existe une solution alternative qui pourrait être adaptable à RichTextBox décrite dans cet article blog - elle utilisait un déclencheur pour échanger le modèle de contrôle lorsque l'utilisation survole celui-ci - devrait améliorer les performances

2
Richard

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

J'ai implémenté SelectableTextBlock dans ma bibliothèque de contrôles opensource. Vous pouvez l'utiliser comme ceci:

<jc:SelectableTextBlock Text="Some text" />
1
Robert Važan

En ajoutant à la réponse de @ torvin et en tant que @Dave Huang mentionné dans les commentaires si vous avez activé TextTrimming="CharacterEllipsis", l'application se bloque lorsque vous survolez les points de suspension.

J'ai essayé d'autres options mentionnées dans le fil à propos de l'utilisation d'une zone de texte, mais cela ne semble vraiment pas être la solution non plus, car il ne montre pas les "Ellipsis" et si le texte est trop long pour s'adapter au conteneur, le contenu de la zone de texte "fait défiler" en interne, ce qui n'est pas un comportement TextBlock.

Je pense que la meilleure solution est la réponse de @ torvin, mais a le vilain accident en survolant les Ellipsis.

Je sais que ce n’est pas beau, mais s’abonner/désabonner en interne à des exceptions non gérées et le traitement de l’exception était le seul moyen que j’ai trouvé pour résoudre ce problème, merci de le partager si quelqu'un a une meilleure solution :)

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;
            }
        }
    }
}
0
rauland
public MainPage()
{
    this.InitializeComponent();
    ...
    ...
    ...
    //Make Start result text copiable
    TextBlockStatusStart.IsTextSelectionEnabled = true;
}
0
Angel T