Un moyen de rendre un textblock WPF sélectionnable?

Je veux rendre le texte affiché dans le Witty, un client Twitter open source, sélectionnable. Il est actuellement affiché à l'aide d'un textblock personnalisé. J'ai besoin d'utiliser un TextBlock parce que je travaille avec les inlines du textblock pour afficher et formater le @username et les liens sous forme d'hyperliens. 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 en utilisant une lecture seule TextBox styled pour ressembler à un textblock mais cela ne fonctionnera pas dans mon cas car une zone de texte n'a pas de lignes. En d'autres termes, Je ne peux pas styliser ou formater le texte dans une zone de texte individuellement comme je peux le faire avec un TextBlock.

Des idées?

190
demandé sur superjos 2008-09-26 01:56:51

13 réponses

<TextBox Background="Transparent"
         BorderThickness="0"
         Text="{Binding Text, Mode=OneWay}"
         IsReadOnly="True"
         TextWrapping="Wrap" />
195
répondu MSB 2014-04-11 10:16:47

Toutes les réponses ici sont simplement en utilisant un TextBox ou en essayant d'implémenter manuellement la sélection de texte, ce qui conduit à de mauvaises performances ou à un comportement non natif (caret clignotant dans TextBox, Pas de support de clavier dans les implémentations manuelles, etc.)

Après des heures de fouille et de lecture du code source WPF , j'ai plutôt découvert un moyen d'activer la sélection de texte WPF native pour les contrôles TextBlock (ou vraiment tout autre contrôle). La plupart des fonctionnalités autour de la sélection de texte est implémenté 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 la classe gestionnaire d'événements

  2. Créer une instance de TextEditor pour chaque instance de votre classe et de passer à l'instance sous-jacente de votre System.Windows.Documents.ITextContainer pour elle

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

C'est ça! Cela semble facile, mais malheureusement TextEditor la classe est marquée comme interne. J'ai donc dû écrire un wrapper de réflexion autour de lui:

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éé un SelectableTextBlock dérivé de TextBlock qui suit les étapes indiquées 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 pour activer la sélection du texte à la demande. Dans ce cas, pour désactiver à nouveau la sélection, il faut détacher un TextEditor en utilisant l'équivalent de réflexion de ce code:

_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
27
répondu torvin 2018-03-14 02:27:53

J'ai été incapable de trouver un exemple de vraiment répondre à la question. Toutes les réponses ont utilisé une zone de texte ou RichTextbox. 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 le faire est d'étendre la classe TextBlock. C'est le code que j'ai utilisé pour étendre la classe TextBlock pour me permettre de sélectionner le texte et de le copier dans le presse-papiers. "sdo" est la référence d'espace de noms que j'ai utilisée dans le WPF.

WPF en utilisant 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 derrière pour la classe étendue:

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);
    }
26
répondu Billy Willoughby 2018-04-22 20:34:37

Créez ControlTemplate pour le TextBlock et placez une zone de texte à l'intérieur avec le jeu de propriétés readonly. Ou utilisez simplement TextBox et faites-le en lecture seule, alors vous pouvez changer la zone de texte.Style pour le faire ressembler à TextBlock.

20
répondu Jobi Joy 2015-01-09 17:55:49

Appliquer ce style à votre zone de texte et c'est tout (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>
18
répondu juanjo.arana 2018-07-25 06:41:38

Je ne suis pas sûr si vous pouvez sélectionner un TextBlock, mais une autre option serait d'utiliser un RichTextBox - c'est comme une zone de texte comme vous l'avez suggéré, mais supporte le formatage que vous voulez.

9
répondu Bruce 2008-09-25 22:45:17

Selon Centre de développement Windows:

TextBlock.Propriété IsTextSelectionEnabled

[mise à jour pour les applications UWP sur Windows 10. Pour Windows 8.x articles, voir l'archive ]

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

8
répondu Jack Pines 2016-01-29 18:10:46

TextBlock n'a pas de modèle. Donc, pour y parvenir, nous devons utiliser une zone de texte dont le style est 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
répondu Saraf Talukder 2011-09-13 09:57:27

Il existe une solution alternative qui pourrait être adaptable à la RichTextBox oultined dans ce blog post - il a utilisé un déclencheur pour échanger le modèle de contrôle lorsque l'utilisation survole le contrôle-devrait aider à la performance

2
répondu Richard 2010-06-28 13:17:34

Alors que la question dit "sélectionnable", je crois que les résultats intentionnels sont d'obtenir le texte dans le presse-papiers. Cela peut facilement et élégamment être réalisé en ajoutant un Menu contextuel et un élément de menu appelé copy qui met la valeur de la propriété Textblock Text dans le presse-papiers. Juste une idée de toute façon.

2
répondu SimperT 2017-05-12 23:29:09

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
répondu Lu55 2010-11-10 14:21:02

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
répondu Robert Važan 2014-07-07 11:06:10
Really nice and easy solution, exactly what I wanted !

J'apporte quelques petites modifications

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;
    }
}
-1
répondu Titwan 2016-03-25 17:17:47