65.9K
CodeProject 正在变化。 阅读更多。
Home

创建一个受“英雄联盟”启发的 WPF 播放按钮

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (16投票s)

2023年11月28日

CPOL

9分钟阅读

viewsIcon

19113

本文详细解释和分析了如何使用纯 WPF 技术开发一个受《英雄联盟》游戏启发的 PLAY 按钮。

引言

本文详细解释和分析了如何使用纯 WPF 技术开发一个受《英雄联盟》游戏启发的PLAY按钮。它强调了利用 WPF 功能创建多功能用户界面组件的过程,并为开源开发提供了新的视角。文章还探讨了诸如动画和触发器等高级 WPF 功能,以增强用户交互体验。

我们将本文的内容制作成了教学视频并发布到了 YouTube,大家可以在视频中跟着一起练习。

YouTube:《英雄联盟》播放按钮- WPF 项目
GitHub:《英雄联盟》播放按钮 - WPF 项目
GitHub:《英雄联盟》完整项目 - WPF

引言

用户界面组件对于提升用户体验至关重要。在游戏中,一个响应迅速且视觉吸引人的PLAY按钮是通往娱乐世界的门户。本文展示了使用 WPF 创建PLAY按钮的过程,WPF 是构建丰富桌面应用程序的强大框架。

项目背景

本文讨论的项目旨在尽可能全面地展示 WPF 技术的强大功能。我们几年前发布了这个项目,并获得了巨大的积极反响,这持续激励着我们为开源开发做出贡献。随着 .NET 技术的不断发展,我们一直在更新和完善之前在 GitHub 上共享的代码。鉴于该整体项目涵盖的内容非常广泛,我们决定将其分解,并详细分析每个部分的组成和技术重点,希望能帮助更多 WPF 爱好者学习。

按钮组成

通过分析器,我们可以看到这个PLAY按钮继承了WPFToggleButton的属性。左侧是《英雄联盟》游戏的一个标志,而右侧则包含边框、图像和具有不同设计的文本等多个元素。此外,还添加了交互式的鼠标悬停和选中触发器效果。

关键内容分析

1. 创建不规则形状

前两个图形可以使用 Border 控件轻松编码。但是,第三个图形包含一个尖端和一个弧形,无法使用简单的 Border 进行编码。因此,我们最初的想法可能是使用 Polygon 和坐标进行绘制。然而,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 属性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
    GradientStopsGradientStop对象的集合,每个对象定义一个颜色和一个相对位置(Offset)。GradientStopColor属性定义了指定位置的颜色,而Offset属性定义了颜色在渐变中的位置,通常范围从01
  • 渐变方向
    渐变的方向由StartPointEndPoint决定。例如,如果StartPoint(0, 0)EndPoint(1, 1),则渐变将从左上角过渡到右下角。
  • 渐变类型
    LinearGradientBrush默认为线性渐变,颜色沿直线过渡。通过调整StartPointEndPoint,您可以更改渐变的方向和起始点,以创建各种渐变效果。

在此项目中,我们的目标是创建一个从形状中心开始向下延伸的垂直渐变。因此,我们将StartPoint设置为(0.5, 0),表示渐变的起始点位于顶部中心(水平中点)。EndPoint设置为(0.5, 1),表示渐变的结束点位于底部中心(水平中点)。

接下来,GradientStops集合包含三个GradientStop对象,每个对象定义了不同的颜色和相对位置:

  1. 第三个 GradientStop
    Color设置为#CC3FE7EE,表示一个颜色值。
    Offset设置为0,表示此颜色位于渐变的起始点。
  2. 第二个 GradientStop
    Color设置为#CC006D7D
    Offset设置为0.5,表示此颜色位于渐变的中点。
  3. 第三个 GradientStop
    Color设置为#CC0493A7
    Offset设置为1,表示此颜色位于渐变的结束点。

3. 处理 Path 和 Border 的粗细

Border 控件中

  • Border 控件的边框线包含在Border本身内部。边框线的粗细由BorderThickness属性控制,该属性以设备无关像素 (DIP) 为单位指定边框线的宽度。

Path 控件中

  • Path 控件的边框线是根据StrokeThickness属性的中心位置绘制的。StrokeThickness控制边框线的粗细,表示边框线从中心延伸的距离。

在这个固定大小的图形中,BorderPath的粗细都设置为2,并且Margin设置为4 4 4 4。然而,这种设置表明Path的上边框超出了Border

因此,需要根据StrokeThickness调整PathMargin。左侧的 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 中,您可以创建各种动态动画,使用户界面更具吸引力。在此项目中,使用 Thickness 动画为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内实现了上下滚动的效果。

历史

  • 2023年11月29日:初始版本
© . All rights reserved.