[WPF自定義控件庫] 給WPF一個HyperlinkButton

  • 2019 年 10 月 3 日
  • 筆記

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