Будь-який спосіб зробити вибір текстового блоку WPF?


224

Я хочу зробити текст, який відображається у Witty , клієнті Twitter із відкритим кодом, обраним. Зараз він відображається за допомогою спеціального текстового блоку. Мені потрібно використовувати TextBlock, оскільки я працюю з рядками textblock, щоб відобразити та відформатувати @username та посилання як гіперпосилання. Частий запит - це можливість копіювати та вставляти текст. Для цього мені потрібно зробити TextBlock вибірним.

Я спробував змусити його працювати, показуючи текст, використовуючи TextBox, доступний лише для читання, стилізований так, щоб виглядати як текстовий блок, але в моєму випадку це не спрацює, оскільки TextBox не має рядків. Іншими словами, я не можу стилювати або форматувати текст в TextBox окремо, як я можу з TextBlock.

Будь-які ідеї?


1
Я спробую скористатися керуванням RichTextBox, щоб побачити, чи буде це спрацьовувати. Але з попереднього досвіду робота з Richtextbox значно більше задіяна.
Алан Ле

Чи думали ви про використання FlowDocumentScrollViewer з FlowDocument, що містить абзаци та запуски? - Це досить добре працює для мене, коли мені потрібен текст, що вибирається, і кожен Абзац та Виконати можна стилізувати окремо.
BrainSlugs83

Спробувавши деякі способи вирішення нижче, FlowDocumentScrollViewer пройшов шлях вперед. Здається, вона займає корисну середину між RichTextBox та TextBlock.
Том Макін

проголосуйте за прийняття відповіді, яка не відповідає вашим вимогам.
Блехдоза

Відповіді:


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

6
У мене є проект, який містить багато TextBlocks / Labels, я не можу реально перетворити їх у TextBoxes. Що я хочу зробити, це додати магію застосунку до всіх стилів до ресурсу рівня додатків, щоб він впливав на всі мітки / текстові блоки та перетворював їх внутрішній текстовий презентатор як текст, прочитаний лише для читання, чи знаєте ви будь-яким чином зробити це?
Шиммі Вайцхандлер

5
Ви можете додати IsTabStop = "Неправдиво" залежно від вашої ситуації
Karsten

1
+1 Дуже приємне рішення! Я додав Padding = "0", оскільки в моєму проекті внизу тексту було вирізано ... Можливо, через стиль десь в іншому місці.
reSPAWNed

123
-1 Питання конкретно запитує, як зробити текстовий блок з вибором. Оскільки він не хоче втрачати властивість "Inlines" (якої textBoxes не мають). Ця "відповідь" просто пропонує зробити текстове поле схожим на текстовий блок.
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;

1
як би ви використовували клас SelectableTextBlock в іншому xaml, який повинен містити його?
Yoav Feuerstein

1
так само, як і будь-який інший користувальницький контроль. см stackoverflow.com/a/3768178/332528 , наприклад
Torvin

3
@BillyWilloughby ваше рішення просто імітує вибір. Їй не вистачає багато рідних особливостей вибору: підтримка клавіатури, контекстне меню і т.д. Моє рішення дозволяє нативную функцію вибору
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>

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 :

Властивість TextBlock.IsTextSelectionEnabled

[Оновлено для програм UWP в Windows 10. Для статей Windows 8.x див. Архів ]

Отримує або встановлює значення, яке вказує, чи ввімкнено вибір тексту в TextBlock , або через дії користувача, або за допомогою API, пов’язаного з вибором.


5
На жаль, не сумісний з Win7 (іноді це обов'язкова умова)
Юрій Шкатула

24
Амсвер видається неправильним. IsTextSelectionEnabled призначений лише для UWP, а не WPF - в оригінальному питанні було вказано WPF.
Puffin

6

Хоча на запитання йдеться "Вибірково", я вважаю, що навмисними результатами є отримання тексту до буфера обміну. Цього можна легко та елегантно досягти, додавши контекстне меню та пункт меню, який називається копія, що додає значення властивості 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 не визначений. Якщо ви прокоментуєте це, він працює чудово
стихій

Цей приклад код відмінний, він показує, як отримати колір за замовчуванням для TextBlock.
Контанго

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


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

0

Я реалізував SelectableTextBlock у своїй бібліотеці управління відкритими ресурсами. Ви можете використовувати його так:

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

4
Це просто використовує TextBox, як і багато інших відповідей за багато років до цього.
Кріс

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

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

1
Вам потрібно пояснити, що ви змінили з відповіді нижче. -1
Алекс Хоуп О'Коннор

Рядок 51 дає: System.ArgumentNullException: 'Значення не може бути нульовим. Назва параметра: position1 '
котиться
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.