如何用WPF复现一个《英雄联盟》风格的PLAY 按键|教学视频内容讲解 (1)

这篇文章是对 WPF《 Riot Play Button》教程视频的技术回顾。

源码链接:https://github.com/vickyqu115/riotplaybutton
教学视频:https://bit.ly/49L6dXu

摘要

项目运行效果

本文详细介绍并分析了使用纯 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 ©、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,表示该颜色位于渐变的终点。
3. 处理 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 内实现上下滚动的效果。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值