Any way to make a WPF textblock selectable?

WpfXamlTextboxTextblock

Wpf Problem Overview


How to allow TextBlock's text to be selectable?

I tried to get it to work by displaying the text using a read-only TextBox styled to look like a textblock but this will not work in my case because a TextBox does not have inlines. In other words, how to make it selectable?

Wpf Solutions


Solution 1 - Wpf

Use a TextBox with these settings instead to make it read only and to look like a TextBlock control.

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

Solution 2 - Wpf

All the answers here are just using a TextBox or trying to implement text selection manually, which leads to poor performance or non-native behaviour (blinking caret in TextBox, no keyboard support in manual implementations etc.)

After hours of digging around and reading the WPF source code, I instead discovered a way of enabling the native WPF text selection for TextBlock controls (or really any other controls). Most of the functionality around text selection is implemented in System.Windows.Documents.TextEditor system class.

To enable text selection for your control you need to do two things:

  1. Call TextEditor.RegisterCommandHandlers() once to register class event handlers

  2. Create an instance of TextEditor for each instance of your class and pass the underlying instance of your System.Windows.Documents.ITextContainer to it

There's also a requirement that your control's Focusable property is set to True.

This is it! Sounds easy, but unfortunately TextEditor class is marked as internal. So I had to write a reflection wrapper around it:

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

I also created a SelectableTextBlock derived from TextBlock that takes the steps noted above:

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

Another option would be to create an attached property for TextBlock to enable text selection on demand. In this case, to disable the selection again, one needs to detach a TextEditor by using the reflection equivalent of this code:

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

Solution 3 - Wpf

I have been unable to find any example of really answering the question. All the answers used a Textbox or RichTextbox. I needed a solution that allowed me to use a TextBlock, and this is the solution I created.

I believe the correct way to do this is to extend the TextBlock class. This is the code I used to extend the TextBlock class to allow me to select the text and copy it to clipboard. "sdo" is the namespace reference I used in the WPF.

WPF Using Extended Class:

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

Example Window Code:

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

Solution 4 - Wpf

Create ControlTemplate for the TextBlock and put a TextBox inside with readonly property set. Or just use TextBox and make it readonly, then you can change the TextBox.Style to make it looks like TextBlock.

Solution 5 - Wpf

Apply this style to your TextBox and that's it (inspired from this 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>

Solution 6 - Wpf

I'm not sure if you can make a TextBlock selectable, but another option would be to use a RichTextBox - it is like a TextBox as you suggested, but supports the formatting you want.

Solution 7 - Wpf

According to Windows Dev Center:

> TextBlock.IsTextSelectionEnabled property > > [ Updated for UWP apps on Windows 10. For Windows 8.x articles, see > the archive ] > > Gets or sets a value that indicates whether text selection is enabled > in the TextBlock, either through user action or calling > selection-related API.

Solution 8 - Wpf

While the question does say 'Selectable' I believe the intentional results is to get the text to the clipboard. This can easily and elegantly be achieved by adding a Context Menu and menu item called copy that puts the Textblock Text property value in clipboard. Just an idea anyway.

Solution 9 - Wpf

TextBlock does not have a template. So inorder to achieve this, we need to use a TextBox whose style is changed to behave as a 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>

Solution 10 - Wpf

There is an alternative solution that might be adaptable to the RichTextBox oultined in this blog post - it used a trigger to swap out the control template when the use hovers over the control - should help with performance

Solution 11 - Wpf


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


Solution 12 - Wpf

Here is what worked for me. I created a class TextBlockEx that is derived from TextBox and is set read-only, and text wrap in the constructor.

public class TextBlockEx : TextBox
{
    public TextBlockEx()
    {
        base.BorderThickness = new Thickness(0);
        IsReadOnly = true;
        TextWrapping = TextWrapping.Wrap;
        //Background = Brushes.Transparent; // Uncomment to get parent's background color
    }
}

Solution 13 - Wpf

Really nice and easy solution, exactly what I wanted !

I bring some small 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;
    }
}

Solution 14 - Wpf

public MainPage()
{
    this.InitializeComponent();
    ...
    ...
    ...
    //Make Start result text copiable
	TextBlockStatusStart.IsTextSelectionEnabled = true;
}

Solution 15 - Wpf

Adding to @torvin's answer and as @Dave Huang mentioned in the comments if you have TextTrimming="CharacterEllipsis" enabled the application crashes when you hover over the ellipsis.

I tried other options mentioned in the thread about using a TextBox but it really doesn't seem to be the solution either as it doesn't show the 'ellipsis' and also if the text is too long to fit the container selecting the content of the textbox 'scrolls' internally which isn't a TextBlock behaviour.

I think the best solution is @torvin's answer but has the nasty crash when hovering over the ellipsis.

I know it isn't pretty, but subscribing/unsubscribing internally to unhandled exceptions and handling the exception was the only way I found of solving this problem, please share if somebody has a better 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;
            }
        }
    }
}

Solution 16 - Wpf

Just use a FlowDocument inside a FlowDocumentScrollViewer, passing your inlines to the element. You can control the style of the element, in my case I added a small border.

<FlowDocumentScrollViewer Grid.Row="2" Margin="5,3" BorderThickness="1" 
                          BorderBrush="{DynamicResource Element.Border}" 
                          VerticalScrollBarVisibility="Auto">
    <FlowDocument>
        <Paragraph>
            <Bold>Some bold text in the paragraph.</Bold>
            Some text that is not bold.
        </Paragraph>

        <List>
            <ListItem>
                <Paragraph>ListItem 1</Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>ListItem 2</Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>ListItem 3</Paragraph>
            </ListItem>
        </List>
    </FlowDocument>
</FlowDocumentScrollViewer>

enter image description here

Solution 17 - Wpf

I agree most answers here do not create a selectable TextBlock. @Billy Willoughby's worked well however it didn't have a visible cue for selection. I'd like to extend his extension which can highlight text as it is selected. It also incorporates double and triple click selection. You can add a context menu with a "Copy" if needed. It uses the Background property to "highlight" the selection so it is limited in that it will overwrite Run.Background

https://github.com/mwagnerEE/WagnerControls

Solution 18 - Wpf

I've implemented SelectableTextBlock in my opensource controls library. You can use it like this:

<jc:SelectableTextBlock Text="Some text" />

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlan LeView Question on Stackoverflow
Solution 1 - WpfMSBView Answer on Stackoverflow
Solution 2 - WpftorvinView Answer on Stackoverflow
Solution 3 - WpfBilly WilloughbyView Answer on Stackoverflow
Solution 4 - WpfJobi JoyView Answer on Stackoverflow
Solution 5 - WpfjdearanaView Answer on Stackoverflow
Solution 6 - WpfBruceView Answer on Stackoverflow
Solution 7 - WpfJack PinesView Answer on Stackoverflow
Solution 8 - WpfSimperTView Answer on Stackoverflow
Solution 9 - WpfSaraf TalukderView Answer on Stackoverflow
Solution 10 - WpfRichardView Answer on Stackoverflow
Solution 11 - WpfLu55View Answer on Stackoverflow
Solution 12 - WpfpongapunditView Answer on Stackoverflow
Solution 13 - WpfTitwanView Answer on Stackoverflow
Solution 14 - WpfAngel TView Answer on Stackoverflow
Solution 15 - WpfRaulandView Answer on Stackoverflow
Solution 16 - WpfNicke ManarinView Answer on Stackoverflow
Solution 17 - WpfMichael WagnerView Answer on Stackoverflow
Solution 18 - WpfRobert VažanView Answer on Stackoverflow