WPF 路径动画完全指南:自绘制控件实战

WPF Shape系列控件的详细使用教程 WPF(Windows Presentation Foundation)中的 Shape 控件提供了一系列用于绘制几何形状的元素,如线条、矩形、椭圆、多边形等。这些控件继承自 System.Windows.Shapes.Shape 基类,具有一致的属性和行为,允许开发者轻松地在应用程序中创建丰富的图形界面。 阅读详情

一、核心概念

1.1 什么是路径动画?

路径动画 = PathGeometry 驱动 UI 元素运动

区别于普通线性动画(From/To/By)和关键帧动画,路径动画以 PathGeometry 为核心,让 UI 元素沿任意复杂轨迹(直线、曲线、弧线、复合图形)运动,还能同步控制旋转、缩放。

自绘制控件 是通过 Path、Polygon、Ellipse、Rectangle 等基础几何元素手动绘制 UI,优势:

  • 样式完全可定制
  • 交互灵活控制
  • 与路径动画天然适配
  • 摆脱默认控件样式束缚
1.2 三大核心路径动画类
动画类驱动类型核心用途关键特性
DoubleAnimationUsingPathDouble 数值平移、角度、透明度控制需绑定 X/Y/Angle,灵活控制单一属性
PointAnimationUsingPathPoint 点直接定位(Canvas.Left/Top)无需拆分 X/Y,简洁高效
MatrixAnimationUsingPathMatrix 矩阵复合变换(平移+旋转)DoesRotateWithTangent 自动随路径旋转
1.3 PathGeometry 路径语法

路径动画的"轨迹蓝图",XAML 中用 Figures 属性描述:

指令含义语法示例
M移动到起点M 50,120
L画直线L 550,120
C三次贝塞尔曲线C 150,30 400,220 550,120
A圆弧A 50,50 0 1,1 200,120
Z闭合路径Z

常用路径示例:

<!-- 1. 直线路径 -->
<!-- M 50,50:画笔移动到坐标(50,50),不绘图 -->
<!-- L 550,50:从当前点直线连接到坐标(550,50) -->
<PathGeometry x:Key="LinePath" Figures="M 50,50 L 550,50"/>

<!-- 2. 三次贝塞尔曲线路径 -->
<!-- M 50,120:起点坐标(50,120) -->
<!-- C 150,30 400,220 550,120:三次贝塞尔曲线 -->
<!-- 150,30=第一个控制点  400,220=第二个控制点  550,120=曲线终点 -->
<PathGeometry x:Key="CurvePath" Figures="M 50,120 C 150,30 400,220 550,120"/>

<!-- 3. 圆弧路径 -->
<!-- M 100,220:圆弧起点坐标(100,220) -->
<!-- A 50,50 0 1,1 200,220:画圆弧 -->
<!-- 50,50=横向半径、纵向半径  0=圆弧旋转角度 -->
<!-- 1=大弧模式  1=顺时针绘制  200,220=圆弧终点坐标 -->
<!-- Z=闭合路径,终点连回起点形成封闭图形 -->
<PathGeometry x:Key="ArcPath" Figures="M 100,220 A 50,50 0 1,1 200,220 Z"/>

<!-- 4. 复合组合路径 -->
<!-- M 50,300:起始坐标(50,300) -->
<!-- L 150,300:画直线到(150,300) -->
<!-- C 200,230 300,370 400,300:接一段三次贝塞尔曲线 -->
<!-- L 550,300:曲线结束后再画直线到(550,300) -->
<PathGeometry x:Key="CompositePath" Figures="M 50,300 L 150,300 C 200,230 300,370 400,300 L 550,300"/>

image

1.4 DoubleAnimation 基础动画

常与路径动画搭配实现复合动效:

核心属性作用说明
From动画起始值(可选)
To动画结束值(必选)
Duration动画时长,格式 时:分:秒
RepeatBehavior重复行为,Forever 无限循环
AutoReverse是否反向播放

1.5 自绘制控件基础

Ellipse(椭圆/圆形)

<!-- Ellipse 圆形控件 -->
<!-- Canvas.Left="50":距离画布左侧50像素(X轴坐标) -->
<!-- Canvas.Top="40":距离画布顶部40像素(Y轴坐标) -->
<!-- Width/Height="30":圆形宽度和高度均为30像素 -->
<!-- Fill="#E53935":内部填充颜色为红色 -->
<!-- Stroke="Black":边框颜色为黑色 -->
<!-- StrokeThickness="1":边框粗细为1像素 -->
<Ellipse Canvas.Left="50" Canvas.Top="40" Width="30" Height="30" Fill="#E53935" Stroke="Black" StrokeThickness="1"/>

Polygon(多边形)

<!-- Polygon 多边形控件(三角形) -->
<!-- Canvas.Left="50":与圆形X轴对齐,距离左侧50像素 -->
<!-- Canvas.Top="120":距离顶部120像素,位于圆形下方 -->
<!-- Points="0,0 20,10 0,20":定义三个顶点坐标,连接成三角形 -->
<!-- Fill="#4CAF50":内部填充颜色为绿色 -->
<Polygon Canvas.Left="50" Canvas.Top="120"  Points="0,0 20,10 0,20" Fill="#4CAF50" Stroke="Black" StrokeThickness="1"/>

Path(路径)

 <!-- Path 路径控件(矩形) -->
 <!-- Canvas.Left="50":与圆形、三角形X轴对齐,距离左侧50像素 -->
 <!-- Canvas.Top="200":距离顶部200像素,位于三角形下方 -->
 <!-- Data 路径语法:M=起点 L=画线 Z=闭合图形 -->
 <!-- Fill="#2196F3":内部填充颜色为蓝色 -->
 <Path Canvas.Left="50" Canvas.Top="200" Data="M 0,0 L 120,0 L 120,40 L 0,40 Z" Fill="#2196F3" Stroke="#1976D2" StrokeThickness="1"/>

image


二、实战案例

项目结构
00025.WPF 路径动画完全指南:自绘制实战/
├── 1_常用路径示例/
├── 2_自绘制控件基础/
├── 3_实战1:直线 + 圆形动画/
├── 4_实战2:曲线 + 箭头动画/
├── 5_效果A:淡入 + 缩放/
├── 6_路径动画 + 淡入 + 缩放/
├── 7_场景1:自绘制按钮(悬浮动效)/
├── 8_自绘制进度条(流动效果)/
├── 9_自绘制窗口控制按钮/

实战1:直线 + 圆形动画(DoubleAnimationUsingPath)

目标: 红色圆形沿直线往返移动,无限循环。

MainWindow.xaml
<Window x:Class="_3_实战1_直线___圆形动画.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_3_实战1_直线___圆形动画"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <!-- 定义运动轨迹资源 -->
    <Window.Resources>
        <!-- 路径几何图形:用于动画的直线路径 -->
        <PathGeometry x:Key="LinePath" Figures="M 0,0 L 500,0"/>
    </Window.Resources>

    <!-- 画布布局容器 -->
    <Canvas Background="#F8F9FA">

        <!-- 路径轨迹线 -->
        <!-- Data:绘制直线路径坐标 -->
        <!-- Stroke:线条颜色蓝色 -->
        <!-- StrokeThickness:线条粗细2 -->
        <!-- StrokeDashArray:虚线样式 -->
        <Path Data="M 50,120 L 550,120"
              Stroke="Blue" 
              StrokeThickness="2" 
              StrokeDashArray="2,2"/>

        <!-- 圆形动画控件 -->
        <!-- Width:圆形宽度30 -->
        <!-- Height:圆形高度30 -->
        <!-- Canvas.Left:水平初始位置50 -->
        <!-- Canvas.Top:垂直居中对齐直线 -->
        <!-- Fill:填充红色 -->
        <!-- Stroke:边框黑色 -->
        <!-- StrokeThickness:边框粗细1 -->
        <Ellipse Width="30" Height="30" 
                 Canvas.Left="35"
                 Canvas.Top="105"
                 Fill="#E53935"
                 Stroke="Black"
                 StrokeThickness="1">

            <!-- 渲染变换:设置位移变换 -->
            <Ellipse.RenderTransform>
                <TranslateTransform x:Name="BallTranslate"/>
            </Ellipse.RenderTransform>

            <!-- 动画触发器集合 -->
            <Ellipse.Triggers>
                <!-- 路由事件:窗体加载完成触发 -->
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <!-- 故事板:动画容器 -->
                        <!-- RepeatBehavior:永久循环 -->
                        <!-- AutoReverse:自动往返 -->
                        <Storyboard RepeatBehavior="Forever" AutoReverse="True">

                            <!-- X轴路径动画 -->
                            <!-- Storyboard.TargetName:目标元素 -->
                            <!-- Storyboard.TargetProperty:动画属性X -->
                            <!-- Source:数据源为X坐标 -->
                            <!-- PathGeometry:路径资源 -->
                            <!-- Duration:时长3秒 -->
                            <DoubleAnimationUsingPath 
                                Storyboard.TargetName="BallTranslate"
                                Storyboard.TargetProperty="X"
                                Source="X"
                                PathGeometry="{StaticResource LinePath}"
                                Duration="0:0:3"/>

                            <!-- Y轴路径动画 -->
                            <!-- Storyboard.TargetName:目标元素 -->
                            <!-- Storyboard.TargetProperty:动画属性Y -->
                            <!-- Source:数据源为Y坐标 -->
                            <!-- PathGeometry:路径资源 -->
                            <!-- Duration:时长3秒 -->
                            <DoubleAnimationUsingPath 
                                Storyboard.TargetName="BallTranslate"
                                Storyboard.TargetProperty="Y"
                                Source="Y"
                                PathGeometry="{StaticResource LinePath}"
                                Duration="0:0:3"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>
    </Canvas>
</Window>
MainWindow.xaml.cs
using System.Windows;

namespace WpfPathAnimationDemo.Views
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

效果: 红色圆形沿蓝色虚线直线往返移动,3秒/轮,无限循环。
output

视频转gif https://tool.bugcome.com/media-tools/video-convert/mp4-to-gif


实战2:曲线 + 箭头动画(MatrixAnimationUsingPath)

目标: 绿色箭头沿贝塞尔曲线移动,自动随切线旋转。

MainWindow.xaml
<Window x:Class="_4_实战2_曲线___箭头动画.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_4_实战2_曲线___箭头动画"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <!-- 定义运动轨迹资源 -->
    <Window.Resources>
        <!-- 路径几何图形:用于动画的曲线路径(相对坐标) -->
        <PathGeometry x:Key="CurvePath" Figures="M 0,0 Q 350,-90 650,80"/>
    </Window.Resources>

    <!-- 画布布局容器 -->
    <Canvas Background="#F8F9FA">

        <!-- 路径轨迹线 -->
        <!-- Data:绘制曲线路径坐标 -->
        <!-- Stroke:线条颜色蓝色 -->
        <!-- StrokeThickness:线条粗细2 -->
        <!-- StrokeDashArray:虚线样式 -->
        <Path Data="M 50,120 Q 400,30 700,200"
              Stroke="Blue" 
              StrokeThickness="2" 
              StrokeDashArray="2,2"/>

        <!-- 箭头动画控件 -->
        <!-- Points:三角形箭头顶点坐标 -->
        <!-- Fill:填充绿色 -->
        <!-- Stroke:边框黑色 -->
        <!-- StrokeThickness:边框粗细1 -->
        <!-- Canvas.Left:水平初始位置50 -->
        <!-- Canvas.Top:垂直初始位置120 -->
        <!-- RenderTransformOrigin:旋转中心点(箭头左侧中心) -->
        <Polygon Points="0 0 20 10 0 20"
                 Fill="#4CAF50"
                 Stroke="Black"
                 StrokeThickness="1"
                 Canvas.Left="50"
                 Canvas.Top="120"
                 RenderTransformOrigin="0,0.5">

            <!-- 渲染变换:矩阵变换控制位置与旋转 -->
            <Polygon.RenderTransform>
                <MatrixTransform x:Name="ArrowMatrix"/>
            </Polygon.RenderTransform>

            <!-- 动画触发器集合 -->
            <Polygon.Triggers>
                <!-- 路由事件:窗体加载完成触发 -->
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <!-- 故事板:动画容器 -->
                        <!-- RepeatBehavior:永久循环 -->
                        <!-- AutoReverse:自动往返 -->
                        <!-- Duration:单次动画时长3秒 -->
                        <Storyboard RepeatBehavior="Forever" AutoReverse="True" Duration="0:0:3">

                            <!-- 曲线路径矩阵动画 -->
                            <!-- Storyboard.TargetName:目标元素 -->
                            <!-- Storyboard.TargetProperty:动画矩阵属性 -->
                            <!-- PathGeometry:路径资源 -->
                            <!-- DoesRotateWithTangent:随路径切线自动旋转 -->
                            <MatrixAnimationUsingPath
                                Storyboard.TargetName="ArrowMatrix"
                                Storyboard.TargetProperty="Matrix"
                                PathGeometry="{StaticResource CurvePath}"
                                DoesRotateWithTangent="True"/>

                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Polygon.Triggers>
        </Polygon>

    </Canvas>
</Window>

效果: 绿色箭头沿曲线平滑移动,方向始终与路径切线一致,4秒/轮。
output (1)

视频转gif https://tool.bugcome.com/media-tools/video-convert/mp4-to-gif


实战3:DoubleAnimation 复合动效
效果A:按钮 + 淡入 + 缩放
<Window x:Class="WpfPathAnimationDemo.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DoubleAnimation 基础实战" Height="400" Width="600">
    <Canvas Background="#F8F9FA">
        <Ellipse x:Name="AnimateBall" 
                 Width="30" Height="30" 
                 Fill="#2196F3" 
                 Stroke="Black" 
                 StrokeThickness="1"
                 Opacity="0"
                 Canvas.Left="300" 
                 Canvas.Top="180">
            <Ellipse.RenderTransform>
                <ScaleTransform x:Name="BallScale" CenterX="15" CenterY="15"/>
            </Ellipse.RenderTransform>

            <Ellipse.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <!-- 淡入 -->
                            <DoubleAnimation 
                                Storyboard.TargetName="AnimateBall"
                                Storyboard.TargetProperty="Opacity"
                                From="0" To="1"
                                Duration="0:0:0.5"/>

                            <!-- 缩放 -->
                            <DoubleAnimation 
                                Storyboard.TargetName="BallScale"
                                Storyboard.TargetProperty="ScaleX"
                                From="0.5" To="1.2"
                                Duration="0:0:0.5"/>
                            <DoubleAnimation 
                                Storyboard.TargetName="BallScale"
                                Storyboard.TargetProperty="ScaleY"
                                From="0.5" To="1.2"
                                Duration="0:0:0.5"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Ellipse.Triggers>
        </Ellipse>
    </Canvas>
</Window>

output (2)

视频转gif https://tool.bugcome.com/media-tools/video-convert/mp4-to-gif

效果B:路径动画 + 淡入 + 缩放 + 位置对齐 + 完整注释 + 按钮控制
<Window x:Class="_6_路径动画___淡入___缩放.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_6_路径动画___淡入___缩放"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <!-- 定义运动轨迹资源 -->
    <Window.Resources>
        <!-- 路径几何图形:相对路径(从0,0开始,确保位置不偏移) -->
        <PathGeometry x:Key="LinePath" Figures="M 0,0 L 400,0"/>

        <!-- 定义复合动画故事板 -->
        <Storyboard x:Key="CombineAnimation">
            <!-- 路径动画:沿直线X轴移动 -->
            <DoubleAnimationUsingPath 
                Storyboard.TargetName="CombineTranslate"
                Storyboard.TargetProperty="X"
                Source="X"
                PathGeometry="{StaticResource LinePath}"
                Duration="0:0:3"
                RepeatBehavior="Forever"
                AutoReverse="True"/>

            <!-- 淡入动画:透明度从0到1 -->
            <DoubleAnimation 
                Storyboard.TargetName="CombineBall"
                Storyboard.TargetProperty="Opacity"
                From="0" To="1"
                Duration="0:0:3"/>

            <!-- X轴缩放动画:从0.8倍到1.2倍 -->
            <DoubleAnimation 
                Storyboard.TargetName="CombineScale"
                Storyboard.TargetProperty="ScaleX"
                From="0.8" To="1.2"
                Duration="0:0:3"
                RepeatBehavior="Forever"
                AutoReverse="True"/>

            <!-- Y轴缩放动画:从0.8倍到1.2倍 -->
            <DoubleAnimation 
                Storyboard.TargetName="CombineScale"
                Storyboard.TargetProperty="ScaleY"
                From="0.8" To="1.2"
                Duration="0:0:3"
                RepeatBehavior="Forever"
                AutoReverse="True"/>
        </Storyboard>
    </Window.Resources>

    <!-- 画布布局容器 -->
    <Canvas Background="#F8F9FA">

        <!-- 控制按钮 -->
        <!-- Content:按钮显示文字 -->
        <!-- Width:按钮宽度120 -->
        <!-- Height:按钮高度40 -->
        <!-- Canvas.Left:按钮水平位置100 -->
        <!-- Canvas.Top:按钮垂直位置30 -->
        <Button x:Name="ToggleBtn"
                Content="启动动画"
                Width="120"
                Height="40"
                Canvas.Left="100"
                Canvas.Top="30"
                Click="ToggleBtn_Click"/>

        <!-- 路径轨迹线 -->
        <!-- Data:绘制直线路径坐标 -->
        <!-- Stroke:线条颜色蓝色 -->
        <!-- StrokeThickness:线条粗细2 -->
        <!-- StrokeDashArray:虚线样式 -->
        <Path Data="M 50,180 L 550,180"
              Stroke="Blue" 
              StrokeThickness="2" 
              StrokeDashArray="2,2"/>

        <!-- 复合动画圆形 -->
        <!-- Width:圆形宽度30 -->
        <!-- Height:圆形高度30 -->
        <!-- Fill:填充橙色 -->
        <!-- Stroke:边框黑色 -->
        <!-- StrokeThickness:边框粗细1 -->
        <!-- Opacity:初始透明度0 -->
        <Ellipse x:Name="CombineBall" 
                 Width="30" Height="30" 
                 Fill="#FF9800" 
                 Stroke="Black" 
                 StrokeThickness="1"
                 Opacity="0"
                 Canvas.Left="50"
                 Canvas.Top="180">

            <!-- 渲染变换:组合变换(位移+缩放) -->
            <Ellipse.RenderTransform>
                <TransformGroup>
                    <!-- 位移变换:控制水平移动 -->
                    <TranslateTransform x:Name="CombineTranslate"/>
                    <!-- 缩放变换:控制大小缩放 -->
                    <ScaleTransform x:Name="CombineScale" CenterX="15" CenterY="15"/>
                </TransformGroup>
            </Ellipse.RenderTransform>
        </Ellipse>
    </Canvas>
</Window>

output (3)

视频转gif https://tool.bugcome.com/media-tools/video-convert/mp4-to-gif


三、进阶实战

场景1:自绘制按钮(悬浮动效)

目标: 鼠标悬浮时沿微小曲线微动 + 放大 + 颜色加深。

<Window x:Class="_7_场景1_自绘制按钮_悬浮动效_.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_7_场景1_自绘制按钮_悬浮动效_"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <!-- 画布布局容器 -->
    <Canvas Background="#F8F9FA">

        <!-- 按钮整体容器:文字 + 按钮一起缩放 -->
        <!-- Canvas.Left:容器水平位置 240 -->
        <!-- Canvas.Top:容器垂直位置 180 -->
        <!-- 绑定鼠标事件:移入、移出、点击 -->
        <Canvas x:Name="ButtonCanvas" 
                Canvas.Left="240" 
                Canvas.Top="180"
                MouseEnter="ButtonCanvas_MouseEnter"
                MouseLeave="ButtonCanvas_MouseLeave"
                MouseLeftButtonDown="ButtonCanvas_MouseLeftButtonDown"
                Cursor="Hand">

            <!-- 渲染变换:缩放控制 -->
            <Canvas.RenderTransform>
                <!-- 从正中心放大 -->
                <ScaleTransform x:Name="ButtonScale" 
                                ScaleX="1" 
                                ScaleY="1"/>
            </Canvas.RenderTransform>

            <!-- 变换中心点:0.5,0.5 为中心缩放 -->
            <Canvas.RenderTransformOrigin>
                <Point>0.5,0.5</Point>
            </Canvas.RenderTransformOrigin>

            <!-- 按钮形状:矩形按钮 -->
            <!-- Width:按钮宽度 120 -->
            <!-- Height:按钮高度 40 -->
            <!-- Fill:按钮填充颜色 -->
            <!-- Stroke:按钮边框颜色 -->
            <Path Width="120" Height="40" 
                  Data="M0,0 L120,0 L120,40 L0,40 Z" 
                  Fill="#2196F3" 
                  Stroke="#1976D2" 
                  StrokeThickness="1"/>

            <!-- 按钮文字 -->
            <!-- Canvas.Left:文字距离左侧 25 -->
            <!-- Canvas.Top:文字距离顶部 10 -->
            <!-- IsHitTestVisible:不接收鼠标事件 -->
            <TextBlock Text="自绘制按钮" 
                       Canvas.Left="25" 
                       Canvas.Top="10" 
                       FontSize="14" 
                       Foreground="White"
                       FontWeight="Medium"
                       IsHitTestVisible="False"/>
        </Canvas>
    </Canvas>
</Window>
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace _7_场景1_自绘制按钮_悬浮动效_
{
    /// <summary>
    /// 自绘制按钮 + 中心悬浮放大动效
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent(); // 初始化界面
        }

        // 鼠标进入:从中心放大 1.2 倍
        private void ButtonCanvas_MouseEnter(object sender, MouseEventArgs e)
        {
            // 创建缩放动画
            DoubleAnimation scale = new DoubleAnimation
            {
                To = 1.2,                      // 目标放大到 1.2 倍
                Duration = TimeSpan.FromSeconds(0.2) // 动画时长 0.2 秒
            };

            // 执行 X 轴缩放
            ButtonScale.BeginAnimation(ScaleTransform.ScaleXProperty, scale);
            // 执行 Y 轴缩放
            ButtonScale.BeginAnimation(ScaleTransform.ScaleYProperty, scale);
        }

        // 鼠标离开:恢复原始大小
        private void ButtonCanvas_MouseLeave(object sender, MouseEventArgs e)
        {
            // 创建复位动画
            DoubleAnimation scale = new DoubleAnimation
            {
                To = 1,                        // 恢复到 1 倍原始大小
                Duration = TimeSpan.FromSeconds(0.2)
            };

            // X 轴复位
            ButtonScale.BeginAnimation(ScaleTransform.ScaleXProperty, scale);
            // Y 轴复位
            ButtonScale.BeginAnimation(ScaleTransform.ScaleYProperty, scale);
        }

        // 鼠标点击事件:弹出提示
        private void ButtonCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("自绘制按钮被点击!", "提示");
        }
    }
}

output (4)

视频转gif https://tool.bugcome.com/media-tools/video-convert/mp4-to-gif

场景2:自绘制进度条(流动效果)
<Window x:Class="_8_自绘制进度条_流动效果_.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_8_自绘制进度条_流动效果_"
        mc:Ignorable="d"
        Title="进度条演示" Height="450" Width="800">

    <!-- 窗口资源:定义进度条背景路径 -->
    <Window.Resources>
        <PathGeometry x:Key="ProgressBgPath" 
                      Figures="M 50,180 L 550,180 L 550,210 L 50,210 Z"/>
    </Window.Resources>

    <!-- 主画布:页面布局容器 -->
    <Canvas Background="#F8F9FA">

        <!-- 进度条背景 -->
        <!-- Data:背景形状路径(引用Window.Resources中的ProgressBgPath) -->
        <!-- Fill:背景填充色 -->
        <!-- Stroke:边框颜色 -->
        <!-- StrokeThickness:边框粗细 -->
        <Path Data="{StaticResource ProgressBgPath}"
              Fill="#E9ECEF"
              Stroke="#DEE2E6"
              StrokeThickness="1"/>

        <!-- 流动进度条(蓝色加载条) -->
        <!-- x:Name:后台代码通过此名称访问该元素 -->
        <!-- Fill:填充色(蓝色) -->
        <!-- Stroke:边框颜色(深蓝色) -->
        <!-- StrokeThickness:边框粗细 -->
        <!-- 初始不设置Data,默认完全隐藏,无任何蓝色 -->
        <Path x:Name="FlowPath"
              Fill="#2196F3"
              Stroke="#1976D2"
              StrokeThickness="1"/>

        <!-- 进度显示文本 -->
        <!-- x:Name:后台代码通过此名称更新文字 -->
        <!-- Text:初始显示文字 -->
        <!-- Canvas.Left:水平位置 -->
        <!-- Canvas.Top:垂直位置 -->
        <!-- FontSize:文字大小 -->
        <!-- Foreground:文字颜色 -->
        <TextBlock x:Name="ProgressText" 
                   Text="加载未开始"
                   Canvas.Left="256"
                   Canvas.Top="148"
                   FontSize="14"
                   Foreground="#333"/>

        <!-- 按钮整体容器 -->
        <!-- Canvas.Left:容器水平位置 -->
        <!-- Canvas.Top:容器垂直位置 -->
        <Canvas Canvas.Left="240" Canvas.Top="230">

            <!-- 按钮形状 -->
            <!-- x:Name:后台代码通过此名称绑定点击事件 -->
            <!-- Data:按钮绘制路径(120x40的矩形) -->
            <!-- Fill:填充色(绿色) -->
            <!-- Stroke:边框色(深绿色) -->
            <!-- StrokeThickness:边框粗细 -->
            <!-- Cursor:鼠标悬浮样式(手型) -->
            <Path x:Name="ControlBtn"
                  Data="M 0,0 L 120,0 L 120,40 L 0,40 Z"
                  Fill="#4CAF50"
                  Stroke="#388E3C"
                  StrokeThickness="1"
                  Cursor="Hand"/>

            <!-- 按钮文字 -->
            <!-- Text:文字内容 -->
            <!-- Canvas.Left:文字左侧偏移 -->
            <!-- Canvas.Top:文字顶部偏移 -->
            <!-- FontSize:文字字号 -->
            <!-- Foreground:文字颜色 -->
            <!-- FontWeight:文字粗细 -->
            <!-- IsHitTestVisible:不接收鼠标事件(让点击穿透到下方的Path按钮) -->
            <TextBlock Text="开始加载"
                       Canvas.Left="22"
                       Canvas.Top="10"
                       FontSize="14"
                       Foreground="White"
                       FontWeight="Medium"
                       IsHitTestVisible="False"/>
        </Canvas>
    </Canvas>
</Window>

using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;

namespace _8_自绘制进度条_流动效果_
{
    /// <summary>
    /// 自绘制进度条 - 3秒从0%到100%
    /// </summary>
    public partial class MainWindow : Window
    {
        // ==================== 常量配置 ====================
        private const double TotalLength = 500;     // 进度条总长度(像素)
        private const double TotalSeconds = 3.0;    // 总时长(秒)
        private const double IntervalMs = 16;       // 定时器间隔 ~60fps

        // ==================== 状态变量 ====================
        private DispatcherTimer _timer;     // 动画定时器
        private DateTime _startTime;        // 开始时间,用于精确计算进度
        private bool _isRunning = false;    // 防止重复点击

        public MainWindow()
        {
            InitializeComponent();

            // 初始状态:隐藏进度条
            FlowPath.Visibility = Visibility.Hidden;

            // 创建定时器
            _timer = new DispatcherTimer();
            _timer.Interval = TimeSpan.FromMilliseconds(IntervalMs);
            _timer.Tick += Timer_Tick;

            // 绑定按钮点击事件
            ControlBtn.MouseLeftButtonDown += (s, e) => StartLoad();
        }

        /// <summary>
        /// 开始加载动画
        /// </summary>
        private void StartLoad()
        {
            // 防止动画过程中重复点击
            if (_isRunning) return;

            _isRunning = true;
            _startTime = DateTime.Now;      // 记录开始时间

            // 显示进度条并重置为0
            FlowPath.Visibility = Visibility.Visible;
            UpdatePath(0);

            // 启动定时器
            _timer.Start();
        }

        /// <summary>
        /// 定时器回调 - 每16ms执行一次
        /// </summary>
        private void Timer_Tick(object sender, EventArgs e)
        {
            // 计算实际经过的秒数(基于真实时间,避免累积误差)
            double elapsed = (DateTime.Now - _startTime).TotalSeconds;

            // 计算进度比例 (0.0 ~ 1.0)
            double progress = elapsed / TotalSeconds;

            if (progress >= 1.0)
            {
                // ========== 动画完成 ==========
                _timer.Stop();
                _isRunning = false;

                ProgressText.Text = "加载完成!100%";
                UpdatePath(TotalLength);    // 确保填满500像素
            }
            else
            {
                // ========== 动画进行中 ==========
                double currentLength = progress * TotalLength;

                // 显示百分比,保留1位小数
                ProgressText.Text = $"加载中... {progress * 100:F1}%";

                UpdatePath(currentLength);
            }
        }

        /// <summary>
        /// 更新进度条路径
        /// </summary>
        /// <param name="length">当前进度长度(像素)</param>
        private void UpdatePath(double length)
        {
            // M:移动到起点
            // L:画直线到终点
            // Z:闭合路径
            double endX = 50 + length;
            string data = $"M 50,180 L {endX},180 L {endX},210 L 50,210 Z";
            FlowPath.Data = Geometry.Parse(data);
        }
    }
}

output (5)

场景3:自绘制窗口控制按钮
<Window x:Class="_9_自绘制窗口控制按钮.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="登录" Height="360" Width="320"
        FontFamily="宋体" FontWeight="ExtraLight"
        WindowStartupLocation="CenterScreen" 
        WindowStyle="None" 
        Icon="/Assets/Images/logo.png"
        ShowInTaskbar="False"
        AllowsTransparency="True" Background="{x:Null}"
        Name="loginView">

    <Window.Resources>
        <!-- 最小化按钮 -->
        <ControlTemplate x:Key="MinimizedButtonTemplate" TargetType="Button">
            <Border Background="Transparent" Name="backMinimized" Width="30" Height="30">
                <Path Data="M0 0 12 0" Stroke="White" StrokeThickness="2" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="backMinimized" Property="Background" Value="#22FFFFFF"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="backMinimized" Property="Background" Value="#44FFFFFF"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!-- 关闭按钮 -->
        <ControlTemplate x:Key="CloseButtonTemplate" TargetType="Button">
            <Border Background="Transparent" Name="backClose" Width="30" Height="30">
                <Path Data="M0 0 12 12M0 12 12 0" Stroke="White" StrokeThickness="2" VerticalAlignment="Center" HorizontalAlignment="Center"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="backClose" Property="Background" Value="#EE4863"/>
                    <Setter TargetName="backClose" Property="CornerRadius" Value="0,8,0,0"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="backClose" Property="Background" Value="#EF1A48"/>
                    <Setter TargetName="backClose" Property="CornerRadius" Value="0,8,0,0"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!-- 找回密码 -->
        <ControlTemplate x:Key="ForgotPasswordButtonTemplate" TargetType="Button">
            <Border Background="Transparent">
                <TextBlock Text="找回密码" FontSize="12" Foreground="#CCC" Name="backForgotPassword"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="backForgotPassword" Property="Foreground" Value="Black"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="backForgotPassword" Property="Foreground" Value="#00B3F6"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!-- 注册账号 -->
        <ControlTemplate x:Key="RegisterButtonTemplate" TargetType="Button">
            <Border Background="Transparent">
                <TextBlock Text="注册账号" FontSize="12" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="8" Foreground="#CCC" Name="backRegister"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="backRegister" Property="Foreground" Value="Black"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="backRegister" Property="Foreground" Value="#00B3F6"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!-- 二维码 -->
        <ControlTemplate x:Key="QRcodeButtonTemplate" TargetType="Button">
            <Border Background="Transparent">
                <TextBlock Text="&#xe642;" Foreground="#CCC" FontSize="30" FontFamily="/Assets/Fonts/#iconfont" Margin="0,0,6,-2" Name="backQRcode"/>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="backQRcode" Property="Foreground" Value="Black"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="backQRcode" Property="Foreground" Value="#00B3F6"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!-- 登录按钮 -->
        <ControlTemplate x:Key="LoginButtonTemplate" TargetType="Button">
            <Border Background="#007DFA" CornerRadius="5" Height="40">
                <Grid>
                    <Border CornerRadius="4" Background="#22FFFFFF" x:Name="backLogin" Visibility="Hidden"/>
                    <ContentControl Content="{TemplateBinding Content}" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="backLogin" Property="Visibility" Value="Visible"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>

        <!-- 账号输入框样式(ComboBox 修复下拉没内容) -->
        <ControlTemplate x:Key="AccountComboBoxTemplate" TargetType="ComboBox">
            <Grid>
                <Border Background="White" BorderBrush="#E7E7E7" BorderThickness="1" CornerRadius="5" Height="40">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="40"/>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="30"/>
                        </Grid.ColumnDefinitions>
                        <!-- 图标 -->
                        <TextBlock Text="&#xe601;" FontFamily="/Assets/Fonts/#iconfont" FontSize="20" 
                                   Foreground="#CCC" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                        <!-- 输入区域 -->
                        <TextBox x:Name="PART_EditableTextBox" Grid.Column="1" BorderThickness="0" 
                                 Background="Transparent" VerticalAlignment="Center" FontSize="14"
                                 Padding="0" Margin="0" CaretBrush="#555"/>
                        <!-- 下拉箭头 -->
                        <ToggleButton Grid.Column="2" Width="30" Height="30" 
                                      IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                                      ClickMode="Press" Focusable="False">
                            <ToggleButton.Template>
                                <ControlTemplate TargetType="ToggleButton">
                                    <Border Background="Transparent">
                                        <Path Data="M0 0 4 4 8 0" Stroke="#999" StrokeThickness="1.5" 
                                              VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0,2,0,0"/>
                                    </Border>
                                </ControlTemplate>
                            </ToggleButton.Template>
                        </ToggleButton>
                    </Grid>
                </Border>
                <!-- 下拉面板 修复空白无内容 -->
                <Popup IsOpen="{TemplateBinding IsDropDownOpen}" Placement="Bottom" 
                       Width="{TemplateBinding ActualWidth}" StaysOpen="False">
                    <Border Background="White" BorderBrush="#E7E7E7" BorderThickness="1" CornerRadius="0,0,5,5" 
                            MaxHeight="150" Margin="0,-1,0,0">
                        <ScrollViewer VerticalScrollBarVisibility="Auto">
                            <!-- 关键:用ItemsPresenter自带默认项模板,不会空白 -->
                            <ItemsPresenter />
                        </ScrollViewer>
                    </Border>
                </Popup>
            </Grid>
        </ControlTemplate>

        <!-- PasswordBox 样式 -->
        <SolidColorBrush x:Key="TextBox.Focus.Border1" Color="#FF569DE5"/>
        <Style x:Key="PasswordBoxStyle" TargetType="{x:Type PasswordBox}">
            <Setter Property="PasswordChar" Value="●"/>
            <Setter Property="Background" Value="White"/>
            <Setter Property="BorderBrush" Value="#E7E7E7"/>
            <Setter Property="Foreground" Value="#555"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FontSize" Value="14"/>
            <Setter Property="Height" Value="40"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type PasswordBox}">
                        <Border x:Name="border" Background="{TemplateBinding Background}" 
                                BorderBrush="{TemplateBinding BorderBrush}" 
                                BorderThickness="{TemplateBinding BorderThickness}" 
                                SnapsToDevicePixels="True" CornerRadius="5" Height="40">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="40"/>
                                    <ColumnDefinition/>
                                    <ColumnDefinition Width="30"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="&#xe74e;" Foreground="#CCC" FontSize="20" 
                                           FontFamily="/Assets/Fonts/#iconfont" VerticalAlignment="Center" HorizontalAlignment="Center"/>
                                <ScrollViewer x:Name="PART_ContentHost" Grid.Column="1"
                                              Focusable="false" 
                                              HorizontalScrollBarVisibility="Hidden" 
                                              VerticalScrollBarVisibility="Hidden"
                                              VerticalAlignment="Center"/>
                            </Grid>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="#959595"/>
                            </Trigger>
                            <Trigger Property="IsKeyboardFocused" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource TextBox.Focus.Border1}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <!-- 复选框样式 -->
        <SolidColorBrush x:Key="OptionMark.Static.Glyph" Color="#00B5FC"/>
        <Style x:Key="CheckBoxStyle" TargetType="{x:Type CheckBox}">
            <Setter Property="Foreground" Value="#CCC"/>
            <Setter Property="FontSize" Value="12"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CheckBox}">
                        <Grid Background="Transparent" SnapsToDevicePixels="True">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Border x:Name="checkBoxBorder" Background="White" BorderBrush="#CCC" BorderThickness="1" 
                                    HorizontalAlignment="Center" Margin="0,0,4,0" VerticalAlignment="Center" Width="14" Height="14">
                                <Grid>
                                    <Path x:Name="optionMark" Data="M 2 7 L 5 10 L 10 3" 
                                          Stroke="{StaticResource OptionMark.Static.Glyph}" StrokeThickness="2" 
                                          Margin="1" Opacity="0" Stretch="None" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
                                </Grid>
                            </Border>
                            <ContentPresenter x:Name="contentPresenter" Grid.Column="1" Focusable="False" 
                                              VerticalAlignment="Center"/>
                        </Grid>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Setter TargetName="checkBoxBorder" Property="BorderBrush" Value="#FF5593FF"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="true">
                                <Setter TargetName="optionMark" Property="Opacity" Value="1"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Border Margin="5" Background="White" CornerRadius="8">
        <Border.Effect>
            <DropShadowEffect Color="Gray" ShadowDepth="0" BlurRadius="8" Opacity="0.5" Direction="0"/>
        </Border.Effect>

        <Grid MouseLeftButtonDown="Grid_MouseLeftButtonDown">
            <Grid.RowDefinitions>
                <RowDefinition Height="1.2*"/>
                <RowDefinition Height="3*"/>
                <RowDefinition Height="45"/>
            </Grid.RowDefinitions>

            <!-- 顶部蓝色栏 -->
            <Border Background="#007DFA" CornerRadius="8,8,0,0"/>

            <!-- 窗口控制按钮 绑定点击事件 -->
            <Button Click="BtnMinimize_Click" VerticalAlignment="Top" HorizontalAlignment="Right" Width="30" Height="30" 
                    Margin="0,0,35,0" Template="{StaticResource MinimizedButtonTemplate}"/>
            <Button Click="BtnClose_Click" VerticalAlignment="Top" HorizontalAlignment="Right" Width="30" Height="30" 
                    Margin="0,0,5,0" Template="{StaticResource CloseButtonTemplate}"/>

            <!-- Logo与标题 原样保留 -->
            <StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Left">
                <Border Width="30" Height="30" Margin="10,10,6,10">
                    <Border.Effect>
                        <DropShadowEffect Color="Gray" ShadowDepth="0" BlurRadius="5" Opacity="0.3" Direction="0"/>
                    </Border.Effect>
                    <Border.Background>
                        <ImageBrush ImageSource="/Assets/Images/logo.png"/>
                    </Border.Background>
                </Border>
                <TextBlock Text="Bugcome" HorizontalAlignment="Center" Foreground="White" 
                           VerticalAlignment="Center" FontSize="12" FontWeight="Black"/>
            </StackPanel>

            <!-- 表单区域 -->
            <Grid Grid.Row="1" Margin="20,15,20,0">
                <Grid.RowDefinitions>
                    <RowDefinition Height="40"/>
                    <RowDefinition Height="15"/>
                    <RowDefinition Height="40"/>
                    <RowDefinition Height="20"/>
                    <RowDefinition Height="26"/>
                    <RowDefinition Height="20"/>
                    <RowDefinition Height="40"/>
                    <RowDefinition Height="22"/>
                </Grid.RowDefinitions>

                <!-- 账号输入框 -->
                <ComboBox x:Name="cboAccount" Template="{StaticResource AccountComboBoxTemplate}" 
                          IsEditable="True" Height="40" VerticalAlignment="Center"
                          FontSize="14" Foreground="#555"/>

                <!-- 密码输入框 -->
                <PasswordBox Style="{StaticResource PasswordBoxStyle}" Grid.Row="2" 
                             VerticalAlignment="Center" HorizontalAlignment="Stretch"/>

                <!-- 选项栏 -->
                <StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
                    <CheckBox Style="{StaticResource CheckBoxStyle}" Content="自动登录" Width="95"/>
                    <CheckBox Style="{StaticResource CheckBoxStyle}" Content="记住密码" Width="95" Margin="10,0"/>
                    <Button Template="{StaticResource ForgotPasswordButtonTemplate}" VerticalAlignment="Center"/>
                </StackPanel>

                <!-- 登录按钮 -->
                <Button Content="登录" Grid.Row="6" Height="40" 
                        Template="{StaticResource LoginButtonTemplate}" 
                        Foreground="White" FontSize="16" IsDefault="True"/>

                <!-- 错误提示 -->
                <TextBlock Foreground="Red" LineHeight="22" Grid.Row="7" 
                           VerticalAlignment="Center" HorizontalAlignment="Center" 
                           TextWrapping="Wrap" Text=""/>
            </Grid>

            <!-- 底部栏 -->
            <Grid Grid.Row="2">
                <UniformGrid Columns="2">
                    <Button VerticalAlignment="Bottom" HorizontalAlignment="Left" Height="32" 
                            Template="{StaticResource RegisterButtonTemplate}" Name="register"/>
                    <Button VerticalAlignment="Bottom" HorizontalAlignment="Right" Height="32" 
                            Template="{StaticResource QRcodeButtonTemplate}" Name="QRcode"/>
                </UniformGrid>
            </Grid>
        </Grid>
    </Border>
</Window>
using System;
using System.Windows;
using System.Windows.Input;

namespace _9_自绘制窗口控制按钮
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // 给下拉框加测试数据,立马能看到下拉有内容
            cboAccount.Items.Add("admin");
            cboAccount.Items.Add("user123");
            cboAccount.Items.Add("test001");
        }

        private void Grid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                this.DragMove();
            }
        }

        private void BtnMinimize_Click(object sender, RoutedEventArgs e)
        {
            this.WindowState = WindowState.Minimized;
        }

        private void BtnClose_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }
}

image


四、避坑指南

问题原因解决方案
动画不生效,提示找不到目标x:Name 未设置或拼写错误检查 TargetName 与 x:Name 完全一致(区分大小写)
路径动画偏移、不贴合轨迹起点与元素初始位置不匹配确保元素位置与 PathGeometry 起点一致
自绘制元素无填充/边框未设置 Fill/Stroke明确设置 Fill、Stroke、StrokeThickness
FindName 返回 null调用时机过早或元素未命名在 Loaded 事件中调用,确保元素已加载
动画卡顿时长设置不合理或路径过于复杂微动效 0.2-0.3 秒,简化路径节点

五、总结

  1. 核心逻辑: 自绘制控件(Path/Polygon/Ellipse)是基础,PathGeometry 是轨迹蓝图,三大路径动画类是核心工具。

  2. 实战重点: 掌握"自绘制元素绘制 → 动画绑定 → 功能联动"的完整流程。

  3. 关键技巧:

    • 熟记路径语法(M、L、C、A、Z)
    • 动画绑定注意 x:Name 正确性
    • 复合动效协调动画时长和变换中心
    • 自绘制样式完全自由定制

👋 关注我!持续分享 C# 实战技巧、代码示例 & 技术干货

  • 获取示例代码,轻松上手!
  • 私信输入数字: g843
  • 获取代码下载链接

六、英文单词汉译对照表

文章有很多陌生单词,我先列举出来,可以分页对照!

英文中文翻译英文中文翻译英文中文翻译
PathGeometry路径几何UI用户界面DoubleAnimation双精度动画
Path路径Polygon多边形Ellipse椭圆
Rectangle矩形DoubleAnimationUsingPath路径双精度动画PointAnimationUsingPath路径点动画
MatrixAnimationUsingPath路径矩阵动画DoesRotateWithTangent随切线旋转From起始值
To目标值Duration持续时间RepeatBehavior重复行为
Forever永远AutoReverse自动反向Figures图形集
StartPoint起点LineSegment线段ArcPath圆弧路径
CompositePath复合路径CurvePath曲线路径LinePath直线路径
Background背景Canvas画布RenderTransform渲染变换
TranslateTransform平移变换transXX轴平移transYY轴平移
Storyboard故事板BeginStoryboard开始故事板EventTrigger事件触发器
Loaded已加载Animation动画MatrixTransform矩阵变换
ArrowMatrix箭头矩阵ScaleTransform缩放变换BallScale球缩放变换
BallTranslate球平移变换CenterX中心X坐标CenterY中心Y坐标
TransformGroup变换组CombineBall复合球CombineScale复合缩放
CombineTranslate复合平移Opacity不透明度SmallCurvePath微小曲线路径
SelfDrawBtn自绘制按钮SelfDrawButtonStyle自绘制按钮样式Style样式
Setter设置器TargetType目标类型Fill填充
Stroke描边StrokeThickness描边粗细RenderTransformOrigin渲染变换原点
Cursor光标Hand手型光标MouseEnter鼠标进入
MouseLeave鼠标离开MouseLeftButtonDown鼠标左键按下IsHitTestVisible命中测试可见
Text文本TextBlock文本块FontSize字体大小
Foreground前景色FontWeight字体粗细SetTarget设置目标
SetTargetProperty设置目标属性TargetName目标名称TargetProperty目标属性
PropertyPath属性路径ScaleXX轴缩放ScaleXPropertyX轴缩放属性
ScaleYY轴缩放ScaleYPropertyY轴缩放属性Source源
PathAnimationSource路径动画源StaticResource静态资源FindResource查找资源
Geometry几何Parse解析FlowGeometry流动几何
FlowPath流动路径flowWidth流动宽度ProgressBgPath进度背景路径
ProgressText进度文本progressTimer进度定时器progressValue进度值
StartProgress开始进度ControlBtn控制按钮DispatcherTimer调度定时器
Interval间隔Tick定时触发TimeSpan时间跨度
FromMilliseconds从毫秒FromSeconds从秒FromArgb从ARGB
WindowControlBtnStyle窗口控制按钮样式Title标题TitleBar标题栏
SizeAll四向调整光标MinimizeBtn最小化按钮MaximizeBtn最大化按钮
CloseBtn关闭按钮isMaximized是否最大化DragMove拖动移动
WindowState窗口状态Minimized已最小化Maximized已最大化
Normal正常None无Transparent透明
SolidColorBrush纯色画刷System系统Input输入
InitializeComponent初始化组件Helpers辅助工具EmptyHelper空辅助类
Models模型层EmptyModel空模型ViewModels视图模型层
MainViewModel主视图模型Views视图层MainWindow主窗口
AssemblyInfo程序集信息WpfPathAnimationDemoWPF路径动画演示WPFWindows呈现基础
winfxWindows框架扩展schemas架构xmlnsXML命名空间
XAML可扩展应用程序标记语言XX轴XPropertyX属性
YY轴YPropertyY属性Z闭合指令
Click点击Close关闭Color颜色
Controls控件集Children子元素集ResizeMode调整大小模式
WindowStyle窗口样式Windows窗口系统MessageBox消息框
Media媒体Medium中等EventArgs事件参数
RoutedEvent路由事件RoutedEventArgs路由事件参数MouseEventArgs鼠标事件参数
MouseButtonEventArgs鼠标按钮事件参数Threading线程FindName查找名称
Resources资源StrokeDashArray描边虚线Brushes画刷集
BtnScale按钮缩放BtnTranslate按钮平移Segments线段集
Points点集Point点PathFigure路径图形
PathFigureCollection路径图形集Property属性presentation表示层

WPF基础:在Canvas上绘制图形 本文介绍了在WPF中如何在Canvas上绘制图形。 阅读详情

相关推荐

WPF基础 | WPF 常用控件实战:Button、TextBox 等的基础应用

WPF基础 | WPF 常用控件实战:Button、TextBox 等的基础应用 , 在当今的软件开发领域,创建直观、易用且美观的用户界面至关重要。WPF 凭借其强大的功能和丰富的可视化设计工具,成为了开发 Windows 桌面应用程序的首选框架之一。其中,Button(按钮)和 TextBox(文本框)等控件是构建用户交互界面的基础元素,熟练掌握它们的应用是开发高效、优质 WPF 应用程序的关键。

不踩坑,不知道坑有多深;不总结,踩过的坑白踩。 3220

WPF动画入门教程

Windows Presentation Foundation (WPF)是一种用于创建Windows客户端应用程序的UI框架。它让我们能够创建丰富的图形界面,包括各种各样的动画效果。接下来,我们将介绍如何在WPF中创建简单的动画。文章最后将给出源码,源码包括文章中的动画和一个水印按钮,一个简单的时钟动画,一个复杂的时钟动画。在WPF中,通常会使用以下的一些标签来创建和控制动画。

极客神殿 644

WPF进阶 | WPF 动画特效揭秘:实现炫酷的界面交互效果

WPF进阶 | WPF 动画特效揭秘:实现炫酷的界面交互效果 ,在当今竞争激烈的软件市场中,用户界面的交互体验至关重要。一个拥有炫酷动画特效的应用程序,不仅能吸引用户的注意力,还能显著提升用户与界面的交互流畅度和愉悦感。Windows Presentation Foundation(WPF)作为微软强大的桌面应用程序开发框架,提供了丰富且强大的动画功能,使开发者能够轻松创建出各种令人惊叹的动画特效,实现出色的界面交互效果。本文将深入剖析 WPF 动画特效的各个方面,通过大量详细的代码示例和对关键概念的^

不踩坑,不知道坑有多深;不总结,踩过的坑白踩。 5万+

WPF中实现动画的几种效果(最基础方式)

在动画之前我们先了解一下几个声明式动画中常用的元素: 一.Storyboard:故事板,是BeginAnimation()方法的XAML等价物,通过故事板将动画指定到合适的元素和属性,故事板是增强的时间线,可以用来分组多个动画,Storyboard类提供的最基本的功能是使用TargetName和TargetProperty属性指向某个特定的元素和属性; 二.DoubleAnimation:线性类插入两个目标值之间的**double**属性值进行处理,也就是线性插入; 1.TranslateTransform

凭时间赢来的东西,时间肯定会为之作证 14万+

【愚公系列】2023年10月 WPF控件专题 Path控件详解

WPF控件是Windows Presentation Foundation(WPF)中的基本用户界面元素。它们是可视化对象,可以用来创建各种用户界面。WPF控件可以分为两类:原生控件和自定义控件。原生控件是由Microsoft提供的内置控件,如Button、TextBox、Label、ComboBox等。这些控件都是WPF中常见的标准用户界面元素。自定义控件则允许开发人员使用XAML和C#等编程语言来创建个性化的用户界面元素。自定义控件可以根据需求提供更多的功能和自定义化选项,以及更好的用户体验。

愚公智库 1万+

WPF 基本动画

WPF 基本动画 旋转 移动 各种动画

WPF动画教程(PointAnimationUsingPath的使用)

通过本示例,我们接触了PointAnimationUsingPath类、PathGeometry类和Storyboard类,并使用了它们的一些属性与方法,以后如果遇到沿着特定路径的动画的需求,可以尝试使用这种方法,希望对你有所帮助。

mingupup的博客 1189

WPF基础动画

WPF基础动画学习,示例中包含代码;WPF技术的学习资料

WPF动画课——让界面“动“起来的完整指南!

今天,我们将彻底解密WPF动画系统,从零开始掌握界面动效的核心技能,让你的应用告别"静态时代",拥有丝滑流畅的视觉体验!:最常用的动画类型,用于变化数值属性(宽度、高度、透明度等):动画的"故事板",可以包含多个动画并行或顺序执行。:事件触发器,通过事件(如鼠标悬停)触发动画。一、 动画初体验:三分钟创建第一个动画。四、 关键帧动画:精准控制动画每一刻。:明确动画目的(引导、反馈、过渡)案例1:加载中动画(旋转+渐变)

杨勇博客之家 339

wpf Animation动画效果演示,包含多个实例

利用WPF Animation 开发的动画,包含多个实例,附源码

C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial 前38节)

C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial)目录 本系列教程目录如下: C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(一)让物体动起来① C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二)让物体动起来② C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三)让物体动起来③ C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(四)实现2D人物动画① C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(五)实现2D人物动画② C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(六)完美移动 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(七)传说中的A*寻径算法 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(八)完美实现A*寻径动态动画 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(九)2D游戏角色在地图上的移动 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十)斜度α地图的构造及算法 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十一)地图遮罩层的实现 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十二)神奇的副本地图 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十三)牵引式地图移动模式① C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十四)精灵控件横空出世!① C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十五)精灵控件横空出世!② C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十六)牵引式地图移动模式② C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十七)完美精灵之八面玲珑(WPF Only)① C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十八)完美精灵之八面玲珑(WPF Only)② C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(十九)完美精灵之八面玲珑(WPF Only)③ C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十)第一部分拓展小结篇 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十一)主位式地图移动模式 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十二)重构 - 让代码插上翅膀飞翔 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十三)自适应性窗口化与全屏化 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十四)Be careful!前方怪物出没 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十五)完美捕获精灵之神器 -- HitTest C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十六)通用型角色头像面板 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十七)战斗前夜之构建动态障碍物系统 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十八)经典式属性设计及完美的物理攻击系统 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十九)人工智能(AI)之追踪者 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十)大法师 - 华丽经典之轮回 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十一) 超酷万变的矢量魔法 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十二) 雷、混、冰、毒、火、风 - 幻化中的魔法魅力! C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十三) 锦上添花之魔法特效装饰 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十四) 地图编辑器诞生啦! C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十五) 地图编辑器的初步使用 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十六) 地图自定义切片与导出 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十七) 地图自适应区域加载 C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(三十八) 地图间的传送与切换 摘自博客园深蓝色右手文章(原文链接:http://www.cnblogs.com/alamiye010/archive/2009/06/17/1505346.html) 压缩包内的2个文件,chm和exe格式内容一样

WPF UI圆弧控件:Arc绘制与动画效果

你是否还在为WPF应用中的圆弧绘制和动态效果实现而烦恼?传统Path控件实现圆弧需要手动计算贝塞尔曲线参数,动画效果更是需要复杂的数学转换。本文将全面解析WPF UI框架中的Arc控件,通过10分钟快速掌握从基础绘制到高级动画的全流程实现,让你的界面设计瞬间提升专业质感。 读完本文你将获得: - 3种圆弧基础绘制模式(整圆/扇形/弧线)的实现方案 - 5类实用动画效果(加载动画/进度指示/状态切...

gitblog_01069的博客 972

WPF3D动画效果演示(超绚)C#

主要实现图片的浮动以及动态缩放、翻转等效果,例子为3D展示图片墙中,鼠标浮动在图片上与点击图片时所产生的3D动态效果

WPF—动画详解

Windows Presentation Foundation (WPF) 提供了一组强大的图形和布局功能,使用户能够创建引起注意的用户界面和有吸引力的文档。 动画不仅可以使引起注意的用户界面更加引人注目,还可以使其更加便于使用。 只需对背景色进行动画处理或应用经过动画处理的 Transform,即可创造出生动的屏幕过渡效果或提供有帮助的视觉提示。

qq_3517289697的博客 3325

【sap2000】部分模型通过.s2k文件合并模型generating a global model through partial models by .s2k text file

现在需要通过选项File → Import → SAP2000 .s2k Text File → Add to existing model(如果使用其中一个部分模型作为基础)或New model(如果使用新文件)添加部分模型,并使用以下导入选项。模型A(阶段1/5) Model A (Phase 1/5)模型B(阶段2/5) Model B (Phase 2/5)模型C(阶段3/5) Model C (Phase 3/5)模型E(阶段5/5) Model E (Phase 5/5)

hmywillstronger的博客 1748

告别轮询!用STM32CubeMX给STM32F030配置ADC多通道+DMA,实测代码分享

本文详细介绍了如何使用STM32CubeMX为STM32F030配置ADC多通道采集与DMA传输,显著提升采集效率。通过实测对比轮询与DMA模式的性能差异,提供完整的代码实现和优化技巧,帮助开发者快速掌握这一高效采集方案。

weixin_42570296的博客 409

终极指南:掌握HandyControl中的动画路径,实现惊艳的WPF路径动画效果

HandyControl是一个包含多种简单且常用WPF控件的开源项目,其中的动画路径功能能够帮助开发者轻松实现流畅、精美的路径动画效果,为WPF应用增添视觉吸引力。 ## 为什么选择HandyControl的动画路径? 在WPF应用开发中,实现复杂的路径动画往往需要编写大量代码,而HandyControl提供的`AnimationPath`控件将这一过程简化,让开发者能够快速上手,即使是新手也

gitblog_00177的博客 751

DeepSeek-V4 本地部署实战指南:4套方案从入门到企业级(附RTX 5090实测数据)

V4-Flash 便宜是便宜,输入 1 元/M token,输出 2 元/M token,但量大了还是肉疼。V4-Pro 更离谱,输入 12 元/M token,输出 24 元/M token,这价格要是天天调用,月账单轻松破万。用 4-bit 量化跑 V4-Flash,显存占用 ~22GB,延迟 ~80ms/token,并发能到 2-3 路。换句话说,同样的硬件,能跑更长的上下文、更大的并发。Flash 是「小而美」,一张 RTX 4090/5090 就能跑,门槛低,效果够用。

datayx的文章 9825

WPFDevelopers完全指南:从入门到精通的WPF控件库学习路线

WPFDevelopers是一个专注于WPF技术的开源项目,提供了丰富的基础控件和自定义控件库示例。本指南将帮助新手快速掌握WPF控件库的使用,从环境搭建到高级应用,逐步提升WPF开发技能。 ## 为什么选择WPFDevelopers? WPF(Windows Presentation Foundation)是微软推出的基于.NET框架的用户界面技术,以其强大的图形渲染能力和灵活的布局系统深受

gitblog_00702的博客 1147

13 万字 C 语言从入门到精通保姆级教程2021 年版

13 万字C语言保姆级教程,从入门到精通。

李南江 61万+

Circos在Windows下的安装教程

这里写自定义目录标题Circos简介安装Perl 的安装Circos安装安装及解压安装Modules测试用例官方文档 Circos简介 (来源官网:http://circos.ca/software/readme/ O.INTRODUCTION部分) Circos is a program for the generation of publication-quality, circularly composited renditions of genomic data and related annot

qq_42318911的博客 1660

【WPF动画】

示例代码1示例代码2:使用 `Storyboard` 组合多个动画代码解释应用场景动画类描述使用示例适用场景用于为double类型的属性(如WidthHeight等)创建动画。控件大小的变化、透明度的渐变等。用于为Color类型的属性(如BackgroundForeground等)创建动画。背景色、前景色的渐变效果。用于为Point类型的属性(如CenterPosition等)创建动画。控件位置的变化、路径动画。用于为Thickness类型的属性(如MarginPadding等)创建动画。

zsr154278963的博客 2052

GIS应用技巧之三招搞定重叠区域数据提取

在很多分析过程中,常常需要分析判断哪些图形发生了重叠,该重叠情况的发生在对应分析环境下具有特定意义。例如在缓冲区分析中,若两个受灾点周边的受灾面积(即缓冲区)有所重叠,分析重叠区域的受灾影响时,就需要考虑相互影响的因素。本例中先用【要素转面】工具提取出发生重叠的区域,其次用【空间连接】工具将原始图层信息连接到重叠数据的图层,分析重叠数据的属性表,即可得到发生重叠的原始图层数据。 Step1 提取重叠区属数据。打开【数据管理工具】|【要素】|【要素转面】工具,输入要素设置为“circles”,输出要素类设置为

端木宛白的博客家园 6万+
上一篇: WPF + Microsoft.ToolKit.Mvvm 技术指南与实战项目
下一篇: WPF + Prism 技术指南与实战项目(一、自行搭建)
bugcome_com
博客等级 码龄3年 32粉丝 131原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcome_com

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值