Выделение текста в TextBlock

127
30 марта 2021, 06:50

У меня есть кастомный TextBlock, который определяет ссылки в тексте и делает их кликабельными. Возникла необходимость добавить возможность выделения произвольного текста в TextBlock для его дальнейшего копирования в буфер памяти. Есть идеи как это можно сделать?

Класс HyperlinkTextBlock:

public class HyperlinkTextBlock : TextBlock
{
    private static readonly Regex HyperlinkRegex = new Regex(@"(https?|ftp):\/\/[^\s/$.?#].[^\s,]*");
    public static readonly DependencyProperty HyperlinkStyleProperty = DependencyProperty.Register("HyperlinkStyle",
        typeof(Style), typeof(HyperlinkTextBlock));
    static HyperlinkTextBlock()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(HyperlinkTextBlock),
            new FrameworkPropertyMetadata(typeof(HyperlinkTextBlock)));
    }
    public HyperlinkTextBlock()
    {
        TargetUpdated += (s, args) =>
            Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(ParseHyperlinks));
    }
    public event EventHandler<HyperlinkPressedEventArgs> HyperlinkPressed;
    public Style HyperlinkStyle
    {
        private get => (Style) GetValue(HyperlinkStyleProperty);
        set => SetValue(HyperlinkStyleProperty, value);
    }
    private void ParseHyperlinks()
    {
        var text = Text;
        var matches = HyperlinkRegex.Matches(text);
        if (matches.Count == 0)
            return;
        Inlines.Clear();
        var lastIndex = 0;
        foreach (Match match in matches)
        {
            Inlines.Add(text.Substring(lastIndex, match.Index - lastIndex));
            lastIndex = match.Index + match.Length;
            var run = new Run(match.Value) {Style = HyperlinkStyle};
            run.MouseDown += RunOnMouseDown;
            Inlines.Add(run);
        }
        Inlines.Add(text.Substring(lastIndex));
    }
    private void RunOnMouseDown(object sender, MouseButtonEventArgs args)
    {
        if (!(sender is Run run)) return;
        var handler = HyperlinkPressed;
        handler?.Invoke(this, new HyperlinkPressedEventArgs(run.Text));
    }
}
public class HyperlinkPressedEventArgs : EventArgs
{
    public readonly Uri Hyperlink;
    public HyperlinkPressedEventArgs(string hyperlink)
    {
        Hyperlink = new Uri(hyperlink);
    }
}

Использование:

<Style x:Key="HyperlinkStyle" TargetType="{x:Type Run}">
    <Style.Setters>
        <Setter Property="Foreground" Value="Blue" />
        <Setter Property="TextDecorations" Value="Underline" />
    </Style.Setters>
    <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter Property="Cursor" Value="Hand" />
            <Setter Property="Foreground" Value="Orange" />
        </Trigger>
    </Style.Triggers>
</Style>
<htb:HyperlinkTextBlock Background="Transparent"
                        Margin="10,0,0,0"
                        HyperlinkStyle="{StaticResource HyperlinkStyle}"
                        HyperlinkPressed="OnHyperlinkPressed"
                        Text="{Binding Message, NotifyOnTargetUpdated=True}"
                        TextWrapping="Wrap" />
private void OnHyperlinkPressed(object sender, HyperlinkPressedEventArgs args)
{
    System.Diagnostics.Process.Start(args.Hyperlink.OriginalString);
}
Answer 1

Обычно делают из TextBox. Стилизуют его под TextBlock (рамку убрать, фон, etc) делают readonly и всё. Вот тебе и выделение с копированием.

Update. Накидал на скорую руку решение на RichTextBox (может кто предложит лучше, я только за):

public class HyperlinkRichTextBox : RichTextBox
{
    #region CustomText Dependency Property
    public static readonly DependencyProperty CustomTextProperty = DependencyProperty.Register("CustomText", typeof(string), typeof(HyperlinkRichTextBox),
        new PropertyMetadata(string.Empty, CustomTextChangedCallback), CustomTextValidateCallback);
    public string CustomText
    {
        get => (string)GetValue(CustomTextProperty);
        set => SetValue(CustomTextProperty, value);
    }
    private static void CustomTextChangedCallback(
                DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        ((HyperlinkRichTextBox) obj).Document = GetCustomDocument(e.NewValue as string);
    }
    private static bool CustomTextValidateCallback(object value) => value != null;
    #endregion
    private const string HttpPrefix = "http://";
    private static readonly Regex LinkRegex =
                new Regex(
            @"([--:А-Яа-я\w?@%&+~#=]*\.[a-zа-я]{2,4}\/{0,2})((?:[?&](?:[А-Яа-я\w]+)=(?:[А-Яа-я\w]+))+|[--:А-Яа-я\w?@%&+~#=]+)?",
            RegexOptions.Compiled);
    private static readonly Regex PrefixRegex = 
        new Regex("^(http|ftp)(s)?", RegexOptions.Compiled);
    private static FlowDocument GetCustomDocument(string text)
    {
        var document = new FlowDocument();
        var para = new Paragraph {Margin = new Thickness(0)};
        var linksMatches = LinkRegex.Matches(text);

        if (linksMatches.Count == 0)
        {
            para.Inlines.Add(text);
        }
        else
        {
            Match lastMatch = null;
            foreach (Match linksMatch in linksMatches)
            {
                var previousText = GetPreviousText(text, lastMatch, linksMatch);
                para.Inlines.Add(previousText);
                var link = CreateLink(linksMatch.Value);
                para.Inlines.Add(link);
                lastMatch = linksMatch;
            }
            var tail = GetPreviousText(text, lastMatch);
            para.Inlines.Add(tail);
        }
        document.Blocks.Add(para);
        return document;
    }
    private static Hyperlink CreateLink(string url)
    {
        var link = new Hyperlink
        {
            IsEnabled = true
        };
        link.Inlines.Add(url);
        link.NavigateUri = PrefixRegex.IsMatch(url)
            ? new Uri(url)
            : new Uri($"{HttpPrefix}{url}");
        link.RequestNavigate += (sender, args) => Process.Start(args.Uri.ToString());
        return link;
    }
    private static string GetPreviousText(string text, Capture lastMatch, Capture currentMatch = null)
    {
        var startIndex = lastMatch != null ? lastMatch.Index + lastMatch.Length : 0;
        return currentMatch != null
            ? text.Substring(startIndex, currentMatch.Index - startIndex)
            : text.Substring(startIndex);
    }
}

Объявляем так:

<local:HyperlinkRichTextBox BorderThickness="0" Padding="-5" CustomText="{Binding Text}" IsReadOnly="True" IsDocumentEnabled="True">

Padding="-5" добавил чтоб отображение совпадало с TextBlock.

Ну и, конечно, выделение и копирование доступно

Answer 2

Не знаю правильные ли это подходы, но можно в ToolTip добавить текст типа при нажатии ПКМ (или Ваш вариант) данная ссылка будет скопирована. Либо организовать MenuItem c Heder = "Копировать ссылку" и вызвать метод копирования. По примерам кода я не знаю что Вам привести.

READ ALSO
Возможно ли отнять число в PlayerPrefs.SetInt?

Возможно ли отнять число в PlayerPrefs.SetInt?

Возможно ли отнять число в PlayerPrefsSetInt?

104
Сложить два массива php

Сложить два массива php

есть два массива:

129
Почему PHPDebug в VScode не видит локальный сайт на Docker

Почему PHPDebug в VScode не видит локальный сайт на Docker

Развернул сайт на Docker, но не могу сконнектить IDE VSCode c моим PHP серверомПосмотрел кучу мануалов, но даже не знаю где посмотреть ошибки от этого...

89