UWP WinUI 制作一个路径矢量图标按钮样式入门

news/2024/10/5 4:54:19/文章来源:https://www.cnblogs.com/lindexi/p/18288632

本文将告诉大家如何在 UWP 或 WinUI3 或 UNO 里,如何制作一个路径按钮。路径按钮就是使用几何路径轮廓表示内容的按钮,常见于各种图标按钮,或 svg 系贴图矢量图按钮

在网上有非常多矢量图库,其中免费的图库也非常多,比如 https://www.iconfont.cn/ 等等。在咱的应用程序里面,可以使用这些矢量图作为按钮的图标,从而更好的进行表意,让界面有更好的设计。使用矢量图还自然带有缩放时依然清晰的功能

最为简单的制作方式就是在按钮里面存放一个 Path 作为内容,比如做一个简单的路径矢量图标按钮

    <Button HorizontalAlignment="Center" VerticalAlignment="Center"><Path Stroke="Black" StrokeThickness="1" Data="M5,5L15,15L5,25"></Path></Button>

可以看到简单的几行代码就可以实现一个图标按钮的功能了。不过哩作为有追求的开发者,可不能像在树上的小猫一样,咱还需要多加一些需求。比如我希望鼠标移动到按钮上的时候,按钮可以变色,比如说我感觉上面的重复代码多了,即我有多个图标按钮都有大量相似的代码,能不能做一个样式实现这些功能?当然是可以的啦

先在一个资源里面定义按钮的样式,资源可以放在自己的应用业务代码 xaml 文件里面,也可以单独做一个资源字典。本文为了简单,就放在 MainPage.xaml 里面了。如果大家想要放在资源字典里面,别忘了引用资源字典哦

    <Style x:Key="Style.Button.PathButtonStyle" TargetType="Button">...</Style>

如上面代码,就定义了一个名为 Style.Button.PathButtonStyle 的代码样式。这样的样式命名方法是我习惯用的,因为如此可以方便一级级点下去,特别在有 ReSharper 的帮助下,会更加好用,在样式特别多的时候,这样写能够和 ReSharper 更好的进行配合

这样的样式,可以应用到按钮代码上,如下面代码

   <Button Style="{StaticResource Style.Button.PathButtonStyle}".../>

此样式都是给路径图标按钮制作,可以制作非常明确的按钮样式实现。对于 xaml 的界面样式实现的编码思路有些会和 C# 不一样,即不追求抽象性,有很多界面逻辑都是越具体越好,且允许有一些代码是重复的。核心追求就是让界面代码在看的时候可以更好的和界面效果联系起来,按照界面组织的方式走而不是按照逻辑的组织方式走。且有些界面效果是追求界面像,而不追求逻辑合理,即只要界面像就好更重要,当然,能两者都兼顾那是最好的。放心,本文提供的方法还是两者都兼顾的。那是否只有本文介绍的附加属性的方法才是最佳实践?当然不是啦,只不过这个方法我用的顺手而已

    <Style x:Key="Style.Button.PathButtonStyle" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid x:Name="RootGrid" Background="{TemplateBinding Background}"><Path x:Name="ButtonContentPath" StrokeThickness="1" Stroke="Black" Data="M5,5L15,15L5,25"></Path></Grid></ControlTemplate></Setter.Value></Setter></Style>

如上面代码,即定义了一个图标按钮的样式,也写明了图标按钮的内容,应用此样式的按钮即可显示出也如上图的效果

样式自然是追求一定的通用性的,上面代码只能显示固定的路径图标,自然不符合咱的需求。能否让 ButtonContentPath 的 Data 参与业务定制?自然是可以的,接下来咱使用简单的附加属性来解决此问题

通过附加属性的方式,既可以用在 UWP 等框架上,同样在 WPF 里面也是可以使用的,毕竟都是相同系列的框架

在后台 cs 代码里面定义一个名为 ButtonHelper 的类型,将在这个类型里面定义附加属性,实现代码如下

public class ButtonHelper
{public static readonly DependencyProperty ButtonPathProperty = DependencyProperty.RegisterAttached("ButtonPath", typeof(string), typeof(ButtonHelper), new PropertyMetadata(default(string)));public static void SetButtonPath(DependencyObject element, string value){element.SetValue(ButtonPathProperty, value);}public static string GetButtonPath(DependencyObject element){return (string)element.GetValue(ButtonPathProperty);}... // 忽略其他代码
}

如此即可在样式里面进行绑定 Data 的内容,核心代码如下

<Path x:Name="ButtonContentPath" StrokeThickness="2" Stroke="#FF666666" Data="{Binding  RelativeSource={RelativeSource TemplatedParent},Path=(local:ButtonHelper.ButtonPath)}"></Path>

如果大家看了本文的内容不知道代码写在哪,可以到本文末尾获取所有代码的下载方法,拉取我的代码跑跑看

通过以上代码,可以看到使用 (local:ButtonHelper.ButtonPath) 将 Data 绑定到 ButtonHelper 的 ButtonPath 附加属性上,属性源是通过 RelativeSource={RelativeSource TemplatedParent} 指定的,在这里就是按钮本身。以上代码的 local: 的 local 表示的 xaml 命名空间,这是因为我将 ButtonHelper 放在和 MainPage 相同的命名空间上,于是就刚好就是 local 的值,如果大家放在其他命名空间,还请在 VisualStudio 的帮助下进行命名空间引用 。以上代码的细节在于必须通过 RelativeSource 和 TemplatedParent 指定,且使用 Binding 进行绑定,不能通过 TemplateBinding 和 Source 指定绑定

应用以上样式的按钮,需要在按钮上给 ButtonHelper 的 ButtonPath 附加属性进行赋值,如以下代码

    <Button Style="{StaticResource Style.Button.PathButtonStyle}"local:ButtonHelper.ButtonPath="M5,5L15,15L5,25".../>

运行代码也可以看到大概如上图的效果,也就是本文以上提供的三个方式都是实现相同的一个按钮效果。可以看到第一个代码最简单,最后一个代码最有通用性,可以将更多的图标按钮使用样式减少重复的代码

那接下来给样式提出更多的要求,如鼠标移动到按钮上方时,修改按钮的图标颜色

对于 Path 元素来说,可以通过 Stroke 和 StrokeThickness 分别修改轮廓颜色画刷和轮廓线条粗细,可以使用 Fill 属性修改填充画刷。在鼠标移动到按钮上方,即 PointerOver 时,通过设置轮廓画刷或填充画刷即可修改按钮的图标颜色

期望在鼠标移动到按钮上方,即 PointerOver 时,设置轮廓画刷或填充画刷,需要配合 VisualStateManager 提供的多个视觉状态,在每个视觉状态下给属性赋值或制作动画

最常用的 VisualStateManager 的 VisualStateGroup 是 CommonStates 组,基础代码组成如下

              <VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Normal"><!-- 正常状态 --></VisualState><VisualState x:Name="PointerOver"><!-- 鼠标移动到控件上,即 WPF 的 Hover 效果 --></VisualState><VisualState x:Name="Pressed"><!-- 鼠标按下的状态,或叫点击的状态 --></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups>

这里有一些细节事项:

  • 由于 VisualStateManager 必须放在容器里面,因此这里必须需要有一个容器控件住 VisualStateManager 的代码。简单的容器就是如 Grid 等控件,想省一点资源的话,可以用 Border 代替 Grid 做容器
  • 一般情况下,第一个状态是 Normal 状态,里面啥都不用做,啥都不用做可以清空其他的 VisualState 的状态。正常状态需要放在第一个

开始编写正式的代码之前,先复习一下 VisualStateManager 的用法,如下面的代码,既可以在 VisualState 里面使用 Setter 修改属性。也可以使用 Storyboard 做动画修改属性。做动画的方式可以比较柔和,有渐变的效果

  <Page.Resources><Style x:Key="Style.Button.FooButtonStyle" TargetType="Button"><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid x:Name="RootGrid" Background="{TemplateBinding Background}"><!-- 由于 VisualStateManager 必须放在容器里面,因此这里必须需要有一个容器 想省一点的话,可以用 Border 代替 Grid 做容器--><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Normal"><Storyboard><!--啥都不用做,清空状态即可--><!-- 正常状态需要放在第一个 --></Storyboard></VisualState><VisualState x:Name="PointerOver"><!-- Hover 效果 --><VisualState.Setters><Setter Target="ButtonContentPath.Stroke" Value="Transparent"></Setter><Setter Target="ButtonContentPath.Fill" Value="Blue"></Setter></VisualState.Setters></VisualState><VisualState x:Name="Pressed"><!-- 按下状态 --><Storyboard><!-- 颜色用 ColorAnimation 也可以 --><!-- <ColorAnimation To=""></ColorAnimation> --><ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonContentPath" Storyboard.TargetProperty="Stroke" ><DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonContentPath" Storyboard.TargetProperty="Fill" ><DiscreteObjectKeyFrame KeyTime="0" Value="#FF666666"/></ObjectAnimationUsingKeyFrames></Storyboard></VisualState><VisualState x:Name="Disabled"><!-- 不可用的状态 --></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><Path x:Name="ButtonContentPath" StrokeThickness="2" Stroke="#FF666666" Data="M7,15C6.85289858,15.5677816,6.85289858,16.4322348,7,17L22,29C22.7348015,29.3762198,24,28.8227297,24,28L24,4C24,3.1772867,22.7348015,2.62379657,22,3L7,15z"></Path></Grid></ControlTemplate></Setter.Value></Setter></Style></Page.Resources><StackPanelHorizontalAlignment="Center"VerticalAlignment="Center"><Border BorderBrush="Black" BorderThickness="0"><Button Style="{StaticResource Style.Button.FooButtonStyle}"Background="Transparent" Padding="0"></Button></Border></StackPanel>

以上代码的细节点在于想要让 VisualStateManager 的 VisualStateGroups 生效,必须放在容器里面,直接写在 ControlTemplate 下面是不行的。第一个 Normal 的 VisualState 需要放在最前面,里面可以不写任何的代码,将会自动清空状态

也如上面代码,设置属性的值时候,既可以使用 Setters 的方式,也可以使用动画的方式。使用 Setters 的代码比较短,如下面代码

 <VisualState x:Name="PointerOver"><!-- Hover 效果 --><VisualState.Setters><Setter Target="ButtonContentPath.Stroke" Value="Transparent"></Setter><Setter Target="ButtonContentPath.Fill" Value="Blue"></Setter></VisualState.Setters></VisualState>

使用 Setters 时不需要管 Property 属性,只需要保证 Target 是 对象.属性 的写法就好了

使用动画的例子如下

 <VisualState x:Name="Pressed"><!-- 按下状态 --><Storyboard><!-- 颜色用 ColorAnimation 也可以 --><!-- <ColorAnimation To=""></ColorAnimation> --><ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonContentPath" Storyboard.TargetProperty="Stroke" ><DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/></ObjectAnimationUsingKeyFrames><ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonContentPath" Storyboard.TargetProperty="Fill" ><DiscreteObjectKeyFrame KeyTime="0" Value="#FF666666"/></ObjectAnimationUsingKeyFrames></Storyboard></VisualState>

动画代码比较多,但是可以实现比较柔和的效果。因为 Setters 是立刻变化的,动画可以实现慢慢变化。对于视觉效果比较大的范围,推荐使用动画

具体一个控件有哪些 VisualState 可以设置,需要查阅文档,详细请看: https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.visualstate

了解基础用法之后,咱就可以继续在 ButtonHelper 里面定义鼠标移动到按钮上的边框轮廓颜色画刷附加属性,如下面代码

public class ButtonHelper
{public static readonly DependencyProperty PointerOverStrokeBrushProperty = DependencyProperty.RegisterAttached("PointerOverStrokeBrush", typeof(Brush), typeof(ButtonHelper), new PropertyMetadata(default(Brush)));public static void SetPointerOverStrokeBrush(DependencyObject element, Brush value){element.SetValue(PointerOverStrokeBrushProperty, value);}public static Brush GetPointerOverStrokeBrush(DependencyObject element){return (Brush)element.GetValue(PointerOverStrokeBrushProperty);}
}

定义完成附加属性,尝试在 XAML 里面使用这个属性。正常的使用方法是会在样式里面,给定附加属性初值的。为什么不在附加属性定义的时候,写附加属性默认值?这是因为不同的样式一般都会有样式自身期望的初值,因此作为样式使用的附加属性,比较少会配置默认值。但也不是不能,取决于你的开森

    <Style x:Key="Style.Button.PathButtonStyle" TargetType="Button"><Setter Property="local:ButtonHelper.PointerOverStroke" Value="#FF666666"/>...</Style>

将此 PointerOverStroke 在 PointerOver 赋值给到按钮的 Path 上,代码如下,以下代码使用 Setter 的方式赋值,代码看起来比较短

                  <VisualState x:Name="PointerOver"><VisualState.Setters><Setter Target="ButtonContentPath.Stroke" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=(local:ButtonHelper.PointerOverStrokeBrush)}"></Setter></VisualState.Setters></VisualState>

如此即可完成样式的对鼠标移动到按钮上,按钮使用附加属性配置的颜色的定义

按钮可以通过 PointerOverStrokeBrush 附加属性定义按钮期望鼠标移动到按钮上的颜色画刷,如以下代码

    <Button Style="{StaticResource Style.Button.PathButtonStyle}"local:ButtonHelper.ButtonPath="M5,5L15,15L5,25"local:ButtonHelper.PointerOverStrokeBrush="Blue"></Button>

可以看到,在完成样式定义的基础上,只需简单的代码就可以让按钮工作起来了

如果刚好有一组按钮都需要做相同的鼠标移动到按钮上的 Hover 颜色画刷更改,可以再定义一个新的样式,继承 Style.Button.PathButton 样式,如以下代码

    <Style x:Key="Style.Button.SlideButtonStyle" TargetType="Button" BasedOn="{StaticResource Style.Button.PathButton}"><Setter Property="Width" Value="50"></Setter><Setter Property="Height" Value="50"></Setter><Setter Property="local:ButtonHelper.PointerOverStrokeBrush" Value="#FF996666"/></Style>

如此可以在不影响阅读界面代码的前提下,减少界面代码的重复量,也能让界面代码编写更加方便

本文定义的 Style.Button.PathButton 按钮样式的代码如下

    <Style x:Key="Style.Button.PathButtonStyle" TargetType="Button"><Setter Property="Background" Value="Transparent"></Setter><Setter Property="HorizontalAlignment" Value="Center"></Setter><Setter Property="local:ButtonHelper.PointerOverStrokeBrush" Value="#FF666666"/><Setter Property="VerticalAlignment" Value="Center"></Setter><Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button"><Grid x:Name="RootGrid" Background="{TemplateBinding Background}"><VisualStateManager.VisualStateGroups><VisualStateGroup x:Name="CommonStates"><VisualState x:Name="Normal"></VisualState><VisualState x:Name="PointerOver"><VisualState.Setters><Setter Target="ButtonContentPath.Stroke" Value="{Binding RelativeSource={RelativeSource TemplatedParent},Path=(local:ButtonHelper.PointerOverStrokeBrush)}"></Setter></VisualState.Setters></VisualState><VisualState x:Name="Pressed"></VisualState></VisualStateGroup></VisualStateManager.VisualStateGroups><Path x:Name="ButtonContentPath" StrokeThickness="2" Stroke="#FF666666" Data="{Binding RelativeSource={RelativeSource TemplatedParent},Path=(local:ButtonHelper.ButtonPath)}"></Path></Grid></ControlTemplate></Setter.Value></Setter></Style>

本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 2eb5da7c4a63d65e1a2424ca40e2ae94f5da7549

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 2eb5da7c4a63d65e1a2424ca40e2ae94f5da7549

获取代码之后,进入 UnoDemo/PathButtonDemo 文件夹,即可获取到源代码

更多 UWP 或 WinUI3 或 UNO 开发教程,请参阅 博客导航

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/739820.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【攻防技术系列+代理转发】工具--netcat

【需求】现在想要实现两个不同网段的私网之间相互通信,我们该如何做呢?🔴实验环境:【kali(攻击端)】:192.168.10.131 【centos7(跳板机)】:192.168.10.39;172.16.80.130 【win7】:172.16.80.131 工具:netcat【kali】: 开启监听【centos7】:【kali】: 获得对方的…

基于FPGA的A律压缩解压缩verilog实现,包含testbench

1.算法仿真效果 VIVADO2019.2仿真结果如下(完整代码运行后无水印):RTL图如下所示:2.算法涉及理论知识概要A律压缩是一种广泛应用于语音编码的非均匀量化技术,尤其在G.711标准中被欧洲和中国等国家采纳。该技术的核心目的是在有限的带宽下高效传输语音信号,同时保持较高的…

LFU算法实现

LFU (Least Frequently Used) 是一种用于缓存管理的算法。它通过跟踪每个缓存项被访问的频率来决定哪些项应该被移除。LFU算法倾向于保留那些使用频率较高的项,而移除那些使用频率较低的项。以下是LFU算法的详细介绍: 工作原理计数器:每个缓存项都有一个计数器,用于记录该项…

灰色预测GM(1,1)模型的理论原理

灰色预测是对时间有关的灰色过程进行预测。通过建立相应的微分方程模型,从而预测事物未来发展趋势的状况。 由于笔者的水平不足,本章只是概括性地介绍GM(1,1)模型的理论原理,便于对初学者的初步理解 目录一、灰色系统二、GM(1,1)灰色预测模型1.生成累加数据与紧临均值生成…

JMonkeyEngine——材质文件备注

默认J3M编辑器不支持编辑纹理参数的Mag/Min滤波选项,只能配置Flip和Wrap模式,但是可以单独编辑J3M源码,如下: 添加你需要的Mag/Min滤波选项,参考源码的解析,就是Mag/Min+拼接对应的Filter值。 虽然打开J3M编辑器会报错: 但实际进游戏时并不会报错,而且一切正常,如下:…

04-JS中的面向对象ES5

JS面向对象基础01 JS对象中key的类型02 创建对象的方法03 对象的常见操作 3.1 访问对象的属性 <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="I…

程序员的AI工作流

AI 工具在日常工作中的应用逐渐成为程序员必备利器。本文介绍了作者常用的一些 AI 工具及使用方式,涵盖需求文档分析、技术文档编写、编程、PR/CR 和技术调研等工作内容,为提升工作效率提供了有力支持。作为一名程序员, 我现在已经深刻的体会到了AI带来的巨大的工作提升 本文…

An Attentive Inductive Bias for Sequential Recommendation beyond the Self-Attention

目录概符号说明BSARec (Beyond Self-Attention for Sequential Recommendation)代码Shin Y., Choi J., Wi H. and Park N. An attentive inductive bias for sequential recommendation beyond the self-attention. AAAI, 2024.概 本文在 attention block 中引入高低频滤波. 符…

[Leetcode]经典算法

检测环 快慢指针法是一种用于检测链表中是否存在环的有效方法,同时也可以找到环的起点。该方法的原理基于两个指针在链表上同时移动,其中一个移动得更快,而另一个移动得更慢。检测环的存在:使用两个指针,一个称为快指针(fast),一个称为慢指针(slow)。 在每一步中,快…

关于import multiprocessing引用出错

关于import multiprocessing引用出错 0. 原因 当前文件名与python包体中关键词出现同名,导致循环引用 1. 排查过程 问题代码 import timefrom multiprocessing import Process, Queue # 这里提示错误def producer(queue):queue.put("a")time.sleep(2)def consumer(q…

进程信号

进程信号的产生,本质,进程信号的操作,进程信号的底层实现,以及阻塞信号,屏蔽信号1. 信号的产生 1.1 信号概念在生活中有很多的信号在我们身边围绕,例如红绿灯,发令枪,上课铃等等 在接受到信号,我们可以做出三种动作 1.立马去做对应信号的事情 2.等一会再做,有自己的…