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

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

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

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

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

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

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

  1. <Style x:Key="{x:Type Hyperlink}"
  2. TargetType="{x:Type Hyperlink}">
  3. <Setter Property="TextElement.Foreground"
  4. Value="{DynamicResource {x:Static SystemColors.HotTrackBrushKey}}" />
  5. <Setter Property="Inline.TextDecorations"
  6. Value="Underline" />
  7. <Style.Triggers>
  8. <MultiDataTrigger>
  9. <MultiDataTrigger.Conditions>
  10. <Condition Binding="{Binding Path=(SystemParameters.HighContrast)}"
  11. Value="false" />
  12. <Condition Binding="{Binding Path=IsMouseOver, RelativeSource={RelativeSource Self}}"
  13. Value="true" />
  14. </MultiDataTrigger.Conditions>
  15. <Setter Property="TextElement.Foreground"
  16. Value="#FFFF00FF" />
  17. </MultiDataTrigger>
  18. <Trigger Property="ContentElement.IsEnabled"
  19. Value="False">
  20. <Setter Property="TextElement.Foreground"
  21. Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
  22. </Trigger>
  23. <Trigger Property="ContentElement.IsEnabled"
  24. Value="True">
  25. <Setter Property="FrameworkContentElement.Cursor"
  26. Value="Hand" />
  27. </Trigger>
  28. </Style.Triggers>
  29. </Style>

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

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

  1. <TextBlock x:Name="UnderlineTextBlock"
  2. Text="{TemplateBinding Content}"
  3. TextDecorations="Underline"
  4. Visibility="Collapsed"/>
  5. <ContentPresenter x:Name="contentPresenter"
  6. Content="{TemplateBinding Content}"/>

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

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

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

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

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

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

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

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

  1. public Uri NavigateUri
  2. {
  3. get => GetValue(NavigateUriProperty) as Uri;
  4. set => SetValue(NavigateUriProperty, value);
  5. }
  6. protected override void OnClick()
  7. {
  8. base.OnClick();
  9. if (NavigateUri != null && NavigateUri.IsAbsoluteUri)
  10. {
  11. try
  12. {
  13. Process.Start(new ProcessStartInfo(NavigateUri.AbsoluteUri));
  14. }
  15. catch (Win32Exception)
  16. {
  17. }
  18. }
  19. }

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

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

  1. <ControlTemplate.Resources>
  2. <Style TargetType="TextBlock">
  3. <Style.Triggers>
  4. <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ButtonBase}, Path=IsMouseOver}"
  5. Value="True">
  6. <Setter Property="TextDecorations"
  7. Value="Underline" />
  8. </DataTrigger>
  9. </Style.Triggers>
  10. </Style>
  11. </ControlTemplate.Resources>
  12. <Grid Cursor="{TemplateBinding Cursor}"
  13. Background="{TemplateBinding Background}">
  14. <VisualStateManager.VisualStateGroups>
  15. <VisualStateGroup x:Name="CommonStates">
  16. <VisualState x:Name="Normal" />
  17. <VisualState x:Name="MouseOver" />
  18. <VisualState x:Name="Pressed">
  19. <!--some xaml-->
  20. </VisualState>
  21. <VisualState x:Name="Disabled">
  22. <!--some xaml-->
  23. </VisualState>
  24. </VisualStateGroup>
  25. </VisualStateManager.VisualStateGroups>
  26. <ContentPresenter x:Name="contentPresenter"
  27. Content="{TemplateBinding Content}"
  28. ContentTemplate="{TemplateBinding ContentTemplate}"
  29. VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
  30. HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
  31. Margin="{TemplateBinding Padding}">
  32. <ContentPresenter.Resources>
  33. <Style TargetType="TextBlock">
  34. <Style.Triggers>
  35. <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ButtonBase}, Path=IsMouseOver}"
  36. Value="True">
  37. <Setter Property="TextDecorations"
  38. Value="Underline" />
  39. </DataTrigger>
  40. </Style.Triggers>
  41. </Style>
  42. </ContentPresenter.Resources>
  43. </ContentPresenter>
  44. </Grid>

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

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

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

  1. <kino:HyperlinkButton Content="Microsoft"
  2. NavigateUri="https://www.microsoft.com/"
  3. Margin="0,16,0,0"
  4. FontSize="20">
  5. <ButtonBase.ContentTemplate>
  6. <DataTemplate>
  7. <StackPanel Orientation="Horizontal">
  8. <Image Height="20"
  9. Width="20"
  10. Source="/Kino.Toolkit.Wpf.Samples;component/Assets/Images/Microsoft_logo.png" />
  11. <TextBlock Text="Microsoft"
  12. Margin="4,0,0,0" />
  13. </StackPanel>
  14. </DataTemplate>
  15. </ButtonBase.ContentTemplate>
  16. </kino:HyperlinkButton>

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

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

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

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

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

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

Hyperlink Class (System.Windows.Documents) Microsoft Docs

Process Class (System.Diagnostics) Microsoft Docs

ProcessStartInfo Class (System.Diagnostics) Microsoft Docs

HyperlinkButton.cs at master

版权声明:本文为dino623原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dino623/p/WPF_HyperlinkButton.html