目录
介绍
本文详细介绍了如何使用纯WPF技术开发受游戏“英雄联盟”启发的 PLAY 按钮。它强调利用WPF功能创建通用用户界面组件的过程,并为开源开发提供了新的视角。本文还探讨了高级WPF功能(如动画和触发器),以增强用户交互体验。
我们已将本文的内容制作成教学视频,并发布在YouTube上,每个人都可以与视频一起练习。
Youtube:英雄联盟PlayButton- WPF项目
GitHub:英雄联盟PLAY按钮- WPF项目
GitHub:英雄联盟完整项目-WPF
介绍
用户界面组件对于增强用户体验至关重要。在游戏中,响应迅速且具有视觉吸引力的 PLAY 按钮是通往娱乐世界的门户。本文演示了使用WPF创建 PLAY 按钮的过程,WPF为构建丰富的桌面应用程序提供了一个强大的框架。
项目背景
本文中讨论的项目旨在尽可能全面地展示WPF技术的功能。几年前,我们发布了这个项目,并收到了巨大的积极回应,这继续激励我们为开源开发做出贡献。随着.NET技术的发展,我们不断更新和完善以前在GitHub上共享的代码。鉴于整个项目涵盖的内容广泛,我们决定将其分解并详细分析每个部分的组成和技术重点,希望能帮助更多的WPF爱好者完成他们的学习之旅。
按钮组成
通过使用分析器,我们可以看到这个PLAY按钮继承了WPFToggleButton的属性。左侧有一个来自“英雄联盟”游戏的标志,而右侧则包含多个元素,例如具有不同设计的边框、图像和文本。此外,还添加了交互式鼠标悬停和选中的触发效果。
关键内容分析
1. 创建不规则形状
前两个图形可以使用边框控件轻松编码。但是,第三个图形(包括尖头和圆弧)不能使用简单的边框进行编码。因此,我们最初的想法可能是使用多边形和坐标进行绘制。尽管如此,该Polygon属性仍无法提供绘制圆弧的功能。因此,我们应该使用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控件使用路径数据来定义形状,路径数据由一系列命令和坐标组成,这些命令和坐标指定如何绘制形状。
它的基本特性包括:
- 数据属性:该Data属性是Path控件的关键属性,用于指定路径数据,该数据由一系列命令和坐标组成,用于描述形状的轮廓。路径数据的格式包括MoveTo(M)、LineTo(L)、CurveTo(C)、ClosePath(Z)等各种命令,结合坐标来定义形状。通过在Data属性中提供路径数据,我们可以创建各种形状,包括线段、曲线、多边形等。
- Fill属性:该Fill属性用于指定形状内的填充颜色。它允许我们使用颜色、渐变、图案或透明度来填充形状的内部。
- Stroke属性:该Stroke属性用于指定形状轮廓的颜色。它允许您使用各种颜色定义轮廓线的颜色。
- StrokeThickness 属性:该StrokeThickness属性用于指定轮廓线的粗细。它决定了轮廓的宽度。
- 命令和坐标:路径数据由一系列命令和坐标组成,这些命令指示WPF如何将形状从一个点绘制到另一个点。常用路径命令包括:
- M(MoveTo):将绘图点移动到指定的坐标。
- L(LineTo):在指定坐标处画一条直线。
- C(CurveTo):它绘制贝塞尔曲线,使用控制点来定义曲线的形状。
- Z(ClosePath):闭合路径,将当前点与起点连接起来,形成闭合形状。
该Data属性是Path控件的关键属性,用于指定路径数据,其中包括用于定义形状轮廓的命令和坐标。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。 - Gradient方向
Gradient的方向由StartPoint和EndPoint决定。例如,如果StartPoint是(0, 0)和EndPoint是(1, 1),Gradient将从左上角过渡到右下角。 - Gradient类型
LinearGradientBrush默认为线性渐变,其中颜色沿直线过渡。通过调整StartPoint和EndPoint,可以更改Gradient的方向和起点以创建各种Gradient效果。
在这个项目中,我们的目标是创建一个垂直渐变,从形状的中心开始,向下移动。因此,我们设置StartPoint为(0.5, 0),表示梯度的起点位于顶部中心(水平中点)。EndPoint设置为(0.5, 1),表示渐变的终点位于底部中心(水平中点)。
接下来,该GradientStops集合包括三个GradientStop对象,每个对象定义不同的颜色和相对位置:
- 第三个GradientStop:
Color设置为#CC3FE7EE,表示颜色值。
Offset设置为0,表示此颜色位于gradient的起点。 - 第二个GradientStop:
Color设置为#CC006D7D。
Offset设置为0.5,表示此颜色位于gradient的中点。 - 第三个GradientStop:
Color设置为#CC0493A7。
Offset设置为1,表示此颜色位于gradient的终点。
3. 处理厚度的路径和边框
在Border控件中:
- Border控件的边界包含在控件Border本身中。边界线的粗细由BorderThickness属性控制,以设备无关的像素(DIP)为单位指定边界线的宽度。
在Path控件中:
- Path控件的边界线是根据StrokeThickness属性的中心位置绘制的。 StrokeThickness控制边界线的粗细,表示边界线从中心延伸的距离。
在此固定大小的图形中,Border和Path两者的厚度都设置为2,并且Margin设置为4 4 4 4。但是,此设置显示Path的上边界延伸到了Border之外。
因此,考虑到StrokeThickness, Path的Margin级需要调整。左边距已设置为40,可以覆盖GreenLine,因此没有问题。顶部Margin应增加1个像素,设置为5个像素,而右侧和底部Margin无需更改。由于Path具有118x28的固定大小,因此只有左侧和顶部Margin需要调整。
此外,由于顶部Margin增加了5个像素,因此底部可能会出现截断,如本例所示。为防止这种情况,您可以将底部Margin设置为-5像素。这通过删除顶部添加的5个像素来平衡布局。另一种方法是将底部Margin保持在0像素处。这两种方法都可以防止由于添加顶部Margin而切断底部。
4. 使用Jamesnet.WPF Nuget创建动画
<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>
在WPF中,可以创建各种动态动画,使用户界面更具吸引力。在此项目中,厚度动画用于向TextBlock的文本部分添加有趣的动画。
可以使用ControlTemplate.Resources定义动画,这允许您定义两个动画资源:“Checked”和“UnChecked“。选中后,将删除“Play”文本,并将“Stop”文本移入,而在“UnChecked”状态下,将删除“Stop”文本,“Play”文本将移入。这将创建一个类似于翻转效果的动画。
为了便于动画的创建和使用,我们已将WPF中的各种动画编译并组织到Jamesnet.WPF Nuget包中。只需添加此包,即可轻松使用和编写动画。
5.使用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内的上下滚动效果。
https://www.codeproject.com/Articles/5373396/Creating-a-League-of-Legends-Inspired-Play-Button