WPF - 使超链接可点击

30

我有一小段文本,想要在列表中展示。其中一些文本含有超链接。我想让这些链接在文本中可点击。我可以想象解决此问题的方法,但它们似乎并不美观。

例如,我可以将字符串拆分为超链接和非超链接,然后动态构建 Textblock,根据需要添加纯文本元素和超链接对象。

我希望有更好的、最好是声明性的解决方案。

示例:"嘿,看看这个链接:http://mylink.com它真的很酷。"

5个回答

47
你需要使用某些东西来解析TextBlock的文本并在运行时创建所有内联对象。为此,您可以创建自己的自定义控件从TextBlock派生或者使用一个附加属性。
对于解析,您可以使用正则表达式在文本中搜索URL。我从A good url regular expression? 借用了一个正则表达式,但网上还有其他可用的正则表达式,因此您可以选择最适合您的那个。
在下面的示例中,我使用了一个附加属性。要使用它,请修改您的TextBlock以使用NavigateService.Text而不是Text属性:
<Window x:Class="DynamicNavigation.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:DynamicNavigation"
    Title="Window1" Height="300" Width="300">
    <StackPanel>
        <!-- Type something here to see it displayed in the TextBlock below -->
        <TextBox x:Name="url"/>
        
        <!-- Dynamically updates to display the text typed in the TextBox -->
        <TextBlock local:NavigationService.Text="{Binding Text, ElementName=url}" />
    </StackPanel>
</Window>

下面是所附属性的代码:

using System;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace DynamicNavigation
{
    public static class NavigationService
    {
        // Copied from http://geekswithblogs.net/casualjim/archive/2005/12/01/61722.aspx
        private static readonly Regex RE_URL = new Regex(@"(?#Protocol)(?:(?:ht|f)tp(?:s?)\:\/\/|~/|/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2}))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?");
        
        public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
            "Text",
            typeof(string),
            typeof(NavigationService),
            new PropertyMetadata(null, OnTextChanged)
        );

        public static string GetText(DependencyObject d)
        { return d.GetValue(TextProperty) as string; }
        
        public static void SetText(DependencyObject d, string value)
        { d.SetValue(TextProperty, value); }
        
        private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var text_block = d as TextBlock;
            if (text_block == null)
                return;
                
            text_block.Inlines.Clear();

            var new_text = (string)e.NewValue;
            if ( string.IsNullOrEmpty(new_text) )
                return;
                
            // Find all URLs using a regular expression
            int last_pos = 0;
            foreach (Match match in RE_URL.Matches(new_text))
            {
                // Copy raw string from the last position up to the match
                if (match.Index != last_pos)
                {
                    var raw_text = new_text.Substring(last_pos, match.Index - last_pos);
                    text_block.Inlines.Add(new Run(raw_text));
                }
                
                // Create a hyperlink for the match
                var link = new Hyperlink(new Run(match.Value))
                {
                    NavigateUri = new Uri(match.Value)
                };
                link.Click += OnUrlClick;
                
                text_block.Inlines.Add(link);
                
                // Update the last matched position
                last_pos = match.Index + match.Length;
            }
            
            // Finally, copy the remainder of the string
            if (last_pos < new_text.Length)
                text_block.Inlines.Add(new Run(new_text.Substring(last_pos)));
        }

        private static void OnUrlClick(object sender, RoutedEventArgs e)
        {
            var link = (Hyperlink)sender;
            // Do something with link.NavigateUri like:
            Process.Start(link.NavigateUri.ToString());
        }
    }
}

太棒了,正是我正在寻找的。它让我对我一直面临的WPF问题有了全新的看法。 - Ben
太棒了!我在下面放置了VB版本的答案。 - Dabblernl
1
一个小问题是,如果协议没有被指定(例如,我只输入www.google.com),当尝试创建Uri时,该组件会抛出异常(UriFormatException - “无效的URI:无法确定URI的格式。”)。 - David_001

11

这是简化版:

<TextBlock>
    Hey, check out this link:        
    <Hyperlink NavigateUri="CNN.COM" Click="cnn_Click">Test</Hyperlink>
</TextBlock>

4
像这样的内容吗?
<TextBlock>
    <TextBlock Text="Hey, check out this link:"/>
    <Hyperlink NavigateUri={Binding ElementName=lvTopics, Path=SelectedValue.Title}
                           Click="Url_Click">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Feed: " FontWeight="Bold"/>
            <TextBlock Text={Binding ElementName=lvTopics, Path=SelectedValue.Url}/>
        </StackPanel>
    </Hyperlink>
</TextBlock>

编辑:如果需要动态绑定,可以使用bind方法。在上面的例子中,lvTopics(未显示)已经绑定到一个包含Title和Url属性的对象列表上。 此外,它不会自动跳转到URL,你需要使用类似的代码处理:

private void Url_Click(object sender, RoutedEventArgs e)
{    browser.Navigate(((Hyperlink)sender).NavigateUri); }

我只是想表明您可以将任何内容嵌入到TextBlock中,包括超链接,并将任何内容嵌入到超链接中。


嗯,是的,但问题是如何将纯文本转换为您在此处提供的内容。这是一个数据源,需要动态完成。 - Ben

2

以下是 Bojan 的回答的 VB.Net 版本。我对其进行了一些改进:此代码将解析类似于 http://support.mycompany.com?username=x&password=y 的 URL,转换成 http://support.mycompany.com,同时仍然使用用户名和密码导航到完整的 URL。

Imports System.Text.RegularExpressions
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Documents

Public Class NavigationService
    ' Copied from http://geekswithblogs.net/casualjim/archive/2005/12/01/61722.aspx '
    Private Shared ReadOnly RE_URL = New Regex("(?#Protocol)(?<domainURL>(?:(?:ht|f)tp(?:s?)\:\/\/|~/|/)?(?#Username:Password)(?:\w+:\w+@)?(?#Subdomains)(?:(?:[-\w]+\.)+(?#TopLevel Domains)(?:com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum|travel|[a-z]{2})))(?#Port)(?::[\d]{1,5})?(?#Directories)(?:(?:(?:/(?:[-\w~!$+|.,=]|%[a-f\d]{2})+)+|/)+|\?|#)?(?#Query)(?:(?:\?(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)(?:&(?:[-\w~!$+|.,*:]|%[a-f\d{2}])+=(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)*)*(?#Anchor)(?:#(?:[-\w~!$+|.,*:=]|%[a-f\d]{2})*)?")

    Public Shared ReadOnly TextProperty = DependencyProperty.RegisterAttached( _
        "Text",
        GetType(String),
        GetType(NavigationService),
        New PropertyMetadata(Nothing, AddressOf OnTextChanged)
    )

    Public Shared Function GetText(d As DependencyObject) As String
        Return TryCast(d.GetValue(TextProperty), String)
    End Function

    Public Shared Sub SetText(d As DependencyObject, value As String)
        d.SetValue(TextProperty, value)
    End Sub

    Private Shared Sub OnTextChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
        Dim text_block = TryCast(d, TextBlock)
        If text_block Is Nothing Then Return

        text_block.Inlines.Clear()

        Dim new_text = CStr(e.NewValue)
        If String.IsNullOrEmpty(new_text) Then Return

        ' Find all URLs using a regular expression '
        Dim last_pos As Integer = 0
        For Each match As Match In RE_URL.Matches(new_text)
            'Copy raw string from the last position up to the match '
            If match.Index <> last_pos Then
                Dim raw_text = new_text.Substring(last_pos, match.Index - last_pos)
                text_block.Inlines.Add(New Run(raw_text))
            End If

            ' Create a hyperlink for the match '
            Dim link = New Hyperlink(New Run(match.Groups("domainURL").Value)) With
                {
                    .NavigateUri = New Uri(match.Value)
                }
            AddHandler link.Click, AddressOf OnUrlClick

            text_block.Inlines.Add(link)

            'Update the last matched position '
            last_pos = match.Index + match.Length
        Next

        ' Finally, copy the remainder of the string '
        If last_pos < new_text.Length Then
            text_block.Inlines.Add(New Run(new_text.Substring(last_pos)))
        End If
    End Sub

    Private Shared Sub OnUrlClick(sender As Object, e As RoutedEventArgs)
        Try
            Dim link = CType(sender, Hyperlink)
            Process.Start(link.NavigateUri.ToString)
        Catch
        End Try
    End Sub
End Class

0
如果您正在使用类似MVVM Light或其他架构,您可以在TextBlock的MouseDown属性上设置交互触发器,并在ViewModel的代码中执行任何操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接