Alguma maneira de tornar um bloco de texto WPF selecionável?

224

Quero tornar o texto exibido no Witty , um cliente do Twitter de código aberto, selecionável. Atualmente, ele é exibido usando um bloco de texto personalizado. Preciso usar um TextBlock porque estou trabalhando com as linhas do bloco de texto para exibir e formatar o @username e links como hiperlinks. Uma solicitação frequente é poder copiar e colar o texto. Para fazer isso, preciso tornar o TextBlock selecionável.

Tentei fazê-lo funcionar, exibindo o texto usando um TextBox somente leitura com estilo para parecer um bloco de texto, mas isso não funcionará no meu caso, porque um TextBox não possui linhas embutidas. Em outras palavras, não consigo estilizar ou formatar o texto dentro de uma TextBox individualmente, como posso com um TextBlock.

Alguma ideia?

Alan Le
fonte
1
Vou tentar usar o controle RichTextBox para ver se isso funcionará. Mas, a partir da experiência anterior, o trabalho com o richtextbox está muito mais envolvido.
27730 Alan Le
Você já pensou em usar um FlowDocumentScrollViewer, com um FlowDocument contendo parágrafos e execuções? - Isso funciona muito bem para mim quando preciso de texto selecionável, e cada parágrafo e execução podem ser estilizados separadamente.
precisa saber é o seguinte
Depois de tentar algumas das soluções alternativas abaixo, o FlowDocumentScrollViewer foi o caminho a seguir. Parece ocupar um meio termo útil entre RichTextBox e TextBlock.
Tom Makin
voto negativo para aceitar uma resposta que não se encaixa nos seus requisitos.
Blechdose

Respostas:

218
<TextBox Background="Transparent"
         BorderThickness="0"
         Text="{Binding Text, Mode=OneWay}"
         IsReadOnly="True"
         TextWrapping="Wrap" />
MSB
fonte
6
Eu tenho um projeto que contém muitos TextBlocks / Labels, eu realmente não posso transformá-los em TextBoxes. O que eu quero fazer é adicionar um estilo de aplicar a todos os recursos mágicos ao recurso no nível do aplicativo, para que ele afete todo o Label / TextBlock e faça seu apresentador de texto interno como um TextBox somente leitura, você conhece alguma maneira fazer isso?
Shimmy Weitzhandler
5
Você pode adicionar IsTabStop = "False", dependendo da sua situação #
Karsten
1
+1 Solução muito boa! Eu adicionei Padding = "0", pois no meu projeto a parte inferior do texto foi cortada de ... Talvez por causa de um estilo em outro lugar.
usar o seguinte código
123
-1 A pergunta pergunta especificamente como tornar um bloco de texto selecionável. Porque ele não quer perder a propriedade "Inlines" (que caixas de texto não possuem). Esta 'resposta' está apenas sugerindo que uma caixa de texto se pareça com um bloco de texto.
00jt
19
@ AlanLe Por que você aceitou esta resposta quando foi explicitamente o que disse que não queria? E por que 147 pessoas sem noção o votaram?
Jim Balter
66

Todas as respostas aqui estão apenas usando TextBoxou tentando implementar a seleção de texto manualmente, o que leva a um desempenho ruim ou a um comportamento não nativo (sinal de intermitência TextBox, nenhum suporte de teclado em implementações manuais etc.)

Depois de horas pesquisando e lendo o código fonte do WPF , descobri uma maneira de ativar a seleção de texto WPF nativa para TextBlockcontroles (ou realmente quaisquer outros controles). A maioria das funcionalidades relacionadas à seleção de texto é implementada na System.Windows.Documents.TextEditorclasse do sistema.

Para habilitar a seleção de texto para seu controle, você precisa fazer duas coisas:

  1. Ligue TextEditor.RegisterCommandHandlers()uma vez para registrar os manipuladores de eventos da classe

  2. Crie uma instância de TextEditorpara cada instância da sua classe e passe a instância subjacente da sua System.Windows.Documents.ITextContainerpara ela

Também é necessário que a Focusablepropriedade do seu controle esteja definida True.

É isso! Parece fácil, mas infelizmente a TextEditorclasse está marcada como interna. Então eu tive que escrever um invólucro de reflexão:

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

Eu também criei um SelectableTextBlockderivado TextBlockque executa as etapas mencionadas acima:

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

Outra opção seria criar uma propriedade anexada para TextBlockativar a seleção de texto sob demanda. Nesse caso, para desativar a seleção novamente, é necessário desanexar a TextEditorusando o equivalente de reflexão deste código:

_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
Torvin
fonte
1
como você usaria a classe SelectableTextBlock dentro de outro xaml que deveria conter?
Yoav Feuerstein
1
da mesma maneira que você usaria qualquer outro controle personalizado. veja stackoverflow.com/a/3768178/332528 por exemplo
torvin
3
@BillyWilloughby sua solução apenas emula a seleção. Falta-lhe um monte de recursos de seleção nativas: suporte de teclado, menu de contexto etc. Minha solução permite que o recurso de seleção nativa
torvin
3
Parece que esta solução faz o trabalho quando o TextBlocktem incorporado Hyperlinks, enquanto o Hyperlinknão é a última linha na mesma. A adição de um rastro vazio Runao conteúdo corrige qualquer problema subjacente que resulte no ExecutionEngineExceptionlançamento.
Anton Tykhyy 6/10/10
2
Isso é ótimo! Exceto se você tiver TextTrimming="CharacterEllipsis"o TextBlocke a largura disponível for insuficiente, se você mover o ponteiro do mouse sobre o…, ele trava com System.ArgumentException "A distância solicitada está fora do conteúdo do documento associado". em System.Windows.Documents.TextPointer.InitializeOffset (posição de TextPointer, distância de Int32, direção de LogicalDirection) :( Não sei se há uma solução alternativa diferente para deixar TextTrimming definido como None.
Dave Huang
32

Não consegui encontrar nenhum exemplo de realmente responder à pergunta. Todas as respostas usaram uma caixa de texto ou RichTextbox. Eu precisava de uma solução que me permitisse usar um TextBlock, e essa é a solução que eu criei.

Acredito que a maneira correta de fazer isso é estender a classe TextBlock. Este é o código que eu usei para estender a classe TextBlock para permitir que eu selecione o texto e copie-o para a área de transferência. "sdo" é a referência de espaço para nome que usei no WPF.

WPF usando classe estendida:

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 Atrás para Classe Estendida:

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

Exemplo de código da janela:

    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
fonte
1
Essa deve ser a resposta aceita! Sem hacks de reflexão, sem usar um TextBox ... E pode ser facilmente refatorado para um comportamento reutilizável. Muito bom, obrigado!
Thomas Levesque
19

Aplique esse estilo ao seu TextBox e pronto (inspirado neste artigo ):

<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
fonte
1
BTW a partir de hoje, o link para o artigo parece morto
superjos
2
Outra adição: o preenchimento deve ser -2,0, -2,0. Dentro do TextBox, é criado um controle TextBoxView que possui uma margem padrão de 2,0,2,0. Infelizmente, você não pode redefinir seu estilo porque está marcado como interno.
Fdub
11
Ninguém parece capaz de ler. O OP precisa de um TextBlock, não de um TextBox com o estilo de um TextBlock.
Jim Balter
18

Crie ControlTemplate para o TextBlock e coloque um TextBox dentro com o conjunto de propriedades readonly. Ou basta usar o TextBox e torná-lo somente leitura, então você pode alterar o TextBox.Style para parecer com o TextBlock.

Jobi Joy
fonte
11
Como você define o ControlTemplate para um TextBlock? Não consigo encontrar a propriedade?
precisa saber é o seguinte
18
Essa abordagem não funcionará se o seu TextBlock tiver elementos embutidos. E se você tiver hiperlinks ou textos em negrito ou itálico? O TextBox não suporta isso.
precisa saber é o seguinte
1
Não funciona se você estiver usando execuções inline e, como o HaxElit perguntou, não sei o que você quer dizer com modelo de controle.
Ritch Melton
7
-1 O TextBlock não possui um ControlTemplate porque é uma subclasse direta de FrameworkElement. TextBox, por outro lado, é uma subclasse de Control.
usar o seguinte comando
5
Por que ninguém pode ler? O OP disse explicitamente que um TextBlock é necessário, não um TextBox, porque o TextBlock suporta formatação embutida e o TextBox não. Por que respostas de lixo completamente erradas como essa recebem inúmeros votos?
Jim Balter
10

Não tenho certeza se você pode tornar um TextBlock selecionável, mas outra opção seria usar um RichTextBox - é como um TextBox, como você sugeriu, mas suporta a formatação desejada.

Bruce
fonte
1
Tentei fazer isso e, no processo, tive que tornar o RichTextBox vinculável com uma propriedade de dependência. Infelizmente, os documentos de fluxo antigos não estão sendo descartados corretamente e a memória está vazando como um louco. Alan, será que você achou uma maneira de contornar isso?
31909 John Noonan
@AlanLe De todas as respostas aqui, essa é apenas uma das duas que realmente responde à pergunta ... todos os outros falam sobre o estilo de um TextBox para parecer um TextBlock, ignorando a necessidade de formatação. É bizarro e lamentável que o OP tenha aceitado uma dessas não respostas, em vez da resposta correta para usar RichTextBox em vez de TextBox.
Jim Balter
9

De acordo com o Windows Dev Center :

Propriedade TextBlock.IsTextSelectionEnabled

[Atualizado para aplicativos UWP no Windows 10. Para artigos do Windows 8.x, consulte o arquivo ]

Obtém ou define um valor que indica se a seleção de texto está ativada no TextBlock , por meio da ação do usuário ou da chamada da API relacionada à seleção.

Jack Pines
fonte
5
Infelizmente, não é compatível com Win7 (às vezes é um must-requisito)
Yury Schkatula
24
Amswer parece incorreto. IsTextSelectionEnabled é apenas para UWP, não para WPF - a pergunta original especificou o WPF.
Puffin
6

Embora a pergunta diga 'Selecionável', acredito que os resultados intencionais são levar o texto para a área de transferência. Isso pode ser feito de maneira fácil e elegante adicionando um Menu de Contexto e um item de menu chamado copy que coloca o valor da propriedade Textblock Text na área de transferência. Apenas uma idéia de qualquer maneira.

SimperT
fonte
4

O TextBlock não possui um modelo. Portanto, para conseguir isso, precisamos usar um TextBox cujo estilo foi alterado para se comportar como um bloco 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
fonte
Que vantagens essa abordagem oferece em comparação com outras respostas? Eu não vejo nenhum.
surfen
Eu tentei este estilo: TextBoxBorder não está definido. Se você comentá-lo, ele funciona bem
sthiers
Este código de exemplo é excelente, mostra como obter a cor padrão para um TextBlock.
Contango 6/02
1
Isso é bastante confuso. Primeiro, a chave x: "TextBlockUsingTextBoxStyle" está ao contrário; deve ser "TextBoxUsingTextBlockStyle". Segundo, o OP já sabia como estilizar um TextBox como um TextBlock, mas disse repetidamente que não podia usá-lo porque precisava de linhas para formatar.
Jim Balter
2

Há uma solução alternativa que pode ser adaptável ao RichTextBox excluído nesta postagem do blog - ele usou um gatilho para trocar o modelo de controle quando o uso passa o mouse sobre o controle - deve ajudar no desempenho

Richard
fonte
1
Seu link está morto. Inclua todas as informações relevantes em uma resposta e use os links apenas como citações.
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
fonte
1
Isso não ajuda. Leia a pergunta para ver o que o OP realmente queria.
Jim Balter
1

Adicionando à resposta de @ torvin e como @Dave Huang mencionado nos comentários, se você tiver TextTrimming="CharacterEllipsis"ativado o aplicativo trava ao passar o mouse sobre as reticências.

Tentei outras opções mencionadas no tópico sobre o uso de um TextBox, mas ele realmente não parece ser a solução, pois não mostra as 'reticências' e também se o texto é muito longo para caber no contêiner, selecionando o conteúdo de a caixa de texto 'rola' internamente, o que não é um comportamento do TextBlock.

Eu acho que a melhor solução é a resposta de @ torvin, mas tem uma falha grave ao passar o mouse sobre as reticências.

Eu sei que não é bonito, mas assinar / cancelar a assinatura internamente de exceções não tratadas e manipular a exceção foi a única maneira que encontrei para resolver esse problema. Compartilhe se alguém tiver uma solução melhor :)

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
fonte
0

Eu implementei o SelectableTextBlock na minha biblioteca de controles de código-fonte aberto. Você pode usá-lo assim:

<jc:SelectableTextBlock Text="Some text" />
Robert Važan
fonte
4
Isso usa apenas um TextBox, como muitas outras respostas de muitos anos anteriores.
Chris
0
public MainPage()
{
    this.InitializeComponent();
    ...
    ...
    ...
    //Make Start result text copiable
    TextBlockStatusStart.IsTextSelectionEnabled = true;
}
Anjo T
fonte
-1
Really nice and easy solution, exactly what I wanted !

Trago algumas pequenas modificações

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
fonte
1
Você precisa explicar o que mudou na resposta abaixo, por favor. -1
Alex Hope O'Connor /
A linha 51 fornece: System.ArgumentNullException: 'O valor não pode ser nulo. Nome do parâmetro: position1 '
rolls