实现【英雄联盟】 PLAY 按键
控件名称:RiotPlayButton
作者:WPFDevelopersOrg - Vicky&James
源码链接 [1] :https://github.com/vickyqu115/riotplaybutton
教学视频 [2] (【小李趣味多】https://bit.ly/3xI9DNh)
这篇文章是对
WPF RiotPlayButton
教程视频的技术回顾。
摘要
本文详细介绍并分析了使用纯
WPF
技术开发受【英雄联盟】游戏启发的
PLAY
按钮的过程。本文强调了利用
WPF
功能创建各种用户界面组件的过程,并为开源开发提供了新的视角。同时,探索了动画和触发器等高级
WPF
功能,以提升用户交互体验。
介绍
用户界面组件在提升用户体验方面非常重要。在游戏中,反应迅速且视觉上有吸引力的
PLAY
按钮是通往娱乐世界的门户。本文展示了使用
WPF
这一构建丰富桌面应用程序的强大框架创建
PLAY
按钮的过程。
项目背景
本文讨论的项目尽可能全面地展示了
WPF
技术的能力。几年前发布这个项目后,获得了积极的反馈,这激励我继续为开源开发做贡献。随着
.NET
技术的发展,我不断更新和改进共享在
GitHub
上的代码。考虑到整个项目中包含的内容丰富,我决定详细分析每个部分的组成和技术重点,希望能为更多喜欢
WPF
技术的人提供帮助。
按钮构成
通过分析器可以看到,这个
PLAY
按钮继承了
WPF ToggleButton
的属性。左边是【英雄联盟】游戏的标志,右边包含边界、图像、文本等多种设计元素。此外,还添加了鼠标悬停和检查触发效果。
主要内容分析
1. 创建不规则形状
第一个和第二个图形可以通过使用
Border
控件轻松编码。然而,第三个尖角和弧形的图形不能简单地使用
Border
编码。因此,尽管最初可以使用多边形和坐标来绘制,但多边形属性不提供绘制弧形的功能。因此,必须使用
Path
控件来编码。
详细分析
< style TargetType="{x:Type Path}" x:Key="Arrow">
<Setter Property="Fill" Value="#1E2328"/>
<Setter Property="Stroke" Value="{StaticResource ArrowStroke}"/>
<Setter Property="StrokeThickness" Value="2"/>
<Setter Property="Data" Value="M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 C 10,14 0,0 0,0 Z"/>
<Setter Property="Margin" Value="40 5 4 -5"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="5" ShadowDepth="2"/>
</Setter.Value>
</Setter>
</ style>
在
WPF
中,
Path
控件是用于绘制各种形状和轮廓的强大工具。
Path
控件通过路径数据定义形状,路径数据是一系列指定如何绘制形状的命令和坐标。
Path
控件的主要属性如下:
Data 属性
:Data 属性是 Path 控件的重要属性,用于指定描述形状轮廓的路径数据。路径数据格式包括
MoveTo (M)、LineTo (L)、CurveTo (C)、ClosePath (Z)
等命令与坐标的结合。
Fill 属性
:
Fill
属性用于指定形状内部的填充颜色,可以使用颜色、渐变、图案或透明度填充形状内部。
Stroke 属性
:
Stroke
属性用于指定形状轮廓的颜色,可以使用各种颜色定义轮廓颜色。
StrokeThickness 属性
:
StrokeThickness
属性用于指定轮廓的厚度,决定轮廓的宽度。
命令和坐标
:路径数据是命令和坐标的连续体,这些命令指示
WPF
从一个点到另一个点如何绘制形状。常见的路径命令包括:
M (MoveTo)
:将绘制点移动到指定坐标。
L (LineTo)
:绘制到指定坐标的直线。
C (CurveTo)
:使用控制点绘制贝塞尔曲线。
Z (ClosePath)
:关闭路径,将当前点连接到起点形成闭合形状。
Data
属性是
Path
控件的关键属性,包含定义形状轮廓的路径数据。这些路径数据由一系列命令和坐标组成,描述了路径的轮廓。在这个项目中,路径数据的命令和坐标的详细描述如下:
可以将其简单地解释为
X/Y
坐标轴。将此形状的长度设置为
118
,宽度设置为
28
:
M 0,0
:这是
「MoveTo」
命令,将绘制点移动到坐标
(0, 0)
作为起点。
L 103,0
:这是
「LineTo」
命令,从当前点
(0, 0)
绘制到坐标
(103, 0)
的直线。接着绘制到 (
118, 14)、(103, 28)、(0, 28)
的线段。
由于这是对称形状,第二条线的
Y
坐标是形状总高度的一半,即
14
。
接下来是绘制曲线的部分:
C 10,14 0,0 0,0 z
:这是 「贝塞尔曲线」 命令,定义了前一个点为控制点,后一个点为终点的贝塞尔曲线。这个命令定义了控制点为
(10, 14)
,终点为
(0, 0)
的贝塞尔曲线,并使用
‘z’
命令关闭路径,将其连接回起点
(0, 0)
。
2. 创建渐变颜色
<LinearGradientBrush x:Key="ArrowStroke" StartPoint="0.5,0" EndPoint="0.5,1" >
<GradientStop Color="#CC3FE7EE" Offset="0"/>
<GradientStop Color="#CC006D7D" Offset="0.5"/>
<GradientStop Color="#CC0493A7" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArrowStrokeOver" StartPoint="0.5,0" EndPoint="0.5,1" >
<GradientStop Color="#FFAFF5FF" Offset="0"/>
<GradientStop Color="#FF46E6FF" Offset="0.5"/>
<GradientStop Color="#FF00ADD4" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ArrowFillOver" StartPoint="0.5,0" EndPoint="0.5,1" >
<GradientStop Color="#FF1D3B4A" Offset="0"/>
<GradientStop Color="#FF082734" Offset="1"/>
</LinearGradientBrush>
在游戏的这个部分,描边部分并不是简单的纯色,而是由多种色调组成的渐变颜色。为了实现这个效果,我们可以使用
LinearGradientBrush
来自定义颜色。
LinearGradientBrush 的主要属性及使用方法
StartPoint 和 EndPoint
:
StartPoint
指定渐变的起点,通常使用相对坐标表示。这里 (0, 0) 是左上角,(1, 1) 是右下角。
EndPoint
使用相对坐标指定渐变的终点。
GradientStops
:
GradientStops
是
GradientStop
对象的集合,每个对象定义颜色和相对位置(Offset)。
GradientStop
的
Color
属性定义指定位置的颜色,
Offset
属性定义渐变中的颜色位置,通常在
0
到
1
之间。
渐变方向
:渐变的方向由
StartPoint
和
EndPoint
决定。例如,
StartPoint
为
(0, 0)
,
EndPoint
为
(1, 1)
时,渐变从左上角到右下角。
渐变类型
:
LinearGradientBrush
默认设置为线性渐变,颜色沿直线渐变。通过调整
StartPoint
和
EndPoint
,可以改变渐变的方向和起点,生成各种渐变效果。
在这个项目中,我们希望创建一个从形状中央开始向下移动的垂直渐变。因此,将
StartPoint
设置为
(0.5, 0)
指定渐变的起点为顶部中央,将
EndPoint
设置为
(0.5, 1)
指定渐变的终点为底部中央。
然后,
GradientStops
集合包含三个
GradientStop
对象,每个对象定义了不同的颜色和相对位置:
第一个
GradientStop
:
Color
设置为
#CC3FE7EE
。
Offset
设置为
0
,表示该颜色位于渐变的起点。
第二个
GradientStop
:
Color
设置为
#CC006D7D
。
Offset
设置为
0.5
,表示该
颜色位于渐变的中间。
第三个
GradientStop
:
Color
设置为
#CC0493A7
。
Offset
设置为 1,表示该颜色位于渐变的终点。
处理
Path
和Border
厚度
在
Border
控件中:
Border
控件的边框线包含在
Border
内。边框线的厚度由
BorderThickness
属性控制,以设备独立像素(
DIPs
)指定边框线的宽度。
在
Path
控件中:
Path
控件的边框线以
StrokeThickness
属性的中心位置为基准绘制。StrokeThickness 控制边框线的厚度,表示边框线从中心延伸的距离。
在这个固定大小的图形中,将
Border
和
Path
的厚度都设置为
2
,边距设置为
4 4 4 4
。但在这种设置下,可以看到
Path
的上边框超出了
Border
。因此,需要根据
StrokeThickness
调整
Path
的边距。左边距已经设置为
40
,可以覆盖
GreenLine
,所以没有问题。上边距需要增加
1
像素,设置为
5
像素,右边距和下边距不需要更改。由于
Path
的尺寸固定为
118x28
,只需调整左边和上边的边距。
此外,上边距增加
5
像素后,下边看起来可能会被裁剪。为避免这种情况,可以将下边距设置为
-5
像素,这样上边增加的
5
像素会被去掉,布局会保持平衡。另一种方法是保持下边距为
0
像素。这两种方法都可以通过增加上边距来防止下边被裁剪。
4. 使用 Jamesnet.WPF Nuget 生成动画
在
WPF
中,可以生成各种动态动画来使用户界面更加有趣。在这个项目中,使用厚度动画为
TextBlock
的文本部分添加有趣的动画效果。
<Application x: class="VickyPlayButton.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:james="https://jamesnet.dev/xaml/presentation"
StartupUri="MainWindow.xaml">
<Application.Resources>
< style TargetType="{x:Type ToggleButton}">
<Setter Property="Height" Value="38"/>
<Setter Property="Width" Value="165"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<ControlTemplate.Resources>
<Storyboard x:Key="Checked">
<james:ThickItem Mode="CubicEaseInOut" TargetName="play" Property="Margin" Duration="0:0:0:0.5" To="30 100 0 0"/>
<james:ThickItem Mode="CubicEaseInOut" TargetName="stop" Property="Margin" Duration="0:0:0:0.5" To="30 0 0 0"/>
</Storyboard>
<Storyboard x:Key="UnChecked">
<james:ThickItem Mode="CubicEaseInOut" TargetName="play" Property="Margin" Duration="0:0:0:0.5" To="30 0 0 0"/>
<james:ThickItem Mode="CubicEaseInOut" TargetName="stop" Property="Margin" Duration="0:0:0:0.5" To="30 0 0 100"/>
</Storyboard>
</ControlTemplate.Resources>
<Grid Background="{TemplateBinding Background}">
<Border style="{StaticResource GoldLine}"/>
<Image style="{StaticResource Emblem}"/>
<Border style="{StaticResource GreenLine}"/>
<Path x:Name="path" style="{StaticResource Arrow}"/>
<Grid>
<Grid.Clip>
<RectangleGeometry Rect="0,5,165,28"/>
</Grid.Clip>
<TextBlock x:Name="play" style="{StaticResource Play}"/>
<TextBlock x:Name="stop" style="{StaticResource Stop}"/>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="path" Property="Fill" Value="{StaticResource ArrowFillOver}"/>
<Setter TargetName="path" Property="Stroke" Value="{StaticResource ArrowStrokeOver}"/>
<Setter Property="Foreground" Value="#FFFCF1DC"/>
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="path" Property="Fill" Value="#1E2328"/>
<Setter TargetName="path" Property="Stroke" Value="#5C5B57"/>
<Setter Property="Foreground" Value="#3C3C41"/>
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource Checked}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource UnChecked}"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</ style>
</Application.Resources>
</Application>
动画可以通过
ControlTemplate.Resources
定义,定义了
「Checked」
和
「UnChecked」
两个动画资源。在
「Checked」
状态下,
「Play」
文本消失,
「Stop」
文本出现;在
「UnChecked」
状态下,
「Stop」
文本消失,
「Play」
文本出现。这就创建了一个翻转效果的动画。
为了更容易地创建和使用动画,我将各种
WPF
动画编译成了
Jamesnet.WPF Nuget
包。只需添加这个包,就可以轻松地使用和编写动画。
5. 使用 Grid.Clip 属性
<Grid Background="{TemplateBinding Background}">
<Border style="{StaticResource GoldLine}"/>
<Image style="{StaticResource Emblem}"/>
<Border style="{StaticResource GreenLine}"/>
<Path x:Name="path" style="{StaticResource Arrow}"/>
<Grid>
<Grid.Clip>
<RectangleGeometry Rect="0,5,165,28"/>
</Grid.Clip>
<TextBlock x:Name="play" style="{StaticResource Play}"/>
<TextBlock x:Name="stop" style="{StaticResource Stop}"/>
</Grid>
</Grid>
由于
Grid
内的元素相互重叠,在创建文本上下滚动动画时,可能会出现文本超出边框的视觉问题。为了解决这个问题,使用
<Grid.Clip>
属性。
<Grid.Clip>
是一个
XAML
元素,用于定义子元素的可见区域。剪辑区域通常是一个矩形,只有剪辑区域内的内容才会显示,超出部分将被隐藏。
在这个项目中,
<Grid.Clip>
区域设置在
Path
的尺寸范围内:
Rect="0,5,165,28"
。这样,文本只会在这个区域内显示,从而在
Path
内实现上下滚动的效果。
* 视频内容纠正
最近有一位观众指出了我们视频中的一个错误,我在此做出更正和说明。
11:34
贝塞尔曲线里
C
不是弧线的中点吧,控制点是在线外边的
「贝塞尔曲线中,
C
点不是曲线的中点。控制点是在曲线外的。」
在
11:34
的视频里,会看到以下内容(图片与视频说明一致)。
经过重新审视,我发现确实在解释贝塞尔曲线时出现了误解。感谢这位观众的指正,现在我来进行修正。
首先,视频中使用的路径如下:
M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 C 10,14 0,0 0,0 Z
Y轴的0,0基准反转对理解没有太大影响,请大家谅解。
通过生成的图表可以看到,
C (10, 14)
控制点的位置实际上在曲线的外部,而不是中点。这正是观众指出的问题。
接下来,我们详细了解一下三次贝塞尔曲线的机制。
三次贝塞尔曲线需要
4
个点:
P0, P1, P2, P3
。根据当前的几何路径数据,分别映射如下:
P0: 0,28 (起点)
P1: 10,14 (控制点1)
P2: 0,0 (控制点2)
P3: 0,0 (终点)
此外,我们绘制了曲线随时间变化的过程。
蓝色线段的 起点和终点 随着控制点的移动而绘制出曲线。具体的可视化效果请参见 这里 [3] ,该博客中有很好的动画说明。
在这种情况下,使用二次贝塞尔曲线比三次贝塞尔曲线更合适,因为二次贝塞尔曲线需要的控制点更少。
三次贝塞尔曲线 (Cubic Bezier)
M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 C 10,14 0,0 0,0 Z
二次贝塞尔曲线(Quadratic Bezier)
M 0,0 L 103,0 L 118,14 L 103,28 L 0,28 Q 10,14 0,0 Z
综上所述,在这种情况下使用二次贝塞尔曲线更为合适。
沟通与支持
我们随时保持沟通渠道开放。大家可以通过以下方式与我们互动:
GitHub [4] : 关注、Fork、Stars
BiliBili [5] : 一键三连
参考资料
[1]
源码链接:
https://github.com/vickyqu115/riotplaybutton
教学视频:
https://bit.ly/3xI9DNh
这里:
https://blog.naver.com/kyuniitale/40022945907
GitHub:
https://github.com/vickyqu115/smartdate
BiliBili:
https://bit.ly/3xI9DNh