WPF核心程序集

🥑本篇为学习博客园大佬圣殿骑士的《WPF基础到企业应用系列》以及部分DotNet菜园的《WPF入门教程系列》所作笔记,对应圣殿骑士《WPF基础到企业应用系列》第 1 - 6 章之间内容,包括 WPF 项目结构、程序的启动和关闭、程序的生命周期、继承关系以及常见的布局控件及其应用。先上链接:

圣殿骑士博文索引

DotNet菜园

WPF核心程序集

创建一个WPF后,项目引用中会默认引用PresentationCore、PresentationFramework、WindowsBase三个WPF核心程序集:

PresentationCore:定义了WPF中基本的UI元素和呈现系统的核心功能,包括布局、渲染、输入、事件等。它包含了许多基本的类和接口,如UIElement、Visual、DispatcherObject、Freezable等。

PresentationFramework:这是WPF中的应用程序框架,提供了一些高级UI控件和应用程序级别的功能,如Windows、Pages、Navigation、Application、Window等。此外,PresentationFramework还定义了WPF的命名空间、样式和主题等。

WindowsBase:它包含了一些基础类和接口,用于支持PresentationCore和PresentationFramework,如DependencyObject、DependencyProperty、RoutedEvent、FrameworkElement等。

文件结构

默认生成的文件结构如图:

在 App.xaml 中,可以指定项目运行时启动的窗体,默认是主窗体:MainWindow, 此外还可以还可以定义需要的系统资源以及引入程序集等操作 - App.xaml:

在MainWindow.xaml中设计主窗体样式:修改标题为:XAMLWithScript,而后添加一个Button按钮,并进行一些对按钮做一些简单的”初始化“ - MainWindow.xaml:

当前XAML样式呈现的是一个标题为XAMLWithScript,有一个内容为OnClick的Button按钮:

样式中定义的事件在当前页面的后台页面MainWindow.xaml.cs中:

WPF 和 Winform 案例

6.WPF 和 WinForm 案例

Application

WPF 和 传统的 WinForm 类似, WPF 同样需要一个 Application 来统领一些全局的行为和操作,并且每个 Domain (应用程序域)中只能有一个 Application 实例存在。和 WinForm 不同的是 WPF Application 默认由两部分组成 : App.xaml 和 App.xaml.cs,这有点类似于 Delphi Form(我对此只是了解,并没有接触过 Delphi ),将定义和行为代码相分离。当然,这个和 WebForm 也比较类似。XAML 从严格意义上说并不是一个纯粹的 XML 格式文件,它更像是一种 DSL(Domain Specific Language,领域特定语言),它的所有定义都直接映射成某些代码,只是具体的翻译工作交给了编译器完成而已。WPF 应用程序由 System.Windows.Application 类来进行管理。

WPF 程序启动项

之前章节有说过WPF程序的启动项默认是通过StartupUri来确定打开哪个窗体,我的理解是它和Winform一样也是通过入口函数来控制打开哪个窗体,只不是默认情况下需要通过StartupUri间接来确定具体打开谁:

新建一个类文件WPFStartupItem.cs重新定义程序的入口,在项目属性中将启动对象修改为自定义的类:

using System;

using System.Windows;

namespace WpfApp2

{

class WPFStartupItem : Application

{

[STAThread]

static void Main()

{

// Method 1 : 创建 Application 对象作为程序入口

Application app = new Application();

Window window = new Window(); // 空窗体 仅做说明

app.Run(window);

// Method 2 : 指定 Application 对象的 MainWindow 属性,调用无参数的 Run 方法

Window window1 = new Window();

app.MainWindow = window1;

window1.Show(); // 必须调用 Show 方法,否则无法显示窗体

app.Run();

// Method 3 : 通过 URL 的方式启动

app.StartupUri = new Uri("MainWindow.xaml", UriKind.Relative);

app.Run();

}

}

}

WPFStartupItem是一个WPF应用程序的启动代码示例,演示了三种不同的方式来启动应用程序。

🍍[STAThread] 是一个线程特性(thread attribute),用于指定应用程序的主线程类型。

在Windows应用程序中,特别是涉及到图形用户界面(GUI)的应用程序中,使用了单线程单元 (STA) 模型。STA模型要求应用程序的主线程(也称为消息循环线程)是单线程的,并且使用了单线程单元的COM组件和功能。

在.NET应用程序中,默认情况下,主线程被标记为多线程单元 (MTA) 模型。但是,对于大多数GUI应用程序,特别是WPF和WinForms应用程序,必须将主线程标记为STA模型,以确保与COM组件和其他GUI相关的功能的兼容性。

因此,为了确保应用程序的主线程被标记为STA模型,需要在主线程的入口方法(例如 Main() 方法)前添加 [STAThread] 特性。

在给定的示例中,[STAThread] 特性被应用于 Main() 方法,用于指定主线程的模型为STA模型,以确保与GUI和COM组件的兼容性。

🍉Application 类是WPF应用程序的核心类之一,它继承自System.Windows.Application。它提供了管理和控制WPF应用程序的功能。

Application 类的主要职责包括:

提供应用程序的入口点:Application 类定义了一个静态的 Main() 方法,作为应用程序的入口点。在 Main() 方法中,可以创建一个 Application 对象,并调用 Run() 方法来启动应用程序。

管理应用程序的生命周期:Application 类负责处理应用程序的启动、关闭和退出过程。它提供了事件和方法,用于在应用程序的不同生命周期阶段执行相应的操作,如 Startup 事件用于处理应用程序启动时的逻辑,Exit 事件用于处理应用程序退出时的逻辑。

管理应用程序的资源:Application 类允许您定义和访问应用程序级别的资源,如样式、模板、资源字典等。这些资源可以在整个应用程序中共享和重用。

处理全局异常:Application 类提供了一个 DispatcherUnhandledException 事件,用于捕获和处理应用程序中未处理的异常。您可以订阅该事件,并在发生异常时执行自定义的异常处理逻辑。

管理应用程序的窗口和导航:Application 类提供了管理应用程序窗口和页面导航的功能。您可以使用 MainWindow 属性设置应用程序的主窗口,使用 NavigationService 属性进行页面之间的导航。

WPF 程序关闭

WPF程序一般通过调用Shutdown()方法关闭程序,当然也可以通过Close()或者Application.Exit()实现。默认WPF项目中,Shutdown()方法是隐式发生的,可以通过在App.xaml中显示调用:

ShutdownMode 参数

作用

OnLastWindowClose(默认值)

最后一个窗体关闭或调用 Application 对象的 Shutdown() 方法时,应用程序关闭。

OnMainWindowClose

启动窗体关闭或调用 Application 对象的 Shutdown() 方法时,应用程序关闭。(和 C# 的 Windows 应用程序的关闭模式比较类似)

OnExplicitShutdown

只有在调用 Application 对象的 Shutdown() 方法时,应用程序才会关闭。

同样你也可以在代码文件(App.xaml.cs)中进行更改,但必须注意这个设置写在app.Run() 方法之前 ,如下代码:

app.ShutdownMode = ShutdownMode.OnExplicitShutdown;

app.Run(win);

Application 对象事件

窗体 Window

窗体均继承自System.Windows.Window基类,前文说过在WPF中,一个窗体通常被分成XAML UI文件和后台.cs代码文件,最早的XAMLWithScript:

// MainWindow.xaml

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:WpfApp2"

mc:Ignorable="d"

Title="XAMLWithScript" Height="450" Width="800" Loaded="Window_Loaded">

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace WpfApp2

{

///

/// MainWindow.xaml 的交互逻辑

///

public partial class MainWindow : Window

{

public MainWindow()

{

InitializeComponent();

}

private void button1_Click(object sender, RoutedEventArgs e)

{

}

private void Window_Loaded(object sender, RoutedEventArgs e)

{

}

}

}

事件等代码逻辑内容是写在后台代码文件中的,也可以通过x:Code内部XAML类型在XAML生产环境中放置代码:

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:WpfApp2"

mc:Ignorable="d"

Title="XAMLWithScript" Height="450" Width="800" Loaded="Window_Loaded">

{

button1.Content="Hello there.";

}

]]>

记得把后台.cs文件中关于点击事件注释掉,否则会造成冲突:

上述XAML代码在点击按钮后,按钮文本会变成Hello there.:

窗体生命周期

WPF 窗口概述 (WPF .NET)

🥉第一次打开窗口时,只有当引发 Activated 后才会引发 Loaded 和 ContentRendered 事件。 记住这一点,在引发 ContentRendered 时,实际上就可认为窗口已打开。

WPF 窗体的详细的属性、方法、事件请参考 MSDN,有很多的属性、方法、事件与Windows应用程序中System.Windows.Forms.Form类颇为相似,其中常用的一些属性、方法、事件有:

窗体边框模式(WindowStyle 属性)和是否允许更改窗体大小(ResizeMode 属性)。

窗体启动位置(WindowStartupLocation 属性)和启动状态(WindowState 属性)等。

窗体标题(Title 属性)及图标 。

是否显示在任务栏(ShowInTaskbar)

始终在最前(TopMost 属性)

Dispatcher 多线程

如何对控件进行线程安全的调用(Windows 窗体 .NET)

Dispatcher 类

微软在WPF引入了Dispatcher,不管是WinForm应用程序还是WPF应用程序,实际上都是一个进程,一个进程可以包含多个线程,其中有一个是主线程,其余的是子线程。在WPF或WinForm应用程序中,主线程负责接收输入、处理事件、绘制屏幕等工作,为了使主线程及时响应,防止假死,在开发过程中对一些耗时的操作、消耗资源比较多的操作,都会去创建一个或多个子线程去完成操作,比如大数据量的循环操作、后台下载。这样一来,由于UI界面是主线程创建的,所以子线程不能直接更新由主线程维护的UI界面。

Dispatcher的作用是用于管理线程工作项队列,类似于Win32中的消息队列,Dispatcher的内部函数,仍然调用了传统的创建窗口类,创建窗口,建立消息泵等操作。Dispatcher本身是一个单例模式,构造函数私有,暴露了一个静态的CurrentDispatcher方法用于获得当前线程的Dispatcher。对于线程来说,它对Dispatcher是一无所知的,Dispatcher内部维护了一个静态的 List _dispatchers, 每当使用CurrentDispatcher方法时,它会在这个_dispatchers中遍历,如果没有找到,则创建一个新的Dispatcher对象,加入到_dispatchers中去。Dispatcher内部维护了一个Thread的属性,创建Dispatcher时会把当前线程赋值给这个 Thread的属性,下次遍历查找的时候就使用这个字段来匹配是否在_dispatchers中已经保存了当前线程的Dispatcher。

继承关系

System.Object 类:基类。

System.Windows.Threading.DispatcherObject 类:从图中看WPF 中的使用到的大部分控件与其他类大多是继承 DispatcherObject 类,它提供了用于处理并发和线程的基本构造。

System.Windows.DependencyObject类:对WPF中的依赖项属性承载支持与 附加属性承载支持,表示参与 依赖项属性 系统的对象。

System.Windows.Media.Visual类:为 WPF 中的呈现提供支持,其中包括命中测试、坐标转换和边界框计算等。

System.Windows.UIElement 类:UIElement 是 WPF 核心级实现的基类,该类是 Windows Presentation Foundation (WPF) 中具有可视外观并可以处理基本输入的大多数对象的基类。

System.Windows.FrameworkElement类:为 Windows Presentation Foundation (WPF) 元素提供 WPF 框架级属性集、事件集和方法集。此类表示附带的 WPF 框架级实现,它是基于由UIElement定义的 WPF 核心级 API 构建的。

System.Windows.Controls.Control 类:表示 用户界面 (UI) 元素的基类,这些元素使用 ControlTemplate 来定义其外观。

System.Windows.Controls.ContentControl类:表示没有任何类型的内容表示单个控件。

WPF的绝大部分的控件,还包括窗口本身都是继承自ContentControl的:

System.Windows.Controls.ItemsControl 类:表示可用于提供项目的集合的控件。

System.Windows.Controls.Panel类:为所有 Panel 元素提供基类。 使用 Panel 元素定位和排列在 Windows Presentation Foundation (WPF) 应用程序的子对象。

System.Windows.Sharps.Sharp类:为 Ellipse、Polygon 和 Rectangle 之类的形状元素提供基类。

走进Dispatcher

WPF 线程分配系统提供一个Dispatcher 属性、VerifyAccess 和CheckAccess方法来操作线程。线程分配系统位于所有WPF类中基类,大部分WPF元素都派生于此类,如下图的Dispatcher类:

与Dispatcher调度对象想对应的就是DispatcherObject,在WPF中绝大部分控件都继承自 DispatcherObject,甚至包括 Application。这些继承自 DispatcherObject 的对象具有线程关联特征,也就意味着只有创建这些对象实例,且包含了Dispatcher的线程 (通常指默认UI线程)才能直接对其进行更新操作。

我们声明一个文本Label并尝试在程序运行过程中更新其显示内容:

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:WpfApp2"

mc:Ignorable="d"

Title="XAMLWithScript" Height="450" Width="800" WindowStartupLocation="CenterScreen" Loaded="Window_Loaded">

后台代码:

using System;

using System.Threading;

using System.Windows;

namespace WpfApp2

{

///

/// MainWindow.xaml 的交互逻辑

///

public partial class MainWindow : Window

{

public MainWindow()

{

InitializeComponent();

Thread thread = new Thread(ModifyUI);

thread.Start();

}

private void button1_Click(object sender, RoutedEventArgs e)

{

button1.Content = "Hello there.";

}

private void Window_Loaded(object sender, RoutedEventArgs e)

{

}

private void ModifyUI()

{

// 模拟一些工作

Thread.Sleep(TimeSpan.FromSeconds(5));

lbl_Hello.Content = "Hello,Dispatcher";

}

}

}

在程序运行五秒后就会报错,System.InvalidOperationException:“调用线程无法访问此对象,因为另一个线程拥有该对象。”

这和Winform跨线程更新UI是类似的,我们一般会使用委托完成对线程UI的更新。在WPF中,按照DispatcherObject的限制原则,我们改用 Window.Dispatcher.Invoke() 即可顺利完成这个更新操作。

如果在其他工程或者类中,我们可以用Application.Current.Dispatcher.Invoke方法来完成同样的操作,它们都指向UI Thread Dispatcher这个唯一的对象。Dispatcher 同时还支持BeginInvoke异步调用,如下代码:

private void btnHello_Click(object sender, RoutedEventArgs e)

{

new Thread(() =>

{

Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,

new Action(() =>

{

Thread.Sleep(TimeSpan.FromSeconds(5));

this.lblHello.Content = DateTime.Now.ToString();

}));

}).Start();

}

布局

虽然UI很重要,但不能为了UI而UI!

WPF 的布局控件都在System.Windows.Controls.Panel这个基类下面,使用Panel元素在WPF应用程序中放置和排列子对象。

一个Panel的呈现是测量和排列Children子元素、然后在屏幕上绘制它们的过程。所以在布局的过程中会经过一系列的计算,那么Children越多,执行的计算次数就越多。如果不需要较为复杂的 Panel(如Grid和自定义复杂的 Panel),则可以使用构造相对简单的布局(如 Canvas、UniformGrid等),这种布局可带来更好的性能。如果有可能,我们应尽量避免不必要地调用UpdateLayout方法。

每当Panel内的子元素改变其位置时,布局系统就可能触发一个新的处理过程。对此,了解哪些事件会调用布局系统就很重要,因为不必要的调用可能导致应用程序性能变差。

换句话说,布局是一个递归系统,实现在屏幕上对元素进行大小调整、定位和绘制,然后进行呈现。具体如下图,要实现控件 0 的布局, 那么先要实现 0 的子控件 01,02... 的布局, 要实现 01 的布局, 那么得实现 01 的子控件 001,002... 的布局, 如此循环直到子控件的布局完成后, 再完成父控件的布局, 最后递归回去直到递归结束, 这样整个布局过程就完成了。

布局系统为Panel中的每个子控件完成两个处理过程:测量处理过程(Measure)和排列处理过程(Arrange)。每个子Panel均提供自己的 MeasureOverride 和ArrangeOverride方法,以实现自己特定的布局行为。

Canvas

Canvas是最基本的面板,只是一个存储控件的容器,它不会自动调整内部元素的排列及大小。不指定元素位置,元素将默认显示在画布的左上方。它仅支持用显式坐标定位控件,它也允许指定相对任何角的坐标,而不仅仅是左上角。可以使用Left、Top、Right、 Bottom附加属性在Canvas中定位控件。通过设置Left和Right属性的值表示元素最靠近的那条边,应该与Canvas左边缘或右边缘保持一个固定的距离,设置Top和Bottom的值也是类似的意思。实质上,你在选择每个控件停靠的角时,附加属性的值是作为外边距使用的。

Canvas的主要用途是用来画图。Canvas 默认不会自动裁减超过自身范围的内容,即溢出的内容会显示在Canvas外面,这是因为默认 ClipToBounds="False";我们可以通过设置ClipToBounds="True来裁剪多出的内容。

接下来我们来看两个实例,通过xaml和C#实现相同视觉效果:

xaml样式:

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:local="clr-namespace:WpfApp2"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

Title="XAMLWithScript"

Width="800"

Height="450"

Loaded="Window_Loaded"

WindowStartupLocation="CenterScreen"

mc:Ignorable="d">

Background="White">

Canvas.Top="101"

Width="250"

Height="200"

Fill="Blue"

Stroke="Azure" />

Canvas.Top="45"

Width="250"

Height="100"

Panel.ZIndex="1"

Fill="Red"

Stroke="Green" />

C#代码实现:

Canvas canv = new Canvas();

//把canv添加为窗体的子控件

Content = canv;

canv.Margin = new Thickness(0, 0, 0, 0);

canv.Background = new SolidColorBrush(Colors.White);

//Rectangle

Rectangle r = new Rectangle();

r.Fill = new SolidColorBrush(Colors.Red);

r.Stroke = new SolidColorBrush(Colors.Red);

r.Width = 200;

r.Height = 140;

r.SetValue(Canvas.LeftProperty, (double)200);

r.SetValue(Canvas.TopProperty, (double)120);

canv.Children.Add(r);

//Ellipse

Ellipse el = new Ellipse();

el.Fill = new SolidColorBrush(Colors.Blue);

el.Stroke = new SolidColorBrush(Colors.Blue);

el.Width = 240;

el.Height = 80;

el.SetValue(Canvas.ZIndexProperty, 1);

el.SetValue(Canvas.LeftProperty, (double)100);

el.SetValue(Canvas.TopProperty, (double)80);

canv.Children.Add(el);

🧨Canvas内的子控件不能使用两个以上的Canvas附加属性,如果同时设置Canvas.Left和Canvas.Right属性,那么后者将会被忽略

StackPanel

堆栈面板,水平或垂直放置元素。通过设置面板的Orientation属性设置了两种排列方式:横排(Horizontal 默认的)和竖排(Vertical)。纵向的StackPanel默认每个元素宽度与面板一样宽,反之横向亦然。如果包含的元素超过了面板空间,它只会截断多出的内容。 元素的Margin属性用于使元素之间产生一定得间隔,当元素空间大于其内容的空间时,剩余空间将由HorizontalAlignment和VerticalAlignment属性来决定如何分配。

同样看实例:

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:local="clr-namespace:WpfApp2"

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

Title="XAMLWithScript"

Width="800"

Height="450"

Loaded="Window_Loaded"

WindowStartupLocation="CenterScreen"

mc:Ignorable="d">

Background="White"

Orientation="Horizontal">

C#代码:

Grid grid = new Grid();

grid.Width = double.NaN;

grid.Height = double.NaN;

grid.ShowGridLines = true;

grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(61, GridUnitType.Star) });

grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(101, GridUnitType.Star) });

grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(108, GridUnitType.Star) });

grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(139) });

grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(184, GridUnitType.Star) });

grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(45, GridUnitType.Star) });

grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(250, GridUnitType.Star) });

TextBlock textBlock = new TextBlock();

textBlock.Text = "第一行、第一列,占1列";

textBlock.Background = Brushes.LightBlue;

textBlock.HorizontalAlignment = HorizontalAlignment.Center;

Grid.SetRow(textBlock, 0);

Grid.SetColumn(textBlock, 0);

Grid.SetColumnSpan(textBlock, 1);

Button button1 = new Button();

button1.Content = "第一行、占3列";

Grid.SetRow(button1, 0);

Grid.SetColumn(button1, 1);

Grid.SetRowSpan(button1, 2);

Grid.SetColumnSpan(button1, 3);

Button button2 = new Button();

button2.Content = "第3行,第1列开始,占4列";

Grid.SetRow(button2, 2);

Grid.SetColumn(button2, 0);

Grid.SetColumnSpan(button2, 4);

grid.Children.Add(textBlock);

grid.Children.Add(button1);

grid.Children.Add(button2);

this.Content = grid;

ViewBox

Viewbox 是一个容器控件,允许其内容根据可用空间进行缩放,同时保持其纵横比。它通常用于为其中的内容提供自动缩放和调整大小的行为。

ViewBox这个控件通常和其他控件结合起来使用,是WPF中非常有用的控件,用来定义一个内容容器。ViewBox组件的作用是拉伸或延展位于其中的组件,以填满可用空间,使之有更好的布局及视觉效果。

🧨一个 Viewbox中只能放一个控件。如果多添加了一个控件就会报错。

以下是一些常用的 Viewbox 属性:

Stretch(拉伸):指定内容在视图框中的拉伸方式。常见的取值包括:

Uniform(均匀):保持内容的纵横比,同时填充视图框,可能导致内容被裁剪。

UniformToFill(均匀填充):保持内容的纵横比,同时填充视图框,可能导致内容被裁剪。

Fill(填充):不保持内容的纵横比,将内容拉伸以填充视图框。

StretchDirection(拉伸方向):指定内容在视图框中拉伸的方向。常见的取值包括:

Both(双向):内容可以在水平和垂直方向上拉伸。

DownOnly(仅向下):内容只能在垂直方向上拉伸。

UpOnly(仅向上):内容只能在水平方向上拉伸。

HorizontalAlignment(水平对齐)和 VerticalAlignment(垂直对齐):指定内容在视图框中的水平和垂直对齐方式。常见的取值包括:

Left(左对齐):内容在视图框的左侧对齐。

Center(居中对齐):内容在视图框的中间对齐。

Right(右对齐):内容在视图框的右侧对齐。

Top(顶部对齐):内容在视图框的顶部对齐。

Bottom(底部对齐):内容在视图框的底部对齐。

MaxWidth(最大宽度)和 MaxHeight(最大高度):指定内容在视图框中的最大宽度和最大高度限制。当内容超过指定的最大尺寸时,将被自动缩放以适应。

Uniform效果下的Viewbox:

如果你真的看到了这儿,那我觉得,这件事真是 - 泰裤辣!!!🤔

自定义Panel

该章节省略未读。。。