Можно ли сделать текстовый блок WPF доступным для выбора?

224

Как разрешить TextBlockвыбор текста?

Я попытался заставить его работать, отображая текст, используя TextBox только для чтения, стилизованный под текстовый блок, но в моем случае это не будет работать, потому что TextBox не имеет встроенных строк. Другими словами, как сделать его выбираемым?

Алан Ле
источник
1
Я попробую использовать элемент управления RichTextBox, чтобы увидеть, сработает ли это. Но из предыдущего опыта работа с richtextbox намного сложнее.
Алан Ле
Задумывались ли вы об использовании FlowDocumentScrollViewer с FlowDocument, содержащим абзацы и прогоны? - Это очень хорошо работает для меня, когда мне нужен выбираемый текст, и каждый абзац и прогон можно стилизовать отдельно.
BrainSlugs83
Попробовав некоторые из обходных путей, приведенных ниже, FlowDocumentScrollViewer стал способом продвижения вперед. Похоже, что он занимает полезное промежуточное положение между RichTextBox и TextBlock.
Том Макин
проголосуйте за принятие ответа, который не соответствует вашим требованиям.
Blechdose

Ответы:

218

TextBoxВместо этого используйте с этими настройками, чтобы сделать его только для чтения и выглядеть как элемент TextBlockуправления.

<TextBox Background="Transparent"
         BorderThickness="0"
         Text="{Binding Text, Mode=OneWay}"
         IsReadOnly="True"
         TextWrapping="Wrap" />
MSB
источник
6
У меня есть проект, который содержит много TextBlocks / Labels, я не могу превратить их в TextBoxes. Что я хочу сделать, так это добавить магический стиль «применить ко всем» к ресурсу уровня приложения, чтобы он влиял на все элементы Label / TextBlock, и сделать их внутренний текстовый презентатор текстовым блоком только для чтения. сделать это?
Шимми Вайцхандлер
5
Вы можете добавить IsTabStop = "False" в зависимости от вашей ситуации
Карстен
1
+1 Очень хорошее решение! Я добавил Padding = "0", так как в моем проекте нижняя часть текста была обрезана ... Возможно, из-за стиля где-то еще.
reSPAWNed
123
-1 Вопрос, в частности, спрашивает, как сделать выделение текстового блока. Потому что он не хочет потерять свойство «Inlines» (которого нет у textBox). Этот «ответ» просто предлагает сделать текстовое поле похожим на текстовый блок.
00jt
19
@AlanLe Почему ты принял этот ответ, когда ты прямо сказал, что не хочешь? И почему 147 невежественных людей поддержали это?
Джим Балтер
66

Все ответы здесь - просто использование TextBoxили попытка осуществить выделение текста вручную, что приводит к низкой производительности или нестандартному поведению (мигание каретки TextBox, отсутствие поддержки клавиатуры в ручных реализациях и т. Д.)

После нескольких часов копания и чтения исходного кода WPF , я вместо этого обнаружил способ включения встроенного выделения текста WPF для TextBlockэлементов управления (или вообще любых других элементов управления). Большая часть функциональности вокруг выделения текста реализована в System.Windows.Documents.TextEditorсистемном классе.

Чтобы включить выделение текста для вашего контроля, вам нужно сделать две вещи:

  1. Вызовите TextEditor.RegisterCommandHandlers()один раз, чтобы зарегистрировать обработчики событий класса

  2. Создание экземпляра TextEditorдля каждого экземпляра вашего класса и передать основной экземпляр ваших System.Windows.Documents.ITextContainerк нему

Также существует требование, чтобы Focusableсвойство вашего элемента управления было установлено на True.

Это оно! Звучит просто, но, к сожалению, TextEditorкласс помечен как внутренний. Поэтому мне пришлось написать обертку для отражения:

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

Я также создал SelectableTextBlockпроизводную от того, TextBlockчто предпринимает шаги, отмеченные выше:

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

Другой вариант - создать прикрепленное свойство, TextBlockчтобы включить выбор текста по требованию. В этом случае, чтобы снова отключить выделение, нужно отсоединить a TextEditor, используя эквивалент отражения этого кода:

_editor.TextContainer.TextView = null;
_editor.OnDetach();
_editor = null;
Torvin
источник
1
Как бы вы использовали класс SelectableTextBlock в другом xaml, который должен содержать его?
Йоав
1
так же, как вы использовали бы любой другой пользовательский элемент управления. см. например, stackoverflow.com/a/3768178/332528
торвин
3
@BillyWillough, ваше решение просто эмулирует выбор. В нем отсутствует множество встроенных функций выбора: поддержка клавиатуры, контекстное меню и т. Д. Мое решение позволяет использовать встроенную функцию выбора
torvin
3
Кажется, что это решение действительно работает, когда TextBlockвстраивает Hyperlinks, пока Hyperlinkоно не является последним встроенным в него. Добавление завершающего пустого Runк содержимому исправляет любую проблему, лежащую в основе, которая приводит к ExecutionEngineExceptionвыбрасыванию.
Антон Тихий
2
Это круто! За исключением , если у вас есть TextTrimming="CharacterEllipsis"на TextBlockи доступная ширина недостаточна, если вы перемещаете указатель мыши над ..., он падает с System.ArgumentException «Запрошенный расстояние находится вне содержания связанного документа.» в System.Windows.Documents.TextPointer.InitializeOffset (позиция TextPointer, расстояние Int32, направление LogicalDirection) :( Не знаю, есть ли обходной путь, кроме как оставить TextTrimming равным None.
Дейв Хуанг
32

Я не смог найти ни одного примера, чтобы действительно ответить на вопрос. Все ответы использовали Textbox или RichTextbox. Мне нужно было решение, которое позволило бы мне использовать TextBlock, и это решение я создал.

Я считаю, что правильный способ сделать это - расширить класс TextBlock. Это код, который я использовал для расширения класса TextBlock, чтобы позволить мне выделять текст и копировать его в буфер обмена. «sdo» - это ссылка на пространство имен, которую я использовал в WPF.

WPF используя расширенный класс:

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>

Код позади для расширенного класса:

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

Пример кода окна:

    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);
    }
Билли Уиллоуби
источник
1
Это должен быть принятый ответ! Нет отражения взлома, без использования TextBox ... И это может быть легко преобразовано в многоразовое поведение. Очень мило спасибо!
Томас Левеск
19

Примените этот стиль к вашему TextBox и все (вдохновлено этой статьей ):

<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
источник
1
Кстати, на сегодняшний день ссылка на статью кажется мертвой
superjos
2
Еще одно дополнение: отступы должны быть -2,0, -2,0. Внутри TextBox создается элемент управления TextBoxView с полем по умолчанию 2,0,2,0. К сожалению, вы не можете переопределить его стиль, потому что он помечен как внутренний.
fdub
11
Кажется, никто не умеет читать. ОП требует TextBlock, а не TextBox, стилизованный под TextBlock.
Джим Балтер
18

Создайте ControlTemplate для TextBlock и поместите TextBox внутрь с установленным свойством только для чтения. Или просто используйте TextBox и сделайте его доступным только для чтения, затем вы можете изменить TextBox.Style, чтобы он выглядел как TextBlock.

Джоби Джой
источник
11
Как установить ControlTemplate для TextBlock? Я не могу найти недвижимость?
HaxElit
18
Этот подход не будет работать, если ваш TextBlock имеет встроенные элементы внутри него. Что делать, если у вас есть гиперссылки или тексты, выделенные жирным или курсивом? TextBox не поддерживает это.
dthrasher
1
Не работает, если вы используете встроенные прогоны, и, как спросил HaxElit, я не уверен, что вы подразумеваете под шаблоном управления.
Ритч Мелтон
7
-1 TextBlock не имеет ControlTemplate, потому что это прямой подкласс FrameworkElement. TextBox, с другой стороны, является подклассом Control.
reSPAWNed
5
Почему никто не может читать? ОП явно сказал, что нужен TextBlock, а не TextBox, потому что TextBlock поддерживает встроенное форматирование, а TextBox - нет. Почему совершенно неправильные ответы на подобные вопросы получают многочисленные отклики?
Джим Балтер
10

Я не уверен, что вы можете сделать TextBlock доступным для выбора, но другой вариант будет использовать RichTextBox - он похож на TextBox, как вы предложили, но поддерживает желаемое форматирование.

Брюс
источник
1
Я попытался сделать это, и в процессе пришлось привязать RichTextBox к свойству зависимости. К сожалению, старые потоковые документы не удаляются должным образом, и память просачивается как сумасшедшая. Алан, интересно, ты нашел способ обойти это?
Джон Нунан
@AlanLe Из всех ответов здесь только один из двух, который фактически отвечает на заданный вопрос ... все остальные говорят о стилизации TextBox, чтобы он выглядел как TextBlock, игнорируя при этом необходимость форматирования. Странно и прискорбно, что ОП принял один из этих ответов, а не правильный ответ, чтобы использовать RichTextBox вместо TextBox.
Джим Балтер
9

По данным Windows Dev Center :

Свойство TextBlock.IsTextSelectionEnabled

[Обновлено для приложений UWP в Windows 10. Статьи о Windows 8.x см. В архиве ].

Получает или задает значение, указывающее, включено ли выделение текста в TextBlock , либо с помощью действий пользователя, либо с помощью API, связанного с выбором.

Джек Пайнс
источник
5
К сожалению, несовместимо с Win7 (иногда это является обязательным требованием)
Юрий Сккатула
24
Amswer кажется неверным. IsTextSelectionEnabled предназначен только для UWP, а не для WPF - в первоначальном вопросе действительно указывался WPF.
Тупик
6

Хотя вопрос говорит «Выбираемый», я полагаю, что преднамеренные результаты - получить текст в буфер обмена. Этого можно легко и элегантно достичь, добавив контекстное меню и пункт меню под названием copy, который помещает значение свойства Textblock Text в буфер обмена. В любом случае, просто идея.

Симперт
источник
4

У TextBlock нет шаблона. Таким образом, чтобы добиться этого, нам нужно использовать TextBox, стиль которого изменен, чтобы вести себя как 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>
Сараф Талукдер
источник
Какие преимущества дает этот подход по сравнению с другими ответами? Я не вижу никого.
Surfen
Я попробовал этот стиль: TextBoxBorder не определен. Если вы это закомментируете, это будет нормально
sthiers
Этот пример кода отличный, он показывает, как получить цвет по умолчанию для TextBlock.
Contango
1
Это довольно запутано. Во-первых, ключ x: «TextBlockUsingTextBoxStyle» - назад; это должно быть "TextBoxUsingTextBlockStyle". Во-вторых, ОП уже знал, как стилизовать TextBox, например TextBlock, но неоднократно говорил, что не может использовать это, потому что ему нужны встроенные символы для форматирования.
Джим Балтер
2

Существует альтернативное решение, которое может быть адаптировано к RichTextBox, описанному в этом сообщении в блоге - оно использует триггер для замены шаблона элемента управления, когда использование наводится на элемент управления - должно помочь с производительностью

Ричард
источник
1
Ваша ссылка мертва. Пожалуйста, включите всю соответствующую информацию в ответ и используйте ссылки только в качестве цитат.
Джим Балтер
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
источник
1
Это не полезно. Прочитайте вопрос, чтобы увидеть, чего на самом деле хотел ОП.
Джим Балтер
1

Добавим к ответу @ torvin и, как упомянул в комментариях @Dave Huang, если вы TextTrimming="CharacterEllipsis"включили приложение, происходит сбой при наведении курсора на многоточие.

Я пробовал другие варианты, упомянутые в теме, об использовании TextBox, но на самом деле это тоже не решение, так как он не показывает «многоточие», а также, если текст слишком длинный, чтобы вместить контейнер, выбирающий содержимое текстовое поле «прокручивается» внутри, что не является поведением TextBlock.

Я думаю, что лучшее решение - ответ @ torvin, но у него неприятный сбой при наведении на многоточие.

Я знаю, что это не красиво, но внутренняя подписка / отмена подписки на необработанные исключения и обработка исключения были единственным способом, который я нашел для решения этой проблемы, пожалуйста, поделитесь, если у кого-то есть лучшее решение :)

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
источник
0

Я реализовал SelectableTextBlock в моей библиотеке элементов управления с открытым исходным кодом. Вы можете использовать это так:

<jc:SelectableTextBlock Text="Some text" />
Роберт Важан
источник
4
Это просто использует TextBox, как и многие другие ответы за много лет до этого.
Крис
-1
Really nice and easy solution, exactly what I wanted !

Я принес несколько небольших модификаций

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
источник
1
Вы должны объяснить, что вы изменили, ответив ниже, пожалуйста. -1
Алекс Хоуп О'Коннор
Строка 51 дает: System.ArgumentNullException: «Значение не может быть нулевым. Имя параметра: position1 '
бросается
-1
public MainPage()
{
    this.InitializeComponent();
    ...
    ...
    ...
    //Make Start result text copiable
    TextBlockStatusStart.IsTextSelectionEnabled = true;
}
Ангел Т
источник
На этот вопрос ответили выше и работают только на UWP, а не на WPF
ΩmegaMan