Pergunta

Eu quero fazer o texto exibido na Witty , um cliente de Twitter de código aberto, selecionável. Ele atualmente é exibido usando um textblock personalizado. Eu preciso usar um TextBlock porque eu estou trabalhando com inlines do TextBlock para exibir e formatar o @username e ligações como hiperlinks. Um pedido freqüente é para ser capaz de copiar e colar o texto. A fim de fazer isso eu preciso fazer o selecionável TextBlock.

Eu tentei fazê-lo funcionar, exibindo o texto usando um read-only TextBox denominado para olhar como um textblock mas isso não vai funcionar no meu caso, porque uma caixa de texto não tem inlines. Em outras palavras, não posso estilo ou formatar o texto dentro de uma caixa de texto individualmente como eu posso com um TextBlock.

Todas as idéias?

Foi útil?

Solução

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

Outras dicas

Todas as respostas aqui são apenas usando um TextBox ou tentando implementar seleção de texto manualmente, o que leva a problemas de desempenho ou comportamento não-nativo (piscar acento circunflexo em TextBox, não suporte a teclado em implementações manuais etc.)

Depois de horas de escavação em torno e ler o WPF código fonte , I vez descoberto uma forma de permitir a seleção de texto WPF nativo para controles TextBlock (ou realmente quaisquer outros controles). A maioria das funcionalidades em torno de seleção de texto é implementado na classe sistema System.Windows.Documents.TextEditor.

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

  1. Chamada TextEditor.RegisterCommandHandlers() uma vez para registar classe manipuladores de eventos

  2. Criar uma instância de TextEditor para cada instância de sua classe e passar a instância subjacente de seu System.Windows.Documents.ITextContainer a ele

Há também uma exigência de que a propriedade Focusable do seu controle é definido como True.

É isso aí! Parece fácil, mas infelizmente classe TextEditor é marcado como interno. Então eu tive que escrever um wrapper reflexão em torno dele:

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 criou um SelectableTextBlock derivado de TextBlock que toma as medidas 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 a criação de uma propriedade anexada para TextBlock para permitir a seleção de texto na demanda. Neste caso, para desativar a seleção novamente, é preciso separar uma TextEditor usando o equivalente reflexo desse código:

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

Eu fui incapaz de encontrar qualquer exemplo de realmente responder à pergunta. Todas as respostas utilizada uma caixa de texto ou RichTextbox. Eu precisava de uma solução que me permitiu usar um TextBlock, e esta é a solução que eu criei.

Eu acredito que a maneira correta de fazer isso é estender a classe TextBlock. Este é o código que usei para estender a classe TextBlock para permitir-me para selecionar o texto e copiá-lo para área de transferência. "Sdo" é o que eu referência namespace usado no WPF.

WPF usando Extensão Classe:

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

Aplicar este estilo para seu TextBox e é isso (inspirado em este 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>

Criar ControlTemplate para o TextBlock e colocar uma dentro de caixa de texto com o conjunto de propriedades somente leitura. Ou simplesmente usar TextBox e torná-lo somente leitura, então você pode mudar o TextBox.Style para torná-lo parece TextBlock.

Eu não tenho certeza se você pode fazer um selecionável TextBlock, mas outra opção seria usar um RichTextBox -. É como uma caixa de texto como você sugeriu, mas suporta a formatação desejada

De acordo com a o Windows Dev Center :

propriedade TextBlock.IsTextSelectionEnabled

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

Obtém ou define um valor que indica se a seleção de texto é habilitado no TextBlock , seja através de acção do utilizador ou chamando API seleção-relacionado.

TextBlock não tem um modelo. Então inorder para conseguir isso, precisamos usar uma caixa de texto cujo estilo é alterado para se comportar como um 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>

Enquanto a questão não diz 'selecionável' Eu acredito que os resultados intencionais é fazer com que o texto para a área de transferência. Isso pode facilmente e elegantemente ser alcançado pela adição de um item de menu de contexto e menu chamado cópia que coloca o valor da propriedade Texto textblock na área de transferência. Apenas uma idéia de qualquer maneira.

Há uma solução alternativa que pode ser adaptável à RichTextBox oultined nesta Blog Post - é usado um gatilho para trocar o modelo de controle quando os paira uso sobre o controle - deve ajudar com o desempenho


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

Eu tenho implementado SelectableTextBlock na minha biblioteca controles opensource . Você pode usá-lo como este:

<jc:SelectableTextBlock Text="Some text" />
public MainPage()
{
    this.InitializeComponent();
    ...
    ...
    ...
    //Make Start result text copiable
    TextBlockStatusStart.IsTextSelectionEnabled = true;
}

Adicionando a resposta de @ torvin e como @ Dave Huang mencionado nos comentários se você tem TextTrimming="CharacterEllipsis" habilitado a aplicação falha quando você passar o mouse sobre a elipse.

Eu tentei outras opções mencionadas no tópico sobre como usar uma caixa de texto, mas ele realmente não parece ser a solução ou como ele não mostra o 'reticências' e também se o texto for muito longo para caber a seleção recipiente o conteúdo da caixa de texto 'rola' internamente que não é um comportamento TextBlock.

Eu acho que a melhor solução é @ resposta de torvin mas tem o acidente desagradável quando pairando sobre as reticências.

Eu sei que não é muito, mas subscrever / cancelar internamente para exceções sem tratamento e manipulação de exceção foi a única maneira que eu encontrei de resolver este problema, por favor, compartilhe, se alguém tem 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;
            }
        }
    }
}
Really nice and easy solution, exactly what I wanted !

Eu 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;
    }
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top