# =🚩 WPF Application Startup
GIF 动画制作工具包括 screen-capture-recorder 与 ffmpeg:
ffmpeg -y -f dshow -i audio="virtual-audio-capturer":video="screen-capture-recorder" -offset_x 0 -offset_y 0 -video_size 1920x1080 yo.mp4
ffmpeg -ss 49 -to 56 -i .\yo.mp4 -r 5 you.gif
这里以下面两条命令创建的 WPF 和 WinForms 模板程序开始:
dotnet new wpf
dotnet new winforms
这两种 Windows GUI 项目有相似的 csproj 配置,差别在于使用了不同开发框架和开发模式。 WPF 使用 UI 和代码分享的开发模式,WinForms 是全代码的旧方式。
它们属于两套界面渲染方式。WinForm 是传统 GDI 绘制方式的封装。 WPF 是基于 DirecX 渲染的界面,也脱离了对传统 Windows 控件的依赖,没有历史包袱,理论上可以展现更炫酷的界面。全新 UI 描述语言,特别是可以通过模板的嵌套实现复杂的元素,通过 style 实现类似 CSS 样式定义的功能。通过完善的数据绑定机制实现业务逻辑可以专心对数据而不是界面进行开发。数据绑定模式 MVVM 是 Web 中流行的开发模式。
XAML - Extensible Application Markup Language 是实现 MVVM - Model View ViewModel 数据驱动编程模式非常好的工具,这是一种能将 UI 和代码很好地解耦的设计模式。做得较好的 MVVM 框架,比如 React/Angular/Vue 都是大量用户在使用,可见这是多么成功的编程模式。
目前,WPF、 UWP、 Xamarin 等一众框架都基于 XAML 技术,Xamarin.Forms 4.1+ 支持热重载 XAML Hot Reload,需要 Visual Studio 2019 支持,但是不能执重载事件处理关系的更改。
Winform 本质上就是在 MFC 上增加一层 .Net API。2006 年开始就只有几个人在维护,基本上不增加任何新功能,只是做 bug fix。WinForm 用的是 Windows 以前那一套每个控件都是一个窗口的设计,而 WPF 本身只有一个窗口,Visual Tree 上绘制所有控件,它还可以有效利用 GPU。
先来认识 WPF - Windows Presentation Foundation 程序的核心,是一个与分辨率无关且基于矢量的呈现引擎,旨在充分利用现代图形硬件。 WPF 通过一套完善的应用程序开发功能对该核心进行了扩展,这些功能包括可扩展应用程序标记语言 XAML、控件、数据绑定、布局、二维和三维图形、动画、样式、模板、文档、媒体、文本和版式。 WPF 属于 .NET,因此可以生成整合 .NET API 其他元素的应用程序。
WPF 主要优势在动画和 3D 上,缺点也明显,平台局限性、不能用于旧系统:
1. XAML 可视化设计可以让设计师直接加入开发团队、降低沟通成本。
2. XAML 图形绘制功能非常强大,可以轻易绘制出复杂的图标、图画。
3. WPF 支持“滤镜”功能,可以像 Photoshop 那样为对象添加各种效果。
4. WPF 原生支持动面开发,设计师、程序员都能够使用 XAML 或 C# 轻松开发制作出炫丽的动画效果。
5. WWF 原生支持 3D 效果,甚至可以将其他 3D 建模工具创建的模型导入进来使用。
6. Blend 作为专门的设计工具让 WPF 如虎添翼,帮助不了解编程的设计师快速上手,建立图形或动画的原型。
从 WPF 的实现原理上看,这个产品的野心是挺大的,一方面想摆脱旧有 WinForms 的各种约束,一方面又期待着能基于 DirectX 搞游戏开发。曾经的 Silverlight 已经成为弃子,所以,大公司的产品有大坑。时代变了,WPF 框架设计很好很彻底,但 Web 成为了跨平台首选。React、Vue、Angular 哪个比不 WPF,哪个不比 WPF 香[doge]。自家还有最新的 WinUI 3 Preview。
微软买来的 Xamarin 也似乎没有在微软带领下发扬光大,桌面开发领域,微软背上了沉重的技术债务。
抛开以上问题,就 WPF 本身而言,尚有值得研究内容。从以往的 UI 框架发展来看,UI 是浮云,而 XAML 这里面潜藏的数据结构与处理技术才是一切的核心啊!
XAML 绘图本身就是矢量的,支持各式各样的填充和效果,还可以添加滤镜。
XAML 矢量图是借助 Expression Studio 中的 Design 和 Blend 两个工具绘制。Blend 可以直接绘制 XAML 图形;Design 可以像 Photoshop 或者 Fireworks 那样绘制图形,再由设计者决定导出 PNG 或 XAML 格式。
可以使用 Blend for Visual Studio 社区版自带的可视化界面开发工具。简单来说,Blend 就是一个基于 XAML 数据可视化开发工具,与流行的节点化编程工具不同,Blend 使用的是基于数据绑定与平面设计相结合的开发模式。在设计视图中,可以添加一些可视的图形控件,也可以添加一些功能性 XML 节点。
WPF 的基本图形派生自 System.Windows.Shapes 命名空间 Shape 基类,包括以下几个:
1. ** Line **:直线段,可以设置其笔触(Stroke)。
2. ** Rectangle **:矩形,既有笔触,又有填充(Fill)。
3. ** Ellipse **:椭圆,长、宽相等的椭圆即为正圆,既有笔触又有填充。
4. ** Polygon **:多边形,由多条直线段围成的闭合区域,既有笔触又有填充。
5. ** Polyline **:折线(不闭合),由多条首尾相接的直线段组成。
6. ** Path **:路径(闭合区域),基本图形中功能最强大的一个,可由若干直线、圆弧、贝塞尔曲线组成。
其中,Rectangle 和 Ellipse 是两种可以通过资产面板添加到设计视图的图形控件。而路径需要通过 Pen 或 Pencil 工具绘制。因为 WFP 是完全基于 DirecX 绘图的框架,可以直接使用笔画工具在设计视图上绘制任意的路径曲线。
在资产面板上按不同的分类罗列,可以将图形控件,或者其它功能性节点添加到 XAML 设计视图中。
概括来说,Blend 提供的设计界面操作真的谈不上非常好用,只能说功能呆滞,不仅会报错,还会卡死。甚至曲线控制点的平滑属性都不能控制,需要用户记住 SVG 绘图指令,以备不时之需要:
- M: 表示绘制起点,如:M 0,0
- L: 表示绘制直线 (H:横线 V:竖线),如:L 100,0
- C: 三次方贝塞尔曲线,如:C 100,200 200,400 300,200
- Q: 二次曲线
- z: 闭合
对象与时间线面板显示的树状结构就是 Visual Tree,与 XAML 中的数据节点对应。在树状结构中选择一个对象后,其属性会在属性面板显示。时间线另一个功能就是用来管理动画资源,** Storyboard ** 情节概要,是动画制作中使用术语,在 XAML 中是一种动画资源。
动画的本质是时间与变化的组合,通过记录某一时间段下控件的属性变化数据,并且在程序执行时回放,这就是最基本的动画技术。
时间线 Timeline,它所做的工作就是,将记录到的数据以关键帧的形式保存起来。可以为 WFP 窗口创建多个 Storyboard,以记录多个基于关键帧的动画。
动画即时间线,时间线即动画。所有动画对象都是记录时间线数据的对象,以最常用的故事板为例,继承关系:
Object → DispatcherObject → DependencyObject → Freezable → Animatable
→ Timeline → TimelineGroup → ParallelTimeline → Storyboard
https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.media.animation.storyboard
通过 Storyboard 可以控制的动画:的播放 Begin()、暂停 Pause()、恢复 Resume()、查找 Seek()、停止 Stop() 和删除 Remove()。
关键帧对象类型定义在 System.Windows.Media.Animation 命名空间,根据控件属性数据的类型不同,关键帧也有不同的类型,Animatable 抽象类提供动画接口支持。例如,很大一部分是让一个属性在起始值和结束值之间变化,这种动画对应 DoubleAnimation,它可以设置一个 Duration,在指定的时间段内,属性值从 From 到 To 之间变换。
1. ** From **: 起始值,在动画开始的时候将目标属性设置为该值;
2. ** To **: 结束值,动画结束是目标属性为改值;
3. ** By **: 偏移值:动画结束的时候目标属性为"初始值+偏移值";
根本上说,双值动画是一种简化的插值方法,Interpolation复杂一点的是缓动函数 Easing Functions,或者称缓动曲线插值算法。通常,动画的两个关键帧之间还有补帧,它们需要通过插值来获得一个确定的状态。
https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/graphics-multimedia/easing-functions
WPF 系统内置四种插值算法:
1. 线性:补帧均匀变化;
2. 离散:补帧是由离散数据确定的,没有过渡效果;
3. 样条:使用贝塞尔曲线实现更精确的加速和减速控制;
4. 缓动:使用缓动函数曲线实现弹性变化;
作为专攻动画的 WPF 框架,不再有类似 WinForm 中的 Timer 控件,资产工具箱中也没有 Timer 控件。System.Windows.Threading 命名空间下的 DispatcherTimer 类是替代方案,这个定时器和 UI 运行在同一线程上。与系统定时器不同,System.Timers.Timer 这种定时器和 UI 并不在同一个线程,会触发无效操作。
System.InvalidOperationException:“调用线程无法访问此对象,因为另一个线程拥有该对象。”
因为 WPF 中只有 UI 线程才能操作 UI 元素,非 UI 线程要访问 UI 时就会报异常。其他线程必须通过 Invoke、委托(安全性)等方式,Winform 可以开启/关闭“只允许UI线程修改UI”。
图形控件的属性面板及属性组织:
1. **
Brushes
** 画笔:设置图形控件的外观,可以使用纯色、渐变色、背景图或系统预置色。
2. **
Layout
** 布局:设置控件的大小、位置、边界等基础排版属性。
3. **
Visibility
** 外观:设置可见性,透明度,以及滤镜效果,如模糊效果 BlurEffect。
4. **
Automatic
** 自动化:设置快捷键、提示信息、控件状态等等。
5. **
Public
** 公共属性:设置 Cursor、有效性,以及数据绑定相关的 DataContext。
6. **
Transform
** 仿射变换:这是图形学中通用的变换方法,包括平移、旋转、缩放、切变、翻转等等。
7. **
Misc
** 杂项:其它未分类属性,包括 BindingGroup、ContexMenu,或 Style 等等。
在属性面板中创建样式后,可以右键菜单“转到源”打开并编辑样式,也可以直接在对象与时间线面板中,使用右键菜单 Edit Template - Apply Resource 创建样式定义。定义好的样式包含了以上这些属性设置,可以复用在其它控件上。
此外,还有事件处理相关的属性,在 Blend 的属性面板中,有专用于设置事件处理函数的列表。
WPF 作为大部分位于 System.Windows 命名空间中的 .NET 类型的一个子集存在。你可以使用最喜欢的 .NET 编程语言(如 C# 或 Visual Basic)来完成实例化类、设置属性、调用方法以及处理事件等操作。
WPF 可视化组件以 Visual 基类派生出关键子类,实现了 WPF 编程中可用的大量公共元素功能:
01. **
UIElement
** WPF 核心级实现的基类,其它 UI 实现是在此元素和基本表示特性上生成的。
02. **
ContentElement
** 为内容元素提供 WPF 核心级基类。 内容元素设计用于流样式显示,它们使用面向标记的直观布局模型和精心设计的简单对象模型。
03. **
FrameworkElement
** 提供 WPF 元素的属性、事件和方法的 WPF 框架级别集。 此类表示所提供的 WPF 框架级别实现基于 UIElement 定义的 WPF 核心级别 API。
04. **
FrameworkContentElement
**, 是 ContentElement 基类的 WPF 框架级别的实现和扩展。增加了针对下列各项的支持:附加输入 API(包括工具提示和上下文菜单)、演示图板、用于数据绑定的 数据上下文、格式支持和逻辑树帮助程序 API。
05. **
Viewport3D
** 由 Viewport3DVisual 在指定的二维视区边界内呈现 Visual3D 子对象。
Visual 对象是一个核心 WPF 对象,它的主要作用是提供呈现支持。 用户界面控件派生自 Visual 类,并使用该类来保存它们的呈现数据。 Visual 对象为以下项提供支持:
01. 输出显示:呈现视觉对象的持久、序列化的绘图内容。
02. 转换:针对视觉对象执行转换。
03. 剪裁:为视觉对象提供剪裁区域支持。
04. 命中测试:确定坐标或几何形状是否包含在视觉对象的边界内。
05. 边框计算:确定视觉对象的边框。
Visual 对象角色的关键之一是,了解**
即时模式
**和**
保留模式
**图形系统之间的区别。GDI 或 GDI+ 应用程序使用即时模式图形系统,这意味着 WinForms 应用程序由于某项操作(如重设窗口大小)或者对象的可视化外观发生变化而失效的工作区部分,将由程序负责重新绘制。
相比之下,WPF 使用保留模式系统。 这意味着具有可视化外观的应用程序对象定义一组序列化绘图数据。 在定义了绘图数据之后,系统会响应所有的重新绘制请求来呈现应用程序对象。 甚至在运行时,用户可以修改或创建应用程序对象,并仍依赖于系统响应绘制请求。 保留模式图形系统中有一个强大功能,即绘图信息总是由应用程序保持为序列化状态,但是呈现功能仍由系统负责。
使用保留模式图形的最大好处之一就是,WPF 可以高效优化需要在应用程序中重绘的内容。
⚡ XAML & Code-Behind
WPF 图形界面应用有两个最基本的概念 Markup & Code-Behind:
-
代码
:这是一个应用程序最基本的构成,代码经过编译器的处理,会转换成 .Net CLR 平台上可运行的程序;
-
XAML
:基于 XML 格式化表达程序图形界面的类型,及其它层次结构,本质就是程序代码要使用到的数据;
从以上两个概念出发,WPF 图形框架可以看作是一个负责协调 XMAL 数据与 GUI 类库层次结构,以及事件处理程序、动画效果的开发工具。WPF 还包括增强属性和事件的其他编程构造: 依赖项属性 和 路由事件。
XAML 是一种基于 XML 的标记语言,像 HTML 一样以声明形式实现应用程序的外观。 通常用它创建窗口、对话框、页和用户控件,并填充控件、形状和图形。
模板创建的 App.xaml 和配套的 App.xaml.cs 是程序的入口,对应 System.Windows.Application 实例,其属性 `
StartupUri
` 指示了程序运行的第一个窗体对象,同时可以设置 `
Startup
` 指定一个启动时要执行的事件,可以不设置 `
StartupUri
` 而通过指定的启动事件来实例化窗体。
启动事件的代码就写在 App.xaml.cs 文件内,比如 `Startup="App_OnStartup"`:
默认的主窗体也一样有两个文件对应 System.Windows.Window 对象。
整个 WPF 程序有两种开发模式,XAML 的标签化 UI 编辑模式,可以在 Visual Studio 中拖放对象组件。
另一种是代码编写模式,用来实现程序逻辑。WPF 通过 XAML 实现了 UI 和代码的分享。这种软件开发模式
明显优于 WinForms 旧式的纯代码开发。
比如,在 MainWindow.xaml 添加一个按钮控件对象:
在 MainWindow.xaml.cs 添加一个事件处理函数:
当构造函数调用
InitializeComponent
方法时,就会将标记定义的 UI 控件与背后的代码合并在一起,它生成应用程序为您正确初始化 UI 组件的实现代码,包括将按钮的 Click 事件与事件处理程序关联。这就是 Markup & Code-Behind,用标记定义控件,用背后的代码自动实现。
使用代码编程,第一个问题是要解决如何获取 XAML 文档中定义的对象。
FrameworkElement 是控件所继承的基类,并且具有 Resources 属性。 因此可以将本地资源字典添加到任何 FrameworkElement,它提供 `
FindResource()
` 查找和引用资源,还提供 `
FindName()
`方法,根据名称来查找 XAML 文档节点定义的其它对象。
假设一个 XAML 在 Window.Resources 本地资源中定义了故事板对象,那么就可以按以下操作获取引用。要获取故事板的状态,可以注册事件处理函数来获取其状态数据。
更好的方法是使用代码生成工具为 XAML 节点定义的内部成员,通过翻查 obj 目录下的生成代码,可以发现XAML 中定义的每个节点都有一个相应的声明为
internal
的成员引用它,成员名称即是 XAML 节点定义的名称。比如,XAML 中有节点名称为
my_grid
,就可以直接使用
this.my_grid
引用它。
⚡ Data Bindding
⚡ Data Bindding
MVVM 编程模式的一个实现方式就是 INotifyPropertyChanged 接口,属性变换通知机制可以在属性数据被修改时触发一个事件通知,绑定到 PropertyChanged 的事件处理函数就有机会响应,数据变更这一行为就变成了触发一个事件,通常这个事件会用来更新用户界面,即 MVVM 编程模式中称作视图 View 的部分。View 通常通过 XAML 文件中定义的 `
BindingContext
` 绑定 ViewModel 的实例为其提供数据。
随着编程技术的发展,“属性”这一概念也由
Attribute
转变为
Property
,从简单的数据转换成为“财产”。并且,逻辑上加入了控制权:
在 WPF 框架的类结构中,几乎所有的控件都间接继承自 DependencyObject 类型,并通过 DependencyProperty 给属性引入依赖管理机制,定义一个依赖属性,就要向此机构注册它。当依赖属性被修改后,即触发 OnPropertyChanged 事件通知,示范如下:
数据保存在 DependencyProperty 内部的一个 IDictionary 字典中,一条记录中的键(Key)就是
该属性的 HashCode,而值(Value)则是已注册好的 DependencyProperty。
为了方便获取方法的名称并传递给依赖者,可以使用 System.Runtime.CompilerServices 命名空间下
提供的一系列编译获取信息的标注,其中就有:
- **CallerMemberNameAttribute** 获取方法调用方的方法或属性名称。
绑定允许用户更改数据,然后将该数据传播回源对象。或者,可能不希望允许用户更新源数据。可以设置
Binding.Mode 来控制数据流。
完成一个绑定,需要有 Source、Binding Object 和 Target 三方,不同类型的数据流控制方式:
- **
OneWay
** 大多数属性默认绑定,源属性的更改会自动更新目标属性,但目标属性的更改不会传播回源属性。
- **
TwoWay
** 交互式控件默认绑定,更改源属性或目标属性时会自动更新另一方,如 TextBox.Text 和 CheckBox.IsChecked。
- **
OneWayToSource
** 绑定与 OneWay 绑定相反;当目标属性更改时,它会更新源属性。
用于确定依赖属性绑定在默认情况下是单向还是双向的编程方法是:
- 使用
DependencyProperty.GetMetadata
获取属性元数据,
- 然后检查
FrameworkPropertyMetadata.BindsTwoWayByDefault
属性的布尔值。
静态绑定在第一次生成界面时,并且永远不会发生更改。
以下是一个 StaticResource 数据绑定示范:
-
在 Resources 属性中定义数据源;
-
设置 DockPanel 的 DataContext 属性,创建数据绑定;
-
然后将数据源绑定到 Button 节点的 Background 属性上。
⚡ Triggers & Routed Events
⚡ Triggers & Routed Events
触发器,Trigger,即按条件应用属性值或执行操作的一类对象,可以直接在 XAML 中可以供设计人员使用,这是一个不需要需编写代码也能做不少事的工具。注意 Blend for Visual Studio 社区版需要使用代码方式添加触发器。
FrameworkElement.Triggers 获取直接在此元素上或在子元素中建立的触发器的集合。
Trigger 对象具有 Setters, EnterActions, ExitActions 等属性,这些属性根据特定属性的状态应用更改或操作。EventTrigger 对象在发生指定的路由事件时启动一组 Actions。
以下是一些常用到触发器的对象类型:
01. 所有控件触发(FrameworkElement.Triggers)
02. 样式触发(Styles.Triggers)
03. 控件模板触发(ControlTemplate.Triggers)
04. 数据模板触发(DataTemplate.Triggers)
例如,当鼠标指针位于某个用户界面 (UI) 控件上时,可能需要使用 EventTrigger 来启动一组动画。 EventTrigger 没有状态终止的概念,因此一旦引发事件的条件不再成立,操作将不会撤消。
通常,可选择在每个按钮的 Triggers 集合中放置事件触发器。然而,对于动画这种方法不能工作。解决方法是在一个地方定义所有事件触发器,例如,在顶级元素的 Triggers 集合中。这样,就可以在用户界面中将按钮移到不同的位置,而不会禁用他们的功能。
使用
EventTrigger.SourceName
属性关联事件触发器,只要此属性和按钮的
Name
属性相匹配,触发器就会应用到恰当的按钮上。
注意,
BeginStoryboard
要播放其它故事板,必须为动作指定目标的名称,Name 属性要与。其他触发器通过
BeginStoryboardName
属性指定相同名称,连接到相同的故事板。
如果要触发其它已经定义好的故事板动画,可以通过 BeginStoryboard 的 Storyboard 属性绑定。
“
路由事件
”,可以从功能或实现的角度来理解它。
- 功能定义:路由事件是一种可以
针对元素树中的多个侦听器调用处理程序
的事件。
- 实现定义:路由事件是一个 CLR 事件,由 RoutedEvent 类的实例提供支持并由 WPF 事件系统处理。
请思考下面的简单元素树:
在这个简化的元素树中,Click 事件的源是某个 Button 元素,而所单击的 Button 是有机会处理该事件的第一个元素。 但是,如果附加到 Button 的所有处理程序均未处理该事件,则该事件向上浮升到元素树中的父级(即 StackPanel),还可能会浮升到 Border,然后会到达元素树的根节点。
换言之,此 Click 事件的事件路由为:
Button-->StackPanel-->Border-->...
路由事件是一个 CLR 事件,它由 RoutedEvent 类的实例提供支持并向 WPF 事件系统注册。 从注册中获取的
RoutedEvent
实例,通常保留为特定类的
public static readonly
字段成员,该类注册路由事件并因此“拥有”路由事件。 与同名 CLR 事件(有时称为“包装器”事件)的连接是通过替代 CLR 事件的 add 和 remove 实现来完成的。路由事件的支持和连接机制在概念上与以下机制相似:依赖属性是一个 CLR 属性,该属性由
DependencyProperty
类提供支持并向 WPF 属性系统注册。
以下示例演示自定义 Tap 路由事件的声明,其中包括注册和公开 RoutedEvent 标识符字段以及对 Tap CLR 事件进行 add 和 remove 实现。
XAML 定义了一种语言组件和称为
attached-events
的事件类型。 通过附加事件的概念,你能够向任意元素(而不是实际定义或继承事件的元素)添加特定事件的处理程序。 在这种情况下,对象既不会引发事件,目标处理实例也不会定义或“拥有”事件。
✨ 参考
✨ 参考
- https://github.com/dotnet/wpf
- https://learn.microsoft.com/zh-cn/visualstudio/xaml-tools/creating-a-ui-by-using-xaml-designer-in-visual-studio
- https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/graphics-multimedia/animation-overview
- https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/wpf-architecture
- https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/xaml-in-wpf
- https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/getting-started/whats-new
- https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/getting-started/walkthrough-my-first-wpf-desktop-application
- https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/data-binding-overview
- https://www.cnblogs.com/KnightsWarrior/archive/2010/08/27/1809739.html
- https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.dependencyobject
- https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.dependencyproperty
- https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.propertymetadata