1. 在WPF怎么在UI上添加超级链接

这篇文章的目的是介绍怎么在WPF里创建自定义的HyperlinkButton控件。很神奇的,WPF居然连HyperlinkButton都没有,不过它提供了另一种方式用于在UI上添加超级链接:

<TextBlock FontSize="20">
<Hyperlink NavigateUri="http://www.google.com" RequestNavigate="Hyperlink_RequestNavigate">
Click here
</Hyperlink>
</TextBlock>
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}

如果需要在超级链接里放图片或其它东西,代码如下:

<TextBlock FontSize="20">
<Hyperlink NavigateUri="https://www.microsoft.com"
RequestNavigate="Hyperlink_RequestNavigate">
<StackPanel Orientation="Horizontal">
<Image Source="Microsoft-Logo1.jpg" Height="20" Width="20"/>
<TextBlock Text="Microsoft" Margin="4,0,0,0" />
</StackPanel>
</Hyperlink>
</TextBlock>

这真是很怪,为什么要先有TextBlock然后再有Hyperlink,为什么TextBlock里面可以放Image,这真的很难理解。

2. Hyperlink怎么设置样式

要给Hyperlink设置样式也有点难搞,因为在对象树上Hyperlink毫无存在感,所以也没办法使用Blend创建它的Style。

我的做法是用ILSpy拿到它的Style再修改。例如我需要MouseOver状态下文字不是红色而是紫色,可以使用下面的Style:

<Style x:Key="{x:Type Hyperlink}"
TargetType="{x:Type Hyperlink}">
<Setter Property="TextElement.Foreground"
Value="{DynamicResource {x:Static SystemColors.HotTrackBrushKey}}" />
<Setter Property="Inline.TextDecorations"
Value="Underline" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(SystemParameters.HighContrast)}"
Value="false" />
<Condition Binding="{Binding Path=IsMouseOver, RelativeSource={RelativeSource Self}}"
Value="true" />
</MultiDataTrigger.Conditions>
<Setter Property="TextElement.Foreground"
Value="#FFFF00FF" />
</MultiDataTrigger>
<Trigger Property="ContentElement.IsEnabled"
Value="False">
<Setter Property="TextElement.Foreground"
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
<Trigger Property="ContentElement.IsEnabled"
Value="True">
<Setter Property="FrameworkContentElement.Cursor"
Value="Hand" />
</Trigger>
</Style.Triggers>
</Style>

3. 自定义一个HyperlinkButton

自定义一个HyperlinkButton有什么好处?因为用起来简单啊,不需要CodeBehind的代码,绑定内容和Command都简单,而且XAML更加简单直观。在外观上,很多人喜欢Hyperlink下面的横线在鼠标MouseOver才显示,另外如上面图片所示插入图片后Hyperlink下面有一条横线,这很奇怪但又取消不了。

Silverlight和UWP都很普通地提供了HyperlinkButton。不过在Silverlight中为了显示MouseOver时出现的下划线使用了两层内容,一层用于正常显示(contentPresenter),另一层用于显示下划线(UnderlineTextBlock),如果HyperlinkButton的内容是文本,当MouseOver时UnderlineTextBlock就会显示UnderlineTextBlock。

<TextBlock x:Name="UnderlineTextBlock"
Text="{TemplateBinding Content}"
TextDecorations="Underline"
Visibility="Collapsed"/>
<ContentPresenter x:Name="contentPresenter"
Content="{TemplateBinding Content}"/>

但是这样效果十分差,重叠在一起的文本看上去变得模糊。

而UWP中的HyperlinkButton的下划线是代码里写死的,大概是这样:

if (VisualTreeHelper.GetChildrenCount(contentPresenter) == 1 && VisualTreeHelper.GetChild(contentPresenter, 0) is TextBlock textBlock)
{
textBlock.TextDecorations = Text.TextDecorations.Underline;
}

而且它还没有提供任何方法关闭或修改这个下划线。我很讨厌这种代码里控制样式的行为,UI和代码应该足够解耦。UWP很多使用代码控制样式的行为,通常宣称理由是为了性能,但Button是整个UI中最不需要性能的部分,毕竟一个UI中不可能有几百个Button,就算有几百个HyperlinkButton,现代的UI框架也不可能仅仅因为下划线就导致性能下降。所以我认为没必要在代码里控制下划线的显示。

而无论Silverlight还是UWP,只要HyperlinkButton的Content不是纯文本就不能显示下划线,这应该也算一个功能缺陷。

我在Kino.Toolkit.Wpf里也提供了一个HyperlinkButton,使用方式如下:

<kino:HyperlinkButton Content="Github"
NavigateUri="https://github.com/DinoChan/Kino.Toolkit.Wpf" />

不仅使用起来简单,HyperlinkButton的代码也很简单。

public Uri NavigateUri
{
get => GetValue(NavigateUriProperty) as Uri;
set => SetValue(NavigateUriProperty, value);
} protected override void OnClick()
{
base.OnClick();
if (NavigateUri != null && NavigateUri.IsAbsoluteUri)
{
try
{
Process.Start(new ProcessStartInfo(NavigateUri.AbsoluteUri));
}
catch (Win32Exception)
{
}
}
}

上面是HyperlinkButton的核心代码,需要一个HyperlinButton被点击后导航到的NavigateUri属性,以及在OnClick函数中使用Process.Start在新进程打开目标Uri。关于Process和ProcessStartInfo的具体用法可见本文最后给出的参考链接。

XAML的部分基本上照抄Silverlight的HyperlinkButton,不过关于下划线的处理稍有不同。

<ControlTemplate.Resources>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ButtonBase}, Path=IsMouseOver}"
Value="True">
<Setter Property="TextDecorations"
Value="Underline" />
</DataTrigger>
</Style.Triggers>
</Style>
</ControlTemplate.Resources>
<Grid Cursor="{TemplateBinding Cursor}"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver" />
<VisualState x:Name="Pressed">
<!--some xaml-->
</VisualState>
<VisualState x:Name="Disabled">
<!--some xaml-->
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups> <ContentPresenter x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}">
<ContentPresenter.Resources>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ButtonBase}, Path=IsMouseOver}"
Value="True">
<Setter Property="TextDecorations"
Value="Underline" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</Grid>

上面是HyperlinkButton的DefaultStyle的大致内容。Pressed和Disabled的状态使用VisualState控制外观,这部分略过。在ControlTemplate.Resources中添加了一个TextBlock的全局样式,里面的DataTrigger设置为当鼠标进入父节点的HyperlinkButton时TextDecorations变为Underline。运行效果如下:

<kino:HyperlinkButton NavigateUri="https://www.microsoft.com/"
Margin="0,16,0,0"
FontSize="20">
<StackPanel Orientation="Horizontal">
<Image Height="20"
Width="20"
Source="/Kino.Toolkit.Wpf.Samples;component/Assets/Images/Microsoft_logo.png" />
<TextBlock Text="Microsoft"
Margin="4,0,0,0"
Resources="{x:Null}" />
</StackPanel>
</kino:HyperlinkButton>

在下面的ContentPresenter.Resources中也添加了同样的DataTrigger,这是为了应对下面这种情况:

<kino:HyperlinkButton Content="Microsoft"
NavigateUri="https://www.microsoft.com/"
Margin="0,16,0,0"
FontSize="20">
<ButtonBase.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Height="20"
Width="20"
Source="/Kino.Toolkit.Wpf.Samples;component/Assets/Images/Microsoft_logo.png" />
<TextBlock Text="Microsoft"
Margin="4,0,0,0" />
</StackPanel>
</DataTemplate>
</ButtonBase.ContentTemplate>
</kino:HyperlinkButton>

这里TextBlock不是HyperlinkButton的逻辑树上的子元素,或许就是因为这样它不能应用ControlTemplate.Resources中的TextBlock的全局样式。

最后记得在最外层的Grid上设置Background:

<Grid Cursor="{TemplateBinding Cursor}" Background="{TemplateBinding Background}">

如果不设置一个透明的background的话,就只有文字部分能捕获鼠标点击事件,这样HyperlinkButton就会很难点中。(我记得在UWP中就没有这个问题,UWP的ContentPresenter自带透明背景)

4. 结语

HyperlinkButton明明很重要但WPF又不提供,幸好自己写起来也很简单。

这么简单的一个控件我也能水这么长的文章,我也很佩服我自己。

5. 参考

Hyperlink Class (System.Windows.Documents) Microsoft Docs

Process Class (System.Diagnostics) Microsoft Docs

ProcessStartInfo Class (System.Diagnostics) Microsoft Docs

6. 源码

HyperlinkButton.cs at master

[WPF自定义控件库] 给WPF一个HyperlinkButton的更多相关文章

  1. [WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画

    1. 前言 这篇文章介绍WPF UI元素的两步布局过程,并且通过Resizer控件介绍只使用Measure可以实现些什么内容. 我不建议初学者做太多动画的工作,但合适的动画可以引导用户视线,提升用户体 ...

  2. WPF 如何创建自己的WPF自定义控件库

    在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...

  3. [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)

    原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...

  4. [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...

  5. [WPF自定义控件库] 让Form在加载后自动获得焦点

    原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...

  6. [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

    1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...

  7. [WPF自定义控件库]简单的表单布局控件

    1. WPF布局一个表单 <Grid Width="400" HorizontalAlignment="Center" VerticalAlignment ...

  8. [WPF自定义控件库]使用WindowChrome的问题

    1. 前言 上一篇文章介绍了使用WindowChrome自定义Window,实际使用下来总有各种各样的问题,这些问题大部分都不影响使用,可能正是因为不影响使用所以一直没得到修复(也有可能别人根本不觉得 ...

  9. [WPF自定义控件库] 自定义控件的代码如何与ControlTemplate交互

    1. 前言 WPF有一个灵活的UI框架,用户可以轻松地使用代码控制控件的外观.例设我需要一个控件在鼠标进入的时候背景变成蓝色,我可以用下面这段代码实现: protected override void ...

随机推荐

  1. TensorFlow笔记-变量,图,会话

    变量 存储一些临时值的作用或者长久存储.在Tensorflow中当训练模型时,用变量来存储和更新参数.变量包含张量(Tensor)存放于内存的缓存区.建模时它们需要被明确地初始化,模型训练后它们必须被 ...

  2. C#编程之JSON序列化与反序列化

    1.在C#管理NuGet程序包中添加Json.NET 2.C#将对象序列化成JSON字符串 模型类1 /// <summary> /// JSON字符串模型.是否出错 /// </s ...

  3. linux下的FTP安装及调优

    前言: 在之前交换平台的开发中,FTP的各种操作算是核心功能点. 在FTP的开发中,遇到了不少坑. 如FTP需要设置被动模式,否则10M以上的包可能会上传失败. 如FTP需要设置囚牢模式,否则访问的文 ...

  4. 从国际象棋与象棋的走法差异,再趣说IT人提升能力和增收方式

    之前我写过篇博文,用象棋的思维趣说IT人的职业发展和钱途,发现象棋中的一些思维能应用到我们程序员平时的职业发展中. 当从大学毕业的程序员干个五六年以后,也达到了高级开发的水平,工作环境应该能摆脱动荡, ...

  5. spring applicationContext.xml文件移到resources目录下

    SpringMVC的框架默认目录结构 修改后的目录结构及web.xml 同时在pom里的配置:将resources目录打包到web-inf/classes目录下<resources>   ...

  6. linux初学者-CIFS网络文件系统篇

    linux初学者-CIFS网络文件系统篇 CIFS是一种通用网络文件系统,主要用于网络设备之间的文件共享.CIFS可以在linux系统和windows系统之间共享文件,因此这种文件系统主要用于客户端是 ...

  7. 第二章 jQuery框架使用准备

    window常用属性: History:有关客户访问过的URL的信息 Location: 有关当前url的信息 常用方法: Confirm()将弹出一个确认对话框 open()在页面上弹出一个新的浏览 ...

  8. JDBC教程

    JDBC代表Java与数据库的连接,这对Java编程语言和广泛的数据库之间独立于数据库的连接标准的Java API. JDBC库包含的API为每个通常与数据库的使用相关联的任务: 使得连接到数据库 创 ...

  9. Linux内容点(部分)

    文件属性 -w      文件或目录,对目前(有效的)用户或组来说是可写的       -x       文件或目录,对目前(有效的)用户或组来说是可执行的       -o       文件或目录, ...

  10. Android native进程间通信实例-binder结合共享内存

    在android源码的驱动目录下,一般会有共享内存的相关实现源码,目录是:kernel\drivers\staging\android\ashmem.c.但是本篇文章不是讲解android共享内存的功 ...