[WPF自定义控件库] 给WPF一个HyperlinkButton

2022-10-17,,,

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.doc》

下载本文的Word格式文档,以方便收藏与打印。