WPF-开发秘籍-全-
WPF 开发秘籍(全)
原文:
zh.annas-archive.org/md5/1df886d59dd71c594a78a9b2b4cc21e7译者:飞龙
前言
在 1985 年,随着 Windows 1.0 的推出,微软引入了图形设备接口(GDI)和 USER 子系统,以构建基于 Windows 的图形用户界面(GUI)。在 1990 年,OpenGL 出现,用于在 Windows 和非 Windows 系统上创建 2D 和 3D 图形。在 1995 年,微软推出了另一种技术,称为 DirectX,用于创建高性能的 2D/3D 图形。后来,GDI+被引入,以在现有的 GDI 之上添加 alpha 混合和渐变画笔支持。
在 2002 年,微软推出了.NET Framework。与此同时,Windows Forms 也被引入,用于使用 C#和 Visual Basic 语言构建 Windows 的用户界面(UI)。它是建立在 GDI+之上的,因此仍然具有 GDI+和 USER 子系统的限制。
多年来,微软决定引入一种新技术来构建基于 Windows 的应用程序的丰富 UI,这不仅帮助用户(开发者和设计师)摆脱了 GDI/GDI+和 USER 子系统的限制,还帮助他们提高构建基于桌面的应用程序时的生产力。
在 2006 年 11 月,随着.NET 3.0 的推出,Windows Presentation Foundation (WPF) 也被引入,为开发者提供了一个统一的编程模型,用于构建动态、数据驱动的 Windows 桌面应用程序。它附带了一系列功能,用于创建图形子系统,使用各种控件、布局、图形、资源等来渲染丰富的 UI,同时考虑到应用程序和数据的安全性。由于它最初作为.NET Framework 3.0 的一部分发布,因此第一个版本被称为 WPF 3.0。
WPF 是一个分辨率无关的框架,它使用基于 XML 的语言 XAML(发音为 Zammel)的矢量渲染引擎,来创建现代的用户体验,为应用程序编程提供了一个声明性模型。使用它,你可以轻松地自定义控件并为其添加皮肤,以获得应用程序 UI 的更好表示。
由于 WPF 与经典的 Windows Forms 不同,因为它使用 XAML、数据绑定、模板、样式、动画、文档等,最初它并没有得到太多的关注。然而,后来它开始获得越来越多的流行和关注。发布了多个更新版本,以增加更多功能,使其更加健壮和强大。
在这本书中,我们将介绍一系列食谱,展示如何使用 WPF 执行常见任务。从 WPF 基础知识开始,我们将涵盖标准控件、布局、面板、数据绑定、自定义控件、用户控件、样式、模板、触发器和动画,然后进一步探讨资源的使用、MVVM 模式、WCF 服务、调试、多线程和 WPF 互操作性,以确保您正确理解基础。
本书中的示例简单易懂,为您提供学习并掌握构建桌面应用程序所需技能所需的知识。到您阅读完本书时,您将足够熟练,对每个章节都有深入的了解。尽管本书已涵盖大多数重要主题,但总会有一些主题是任何书籍都无法完全涵盖的。您一定会喜欢阅读这本书,因为其中包含大量图形和文本步骤,帮助您在处理 Windows Presentation Foundation 时建立信心。
第一章:本书面向对象
本书旨在为相对较新接触 Windows Presentation Foundation (WPF)的开发者或那些已经使用 WPF 一段时间但希望更深入理解其基础和概念以获得实用知识的人编写。假设读者具备 C#和 Visual Studio 的基本知识。
本书涵盖内容
第一章,WPF 基础,专注于 WPF 的架构、应用程序类型以及 XAML 语法、术语,并解释如何使用 Visual Studio 2017 安装 WPF 工作负载以创建针对 Windows Presentation Foundation 的第一个应用程序。它将涵盖导航机制、各种对话框、在多个窗口之间建立所有权,然后继续创建单实例应用程序。本章还将介绍如何向 WPF 应用程序传递参数以及如何处理在 WPF 中抛出的未处理异常。
第二章,使用 WPF 标准控件,为您提供深入了解,帮助您了解 WPF 的各种常见控件。本章将从 TextBlock、Label、TextBox 和 Image 控件开始,然后继续介绍 2D 形状、Tooltip、标准菜单、上下文菜单、单选按钮和复选框控件。本章还将涵盖如何使用进度条、滑块、日历、列表框、组合框、状态栏和工具栏面板。
第三章,布局和面板,为您快速介绍标准布局和面板。本章将涵盖如何使用面板创建适当的布局。它还将简要介绍实现拖放功能。
第四章,使用数据绑定,讨论了数据绑定的重要概念以及如何在 WPF 中使用它。它还讨论了 CLR 属性、依赖属性、附加属性、转换器和数据操作(如排序、分组和筛选)。逐步方法将指导您熟练掌握所有类型的数据绑定。
第五章,使用自定义控件和用户控件,提供了您创建可重用自定义控件和用户控件所需的基本构建块。您还将学习如何使用自定义控件和用户控件的自定义属性和事件来定制控件模板。
第六章,使用样式、模板和触发器,深入探讨了控制器的样式和模板,随后介绍了您可以直接从 XAML 执行某些操作或 UI 更改的各种触发器,而不需要使用任何 C#代码。
第七章,使用资源和 MVVM 模式,首先演示了使用和管理二进制资源、逻辑资源和静态资源的各种方法。然后,它将继续使用 Model View ViewModel (MVVM)模式,通过在代码后文件中编写更少的代码来构建 WPF 应用程序。MVVM 模式通过一些示例介绍,展示了您如何构建命令绑定。
第八章,使用动画,提供了对 WPF 动画功能的全面介绍,并讨论了如何使用各种转换和动画以及将效果应用于动画。
第九章,使用 WCF 服务,使您轻松理解 WCF 服务的ABC,并解释了如何在 WPF 应用程序中创建、托管和消费它们。
第十章,调试和线程,讨论了 WPF 对使用实时视觉树和实时属性浏览器调试 XAML 应用程序 UI 的支持。本章帮助您创建异步操作,以便应用程序 UI 始终保持响应。
第十一章,与 Win32 和 WinForm 的互操作性,重点在于理解 WPF 与 Win32 和 Windows Forms 的互操作性。在本章中,您将学习如何将一个技术(WPF/WinForm)的元素托管到另一个技术(WinForm/WPF)中,然后调用 Win32 API 并在 WPF 应用程序中嵌入 ActiveX 控件。
要充分利用本书
本书假设读者具备.NET Framework 和 C#(至少 C#版本 3.0,但 C# 7.0 或更高版本更佳)的知识,并且有 Visual Studio 2015 或更高版本(Visual Studio 2017 更佳)的实际使用经验。已假设具备 WPF 和 XAML 的基本知识。
下载示例代码文件
您可以从www.packtpub.com的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在www.packtpub.com登录或注册。
-
选择支持选项卡。
-
点击代码下载和勘误表。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本解压缩或提取文件夹:
-
Windows 的 WinRAR/7-Zip
-
Mac 的 Zipeg/iZip/UnRarX
-
Linux 的 7-Zip/PeaZip
本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Windows-Presentation-Foundation-Development-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/WindowsPresentationFoundationDevelopmentCookbook_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“表示媒体集成库的包装的 Presentation Core 层是presentationcore.dll的一部分,为您提供了包装。”
代码块设置如下:
<Button>
<Button.Background>
<SolidColorBrush Color="Red" />
</Button.Background>
</Button>
任何命令行输入或输出都按以下方式编写:
svcutil.exe http://localhost:59795/Services/EmployeeService.svc?wsdl
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“要构建针对.NET Framework 的 WPF 应用程序,请选择.NET 桌面开发工作负载。”
警告或重要注意事项看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们欢迎读者的反馈。
一般反馈:请将电子邮件发送至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件发送至questions@packtpub.com。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至copyright@packtpub.com与我们联系。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评价
请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问packtpub.com。
WPF 基础
在本章中,我们将涵盖以下食谱:
-
使用 Visual Studio 2017 安装 WPF 工作负载
-
创建 WPF 应用程序
-
从一个窗口创建和导航到另一个窗口
-
创建和导航到另一页
-
创建对话框
-
创建窗口之间的所有权
-
创建单实例应用程序
-
向 WPF 应用程序传递参数
-
处理未处理的异常
第二章:简介
Windows 展示基础(WPF)为开发者提供了一个统一的编程模型,用于构建动态、数据驱动的 Windows 桌面应用程序。它首次于 2006 年与 .NET 3.0 一起发布。它是 .NET 框架本身的一部分。
WPF 是一个图形子系统,用于渲染丰富的 用户界面(UI),是一个基于矢量的渲染引擎,在 可扩展应用程序标记语言(XAML)中创建令人惊叹的用户界面。它支持广泛的功能,包括应用程序模型、控件、布局、图形、资源、安全性等。
用于其执行的运行时库自 Windows Vista 和 Windows Server 2008 以来已包含在 Windows 中。如果你使用的是带有 SP2/SP3 的 Windows XP 或 Windows Server 2003,你可以选择安装必要的库。
要开始学习 WPF 的不同食谱,你应该对基本原理有一个清晰的理解。在本章中,我们将从架构和语法开始,并指导你创建构建块。
WPF 架构
WPF 使用分层架构,包括托管、非托管和五个不同层中的核心 API,这些层被称为 展示框架、展示核心、公共语言运行时、媒体集成库和 OS 核心层。编程模型通过托管代码公开。
在以下图中,你可以看到架构的清晰图示:

展示框架
展示框架,是 presentationframework.dll 的一部分,为你提供了开始构建 WPF 应用程序 UI 所需的基本组件(如控件、布局、图形、媒体、样式、模板、动画等)。它是托管层的一部分。
展示核心
展示核心层,是 presentationcore.dll 的一部分,为你提供了对 媒体集成库(MIL)的包装。它为你提供了访问 MIL 核心和视觉系统的公共接口,以开发视觉树。它包含视觉元素和渲染指令,用于使用 XAML 工具构建 Windows 应用程序。这也是托管代码的一部分。
公共语言运行时
公共语言运行时,通常称为 CLR,是托管层的一部分,为你提供了构建健壮应用程序的几个功能,包括 公共类型系统(CTS)、错误处理、内存管理等。
媒体集成库
媒体集成库(MIL),位于milcore.dll中,是用于通过 DirectX 引擎渲染的所有图形显示的未管理层的一部分。它为您提供了对 2D 和 3D 表面的基本支持,并允许您访问未管理组件以实现与 DirectX 的紧密集成。它还允许您在从视觉系统到公共语言运行时(CLR)渲染指令时获得性能。
操作系统核心
在 MIL 之后,下一层是操作系统核心,它为您提供了访问操作系统核心组件的低级 API 的权限。这一层包括内核、User32、DirectX、GDI 和设备驱动程序。
WPF 应用程序类型
虽然 WPF 主要用于桌面应用程序,但您也可以创建基于网络的程序。因此,WPF 应用程序可以分为两种类型:
-
基于桌面的可执行文件(EXE)
-
基于网络的程序(XBAP)
桌面应用程序是常规的 .exe 可执行文件,您通常在任一基于 Windows 的系统上运行它们,而基于网络的程序是可以在 Web 服务器上部署的 .xbap 文件,并且可以在任何支持的浏览器中运行。运行这些应用程序类型都需要 .NET Framework。
当您运行一个 WPF 应用程序时,它将在两个线程中启动。UI 线程使用System.Threading.DispatcherObject来创建消息系统,并维护 UI 操作队列。就像 Win32 消息泵一样,它根据设置的优先级执行 UI 操作。
另一个线程是后台线程,用于处理由 WPF 管理的渲染引擎。它获取视觉树的副本,并执行操作以在 Direct3D 表面上显示视觉组件。然后它调用 UI 元素来确定大小并按其父元素排列子元素。
XAML 概述
XAML代表可扩展应用程序标记语言。它是一种基于 XML 的标记语言,用于声明性地创建任何基于 XAML 的应用程序的 UI,例如Windows 平台基金会(WPF)、通用 Windows 平台(UWP)和Xamarin.Forms。您可以使用声明性的 XAML 语法创建可见的 UI 元素来设计丰富的 UI,然后编写后台代码以执行运行时逻辑。
微软最近推出了XAML 标准,这是一个定义标准 XAML 词汇的规范,它将允许支持的框架共享基于 XAML 的 UI 定义。
您可以通过访问 GitHub 上的此处了解更多关于此规范的信息:
虽然使用 XAML 标记来创建 UI 不是强制性的,但它已被广泛接受为创建整个应用程序 UI 的智能选项,因为它使创建变得更容易。你也可以通过编写 C# 或 VB.NET 代码来创建 UI,但这会使创建更困难,也更难以维护。此外,这也使得设计师难以独立工作。
使用 XAML 设计应用程序 UI 与编写带有几个可选属性的 XML 节点一样简单。属性用于设置额外的样式、行为和属性。要在 UI 中创建一个简单的按钮,你只需在 XAML 文件中写下 <Button />。同样,你也可以只写 <TextBox /> 来创建一个用户输入框。
此外,你还可以向控件添加更多详细信息。例如,要向按钮添加标签,使用其 Content 属性,并设置其尺寸,使用 Height 和 Width 属性,如下面的代码所示:
<Button Content="Click Here" />
<Button Height="36" Width="120" />
通常,当你将 XAML 页面添加到你的 WPF 应用程序项目中时,它将与项目一起编译,并产生一个名为 二进制应用程序标记语言(BAML)的二进制文件。项目的最终输出(即,程序集文件)包含这个 BAML 文件作为资源。当应用程序加载到内存中时,BAML 将在运行时被解析。
你也可以将 XAML 加载到内存中,并直接在 UI 上渲染它。但是,在这种情况下,如果它有任何 XAML 语法错误,它将在运行时抛出这些错误。如果你将性能与第一个过程进行比较,后者较慢,因为它将整个 XAML 语法渲染到 UI 上。
这里有一个流程图,展示了加载和渲染/解析 XAML UI 的方法:

XAML 语法术语
XAML 使用一些语法术语来定义 UI 中的元素并创建其实例。在你开始工作之前,你必须了解它提供的不同术语。让我们看看其中的一些。
对象元素语法
每个类型的实例都使用适当的 XAML 语法来在 UI 中创建一个对象元素。这些对象元素以一个左尖括号 (<) 开头,并定义了元素的名称。当它定义在默认作用域之外时,你可以选择性地添加命名空间前缀。你可以使用一个自闭合的尖括号 (/>) 或一个右尖括号 (>) 来关闭对象元素的定义。如果一个对象元素没有子元素,则使用自闭合的尖括号。例如,(<Button Content="点击这里" />)使用自闭合的尖括号。如果你写入带有子元素的内容,它将使用结束标签关闭(<Button>点击这里</Button>),如下所示。
当你在 XAML 页面上定义对象元素时,创建元素实例的指令被生成,并在你将其加载到内存中时通过调用元素的构造函数来创建其实例。
属性属性语法
您可以定义一个或多个属性到元素。这些是通过将一个名为 属性属性语法 的属性写入元素来完成的。它以属性名称和一个赋值运算符(=)开头,后跟引号内的值。以下示例演示了定义按钮元素具有标签作为其内容是多么容易,以及如何在 UI 中设置其尺寸:
<Button Content="Click Here" />
<Button Content="Click Here" Width="120" Height="30" />
属性元素语法
这是一种 XAML 语法,允许您将属性定义为元素。这通常用于您无法在引号内分配属性值的情况。如果我们以之前的例子为例,文本 Click Here 可以轻松地分配给按钮内容。但是,当您有另一个元素或组合属性值时,您不能将其写入引号内。为此,XAML 引入了 属性元素语法 以帮助您轻松定义属性值。
它以 <element.PropertyName> 开头,以 </element.PropertyName> 结尾。以下示例演示了如何使用 SolidColorBrush 对象为按钮背景分配颜色:
<Button>
<Button.Background>
<SolidColorBrush Color="Red" />
</Button.Background>
</Button>
内容语法
这是一种另一种类型的 XAML 语法,用于设置 UI 元素的内容。它可以设置为子元素的值。以下示例演示了如何将 Border 控件的文本内容属性设置为包含 Button 控件作为其 子 元素:
<Border>
<Border.Child>
<Button Content="Click Here" />
</Border.Child>
</Border>
在使用 Content 语法 时,您应该记住以下要点:
-
Content属性的值必须是连续的 -
您不能在单个实例中两次定义 XAML
Content属性
因此,以下是不合法的,因为它将抛出 XAML 错误:
<Border>
<Border.Child>
<Button Content="Button One" />
</Border.Child>
<Border.Child>
<Button Content="Button Two" />
</Border.Child>
</Border>
集合语法
当您需要将一组元素定义到父根元素时,使用 集合语法 可以使其易于阅读。例如,要在 StackPanel 内添加元素,我们使用其 Children 属性,如下面的代码所示:
<StackPanel>
<StackPanel.Children>
<Button Content="Button One" />
<Button Content="Button Two" />
</StackPanel.Children>
</StackPanel>
这也可以写成如下,解析器知道如何创建和将元素分配给 StackPanel:
<StackPanel>
<Button Content="Button One" />
<Button Content="Button Two" />
</StackPanel>
事件属性语法
当您添加按钮时,需要将其与事件监听器关联,以执行某些操作。对于添加其他控件和 UI 布局也是如此。XAML 允许您使用 事件属性语法 为特定的 XAML 对象元素定义事件。
语法看起来像属性属性,但它用于将事件监听器关联到元素。以下示例演示了如何将点击事件分配给按钮控件:
<Button Content="Click Here" Click="OnButtonClicked" />
关联的事件由 XAML 页面的代码后端生成,您可以在其中执行实际操作。以下是前面按钮点击事件实现代码片段:
void OnButtonClicked (object sender, RoutedEventArgs e)
{
// event implementation
}
使用 Visual Studio 2017 安装 WPF 工作负载
由于我们已经学习了 WPF 架构和 XAML 语法的基本概念,我们可以开始学习不同的菜谱,使用 WPF 的 XAML 工具构建 Windows 应用程序。但在那之前,让我们安装 Visual Studio 2017 所需的工作负载/组件。如果您使用的是 Visual Studio 的早期版本,这一步骤将有所不同。
准备工作
要安装构建 WPF 应用程序所需的组件,请运行 Visual Studio 2017 安装程序。如果您没有安装程序,您可以去 www.visualstudio.com/downloads 下载正确的版本。让我们下载 Visual Studio Community 2017 版本,因为它是一个功能齐全的 IDE,并且对学生、开源和个人开发者免费提供。
如何操作...
下载 Visual Studio 2017 安装程序后,请按照以下步骤安装正确的 workload:
-
运行安装程序后,它将显示以下屏幕。点击继续:
![]()
-
等待几分钟,让安装程序为安装过程做好准备。一个进度条将显示当前进度的状态:
![]()
-
然后会出现以下屏幕,其中将要求您选择要安装的 workloads 或 components:

-
要构建针对 .NET Framework 的 WPF 应用程序,请选择前面的截图所示的 .NET 桌面开发 workload。
-
点击安装按钮继续安装。
-
以下屏幕将显示,显示安装状态。根据您的网络带宽,这将花费一些时间,因为它将根据您的选择从 Microsoft 服务器下载所需的组件,并逐个安装:

- 安装完成后,您可能需要重新启动系统以使更改生效。在这种情况下,屏幕上会出现一个弹出窗口,要求您重新启动您的电脑。
安装了 .NET 桌面开发组件并重新启动系统后,你就可以开始构建你的第一个 WPF 应用程序了。
创建 WPF 应用程序
WPF 开发平台支持一系列广泛的功能,包括 UI 控件、布局、资源、图形、数据绑定、应用程序模型等。在使用这些功能之前,您需要使用 Visual Studio 创建 WPF 项目。
本菜谱的目标是创建一个 WPF 项目,并学习基本的项目结构和组件。让我们开始使用 XAML 工具构建我们的第一个 WPF 应用程序。
准备工作
要开始使用 WPF 应用程序开发,您必须在系统上运行 Visual Studio,并且已经安装了所需的组件。
如何操作...
按照以下步骤创建您的第一个 WPF 应用程序:
-
在您的 Visual Studio IDE 中,导航到文件 | 新建 | 项目...菜单,如图以下截图所示:
![图片]()
-
这将在屏幕上打开新建项目对话框。您也可以通过按键盘快捷键Ctrl + Shift + N来打开它。
-
在新建项目对话框中,导航到已安装 | 模板 | Visual C# | Windows 经典桌面,如图以下截图的左侧所示:

-
在右侧面板中,首先选择您希望应用程序针对的.NET Framework 版本。我们在这里选择了.NET Framework 4.7。
-
然后从可用的模板列表中选择 WPF App (.NET Framework)。
-
给项目起一个名字(在我们的例子中是
CH01.HelloWPFDemo)。 -
可选地,选择项目位置,您希望创建它的位置。
-
可选地,您也可以为解决方案提供不同的名称。
-
当您准备好时,单击“确定”按钮,让 Visual Studio 根据您选择的模板创建项目。
一旦项目创建完成,Visual Studio 将打开解决方案资源管理器,其中列出了项目及其上创建的所有默认文件。项目结构将类似于以下截图:

更多内容...
使用 Visual Studio 默认模板创建的每个 WPF 应用程序项目都包含以下文件:
App.config:这是您的 WPF 应用程序的配置文件。默认情况下,它包含以下行,描述了应用程序运行所支持的运行时版本。这包含我们在项目创建期间选择的完全相同的运行时版本:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime
version="v4.0"sku=".NETFramework,Version=v4.7" />
</startup>
</configuration>
config文件还可以包含应用程序设置和其他您想要在应用程序中使用/引用的配置设置。
App.xaml:当您创建 WPF 项目时,Visual Studio 会自动创建App.xaml文件。它是应用程序的声明性起点。此文件的根元素是Application实例,它定义了应用程序特定的属性和事件:
<Application x:Class="CH01.HelloWPFDemo.App"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
Application类的实例定义了Window或Page,它将成为启动 UI,并通过StartupUri属性进行注册。在前面代码中,(StartupUri="MainWindow.xaml")表示当您运行应用程序时,将加载MainWindow.xaml页面。
应用程序实例还可以持有全局/应用程序级别的资源(如,样式、模板和转换器),这些资源可以在整个应用程序中全局使用。
App.xaml.cs:这是App.xaml的后台代码类文件,它扩展了框架的Application类以编写应用程序特定的代码。您可以使用此文件订阅事件,如Startup、UnhandledException以执行常见操作:
namespace CH01.HelloWPFDemo
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}
此类通常用于操作命令行参数并根据这些参数加载不同的 XAML 页面。
MainWindow.xaml:这是 Visual Studio 在创建 WPF 项目时生成的默认 UI 页面。它是注册为App.xaml中的StartupUri的页面。此页面的根元素是Window,默认包含一个Grid布局。以下是默认代码片段:
<Window x:Class="CH01.HelloWPFDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="MainWindow" Height="350" Width="525">
<Grid>
</Grid>
</Window>
x:Class属性定义了包含 UI 逻辑的关联部分类。您可以修改此 XAML 以提供应用程序启动页的新外观。本书的后续章节将涵盖各种 UI 控件和布局。
MainWindow.xaml.cs:这是MainWindow.xaml的后台代码类,包含与 UI 操作相关的逻辑。通常,开发者在类中编写各种 UI 操作的实现。
每当您向 XAML 页面添加任何 UI 元素时,该控件都会在具有.g.i.cs扩展名的部分类文件中内部注册。例如,如果您在MainWindow.xaml文件中添加一个控件,它将注册在obj文件夹中的MainWindow.g.i.cs中。如果您打开该文件,您可以在InitializeComponent()方法中观察到整个加载过程。
创建并从一个窗口导航到另一个窗口
在 WPF 独立应用程序中,窗口用于托管 UI 元素,使用户能够与 UI 和数据交互。基类Window提供了创建和与窗口 UI 交互的所有 API。
在 WPF 应用程序中,通用的窗口布局被分为多个部分。以下是基本窗口的截图,包含其各个部分:

窗口的各个部分如下所述:
-
窗口的外部部分是一个Border,您可以利用它来启用调整大小选项:
- 外部边框可以包含一个调整大小把手,使您能够以对角线方式调整窗口大小
-
窗口顶部有一个标题栏,它由以下部分组成:
-
图标,为您的应用程序窗口提供独特的品牌
-
标题,显示窗口的可识别名称
-
一个包含最小化、最大化/还原和关闭按钮的小面板
-
系统菜单,包含允许用户在窗口上执行最小化、最大化/还原、移动、大小和关闭操作的菜单项
-
-
开发者可以在此处添加应用程序/窗口特定的布局和控制客户端区域
准备工作
要开始此食谱,请打开您的 Visual Studio 实例,并根据 WPF App (.NET Framework)模板创建一个名为CH01.WindowDemo的 WPF 项目。一旦创建项目,它将包含名为MainWindow.xaml和MainWindow.xaml.cs的文件,以及其他默认文件。
让我们在同一项目中创建一个新的窗口,并从MainWindow中调用按钮以打开新窗口。
如何操作...
要创建一个新窗口,请按照以下简单步骤操作:
-
打开“解决方案资源管理器”,在项目节点上右键单击。
-
从右键单击上下文菜单中,导航到“添加 | 窗口...”,如下面的截图所示:

- 屏幕上会出现以下“添加新项”对话框:

-
确保选中的模板是“窗口(WPF)”。给它起个名字,
SecondWindow.xaml,然后点击“添加”按钮。 -
这将在项目目录中创建
SecondWindow.xaml文件及其关联的后台代码文件SecondWindow.xaml.cs。 -
打开 XAML 文件(
SecondWindow.xaml)并将全部内容替换为以下 XAML 代码:
<Window x:Class="CH01.WindowDemo.SecondWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Second Window" Height="200" Width="300">
<Grid>
<TextBlock Text="Second Window Instance"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"/>
</Grid>
</Window>
- 现在打开
MainWindow.xaml文件,向其中添加一个按钮。将整个<Grid> </Grid>块替换为以下 XAML 代码:
<Grid>
<Button Content="Open Second Window"
Height="30" Width="150"
Click="OnSecondWindowButtonClicked"/>
</Grid>
- 现在我们需要添加按钮点击事件的实现。只需打开
MainWindow.xaml.cs文件,并在类定义内添加以下代码行:
private void OnSecondWindowButtonClicked(object sender,
RoutedEventArgs e)
{
var window = new SecondWindow();
window.Show();
}
- 现在,当你运行应用程序时,你会在屏幕上看到 MainWindow 打开,其中包含一个标签为“打开第二个窗口”的按钮。点击此按钮将在屏幕上打开第二个窗口,其文本内容为“第二个窗口实例”。以下是供您参考的截图!
![截图]()
请注意,如果你再次点击按钮,因为它是无模式的,它将创建第二个窗口的另一个实例。
它是如何工作的...
当你创建 Window 类的实例时,它不会对用户可见。它只有在调用 Show() 方法时才会可见,该方法返回原始调用者的句柄,而不等待窗口关闭。
当你调用 Show() 方法时,它基本上创建了一个无模式窗口,因此当同一个窗口已经打开时,你可以在同一应用程序中与其他窗口进行交互。Window 类还公开了一个名为 ShowDialog() 的方法,它创建了一个模式窗口,并阻止用户与应用程序的其他窗口交互。我们将在本章的“创建对话框”部分中进一步讨论。
还有更多...
Window 类为你提供了一组属性、方法和事件,以自定义窗口的外观,执行特定操作或通知当前上下文。要使客户端区域支持透明度,请将窗口的 AllowsTransparency 属性设置为 true。这在你想创建一个自定义形状的窗口或皮肤主题时非常有用。
你可以通过设置 Icon 属性来更改窗口的默认图标,并通过设置 ResizeMode 属性来启用/禁用窗口大小调整。你还可以通过设置 Title、WindowStartupLocation、WindowState、WindowStyle 和 ShowInTaskbar 属性来设置窗口标题、启动位置、窗口状态、窗口样式和任务栏可见性。
不仅这些,你还可以通过调用其Activate()方法将窗口带到前台,通过调用Window类中可用的Close()方法关闭窗口。有时,当你想隐藏窗口而不是完全退出时,你可以利用Hide()方法使窗口隐藏,并通过在相同实例上调用Show()方法将其再次显示出来。
该类还公开了一些事件来通知你当前上下文信息。你可以在你的代码中使用Activated、Deactivated、Closing、Closed和StateChanged事件来获取此类通知。
创建和导航到另一个页面
WPF 应用程序支持浏览器风格的导航机制,它既可以用在独立应用程序中,也可以用在 XBAP 应用程序中。为了实现它,WPF 提供了Page类来封装可以由浏览器导航和托管的Page内容,以及NavigationWindow和/或Frame。
准备工作
要开始构建支持从 WPF 页面到另一个页面导航机制的应用程序,请打开 Visual Studio IDE,并基于 WPF App (.NET Framework)模板创建一个项目。给它起个名字(在我们的例子中,它是CH01.PageDemo)。
如何做到这一点...
一旦你根据 WPF App (.NET Framework)模板创建了项目,请按照以下步骤将页面添加到项目中,并与NavigationService集成:
-
右键单击要创建页面的项目节点。
-
如此截图所示,从上下文菜单中导航到添加 | 页面...:
![图片]()
-
这将打开以下“添加新项目”对话框窗口,其中已选中标题为“页面(WPF)”的项目。给它起个名字,
Page1.xaml,然后点击添加。它将在你的项目中创建Page1.xaml和相关代码后文件Page1.xaml.cs:

-
现在按照相同的步骤,1 到 3,创建另一个页面
Page2.xaml,这将把 XAML 和相关 C#代码后文件添加到项目中。 -
打开
Page1.xaml文件,将Grid替换为以下 XAML:
<Grid>
<TextBlock Text="This is Page 1" FontSize="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Button Content="Next" Height="30" Width="120"
Margin="20"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="OnNextButtonClicked"/>
</Grid>
- 在相关的代码后文件(
Page1.xaml.cs)中,添加以下按钮点击事件处理器:
private void OnNextButtonClicked(object sender,
RoutedEventArgs e)
{
NavigationService.Navigate(new Uri("Page2.xaml",
UriKind.Relative));
}
- 类似地,将以下 XAML 添加到
Page2.xaml页面中,替换现有的Grid:
<Grid>
<TextBlock Text="This is Page 2" FontSize="20"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Button Content="Previous" Height="30" Width="120"
Margin="20"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Click="OnPreviousButtonClicked"/>
</Grid>
- 将以下按钮点击事件处理器添加到
Page2.xaml.cs文件中:
private void OnPreviousButtonClicked(object sender, RoutedEventArgs e)
{
if (NavigationService.CanGoBack)
{
NavigationService.GoBack();
}
}
- 现在打开
MainWindow.xaml文件,将 XAML 内容替换为以下内容:
<NavigationWindow x:Class="CH01.PageDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="MainWindow" Height="350" Width="525"
Source="Page1.xaml">
</NavigationWindow>
-
现在打开
MainWindow.xaml.cs文件,将其基类更改为NavigationWindow,而不是Window。 -
运行应用程序,它将打开以下包含第 1 页的屏幕:

- 点击“下一步”按钮将导航到第 2 页,如这里所示,它包含由 WPF 框架自动提供的激活导航按钮:

- 现在,如果你点击导航面板中的“上一步”按钮或后退按钮,它将导航你到页面 1。
它是如何工作的...
在MainWindow.xaml页面中定义的NavigationWindow提供了支持内容导航的基本机制。Source属性(Source="Page1.xaml"),定义为URI,要求NavigationWindow默认加载所提到的页面(Page1.xaml)。
当你点击Page1的“下一步”按钮时,NavigationService.Navigate方法会被执行,传递你想要加载的下一页的URI。导航按钮会根据你执行的历史导航自动激活。
在Page2中,当你点击“上一步”按钮时,它首先检查NavigationService是否有立即的历史条目可以导航到你之前的一页。如果找到前一页,它将通过调用NavigationService.GoBack()方法自动将你导航到所需的页面。在这种情况下,你不需要传递页面的URI。
还有更多...
NavigationService提供了各种属性、方法和事件,用于在页面内容上执行导航机制。CanGoBack()和CanGoForward()返回一个Boolean值,分别指示后退和前进导航历史中是否至少有一个条目。GoBack()方法将你导航到后退导航历史中最新的条目,而GoForward()方法(如果可用)将你导航到前进导航历史。
要刷新当前内容,你可以调用Refresh()方法。StopLoading()方法停止当前执行从当前导航上下文的内容部分下载/加载。你也可以通过编程方式添加或从导航历史中删除条目。AddBackEntry方法接受一个参数CustomContentState对象,将条目添加到后退导航历史中。RemoveBackEntry()方法从后退导航历史中删除最近的条目。
存在诸如Navigating、Navigated、NavigationFailed、NavigationStopped、NavigationProgress和LoadCompleted等事件,用于通知你当前导航过程的各个状态。根据你的需求明智地使用它们。
创建对话框
对话框也是一种窗口,通常用于从用户那里获取输入或向用户显示消息。它使用模型窗口来防止用户在对话框已打开时与其他相同应用程序的窗口交互。在本教程中,我们将学习如何创建模型对话框并使用框架提供的常用对话框。
准备工作
要在 WPF 应用程序中开始构建和使用对话框,打开你的 Visual Studio IDE 并创建一个新的 WPF 项目,命名为CH01.DialogBoxDemo。
如何操作...
按照以下步骤创建对话框窗口,并从 MainWindow 中调用它以向用户显示消息:
-
打开解决方案资源管理器,在项目节点上右键单击。
-
从上下文菜单中选择添加 | 窗口... 以打开添加新项目对话框。
-
确保选择了窗口(WPF)模板,将其命名为
MessageDialog,然后点击添加以继续。这将创建项目中的MessageDialog.xaml和MessageDialog.xaml.cs文件。 -
打开
MessageDialog.xaml文件,并用以下内容替换整个 XAML:
<Window x:Class="CH01.DialogBoxDemo.MessageDialog"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
ShowInTaskbar="False" WindowStyle="SingleBorderWindow"
Title="Message" Height="150" Width="400"
FontSize="14" Topmost="True" ResizeMode="NoResize">
<Grid>
<TextBlock TextWrapping="Wrap" Margin="8"
Text="Thank you for reading 'Windows Presentation
Foundation Cookbook'. Click 'OK' to continue next."/>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="4">
<Button Content="OK" Width="60" Height="30"
Margin="4" IsDefault="True"
Click="OnOKClicked"/>
<Button Content="Cancel" Width="60" Height="30"
Margin="4" IsCancel="True"
Click="OnCancelClicked"/>
</StackPanel>
</Grid>
</Window>
- 打开
MessageDialog.xaml.cs文件,并为确定按钮和取消按钮添加以下事件实现:
private void OnOKClicked(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void OnCancelClicked(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
- 现在,打开
MainWindow.xaml页面,并用以下 XAML 内容替换Grid:
<Grid>
<ListBox x:Name="result" Height="100" Margin="8"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
<Button Content="Show Message" Width="150" Height="30"
VerticalAlignment="Bottom" Margin="8"
Click="OnShowMessageButtonClicked"/>
</Grid>
- 前往代码后置文件,
MainWindow.xaml.cs,并添加以下按钮事件实现,如以下代码段所示:
private void OnShowMessageButtonClicked(object sender, RoutedEventArgs e)
{
var messageDialog = new MessageDialog();
var dialogResult = messageDialog.ShowDialog();
if (dialogResult == true)
{
result.Items.Add("You clicked 'OK' button.");
}
else if (dialogResult == false)
{
result.Items.Add("You clicked 'Cancel' button.");
}
}
- 现在,运行应用程序。可见的窗口将有一个标有显示消息的按钮。点击它以调用我们创建的消息对话框窗口:

-
点击取消按钮,这将把“你点击了'取消'按钮”文本添加到 MainWindow 中的列表中。
-
再次启动消息窗口并点击确定按钮。这将把“你点击了'确定'按钮”添加到列表中。
工作原理...
当你调用 Window 实例的 ShowDialog() 方法时,它将以模式对话框的形式打开,并等待用户提供输入。在这种情况下,用户输入是与确定和取消按钮的交互。当你点击确定按钮时,相关的事件处理器将 true 赋值给 DialogResult 属性并返回给调用者。同样,取消按钮的事件处理器将 false 赋值给 DialogResult 属性并返回。
根据返回的 ShowDialog() 方法值,实际上返回的是 DialogResult 的值,你可以决定用户是否点击了确定或取消按钮。
通过将以下属性设置为 Window 实例,已自定义对话框窗口:
-
ShowInTaskbar属性已被设置为False以防止窗口在任务栏中可见。 -
WindowStyle属性已被设置为SingleBorderWindow以向窗口添加细边框,从标题栏中移除最小化和最大化按钮。 -
Topmost属性已被设置为True以保持它始终位于其他窗口之上。这是可选的,但很好。 -
ResizeMode属性已被设置为NoResize以防止用户调整对话框窗口的大小。
更多内容...
操作系统提供了一些可重用的对话框,这些对话框提供了与应用程序运行的操作系统的版本一致的用户体验。这种体验在所有应用程序中保持一致,以提供执行常见操作(如打开文件、保存文件、打印文件、颜色选择等)的独特界面。
WPF 提供这些可重用的常见对话框作为托管包装类,封装了核心实现。这减少了创建和管理常见操作所需的额外努力。
使用打开文件对话框
要在您的 WPF 应用程序中打开文件,您可以使用位于Microsoft.Win32命名空间下的托管包装类OpenFileDialog。您只需创建实例并通过可选设置一些属性进行 UI 自定义来调用ShowDialog()方法。
一个基本的打开文件对话框看起来如下截图所示,为您提供选择一个或多个文件打开的选项:

以下代码片段演示了如何通过可选填充文件扩展名过滤器来启动打开文件对话框:
private void OnOpenButtonClicked(object sender, RoutedEventArgs e)
{
var openfileDialog = new OpenFileDialog
{
Filter = "Text documents (.txt) | *.txt | Log files (.log) |
*.log"
};
var dialogResult = openfileDialog.ShowDialog();
if (dialogResult == true)
{
var fileName = openfileDialog.FileName;
}
}
ShowDialog()方法返回的dialogResult告诉我们操作是否成功执行。基于此,您可以调用文件对话框的实例以获取有关所选文件的更多详细信息。
使用保存文件对话框
除了OpenFileDialog接口外,Microsoft.Win32命名空间还提供了SaveFileDialog托管包装类,以从您的 WPF 应用程序执行文件保存操作。与打开文件对话框类似,您需要创建其实例,并通过可选填充其各种属性来最终调用ShowDialog()方法。
保存文件对话框看起来如下截图所示,您可以为要保存的文件提供名称:

可选地,您可以在启动对话框窗口之前设置扩展名过滤器、默认文件名和其他属性,如下面的代码片段所示:
private void OnSaveButtonClicked(object sender, RoutedEventArgs e)
{
var saveFileDialog = new SaveFileDialog
{
Filter = "Text documents (.txt) | *.txt | Log files (.log) |
*.log"
};
var dialogResult = saveFileDialog.ShowDialog();
if (dialogResult == true)
{
var fileName = saveFileDialog.FileName;
}
}
根据ShowDialog()调用返回的dialogResult,您可以决定保存操作是否成功,并从文件对话框实例中检索有关保存文件的更多信息。
使用打印对话框
托管包装PrintDialog也存在于Microsoft.Win32命名空间中,并为您提供调用操作系统的打印机属性和执行打印操作的接口。对话框为您提供选择打印机、配置打印首选项以及选择页面范围和其他参数的选项,如下面的截图所示:

要调用相同的操作,只需创建PrintDialog的实例并调用其ShowDialog()方法。您可以可选地设置页面范围、可打印区域和其他属性。如果ShowDialog()方法返回的dialogResult设置为true,则确认打印作业已成功排队,基于此您可以执行下一组操作。
这里是供您参考的代码片段:
private void OnPrintButtonClicked(object sender, RoutedEventArgs e)
{
var printDialog = new PrintDialog();
var dialogResult = printDialog.ShowDialog();
if (dialogResult == true)
{
// perform the print operation
}
}
其他常见对话框
WPF 还提供了一些其他常见的对话框,用于执行各种格式选项的选择,如字体、字体样式、字体大小、文本效果和颜色。你可以使用位于System.Windows.Forms命名空间下的FontDialog和ColorDialog,分别添加对字体和颜色选择的支持。
这里是展示字体选择器和颜色选择器对话框的截图:

创建窗口之间的所有权
在 WPF 应用程序中,你创建的窗口对象默认情况下是相互独立的。但是,有时你可能想要在它们之间创建所有者-所有者关系。例如,你通常在 Visual Studio IDE 和/或 Photoshop 应用程序中看到的工具箱窗口。
当你设置一个窗口的所有者时,它将根据所有者实例的行为。例如,如果你最小化或关闭所有者窗口,属于所有者关系的其他窗口将自动根据其所有者进行最小化或关闭。
让我们开始创建这个菜谱,以便在两个窗口之间建立所有者-所有者关系。
准备工作
要开始这个菜谱,打开你的 Visual Studio IDE 并创建一个新的名为CH01.OwnershipDemo的 WPF 项目。
如何操作...
执行以下步骤以创建一个ToolBox窗口并将其所有者属性分配给MainWindow,以便它可以根据其所有者进行操作:
-
右键单击项目节点,从上下文菜单中选择“添加 | 窗口...”。将在屏幕上显示“添加新项”对话框。
-
从可用列表中选择“窗口(WPF)”,将其命名为
ToolBox,然后单击“添加”继续。这将把ToolBox.xaml和ToolBox.xaml.cs添加到你的项目中。 -
打开
ToolBox.xaml文件,并用以下 XAML 代码替换其内容:
<Window x:Class="CH01.OwnershipDemo.ToolBox"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
SizeToContent="WidthAndHeight"
ResizeMode="NoResize"
Title="ToolBox">
<StackPanel Margin="10">
<Button Content="Bold" Width="70" Margin="4"/>
<Button Content="Italics" Width="70" Margin="4"/>
<Button Content="Underlined" Width="70"
Margin="4"/>
</StackPanel>
</Window>
-
现在打开
App.xaml页面,并从其中移除属性属性StartupUri,定义为(StartupUri="MainWindow.xaml")。 -
前往其代码后文件
App.xaml.cs并重写OnStartup事件。我们需要根据我们的需求修改实现。用以下代码块替换整个OnStartup事件处理器:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = new MainWindow();
mainWindow.Show(); // must show before setting it
as owner of some other window
var toolBox = new ToolBox { Owner = mainWindow };
toolBox.Show();
}
-
运行应用程序以查看两个窗口之间的关系。窗口将看起来像以下截图所示:
![图片]()
-
拖动工具箱窗口,你可以看到你能够将其移动到 MainWindow 之外。现在对 MainWindow 执行一些操作,例如最小化和关闭,你将看到工具箱窗口也会根据其所有者进行操作。
它是如何工作的...
默认情况下,每个窗口对象的所有者设置为 null,因此每个窗口都是相互独立的。但是,当你设置其所有者时,它将遵循所有者-所有者关系并与所有者窗口一起操作。
窗口所有权不是 WPF 的功能,而是 Win32 用户 API 的能力,并且可以从 WPF 应用程序中访问。
还有更多...
确保在将其设置为其他窗口的所有者之前,首先显示所有者窗口,否则系统将抛出InvalidOperationException:

关于窗口所有权的注意事项:
-
与其他窗口有所有权关系的窗口始终出现在所有者窗口之上。
-
你可以将窗口拖到所有者窗口之外。
-
当你最小化或关闭所有者时,与之相关的其他窗口将分别跟随所有者最小化或关闭。
-
默认情况下,相关联的窗口会显示在任务栏中,但当你最小化所有者时,它会被从任务栏中移除。
-
当你想断开关系时,只需将
Owner属性设置为null。
创建单实例应用程序
当你为 Windows 构建应用程序时,有许多原因会让你想要限制用户启动你的应用程序的多个实例。一些常见的例子是安装程序、卸载程序、更新工具、媒体应用程序、实用工具等。
在一个普通的应用程序中,当你启动应用程序时,它会创建一个 Windows 进程,并为其分配自己的内存空间和资源。但是,当你不希望为已经运行的单个应用程序创建多个进程实例时,你希望静默退出新实例并将运行中的进程带到前台。
在本食谱中,我们将学习如何使用Mutex(互斥)和非托管代码来实现这一点。
准备工作
要开始,请打开您的 Visual Studio 实例,并基于 WPF 应用程序模板创建一个新项目。在项目创建过程中,将其命名为CH01.SingleInstanceDemo。
如何做到这一点...
一旦创建了 WPF 项目,请按照以下步骤创建 WPF 应用程序的单实例:
-
通过按CTRL + F5键组合运行应用程序。这将启动应用程序的一个实例。
-
按多次CTRL + F5以启动多个应用程序实例。现在,是时候将应用程序变成单实例应用程序了!
![图片]()
-
关闭所有正在运行的过程,然后按照以下步骤实现单实例行为。
-
打开
MainWindow.xaml并添加窗口标题到Single Instance Demo。在这里你可以找到整个 XAML 代码:
<Window x:Class="CH01.SingleInstanceDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Single Instance Demo"
Height="250" Width="400">
<Grid>
</Grid>
</Window>
-
打开
App.xaml.cs文件并重写OnStartup方法的基本实现。 -
修改
OnStartup方法的代码,使其看起来像以下代码:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mutex = new Mutex(true, "SingleInstanceDemo",
out bool isNewInstance);
if (!isNewInstance)
{
MessageBox.Show("Application instance is
already running!");
Shutdown();
}
}
-
添加
System.Threading命名空间声明,以便 Mutex 可以被发现。Mutex 位于上述命名空间中。 -
现在编译项目以确保没有编译错误。
-
按CTRL + F5,这将运行应用程序的第一个实例。
-
现在返回 Visual Studio,不关闭应用程序,然后按CTRL + F5。这次,不是启动应用程序 UI,屏幕上会弹出“一个应用程序实例已经在运行!”的消息。点击 OK 将关闭消息。
-
再次按CTRL + F5。观察屏幕上没有第二个 UI 实例可见。
它是如何工作的...
这是一个处理应用程序以使其只有一个实例的技巧。互斥锁(Mutex)(互斥)对象用于定义具有唯一名称的实例。在这里我们称之为SingleInstanceDemo。布尔out参数返回当前调用线程是否被授予mutex对象的初始所有权。
互斥锁(Mutex)对象是一个同步对象,通常用于同步对共享资源的访问,以确保只有一个线程可以在某个时间点访问该资源。
对于应用程序的第一个实例,它将被授予初始所有权。当第二个实例运行时,调用线程将不会获得初始所有权,因为具有相同名称的mutex对象SingleInstanceDemo已经存在并且正在运行。
因此,isNewInstance的布尔值将是false,消息框将显示在屏幕上。此时应用程序的第二个实例仍在运行,当您点击 OK 按钮关闭消息框时,它将调用Shutdown()方法。
因此,第二个实例将从进程列表中移除。第一个实例将继续在系统上运行。
还有更多...
可能存在一种情况,应用程序在后台进程中运行,而用户尝试重新启动应用程序。在这种情况下,您可能希望激活已运行的程序并显示其 UI,而不是向用户显示消息。
您可以通过稍微修改现有代码并集成未管理代码调用来做到这一点。为此,再次打开App.xaml.cs文件,并按照以下步骤操作:
-
在文件中添加以下
using namespace:System.Runtime.InteropServices。 -
然后,您需要将以下未管理代码声明从
user32.dll添加到App.xaml.cs文件中:
[DllImport("user32", CharSet = CharSet.Unicode)]
static extern IntPtr FindWindow(string cls, string win);
[DllImport("user32")]
static extern IntPtr SetForegroundWindow(IntPtr hWnd);
- 添加以下方法以激活已运行的窗口,前提是窗口标题是静态的。在我们的例子中,它是 Single Instance Demo,在
MainWindow.xaml页面中进行了修改:
private static void ActivateWindow()
{
var otherWindow = FindWindow(null, "Single Instance Demo");
if (otherWindow != IntPtr.Zero)
{
SetForegroundWindow(otherWindow);
}
}
- 现在,您不需要调用
MessageBox,而是在OnStartup中调用ActivateWindow()方法。在这里,您可以找到以下新代码:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mutex = new Mutex(true,
"SingleInstanceDemo",
out bool isNewInstance);
if (!isNewInstance)
{
// MessageBox.Show("Application instance is
already running!");
ActivateWindow();
Shutdown();
}
}
-
现在运行应用程序。它将在屏幕上启动标题为 Single Instance Demo 的
MainWindow。 -
返回 Visual Studio。这将使应用程序窗口进入后台。现在通过按键盘快捷键CTRL + F5再次运行应用程序。这次,它不会运行不同的实例来显示 UI,而是激活现有窗口并将运行的应用程序推向前台。
应用程序窗口不一定要始终具有静态标题。在这种情况下,处理该场景将变得更加复杂。
向 WPF 应用程序传递参数
命令行参数用于在启动应用程序时从用户那里获取可选参数或值。这些通常用于从外部执行特定命令。
在本教程中,我们将学习如何向 WPF 应用程序传递命令行参数。
准备工作
要开始,打开 Visual Studio IDE 并创建一个名为 CH01.CommandLineArgumentDemo 的 WPF 应用程序项目。
如何操作...
现在按照以下步骤让应用程序支持命令行参数并根据这些参数执行操作:
- 打开
MainWindow.xaml并将TextBlock添加到Grid面板中。将整个 XAML 内容替换为以下行:
<Window x:Class="CH01.CommandLineArgumentDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Main Window" Height="200" Width="400">
<Grid>
<TextBlock Text="This is 'Main Window'
of the application."
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18" />
</Grid>
</Window>
-
通过在项目节点上右键单击并遵循上下文菜单路径添加 | 窗口...来在项目中创建一个新窗口。将其命名为
OtherWindow并单击添加按钮。这将OtherWindow.xaml和OtherWindow.xaml.cs添加到项目中。 -
现在打开
OtherWindow.xaml并更改其 UI 以显示不同的文本。让我们将整个 XAML 代码替换为以下行:
<Window x:Class="CH01.CommandLineArgumentDemo.OtherWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Other Window" Height="200" Width="400">
<Grid>
<TextBlock Text="This is 'Other Window' of the
application."
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="18" />
</Grid>
</Window>
-
现在打开
App.xaml并移除StartupUri="MainWindow.xaml"。这样做是为了根据传递给应用程序的参数来控制正确窗口的启动。 -
打开
App.xaml.cs并重写其OnStartup方法以检索传递给它的参数并根据这些参数打开所需的窗口。让我们为OnStartup方法添加以下代码实现:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var args = e.Args;
if (args.Contains("/other"))
{
new OtherWindow().Show();
}
else
{
new MainWindow().Show();
}
}
-
现在构建项目。导航到
bin\Debug文件夹并在该位置启动一个命令窗口。或者,您可以在bin\Debug路径启动一个 命令窗口(cmd.exe)并导航到您的应用程序所在路径。 -
在控制台窗口中,不传递任何参数输入应用程序名称,如下所示:
CH01.CommandLineArgumentDemo.exe
-
这将启动我们的应用程序的
MainWindow,如下所示:![图片]()
-
关闭应用程序窗口,并在控制台窗口中输入应用程序名称,指定
/other参数,如下所示:
CH01.CommandLineArgumentDemo.exe /other
- 这将启动应用程序的
OtherWindow而不是MainWindow:![图片]()
工作原理...
OnStartup(StartupEventArgs e) 方法签名包含 StartupEventArgs 作为方法参数。它包含一个属性 Args,该属性返回一个字符串数组,表示传递给应用程序的命令行参数。如果没有传递命令行参数,则字符串数组中将没有项。
现在,通过检查条件,我们启动用户想要显示的窗口。您还可以传递参数,使应用程序以正常模式、最大化模式或最小化模式启动。在某些特定情况下,您还可以使用它以隐藏方式打开应用程序。
还有更多...
如我们所见,通过传递参数从命令行启动 WPF 应用程序,现在让我们学习如何从 Visual Studio 本身启动它以调试模式运行。
要在调试模式下从 Visual Studio 将命令行参数传递给您的 WPF 应用程序,请右键单击项目节点,并从上下文菜单中单击“属性”。这将打开项目属性。现在导航到“调试”选项卡。请参考以下截图:

在“启动选项”下,输入 /other 作为命令行参数。现在按 F5 键以调试模式运行应用程序。你会看到屏幕上打开了 OtherWindow。要启动 MainWindow,只需从前面提到的项目属性中移除 /other 参数,再次运行应用程序。这次你会看到打开的是 MainWindow 而不是 OtherWindow。
处理未处理的异常
异常处理是软件开发的重要组成部分。当运行时发生异常,由于代码中的任何错误,我们使用 try {} catch {} 块来处理这些异常。try {} 块包含发生异常的代码;catch {} 块知道如何根据异常类型来处理它。在异常被处理后,程序的正常执行继续,不会影响应用程序。
尽管在大多数情况下我们都会处理,但仍可能存在一些可能被忽视并在运行时出现的异常。这种未处理的异常会导致应用程序崩溃。在本教程中,我们将学习如何在 WPF 应用程序中捕获未处理的异常并正确关闭应用程序。
准备工作
要开始,请打开 Visual Studio IDE。现在创建一个新的项目,基于 WPF 应用程序模板,并将其命名为 CH01.UnhandledExceptionDemo。
如何操作...
让我们按照以下步骤开始演示:
- 打开
MainWindow.xaml页面,并在其上添加两个单选按钮和一个按钮。第一个单选按钮将在try {} catch {}块中处理异常,而第二个单选按钮将抛出一个未处理的异常。将以下代码添加到您的MainWindow.xaml中:
<Window x:Class="CH01.UnhandledExceptionDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="UnhandledException Demo"
Height="120" Width="400">
<Grid Margin="10">
<StackPanel Orientation="Vertical">
<RadioButton x:Name="radioOne" GroupName="type"
Content="Handle in Try/Catch Block"
IsChecked="True" Margin="4"/>
<RadioButton x:Name="radioTwo" GroupName="type"
Content="Handle in Unhandled Block"
IsChecked="False" Margin="4"/>
</StackPanel>
<Button Content="Throw Exception"
Width="120" Height="30"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="10"
Click="OnThrowExceptionClicked"/>
</Grid>
</Window>
- 打开
MainWindow.xaml.cs文件,添加按钮点击事件处理程序。在类内部添加以下代码块:
private void OnThrowExceptionClicked(object sender, RoutedEventArgs e)
{
if (radioOne.IsChecked == true)
{
try { throw new Exception("Demo Exception"); }
catch (Exception ex)
{
MessageBox.Show("'" + ex.Message +
"' handled in Try/Catch block");
}
}
else
{
throw new Exception("Demo Exception");
}
}
- 打开
App.xaml.cs文件,并重写OnStartup方法,以便将应用程序级别的DispatcherUnhandledException事件注册,如下面的代码所示:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
DispatcherUnhandledException += OnUnhandledException;
}
- 将
DispatcherUnhandledException事件处理程序添加到App.xaml.cs中,并按以下代码处理异常,但使用一个空的代码块:
private void OnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
}
-
让我们构建并运行应用程序。你将在屏幕上看到以下 UI
![图片]()
-
应用程序窗口中将有两个单选选择器和一个按钮。当第一个单选按钮被选中并且你点击“抛出异常”按钮时,它将在
try {}块中生成一个异常,然后立即被相关的catch {}块处理,而不会导致应用程序崩溃。以下消息框将在 UI 上显示:

- 对于第二个单选按钮,当被选中时,如果你点击“抛出异常”按钮,异常将不会被处理,并在
App.xaml.cs文件中的OnUnhandledException事件中被捕获,应用程序将崩溃:

- 再次打开
App.xaml.cs并修改OnUnhandledException事件实现,如下所示,以处理抛出的异常:
private void OnUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
e.Handled = true;
}
-
现在再次运行应用程序,选中第二个单选按钮并点击按钮。你会注意到这次应用程序不会崩溃。
-
多次点击“抛出异常”按钮。应用程序将继续按原样运行,而不会导致 UI 崩溃。
它是如何工作的...
当你通过指定e.Handled = true来处理这种未捕获/未处理的异常时,你的应用程序将不会崩溃并继续运行。捕获未处理异常的最好部分是记录未知/未处理的错误,这样你就可以调查这些异常背后的根本原因,并在未来的构建中修复它们。
当出现关键错误时,你可以从这个块中以编程方式重新启动应用程序。
还有更多...
你还可以使用AppDomain.CurrentDomain.UnhandledException事件处理程序来捕获任何未处理的异常,但你将无法以继续运行应用程序的方式处理它。当使用时,你可以记录错误并终止/重新启动应用程序。
在DispatcherUnhandledException事件中通过指定e.Handled = true处理的未处理异常不会路由到AppDomain.CurrentDomain.UnhandledException。
使用 WPF 标准控件
在本章中,我们将介绍以下菜谱:
-
使用
TextBlock控件添加纯文本 -
使用
Label在文本中添加其他控件 -
提供用户输入文本的选项
-
在应用程序 UI 中添加图像
-
与现成的 2D 形状一起工作
-
添加工具提示以显示附加信息
-
向 WPF 应用程序添加标准菜单
-
使用上下文菜单提供额外功能
-
使用单选按钮和复选框添加用户选项
-
与进度条控件一起工作
-
使用
Slider控件选择数值 -
在您的应用程序中使用日历控件
-
在
ListBox控件中列出项目 -
提供从 ComboBox 中选择选项
-
向窗口添加状态栏
-
向窗口添加工具栏面板以执行快速任务
第三章:简介
每个 UI 框架都必须提供标准控件来设计应用程序 UI,Windows Presentation Foundation(WPF)就是其中之一。WPF 提供了一套标准控件和 UI 元素,例如 TextBlock、TextBox、Button、Image、各种形状、ProgressBar、Slider、各种菜单、Toolbar、ListBox、ComboBox、DataGrid 以及更多。
如以下图所示,UI 控件可以分为两种类型——ItemsControl 和 ContentControl,它们继承自 Control 类。WPF 中所有可用的面板都共享相同的基类 Panel。Control 和 Panel 类都有基 FrameworkElement,它再次继承自 UIElement。它以 DependencyObject 为基类,以 Object 为超基类:

每个控件都有一组常见的属性可供使用。这包括 FontFamily、FontSize、FontStyle、Foreground、Background、BorderBrush、BorderThickness 以及更多。每个框架元素都公开了额外的属性,例如 Width、MaxWidth、MinWidth、ToolTip、Height、Name、Language、Margin 以及更多。当在 UI 中使用任何元素时,您将使用这些常见属性来设置 UIElement 的样式和其他参数。
使用 TextBlock 控件添加纯文本
WPF 中的 TextBlock 控件是一个轻量级 UI 元素,用于在屏幕上显示文本内容。几乎在应用程序 UI 的任何地方,你都会使用这个元素来显示单行或多行格式的纯文本。要添加简单的纯文本,你可以在 XAML 页面中写入 <TextBlock Text="Text message" /> 或 <TextBlock>Text message</TextBlock>。
在这个菜谱中,我们将更深入地探索这个 UI 元素。
准备工作
要开始,打开您的 Visual Studio IDE,并创建一个名为 CH02.TextBlockDemo 的新 WPF 项目。
如何做到...
现在打开 MainWindow.xaml,按照以下步骤添加具有各种格式化选项的 TextBlock 控件:
-
首先,将现有的
Grid面板更改为StackPanel。 -
现在向其中添加以下两个
TextBlock控件,它们将包含纯文本:
<TextBlock Text="1\. This is a TextBlock control, with 'Text'
property" Margin="10 5" />
<TextBlock Margin="10 5">
2\. This is a TextBlock control, having text as Content
</TextBlock>
- 添加以下 XAML 以添加更多
TextBlock控件,并对其应用一些基本的文本格式化:
<TextBlock Text="3\. This is a TextBlock control, having text
formatting"
FontWeight="Bold"
FontStyle="Italic"
TextDecorations="Underline"
Foreground="Red"
Margin="10 5" />
<TextBlock Text="4\. TextBlock with different FontFamily"
FontFamily="Lucida Handwriting"
FontSize="16" Foreground="Blue"
Margin="10 5" />
<TextBlock Text="5\. This is a TextBlock control,
having long text
content, wrapped automatically using
'TextWrapping' property."
TextWrapping="Wrap"
Margin="10 5" />
<TextBlock Text="6\. This is a TextBlock control,
having long text content, trimmed
automatically using
'TextTrimming' property."
TextTrimming="CharacterEllipsis"
Margin="10 5" />
- 让我们构建项目并运行它。你将在屏幕上看到以下 UI:
![图片]()
它是如何工作的...
对于前两个 TextBlock 控件,UI 上将显示纯文本。第三个 TextBlock 控件将应用 Bold、Italic 和 Underline,通过指定控件的 FontWeight、FontStyle 和 TextDecoration 属性来实现。此外,它的前景色已被设置为 红色,通过指定 Foreground 属性。
你还可以为 TextBlock 控件设置不同的字体。使用 FontFamily 属性来设置。正如你所见,第四个 TextBlock 控件应用了 Lucida Handwriting 字体。
当你有一个长文本,它不能在单行中查看时,你可以根据可用空间将其换行或截断。TextWrapping="Wrap" 在第五个 TextBlock 中将其换行。尝试调整窗口的大小,你会看到 TextBlock 会自动调整以匹配可用空间,而第六个 TextBlock 控件的文本则通过将 TextTrimming 属性设置为字符省略号(末尾的三个点)进行截断。这意味着还有更多文本可用,但它已经被 裁剪。
作为 CharacterEllipsis 的替代,你可以使用 WordEllipsis,它将在最后一个可能的单词的末尾截断文本,而不是最后一个可能的字符。
还有更多...
TextBlock 控件也支持内联格式化。就像 HTML 标签一样,你可以用 Bold、Italic 和 Underline 标签包围文本内容来格式化它,如下面的 XAML 代码所示:
<TextBlock Margin="10, 5">
7\. TextBlock with <Bold>Bold</Bold>, <Italic>Italics</Italic>, <Underline>Underlined</Underline> text
</TextBlock>
你也可以在文本内容中添加换行符,如下所示:
<TextBlock Margin="10, 5">
8\. TextBlock with LineBreak<LineBreak/> in between the text
</TextBlock>
下面的 XAML 代码演示了如何向 TextBlock 控件添加一个匹配 Windows 主题风格的超链接元素:
<TextBlock Margin="10, 5">
9\. TextBlock with a <Hyperlink NavigateUri="http://www.kunal-chowdhury.com">Hyperlink</Hyperlink> text in it
</TextBlock>
NavigateUri 属性用于定义你希望导航到的 URL。
你可以使用 Span 元素来设置单个文本内容的样式,包括字体样式、大小、前景色等。它还允许你在其中指定其他内联元素。Run 元素允许你使用 Span 元素的全部可用属性来设置文本内容样式。以下示例演示了在 TextBlock 控件中使用 Span 和 Run 元素是多么简单:
<TextBlock Margin="10, 5"
TextWrapping="Wrap">
10\. This is a <Span><Bold>TextBlock</Bold></Span> control, with <Span Foreground="Brown">Span</Span> Elements and <Run TextDecorations="Underline">Run</Run> commands in it
</TextBlock>
Span 元素可以包含其他内联元素,但 Run 元素只能包含纯文本。
运行前面的示例将产生以下输出:

使用标签添加文本中的其他控件
Label 控件是 WPF 应用程序中表示文本的另一种方式。它看起来像 TextBlock 控件提供的外观,但它不仅可以支持文本,还可以托管任何类型的其他控件。它公开了 Content 属性以托管文本和其他控件。
在这个菜谱中,我们将探讨如何在 WPF 中使用 Label 控件。
准备工作
要开始使用此控件,打开 Visual Studio 以创建基于 WPF 项目模板的应用程序,并将其命名为 CH02.LabelDemo。
如何操作...
一旦项目创建完成,请按照以下简单步骤使用 Label 控件在应用程序 UI 中添加文本:
-
打开
MainWindow.xaml文件以更改应用程序 UI。 -
将现有的
Grid面板替换为以下 XAML 代码:
<StackPanel Margin="10 10 10 20">
<Label Content="1\. This is a Label control" />
<Label Content="2\. A Label control with text formatting"
FontWeight="Bold" Foreground="Red"
FontStyle="Italic"/>
<Label>
<StackPanel Orientation="Horizontal">
<TextBlock Text="3\. A Rectangle" />
<Rectangle Width="20" Height="20" Fill="Red"
Margin="10 0" />
<TextBlock Text="inside a Label control" />
</StackPanel>
</Label>
</StackPanel>
- 现在构建并运行应用程序。你将在屏幕上看到以下输出:
![]()
它是如何工作的...
在 StackPanel 中添加的第一个控件是一个非常基本的标签,其 Content 属性包含纯文本。第二个 Label 控件也包含纯文本,但对其应用了各种格式(例如,FontWeight、Foreground 和 FontStyle),使其样式看起来加粗、斜体和红色。
由于 Label 控件继承自 System.Windows.Controls.ContentControl,它也支持向其内容添加其他控件。添加到 UI 的第三个标签与前面的两个示例略有不同。它不仅包含文本,还包含其他控件,如 StackPanel、TextBlock 和一个 Rectangle,这要归功于它的 Content 属性。
在前面的示例中,对于第三个标签,使用 TextBlock 控件来保存实际的文本内容,而 StackPanel 被用作面板控件来保存 TextBlock 和 Rectangle。
需要记住的一个点是 Label 比一个 TextBlock 更重。所以,当你需要在 UI 上渲染纯文本时,最好只使用 TextBlock。
还有更多...
在 Windows 和其他操作系统中,通过按住 Alt 键然后按下一个定义为访问键的字符来访问窗口中的控件是一种普遍的做法。例如,要打开任何 Windows 应用程序的文件菜单,我们使用 Alt + F。在这里,字符 F 是访问键,当我们按下 Alt 时会被调用。
让我们学习如何在 WPF 应用程序中使用 Label 控件给标签添加访问键。创建一个名为 CH02.LabelAccessKeyDemo 的新项目,打开 MainWindow.xaml 页面,并将默认的 Grid 替换为 StackPanel。现在在 StackPanel 内添加两个标签和两个文本框,如下所示:
<StackPanel Margin="10 10 10 20">
<Label Content="Enter _Username:"
Target="{Binding ElementName=txbUsername}" />
<TextBox x:Name="txbUsername" Margin="6 0" />
<Label Content="Enter _Password:"
Target="{Binding ElementName=txbPassword}" />
<TextBox x:Name="txbPassword" Margin="6 0" />
</StackPanel>
现在运行应用程序。按 Alt + U 激活第一个标签的访问键,并将焦点放在 txbUsername 字段上。按 Alt + P 自动将焦点放在 txbPassword 字段上:

Windows 窗体应用程序使用"&"作为其访问键指定符,但在 WPF 应用程序中略有不同,因为它使用 XML 标记来创建 UI。因此,在 WPF 应用程序中,如果你想向标签添加访问键指定符,你需要在想要突出显示的字符前指定_(下划线)。
例如,在用户名中的U前添加_,当按下Alt + U时激活该标签。前一个示例中的密码字段也是类似的情况。
常用的做法是使用尚未用作其他控件访问键的第一个字符作为访问键。但在需要的情况下,你可以指定标签内容中的任何字符。
Label控件的Target属性将指令传递给指定的控件,当用户触发访问键时,该控件会自动激活。存在于绑定中的ElementName属性(Target="{Binding ElementName=txbPassword}")告诉了你想发送激活指令的控件名称。
提供用户输入文本的选项
WPF 中的TextBox控件用于允许用户以单行或多行格式输入纯文本。单行文本框是常用的表单输入控件;而多行文本框则用作编辑器。
准备工作
打开你的 Visual Studio IDE,创建一个名为CH02.TextBoxDemo的新项目,基于 WPF 应用程序模板。
如何操作...
一旦项目创建完成,按照以下步骤操作来尝试一些TextBox属性:
-
打开
MainWindow.xaml页面,将默认的Grid替换为StackPanel,这样我们就可以以堆叠的方式添加控件。 -
现在在
StackPanel中添加五个TextBox控件,并设置以下各种属性:
<StackPanel Margin="10 10 10 20">
<TextBox Height="30" Margin="10 5"
Text="Hello"/>
<TextBox Text="Hello WPF!"
FontSize="18" Foreground="Blue"
FontWeight="Bold"
Height="30" Margin="10 5"/>
<TextBox Text="This is a 'ReadOnly' TextBox control"
IsReadOnly="True" Height="30" Margin="10 5"/>
<TextBox Text="This is a 'Disabled' TextBox control"
IsEnabled="False" Height="30" Margin="10 5"/>
<TextBox TextWrapping="Wrap" AcceptsReturn="True"
Height="60" VerticalScrollBarVisibility="Auto"
Margin="10 5">
This is multiline textbox.
User can press 'Enter' key to move to next line.
</TextBox>
</StackPanel>
- 运行应用程序,使其在屏幕上显示以下 UI:
![图片]()
它是如何工作的...
我们添加到StackPanel中的第一个TextBox控件是最简单的一个,当它在 UI 中渲染时,它包含空文本。用户可以在此处输入任何纯文本。你也可以通过使用Text属性从代码中指定文本,如第二个控件所示。
你还可以为TextBox控件的文本定义一系列样式。如第二个控件所示,我们指定了FontSize、Foreground、FontWeight。你也可以指定其他属性,作为任何控件的一部分。
第三个是一个ReadOnly文本框,你可以通过将IsReadOnly属性值设置为True来定义它。当你想要禁用TextBox时,将其IsEnabled属性设置为False,如第四个示例所示。
第五个示例演示了定义多行文本框有多简单。只需将其AcceptsReturn属性设置为True和TextWrapping设置为Wrap,控件就会像多行文本编辑器一样运行。
还有更多...
当你使用TextBox作为多行文本输入控件时,别忘了设置其VerticalScrollBarVisibility。这将允许用户滚动文本内容。正如最后一个示例所示,将其设置为Auto以根据其内容按需启用。
Windows 剪贴板支持
TextBox控件自动支持Windows 剪贴板。右键单击它,可以在屏幕上弹出带有常见剪贴板功能的上下文菜单,例如全选、剪切、复制和粘贴。除了这些功能外,它还默认支持剪贴板操作的常见键盘快捷键,如撤销/重做。
添加拼写检查支持
附加的SpellCheck.IsEnabled属性允许你向TextBox控件添加拼写检查支持。将其设置为True以启用它。让我们在 UI 中添加一个带有此功能的多行文本框:
<TextBox TextWrapping="Wrap" AcceptsReturn="True"
Height="60" VerticalScrollBarVisibility="Auto"
SpellCheck.IsEnabled="True"
Margin="10 5" />
现在运行应用程序,你将在 UI 中看到一个带有多行文本输入字段的窗口。输入一些带有拼写错误的文本。你会看到拼写错误的单词被红色下划线突出显示。右键单击它,可以看到一个上下文菜单,其中建议来自词典的单词。
如以下截图所示,选择最适合此上下文的一个:

向应用程序 UI 添加图片
图片用于创建具有背景、图标和缩略图的 UI,并向用户传达更多信息。在 WPF 中,使用Image元素来显示图片。让我们看看这个。
准备工作
要开始使用 WPF 中的图片,启动你的 Visual Studio IDE,创建一个名为CH02.ImageDemo的 WPF 项目,并添加一个名为demoImage.jpg的图片。
如何做到这一点...
让我们按照以下步骤在MainWindow.xaml页面中添加图片:
-
打开
MainWindow.xaml页面,并将现有的Grid替换为StackPanel。将其Orientation属性设置为Horizontal,以便添加到该面板的项目水平堆叠。 -
将四张图片添加到
StackPanel中,并将它们的Source属性设置为demoImage.jpg,该图片位于项目目录中。 -
将每张图片的宽度和高度设置为
100。 -
对于第一张图片,将其
Stretch属性设置为None。 -
对于第二张图片,将其
Stretch属性设置为Fill。 -
对于第三张和第四张图片,分别将它们的
Stretch属性设置为Uniform和UniformToFill。 -
这里是完整的 XAML 代码,你可以参考:
<StackPanel Orientation="Horizontal">
<Image Source="demoImage.jpg"
Stretch="None"
Width="100" Height="100"
Margin="10 10 5 10" />
<Image Source="demoImage.jpg"
Stretch="Fill"
Width="100" Height="100"
Margin="10 10 5 10" />
<Image Source="demoImage.jpg"
Stretch="Uniform"
Width="100" Height="100"
Margin="10 10 5 10" />
<Image Source="demoImage.jpg"
Stretch="UniformToFill"
Width="100" Height="100"
Margin="10 10 5 10" />
</StackPanel>
- 让我们构建并运行应用程序。你将在应用程序 UI 中看到以下输出
![图片]()
它是如何工作的...
在 XAML 中,Image控件的Source属性是你想要显示的图片文件的路径。当你从代码中访问它时,它是一个BitmapImage。
图像的Stretch属性描述了它应该如何拉伸以填充目标。对于第一个图像,我们将其设置为Stretch="None",它保留了图像的原始大小。当您将其设置为Fill时,例如示例中的第二个图像,内容将调整大小以填充目标尺寸,而不保留其宽高比。
对于第三和第四个图像,分别将其设置为Uniform和UniformToFill,将内容调整大小以适应目标尺寸,同时保留其原始宽高比。但对于第四种情况,如果目标图像的宽高比与源图像不同,源内容将被裁剪以适应目标尺寸。
图像Stretch属性的默认值是Uniform。这意味着,当您将图像添加到 UI 中时,默认情况下,它会将内容调整大小以适应目标尺寸。
还有更多...
您还可以通过创建BitmapImage实例并将其分配给其Source属性来在 XAML 中设置图像。BitmapImage实例公开了UriSource属性来设置图像路径。以下是如何在 XAML 中使用BitmapImage元素设置图像源的示例:
<Image>
<Image.Source>
<BitmapImage UriSource="demoImage.jpg" />
</Image.Source>
</Image>
您还可以通过设置BitmapImage的Rotation属性来旋转图像。它包含四个值:Rotate0、Rotate90、Rotate180和Rotate270。以下是一个如何将图像旋转 180 度的示例:
<Image>
<Image.Source>
<BitmapImage UriSource="demoImage.jpg"
Rotation="Rotate180"/>
</Image.Source>
</Image>
此外,您还可以使用Image控件中的StretchDirection属性。该值表示图像如何缩放。有三个值:UpOnly、DownOnly和Both。内容根据图像内容的大小向上、向下或双向缩放。
使用现成的 2D 形状进行操作
在 WPF 中,Shape是一个UIElement,它允许您在应用程序中绘制 2D 形状。WPF 已经提供了一些现成的形状,如下所示:
-
矩形
-
椭圆
-
直线
-
多边形
-
多边形
-
路径
所有这些UIElements都公开了一些共同属性来绘制形状。Stroke和StrokeThickness属性描述了绘制形状轮廓的颜色和厚度。Fill属性描述了用于装饰形状内部的颜色。
在本教程中,我们将学习如何创建各种形状。
准备工作
让我们从创建一个新的项目开始。打开您的 Visual Studio,创建一个名为CH02.ShapesDemo的 WPF 项目。由于我们将创建多个形状,我们将使用UniformGrid面板来在本次演示中托管形状。您可以在下一章中了解更多关于此面板的信息。
如何操作...
按照以下步骤在您的应用程序中创建各种形状:
-
打开您的
MainWindow.xaml文件,并将现有的Grid面板替换为UniformGrid。通过设置其Column属性,将其最大列数设置为3。 -
让我们添加第一个形状,一个
Rectangle。在UniformGrid内添加以下 XAML 代码:
<Rectangle Width="200" Height="100"
Stroke="DarkBlue" StrokeThickness="5"
Fill="SkyBlue" Margin="10 5" />
- 现在,让我们添加一个
Ellipse,你可以通过设置相同的值到其Height和Width属性来将其更改为一个圆。添加以下代码来创建椭圆:
<Ellipse Width="200" Height="100"
Stroke="DarkBlue" StrokeThickness="5"
Fill="SkyBlue" Margin="10 5" />
- 在面板中添加
Line,请添加以下 XAML:
<Line X1="10" Y1="80" X2="190" Y2="20"
Stroke="DarkBlue" StrokeThickness="5"
Margin="10 5" />
Polyline是一系列连接的直线。添加以下 XAML 以轻松创建多段线形状,其中线条是根据Points属性中提供的点绘制的:
<Polyline Points="10,60 60,180 100,20 180,80 120,140"
Stroke="DarkBlue" StrokeThickness="5"
Margin="10 5" />
- 同样,你可以在 UI 中添加
Polygon形状。在UniformGrid内添加以下代码来绘制形状:
<Polygon Points="10,60 60,180 100,20 180,80 120,140"
Fill="SkyBlue" Stroke="DarkBlue"
StrokeThickness="5" Margin="10 5" />
- 要添加
Path形状控件,请添加以下 XAML 代码:
<Path Data="M10,60 60,180 C100,20 180,80 120,140"
Stroke="DarkBlue" StrokeThickness="5"
Margin="10 5" />
- 现在,让我们构建你的项目并运行应用程序。你将在屏幕上看到以下形状,因为我们已经添加了前面的代码!
![图片]()
它是如何工作的...
通过设置 Rectangle 类的 Height 和 Width 属性以及其轮廓颜色和粗细来绘制矩形形状。要创建一个正方形,你可以通过设置其维度来使用此形状。
在第二个示例中,使用 Ellipse 控件绘制了一个圆形形状。它使用相同的属性集来创建形状。要使其成为一个完整的圆,请将其 Height 和 Width 设置为相同的值。
如果你想在 UI 中绘制一条直线,请使用 Line 类。它公开了四个属性来绘制线条。将 X1 和 Y1 属性设置为标记起点;将 X2 和 Y2 属性设置为标记线的终点。在上一个示例中,一条线从 (10,80) 坐标点绘制到 (190,20) 坐标点。
在第四个示例中,我们看到了如何使用 Polyline 形状控件创建一系列连接的直线。你需要设置线条的 (X, Y) 坐标点在其 Points 属性中。在上一个示例中,它创建了连接以下坐标点 (10,60), (60,180), (100,20), (180,80), 和 (120,140) 的形状。
Polygon 也使用相同的概念来绘制一系列线条,但它将连接的线条系列完成以绘制一个封闭形状。
使用 Path 控件,在第六个示例中,你可以绘制一系列连接的线条和曲线。Data 属性用于设置指定要绘制的形状的几何形状。数据点始终以 M 开头,以开始绘制线条。在任何部分,如果你想创建一个曲线,在该点前缀字符 C。
还有更多...
PathGeometry 对象用于绘制线条、曲线、弧和复杂形状。WPF 提供了两个类来使用迷你语言 Path Markup Syntax 描述几何路径。
你可以在这里了解更多信息:
如果你想要绘制简单的形状,你可以使用 EllipseGeometry、LineGeometry 和 RectangleGeometry 对象。复合几何形状是通过 GeometryGroup 创建的,要创建组合几何形状,请使用 CombineGeometry。
让我们通过以下示例来演示使用三个段落的 PathSegmentCollection 的复杂路径几何形状:
<Path Stroke="DarkBlue" StrokeThickness="5">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure StartPoint="10,100">
<PathFigure.Segments>
<PathSegmentCollection>
<ArcSegment Point="40,80" />
<BezierSegment Point1="100,300"
Point2="100,-100"
Point3="200,150" />
<BezierSegment Point1="100,200"
Point2="200,-10"
Point3="100,150" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
该集合包含一个 ArcSegment 和两个 BeizerSegments,用于设置绘制以下形状的几何点,但你也可以添加额外的段,例如 LineSegment、PolyBeizerSegment、PolyLineSegment、PolyQuadraticBeizerSegment 和 QuadraticBeizerSegment 以创建更复杂的路径:

注意,所有形状都是可拉伸的。你可以使用 Stretch 属性来定义形状的拉伸行为。如果你将其设置为 None,则 Shape 对象将不可拉伸。如果你将其设置为 Fill、Uniform 或 UniformToFill,则 Shape 内容将被拉伸以填充空间,或者在不保留纵横比的情况下填充空间。
添加工具提示以显示附加信息
工具提示用于在鼠标悬停在特定控件或链接上时显示有关该控件或链接的附加信息。FrameworkElement 类公开了一个名为 Tooltip 的属性,你可以在 WPF 中找到所有可用的控件。
在这个菜谱中,我们将学习如何在 WPF 中使用工具提示。我们还将介绍如何使用其他控件设计工具提示。
准备工作
打开你的 Visual Studio IDE,创建一个名为 CH02.TooltipDemo 的新 WPF 应用程序项目。
如何做...
打开 MainWindow.xaml 页面,然后按照以下步骤添加简单的工具提示到 UI:
-
首先,将默认的
Grid替换为StackPanel,并将其Orientation属性设置为Horizontal以水平堆叠一些项目。 -
向
StackPanel添加三个按钮,并设置它们的ToolTip属性。要添加工具提示的显示持续时间,请将其ToolTipService.ShowDuration附加属性设置为毫秒值。以下 XAML 可以作为参考:
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="20">
<Button Content="New" Width="60" Height="30"
ToolTip="Create a New file"
Margin="4" />
<Button Content="Open" Width="60" Height="30"
ToolTip="Open a file"
ToolTipService.ShowDuration="2000"
Margin="4" />
<Button Content="Save" Width="60" Height="30"
ToolTip="Clicking on this button,
saves the file to disk"
Margin="4" />
</StackPanel>
- 运行应用程序,并将鼠标悬停在按钮上以在屏幕上看到弹出的工具提示,如下面的截图所示
![图片]()
它是如何工作的...
ToolTip 属性,当在任何 WPF 控件中设置时,当你悬停在控件上时变得可见。除此之外,ToolTipService 类有一系列附加属性来帮助你设置工具提示的各种行为。
如前所述的第二个示例,如果你将鼠标悬停在 Open 按钮上,屏幕上的 Tooltip 属性将可见 2 秒。这是因为我们将 ToolTipService 的 ShowDuration 属性设置为 2000 毫秒(2 秒)。
你还可以使用 ToolTipService.ShowOnDisabled 属性来显示或隐藏一个禁用元素上的 Tooltip。该类中的 HasDropShadow 属性确保 Tooltip 是否会有阴影。
更多内容...
由于 ToolTip 属性是 object 类型,您可以将其分配给任何内容,包括各种 UI 控件。因此,它帮助您通过更丰富的体验来自定义工具提示的 UI。
让我们修改先前列出的示例中第三个按钮的 Tooltip 属性。在 StackPanel 中放置几个 TextBlock 和 Border 控件来设计 UI,如下面的 XAML 代码片段所示:
<Button Content="Save" Width="60" Height="30"
Margin="4">
<Button.ToolTip>
<StackPanel>
<TextBlock FontWeight="Bold"
Text="Save File" />
<TextBlock Text="Clicking on this button,
saves the file to disk"
FontSize="10" />
<Border BorderBrush="Silver"
BorderThickness="0,1,0,0"
Margin="0 4" />
<TextBlock FontStyle="Italic"
FontSize="9"
Text="Press F1 for more help" />
</StackPanel>
</Button.ToolTip>
</Button>
当您运行应用程序时,将鼠标悬停在第三个按钮上,可以看到该按钮定制的工具提示 UI,如下面的屏幕截图所示:

ToolTipService 类还公开了一些额外的属性,例如 HorizontalOffset 和 VerticalOffset,用于在屏幕上的特定位置定位 Tooltip。
将标准菜单添加到 WPF 应用程序中
WPF 应用程序中最常见的部分之一是菜单,因为它在很小的空间内提供了各种选项。WPF 提供了一个名为 Menu 的控件来容纳名为 MenuItem 的项。
让我们更深入地了解这个菜单控件以及如何将其添加到 Windows 应用程序中。
准备工作
打开您的 Visual Studio,创建一个名为 CH02.MenuDemo 的新 WPF 项目。
如何操作...
按照以下步骤将菜单添加到您的 WPF 应用程序中:
-
打开
MainWindow.xaml页面,并将默认的Grid替换为DockPanel。我们将在下一章中更详细地讨论这个面板。 -
现在将
Menu控件添加到DockPanel内部。这将创建一个基础,用于容纳所有菜单项。 -
然后,您可以以分层的方式添加根级菜单项和子菜单项,如下面的代码片段所示:
<DockPanel>
<Menu>
<MenuItem Header="File">
<MenuItem Header="New" />
<MenuItem Header="Open" />
<MenuItem Header="Save" />
<Separator />
<MenuItem Header="Exit" />
</MenuItem>
<MenuItem Header="Edit">
<MenuItem Header="Undo" />
<MenuItem Header="Redo" />
</MenuItem>
</Menu>
</DockPanel>
- 运行应用程序以查看包含添加的菜单项的以下窗口:
![图片]()
工作原理...
当您在 <Menu> 标签下添加第一个菜单项时,它将创建根级菜单项;例如,文件菜单,编辑菜单。每个根菜单项可以包含一个或多个分层子菜单项。在先前的示例中,文件菜单包含四个子菜单项。
MenuItem 的标题属性用于添加每个项目的标签。当您想添加分隔符时,可以通过添加 <Separator /> 标签来实现,如先前的示例所示。分隔符不需要任何 Header 内容。
更多内容...
您可以进一步自定义菜单项,以包含图标、勾选标记、快捷键或键盘访问指定符。让我们讨论每个选项。
将访问键添加到菜单中
按照常规做法,通过按住 Alt 键然后按定义为其访问键的字符来访问应用程序菜单。例如,要打开任何 Windows 应用程序的文件菜单,我们使用 Alt + F,要访问文件 | 新建菜单,我们使用 Alt + F,N。在这里,字符 F 和 N 被用作访问键,当我们按 Alt 时被调用。
在 WPF 应用程序中,您需要在要突出显示为访问键的字符之前指定下划线_。例如,在文件菜单标题内容中在F之前添加下划线_,当按下Alt + F时激活该菜单:
<MenuItem Header="_File">
<MenuItem Header="_New" />
<MenuItem Header="_Open" />
</MenuItem>
常用的做法是使用尚未用作其他控件访问键的第一个字符作为访问键。但是,根据需要,您可以指定标签内容中的任何字符。
将图标添加到菜单
您可以向菜单添加图标,以使应用程序的菜单项看起来更好。MenuItem元素包含一个名为Icon的属性,可以将其添加为图像图标或 Unicode 字符作为图标。
让我们添加一个 Unicode 字符,为打开和保存菜单项添加一个图标:

现在运行应用程序,查看已添加到相应菜单中的图标,如下截图所示:

添加可复选菜单项
您也可以添加可复选菜单项。WPF 菜单项公开了两个属性来处理此功能。IsCheckable属性告诉菜单项它可以处理勾选/取消勾选选项。当IsCheckable设置为True时,它会在该菜单项的交替单击上设置勾选/取消勾选图标。
您也可以通过编程方式勾选/取消勾选菜单项。将其IsChecked属性设置为True或False。确保设置IsCheckable="True"。让我们在编辑菜单下添加以下菜单项:
<MenuItem Header="Save _settings on exit"
IsCheckable="True" IsChecked="True" />
将点击事件处理程序添加到菜单
菜单不仅仅是添加到应用程序;您需要通过添加Click事件处理程序在菜单上执行一些操作,如下所示代码片段:
<MenuItem Header="E_xit" Click="OnExitMenuClicked" />
在代码背后实现处理程序,如下所示:
private void OnExitMenuClicked(object sender, RoutedEventArgs e)
{
MessageBox.Show("'Exit' menu item clicked!");
Environment.Exit(0);
}
当用户单击退出菜单项时,这将首先显示一个消息框,然后退出应用程序。
使用上下文菜单提供额外功能
上下文菜单在提供额外功能的任何 Windows 应用程序中都发挥着至关重要的作用,这些功能与上下文相关。这通常与单个控件或窗口相关。
当您在控件或窗口上右键单击时,您可以向用户提供一个弹出上下文菜单,以执行单击操作。WPF 为所有框架元素提供了一个ContextMenu属性,以持有具有分层MenuItems的ContextMenu。
请参考此配方以了解如何在您的 WPF 应用程序中添加上下文菜单。
准备工作
使用 Visual Studio 的 WPF 应用程序项目模板创建一个名为CH02.ContextMenuDemo的新项目。
如何操作...
按照以下步骤将上下文菜单添加到TextBlock控件。相同的步骤也可以用于将上下文菜单添加到继承自FrameworkElement的任何控件:
-
打开
MainWindow.xaml文件以修改应用程序 UI。 -
将整个
Grid块替换为以下 XAML 代码:
<Grid>
<TextBlock Text="Right-click on me to open Context Menu!"
Margin="10">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Menu item 1" />
<MenuItem Header="Menu item 2"
InputGestureText="Ctrl + R, Ctrl + G"/>
<Separator />
<MenuItem Header="Menu item 3"
IsCheckable="True"
IsChecked="True" />
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</Grid>
-
运行应用程序。您将看到一个文本,提示您右键单击以打开上下文菜单!。
-
右键单击窗口。你将在屏幕上看到以下上下文菜单弹出![img/c711319d-e3e4-4b76-8f6f-ac764520cb7f.png]
它是如何工作的...
如前例所示,每个 FrameworkElement 都公开了一个名为 ContextMenu 的属性,它可以包含一个 ContextMenu 项。就像我们在前面的菜谱中学到的菜单一样,上下文菜单也可以包含多个项作为 MenuItem,并且每个菜单项又可以包含一个或多个菜单项以使上下文菜单具有层次结构。
菜单项的标签是通过设置其 Header 属性来分配的。你还可以为每个菜单项设置图标,通过将其 Icon 属性分配给图像或 Unicode 字符。如果你已将命令绑定到菜单,则可以将快捷键文本分配为 InputGestureText 属性。
此外,你还可以创建可勾选的上下文菜单项。如图菜单项 3 所示,你可以将 IsCheckable 属性设置为 True,使菜单可勾选。然后你可以使用 IsCheck 属性来显示/隐藏其上的勾选标记。
要在一系列上下文菜单项之间添加分隔符,你可以使用 <Separator /> 标签,如前例所示。
使用单选按钮和复选框添加用户选项
单选按钮和复选框在 Windows 应用程序开发中起着至关重要的作用。它们主要用于向用户提供从一组项目中选择选项的功能。单选按钮允许你从一组选项中选择一个,而复选框允许你切换选项。
在这个菜谱中,我们将学习如何在 WPF 应用程序中使用 RadioButton 和 CheckBox 控件。
准备工作
要开始,打开你的 Visual Studio IDE,并创建一个名为 CH02.OptionSelectorsDemo 的新项目。确保你选择 WPF 应用程序项目模板。
如何操作...
打开 MainWindow.xaml 页面,按照以下步骤向其中添加一组单选按钮和复选框控件:
-
首先,将默认的
Grid面板替换为StackPanel以垂直堆叠项目。 -
现在添加以下具有一组单选按钮的
StackPanel,其GroupName="rdoGroup1":
<StackPanel Orientation="Horizontal">
<RadioButton GroupName="rdoGroup1"
Content="Radio 1"
IsChecked="True"
Margin="4" />
<RadioButton GroupName="rdoGroup1"
Content="Radio 2"
Margin="4" />
<RadioButton GroupName="rdoGroup1"
Content="Radio 3"
Margin="4" />
</StackPanel>
- 在水平放置的
StackPanel中添加另一组单选按钮,其GroupName="rdoGroup2",并将其添加到根StackPanel:
<StackPanel Orientation="Horizontal">
<RadioButton GroupName="rdoGroup2"
Content="Radio 1"
Margin="4" />
<RadioButton GroupName="rdoGroup2"
Content="Radio 2"
IsChecked="True"
Margin="4" />
<RadioButton GroupName="rdoGroup2"
Content="Radio 3"
Margin="4" />
</StackPanel>
- 现在将以下
CheckBox控件放置在水平StackPanel中,并将其添加到根:
<StackPanel Orientation="Horizontal">
<CheckBox Content="Checkbox 1"
IsChecked="True"
Margin="4" />
<CheckBox Content="Checkbox 2"
IsChecked="True"
Margin="4" />
<CheckBox Content="Checkbox 3"
Margin="4" />
</StackPanel>
-
运行应用程序,屏幕上将显示以下输出![img/34fdcd90-9735-4fe5-9315-6f5121fdd8ce.png]
-
选择几个单选按钮和复选框控件来感受其行为。
它是如何工作的...
第一组单选按钮控件放置在与同一名称 rdoGroup1 的组中。当组名称设置为单选按钮的一组时,选择将遵循该组。该组中的第一个单选按钮默认选中,通过将其 IsChecked 属性设置为 True。如果你选择该组中的任何其他单选按钮,则之前的选中状态将重置为未选中状态。
第二组也是如此,但选择一个组不会影响另一个组。因此,当你从第一组检查一个单选按钮时,它不会取消选中另一组的单选按钮。
对于复选框控件来说,情况并非如此。复选框控件允许你拥有多个选中的项。当你选择一个复选框时,它可以从一个状态切换到另一个状态。
单选按钮和复选框控件都暴露了IsChecked属性,以返回一个布尔值来指示控件是被选中还是未选中。
还有更多...
要禁用单选按钮或复选框控件,将其IsEnabled属性设置为False。这两个控件都暴露了两个事件——Checked和Unchecked。当你注册这些事件时,当检查该控件时,控件的Checked事件将被触发。同样,当取消选中该控件时,Unchecked事件将被触发。
与进度条控件一起工作
当你在后台执行长时间任务时,你可能想在应用程序 UI 中添加一个进度指示器,以提供一些工作正在进行中的视觉指示。WPF 为我们提供了一个名为ProgressBar的控件,以显示 0%到 100%之间的工作百分比,通常是。
在这个菜谱中,我们将了解进度条控件及其各种属性。
准备工作
让我们打开 Visual Studio 并创建一个新的 WPF 应用程序项目。将其命名为CH02.ProgressBarDemo。
如何操作...
一旦项目创建完成,请按照以下步骤将进度指示器添加到应用程序的 UI 中:
-
打开
MainWindow.xaml,并将现有的Grid面板替换为StackPanel,这样我们就可以垂直堆叠添加我们的控件。 -
如以下代码片段所示,在
StackPanel中添加三个ProgressBar控件:
<StackPanel Margin="10">
<TextBlock Text="Progress Indicator set at: 20%" />
<ProgressBar Height="30"
Margin="0 4"
Minimum="0"
Maximum="100"
Value="20" />
<TextBlock Text="Progress Indicator set at: 70%" />
<ProgressBar Height="30"
Margin="0 4"
Minimum="0"
Maximum="100"
Value="70" />
<TextBlock Text="Progress Indicator set at:
Indeterminate" />
<ProgressBar Height="30"
Margin="0 4"
Minimum="0"
Maximum="100"
IsIndeterminate="True" />
</StackPanel>
-
将三个控件的
Minimum和Maximum属性分别设置为0(零)和100(百)。 -
如前述 XAML 代码片段所示,将第一个进度条的
Value设置为20,第二个进度条的Value设置为70。 -
将第三个进度条的
IsIndeterminate属性设置为True。 -
现在运行应用程序。你将看到我们之前分享的 XAML 代码的以下输出!
![图片]()
它是如何工作的...
首个进度指示器的值设置为20,而第二个进度指示器的值设置为70。这表示分别完成了 20%和 70%的工作。随着你任务的进展,你可以增加值以在 UI 中的ProgressBar控件上获得进度视觉指示。
对于第三个ProgressBar控件,在先前的示例中,它略有不同。当你不确定要完成的总工作量时,你可以将其IsIndeterminate属性设置为True,如先前的截图所示。当你的工作完成时,你可以停止不确定状态并将其Value设置为 100。
使用滑动条控件选择数值
Slider 控件用于通过拖动滑块按钮沿水平或垂直线选择一个数值。这通常用于提供播放视频的可视化以及音量指示器。
WPF 为我们提供了一个名为 Slider 的控件,可以快速在应用程序 UI 中实现此功能,并且提供了许多属性以进行各种配置。让我们在本食谱中了解更多关于它的信息。
准备工作
首先,创建一个名为 CH02.SliderDemo 的项目,基于 WPF 应用程序模板。
如何实现...
在 WPF 中集成滑块非常简单。只需在 XAML 页面中放置 <Slider />,它就会开始工作。但为了进一步自定义它,让我们遵循以下步骤:
-
打开
MainWindow.xaml页面,并将默认的Grid替换为StackPanel。 -
现在在
StackPanel中添加一个Slider和一个TextBlock控件,如下面的 XAML 片段所示:
<StackPanel Margin="10">
<Slider x:Name="slider"
Minimum="0" Maximum="100"
Value="25"
SmallChange="1"
LargeChange="5" />
<TextBlock Margin="4">
<Run Text="Current slider value: " />
<Run Text="{Binding Value, ElementName=slider}" />
</TextBlock>
</StackPanel>
- 运行应用程序。您将在 UI 中看到一个
Slider控件,以及显示当前值的文本,该值设置为25。将滑块滑块向右移动,它将显示当前选定的值。在我们的演示中,它现在是65,如下面的截图所示![图片]()
工作原理...
它基于当前值工作。名为 Value 的属性为我们提供一个整数,表示当前位置。您可以通过编程方式将其设置为将滑块滑块移动到较小的或较大的值。
Minimum 和 Maximum 属性表示滑块可以接受的最小和最大值。在我们的例子中,我们将其分别设置为 0(零)和 100(百)。
在我们的示例代码中,其他控件 TextBlock 与滑块的 Value 属性进行数据绑定,该属性在 XAML 中。它以纯文本格式显示滑块的当前值。
更多内容...
您还可以在滑块控件中启用刻度显示,以提供更好的滑块位置指示。使用 TickPlacement 属性打开刻度标记。它有四个值 None、TopLeft、BottomRight 和 Both。让我们在我们的上一个滑块控件中添加 TickPlacement="BottomRight"。
TickFrequency 属性用于设置 0 和 100 之间可能值的范围。让我们在我们的代码中添加 TickFrequency="20" 并再次运行应用程序。您将看到以下屏幕:

如前一个截图所示,您可以看到在滑块的底部添加了一些点。它们代表刻度。由于我们已将 TickFrequency 设置为 20,它将整个滑块范围划分为 100/20 = 5 个部分。
通常,移动滑块不会自动对齐到刻度。因此,您将观察到滑块位于刻度之间。使用 IsSnapToTickEnabled 属性并将其设置为 True,以确保滑块始终仅位于刻度标记上。在这种情况下,拖动滑块将根据刻度频率计数移动滑块。
在您的应用程序中使用日历控件
Calendar 控件是 System.Windows.Controls 命名空间的一部分,它允许您在 WPF 应用程序中创建一个可视化的日历。它允许您选择一个日期或一组日期。由于它继承自 Control 类,因此 Control 类的所有常见属性和事件都对它可用。
在此配方中,我们将了解更多关于 Calendar 控件及其使用方法。
准备工作
要开始使用此配方,让我们创建一个名为 CH02.CalendarDemo 的 WPF 应用程序项目。
如何操作...
按照以下步骤将基本控件添加到主窗口:
-
打开
MainWindow.xaml页面。 -
在默认的
Grid面板内部,添加<Calendar />标签以在应用程序 UI 中创建基本的日历控件。 -
要检索用户选择的日期,将
SelectedDatesChanged事件注册到它,如下面的代码片段所示:
<Grid Margin="10">
<Calendar SelectedDatesChanged="OnSelectedDateChanged"
HorizontalAlignment="Left" />
</Grid>
- 在代码后置类 (
MainWindow.xaml.cs) 中添加相关的事件处理程序 (OnSelectedDateChanged),如下所示,以检索所选日期并在消息框中显示:
private void OnSelectedDateChanged(object sender,
SelectionChangedEventArgs e)
{
MessageBox.Show("You selected: " +
((DateTime)e.AddedItems[0]).ToString("dd-MMM-yyyy"));
}
-
让我们运行应用程序。您将在屏幕上看到以下 UI:
![图片]()
-
一旦从日历中选择了一个日期,所选日期将在消息框中显示,如下所示:
![图片]()
-
选择不同的日期。这将向 UI 显示新选定的日期的消息。
它是如何工作的...
WPF Calendar 控件为您提供了基本的 UI,以便在应用程序中开始日历集成。顶部两个箭头允许您在月份之间导航并从日历中选择所需的日期。
导航还支持年视图和十年视图,因此您可以非常容易地选择所需的年份和月份。点击顶部显示的月份名称(在我们的例子中是 2017 年 8 月),以导航到年视图。当您处于年视图时,它将显示 Jan–Dec 的月份范围,点击年份将导航到十年视图,在那里您可以选择所需的年份。
还有更多...
Calendar 控件公开了许多属性和事件,供您自定义控件的行为和外观。让我们进一步讨论这个问题。
SelectionModes 属性
SelectionMode 属性允许您获取或设置表示在日历上允许哪种选择的值。有四个可用的值,分别命名为 None、SingleDate、SingleRange 和 MultipleRange。默认的 enum 值 SingleDate 允许您仅选择单个日期。但是,当您想要多选时,将其设置为 MultipleRange:
<Calendar SelectionMode="MultipleRange" />
DisplayDate 属性
Calendar控件允许你设置起始和结束显示日期。DisplayDate属性表示要显示的当前日期;而设置DisplayDateStart和DisplayDateEnd属性则限制你只能选择从起始日期到结束日期的日期范围内的日期。
以下 XAML 代码演示了如何在Calendar控件中设置DisplayDate、DisplayDateStart和DisplayDateEnd属性:
<Calendar SelectionMode="MultipleRange"
DisplayDateStart="8/10/2017"
DisplayDateEnd="8/21/2017"
DisplayDate="8/16/2017" />
现在运行应用程序以查看以下输出:

显示模式属性
DisplayMode属性允许你选择日历的格式,可以是月份、年份或十年。当你启动一个基本的日历时,默认情况下,它显示月份视图:

但是用户可以通过点击Calendar控件的头文字轻松地从月份导航到年份再到十年。
要从代码中更改显示模式,可以将DisplayMode属性设置为Month、Year或Decade:
<Calendar DisplayMode="Month" /> <!-- default mode -->
<Calendar DisplayMode="Year" />
<Calendar DisplayMode="Decade" />
用户可以通过点击任何日历单元格来启动向下转换,并且他们可以轻松地从十年导航到年份再到月份并选择正确的日期。
黑名单日期属性
尽管显示,你可以选择不选择某些日期范围。你可以通过使用日历的BlackoutDates属性来实现,它接受一个CalendarDateRange对象的集合。
以下Calendar控件将阻止从 2017 年 8 月 1 日到 2017 年 8 月 8 日以及从 2017 年 8 月 21 日到 2017 年 8 月 31 日的日期范围:
<Calendar>
<Calendar.BlackoutDates>
<CalendarDateRange Start="8/1/2017" End="8/8/2017" />
<CalendarDateRange Start="8/21/2017" End="8/31/2017" />
</Calendar.BlackoutDates>
</Calendar>
所有非选择日期都由一个十字标记,如下面的截图所示:

在 Listbox 控件中列出项目
在 WPF 中,ListBox控件用于显示项目列表。用户可以根据指定的SelectionMode从列表中选择一个或多个项目。
在这个菜谱中,我们将学习如何创建ListBox控件并在 WPF 应用程序中使用它。
准备工作
打开你的 Visual Studio IDE 并创建一个新的 WPF 应用程序项目,命名为CH02.ListBoxDemo。
如何操作...
在 UI 中添加ListBox控件就像在任何 XAML 页面中编写<ListBox />标签一样简单。但要存储其中的数据,您必须正确使用其属性。按照以下步骤添加带有一些静态数据的ListBox控件:
-
打开 WPF 项目的
MainWindow.xaml页面。 -
在默认的
Grid面板下,添加<ListBox></ListBox>标签以添加控件。 -
在控件内部添加几个
ListBoxItem,如下所示:
<ListBox x:Name="lstBox"
Width="120" Height="85"
Margin="10 10 20 5">
<ListBoxItem Content="Item 1" />
<ListBoxItem Content="Item 2" IsSelected="True" />
<ListBoxItem Content="Item 3" />
<ListBoxItem Content="Item 4" />
<ListBoxItem Content="Item 5" />
</ListBox>
- 向控件添加两个按钮,分别标记为
+和-以执行对所述Listbox控件的add和delete操作。注册两个按钮的Click事件:
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Content="+"
Width="20" Height="20"
Margin="0 0 4 0"
Click="OnAddItemClicked" />
<Button Content="-"
Width="20" Height="20"
Margin="0 0 4 0"
Click="OnDeleteItemClicked" />
</StackPanel>
- 在代码隐藏文件
MainWindow.xaml.cs中,实现如下所示的按钮点击事件处理程序:
private void OnAddItemClicked(object sender,
RoutedEventArgs e)
{
var itemsCount = lstBox.Items.Count;
var newitem = new ListBoxItem
{
Content = "Item " + (itemsCount + 1)
};
lstBox.Items.Add(newitem);
lstBox.SelectedItem = newitem;
}
private void OnDeleteItemClicked(object sender,
RoutedEventArgs e)
{
var selectedItem = lstBox.SelectedItem;
if (selectedItem != null)
{
lstBox.Items.Remove(selectedItem);
lstBox.SelectedIndex = 0;
}
}
- 现在运行应用程序。你将在屏幕上看到以下 UI:
![图片]()
它是如何工作的...
在前面的示例中,ListBox控件包含五个作为ListBoxItem的项目。当您启动应用程序时,默认情况下,第二个项目被选中,因为其IsSelected属性被设置为True。
这两个按钮用于在Listbox控件中添加或删除项目。点击+按钮将触发OnAddItemClicked事件,这将创建一个新的ListBoxItem实例并将其添加到ListBox控件中。滚动列表以查看新添加的条目。由于ListBox的SelectedItem属性被分配了最新的项目,它现在将被选中,从而取消之前的选中。
点击-按钮以触发OnDeleteItemClicked事件。这将获取当前选中的项目,如果它不是null,它将被从ListBox控件中删除。SelectedIndex属性将设置为0(零),以便在删除后选择第一个元素。
还有更多...
ListBox有许多属性可以执行特定操作。让我们学习其中的一些。在本节的后面,我们还将介绍如何添加具有附加 UI 控件的定制ListBoxItem。
实现多选
ListBox支持多选。默认情况下,当SelectionMode属性设置为Single时,它只接受单个项目的选择。如果您将SelectionMode设置为Multiple,它将接受多选。Extended模式允许您执行单选,但如果在选择项目时按下Ctrl键,它将作为多选操作。
使用多个控件自定义 ListBoxItem
您可以通过向其中添加额外的 UI 控件轻松地自定义ListBoxItem。考虑以下 XAML 代码片段,其中我们添加了一个ListBox,它包含四个ListBoxItem:
<ListBox Width="150" Margin="20 10 10 10">
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<Rectangle Width="10"
Height="10"
Fill="Red"
Margin="0 0 8 0" />
<TextBlock Text="Red (#FFFF0000)" />
</StackPanel>
</ListBoxItem>
<ListBoxItem IsSelected="True">
<StackPanel Orientation="Horizontal">
<Rectangle Width="10"
Height="10"
Fill="Green"
Margin="0 0 8 0" />
<TextBlock Text="Green (#FF00FF00)" />
</StackPanel>
</ListBoxItem>
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<Rectangle Width="10"
Height="10"
Fill="Blue"
Margin="0 0 8 0" />
<TextBlock Text="Red (#FF0000FF)" />
</StackPanel>
</ListBoxItem>
</ListBox>
如果您看到前面的代码片段,每个ListBoxItem都有一个StackPanel来容纳一个Rectangle控件和一个TextBlock控件。如果您运行前面的代码,您将看到以下 UI:

在前面的屏幕截图中,注意项目是如何列出的。每个项目都包含一个矩形来预览作为项目列出的颜色。这在显示实体信息时更有用。
通常,这是通过使用ListBox控件的DataTemplate属性来完成的,我们将在本书的后续章节中学习。
提供从 ComboBox 中选择选项
ComboBox控件是一个项目控件,其工作方式类似于ListBox,但列表中只有一个项目可选中。ListBox控件默认情况下在屏幕上列出多个项目,但ComboBox控件仅在用户点击时显示可滚动的列表。因此,它占用的空间要小得多。
本食谱将讨论ComboBox控件及其使用方法。
准备工作
开始创建一个新的 WPF 应用程序项目,使用您的 Visual Studio IDE,命名为CH02.ComboBoxDemo。
如何操作...
按照以下简单步骤将ComboBox控件添加到您的应用程序 UI 中:
-
将默认的
Grid替换为StackPanel以水平堆叠托管 UI 控件。 -
在
StackPanel内部添加以下 XAML 代码,以创建一个包含一些项目的简单ComboBox控件:
<ComboBox Width="150" Height="26"
Margin="10">
<ComboBoxItem Content="Item 1" />
<ComboBoxItem Content="Item 2" IsSelected="True" />
<ComboBoxItem Content="Item 3" />
<ComboBoxItem Content="Item 4" />
<ComboBoxItem Content="Item 5" />
</ComboBox>
- 添加另一个
ComboBox以具有自定义项目,如下面的示例代码所示:
<ComboBox Width="150" Height="26"
Margin="10">
<ComboBoxItem>
<StackPanel Orientation="Horizontal">
<Rectangle Width="10"
Height="10"
Fill="Red"
Margin="0 0 8 0" />
<TextBlock Text="Red (#FFFF0000)" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem>
<StackPanel Orientation="Horizontal">
<Rectangle Width="10"
Height="10"
Fill="Green"
Margin="0 0 8 0" />
<TextBlock Text="Green (#FF00FF00)" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem>
<StackPanel Orientation="Horizontal">
<Rectangle Width="10"
Height="10"
Fill="Blue"
Margin="0 0 8 0" />
<TextBlock Text="Red (#FF0000FF)" />
</StackPanel>
</ComboBoxItem>
</ComboBox>
- 现在运行应用程序,它将看起来如下截图所示,具有可展开的弹出菜单:

工作原理...
虽然一个ComboBox控件类似于ListBox,但它默认不显示项目列表。需要用户干预才能显示项目。ComboBox的 UI 是三个控制器的组合:
-
一个TextBox,用于显示选定的项目
-
一个Button,用于显示或隐藏可用项目
-
一个Popup,在可滚动的面板内显示项目列表,并给用户选择一个可用列表项的选项
ComboBox包含一个ComboBoxItem集合。您可以将这些添加到其Items属性中。当您点击箭头时,项目列表将在屏幕上弹出,如前一张截图所示。要从代码中预选一个项目,将其IsSelected属性设置为True。
您还可以向ComboBoxItem添加自定义内容,以表示更好的 UI 组件。前一个示例中的第二个ComboBox演示了如何轻松自定义 UI。
就像ListBox一样,它也公开了SelectedItem、SelectedIndex、SelectedValue属性,以帮助您轻松设置或获取选定的项目。
更多...
ComboBox控件默认情况下不可编辑。但您可以控制此行为,为用户提供在ComboBox控件中手动输入所需值的选项。IsEditable属性用于添加此功能。将其设置为True,将其更改为可编辑的ComboBox。考虑以下代码:
<ComboBox Width="150" Height="26"
Margin="10" IsEditable="True">
<ComboBoxItem Content="Item 1" />
<ComboBoxItem Content="Item 2" IsSelected="True" />
<ComboBoxItem Content="Item 3" />
<ComboBoxItem Content="Item 4" />
<ComboBoxItem Content="Item 5" />
</ComboBox>
如果运行前面的代码,您可以看到以下 UI,其中控件现在允许您向其中输入文本:

向窗口添加状态栏
状态栏用于显示有关应用程序当前状态的各项信息。您可以使用此功能显示光标位置、单词计数、任务进度等。通常,状态栏放置在窗口底部,而菜单、工具栏放置在顶部。
在本食谱中,我们将学习如何在 WPF 窗口中添加状态栏。
准备中
要开始使用状态栏,让我们创建一个名为CH02.StatusBarDemo的 WPF 应用程序项目。
如何做...
一旦创建 WPF 项目,打开MainWindow.xaml页面,按照以下步骤将StatusBar控件添加到窗口中:
-
在
Grid面板内部,添加一个StatusBar标签,并将其Height设置为26,VerticalAlignment设置为Bottom。 -
现在更改其项目面板模板以托管一个具有五列的
Grid(我们将在下一章中讨论更多关于网格列的内容),如下所示:
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
- 现在,在
StatusBar标签内添加您想要显示的控件。让我们添加两个StatusBarItem;一个包含纯文本内容,另一个包含ProgressBar控件。在它们之间放置两个分隔符,如下面的 XAML 代码片段所示:
<StatusBarItem Content="Running Process..."
Grid.Column="0"/>
<Separator Width="1" Grid.Column="1" />
<Separator Width="1" Grid.Column="3" />
<StatusBarItem Grid.Column="4">
<ProgressBar IsIndeterminate="True"
Width="100" Height="15" />
</StatusBarItem>
- 这是完整的 XAML 代码,您需要将其放置在默认的
Grid面板中:
<StatusBar Height="26" VerticalAlignment="Bottom">
<StatusBar.ItemsPanel>
<ItemsPanelTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</StatusBar.ItemsPanel>
<StatusBarItem Content="Running Process..."
Grid.Column="0"/>
<Separator Width="1" Grid.Column="1" />
<Separator Width="1" Grid.Column="3" />
<StatusBarItem Grid.Column="4">
<ProgressBar IsIndeterminate="True"
Width="100" Height="15" />
</StatusBarItem>
</StatusBar>
- 当您的 UI 准备就绪后,让我们运行应用程序。您将看到以下屏幕:
![图片]()
工作原理...
在前面的示例中,我们将纯文本内容“正在运行进程...”作为StatusBarItem放置在Grid的第一个列中。Grid的第二列和第四列包含一个宽度为一像素的Separator控件。第五列包含一个具有不确定状态的ProgressBar控件。
当您调整窗口大小时,状态栏将跟随其父级自动调整大小并定位到窗口底部。除了Grid之外,您还可以使用DockPanel将状态栏停靠到底部。
添加工具栏面板以执行快速任务
在任何基于 Windows 的应用程序中,您都可以找到一个工具栏,通常位于窗口主菜单的下方。它包含一组控件,提供对常用功能的便捷访问。
WPF 提供了一个ToolBarTray元素来托管一个或多个ToolBar控件,包含各种 UI 控件。它为您提供了额外的功能,例如自动溢出机制和手动重新定位功能。
在本教程中,我们将学习如何在 WPF 应用程序中处理工具栏。
准备工作
首先,打开您的 Visual Studio IDE 并创建一个新的 WPF 应用程序项目,命名为CH03.ToolBarDemo。
如何操作...
一旦项目创建完成,按照以下步骤在应用程序窗口中添加工具栏:
-
从解决方案资源管理器打开
MainWindow.xaml页面。 -
现在,将现有的
Grid替换为DockPanel,以便我们可以将工具栏停靠到窗口顶部。 -
在
DockPanel中添加一个ToolBarTray元素并将其停靠到Top。 -
在
ToolBarTray中添加一个ToolBar控件,然后在其内部添加几个按钮,如下面的 XAML 标记所示:
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Content="B" FontWeight="Bold"
Width="20"
Click="OnBoldButtonClicked"/>
<Button Content="I" FontStyle="Italic"
Width="20"/>
<Button Width="20">
<TextBlock Text="U"
TextDecorations="Underline"/>
</Button>
</ToolBar>
</ToolBarTray>
-
在
DockPanel中添加一个TextBox控件,位于ToolBarTray下方,以便它可以覆盖窗口剩余的空间。给它命名为txtBox。 -
您可以在
ToolBarTray中添加多个工具栏。您还可以在ToolBar中添加其他控件。让我们添加以下包含ComboBox的ToolBar。将其放置在第一个ToolBar控件之后:
<ToolBar>
<ComboBox Width="50">
<ComboBoxItem Content="8"/>
<ComboBoxItem Content="10"/>
<ComboBoxItem Content="12"/>
<ComboBoxItem Content="14"
IsSelected="True"/>
<ComboBoxItem Content="16"/>
</ComboBox>
</ToolBar>
- 这是完整的 XAML 代码,供您参考:
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Content="B" FontWeight="Bold"
Width="20"
Click="OnBoldButtonClicked"/>
<Button Content="I" FontStyle="Italic"
Width="20"/>
<Button Width="20">
<TextBlock Text="U"
TextDecorations="Underline"/>
</Button>
</ToolBar>
<ToolBar>
<ComboBox Width="50">
<ComboBoxItem Content="8"/>
<ComboBoxItem Content="10"/>
<ComboBoxItem Content="12"/>
<ComboBoxItem Content="14"
IsSelected="True"/>
<ComboBoxItem Content="16"/>
</ComboBox>
</ToolBar>
</ToolBarTray>
<TextBox x:Name="txtBox" Text="Sample Text"
AcceptsReturn="True" TextWrapping="Wrap" />
</DockPanel>
- 由于我们已将第一个工具栏的第一个按钮的点击事件与类相关联,我们需要编写事件主体。打开
MainWindow.xaml.cs文件,并在类中添加以下按钮点击事件实现:
private void OnBoldButtonClicked(object sender,
RoutedEventArgs e)
{
txtBox.FontWeight =
txtBox.FontWeight == FontWeights.Bold ?
FontWeights.Normal : FontWeights.Bold;
}
-
一旦运行应用程序,你将看到以下 UI,其中包含一个工具栏面板内的两个工具栏:
![图片]()
-
点击第一个按钮(用字符 B 表示)。你会看到文本 Sample Text 变得加粗。如果你再次点击相同的按钮,文本的字体重量将变为正常!
![图片]()
它是如何工作的...
ToolBarTray 可以包含一个或多个 ToolBar 控件。每个 ToolBar 控件可以包含一个或多个控件。ToolBar 控件也可以保持为空。当你开始向其中添加其他控件时,工具栏开始根据可用空间改变其大小和位置。
放置在 ToolBar 内部的控件可以注册其相关的事件。如果你想的话,你也可以使用命令绑定来在视图和代码之间建立更精细的关联。
在前面的例子中,第一个按钮,用字符 B 表示,代表为相关的 TextBox 应用 Bold 加权。当你第一次点击它时,文本的 FontWeight 属性将被设置为 Bold。当你再次点击它时,它将被设置为 Normal。按照相同的逻辑,你可以为其他按钮添加 Click 事件,并为组合框添加 SelectionChange 事件,如前面的例子所示。
布局和面板
在本章中,我们将涵盖以下配方:
-
使用 Grid 构建 UI 布局
-
在均匀的单元格中放置元素
-
使用
WrapPanel自动重新定位控件 -
在堆叠中放置控件
-
在 Canvas 内部定位控件
-
使用 Border 包装 UI 元素
-
创建可滚动的面板
-
使用 DockPanel 锚定控件
-
使用
ViewBox缩放 UI 元素 -
创建标签布局
-
在面板中动态添加/删除元素
-
实现拖放功能
第四章:简介
WPF 提供了适当的布局和定位,以提供具有适当容器元素的交互式、用户友好的应用程序,该容器元素可以帮助您定位子 UI 元素。父容器通常是窗口的内容。您可以使用适当的边距、填充和对齐方式放置子级容器和元素。
在 WPF 中,Panel 是提供布局支持的基类。WPF 中有许多派生面板,可以帮助您创建简单到复杂的布局,并且所有这些都在 System.Windows.Controls 命名空间中定义。
所有 Panel 元素都支持由 FrameworkElement 定义的尺寸和定位。您可以通过设置 Height、Width、Margin、Padding、HorizontalAlignment 和 VerticalAlignment 属性来设计您的 UI。以下图表描述了您将在每个地方使用的重要属性:

面板还公开了其他属性,例如 Background、Children、ZIndex 等。由于窗口只能包含一个子元素,因此面板通常用于划分空间以容纳另一个控件或面板。选择正确的面板对于创建布局非常重要。在本章中,我们将学习各种配方,使用各种面板来设计应用程序布局。
使用 Grid 构建 UI 布局
Grid 面板使您能够以表格格式排列子元素,这些元素由行和列中的单元格表示。这是您在创建新的 WPF 项目并导航到 MainWindow.xaml 文件时将看到的默认面板。Visual Studio 会自动将其添加到每个窗口的第一个容器中。
当您想以表格或矩阵形式表示数据时,这通常很有用。在创建表单布局时也很有用。
在这个配方中,我们将详细讨论 Grid 面板,以便你在设计应用程序布局时能够正确使用它。
准备工作
让我们从 Grid 作为布局面板开始,通过创建一个新的项目。打开 Visual Studio 并创建一个名为 CH03.GridDemo 的新项目,选择 WPF 应用程序模板。
如何操作...
执行以下步骤以创建一个示例 Grid 布局,用于在每个单元格中托管几个矩形:
-
在解决方案资源管理器中,打开你的
MainWindow.xaml页面。 -
在默认的
Grid面板内创建几行和列,如下面的代码所示:
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
- 在
Grid内添加六个矩形,并使用Grid.Row和Grid.Column附加属性正确放置它们。您可以参考以下示例代码:
<Rectangle Width="100" Height="60"
Fill="OrangeRed"
Grid.Row="0" Grid.Column="0"/>
<Rectangle Width="100" Height="60"
Fill="OrangeRed"
Grid.Row="0" Grid.Column="1"/>
<Rectangle Width="100" Height="60"
Fill="OrangeRed"
Grid.Row="0" Grid.Column="2"/>
<Rectangle Width="100" Height="60"
Fill="OrangeRed"
Grid.Row="1" Grid.Column="0"/>
<Rectangle Width="100" Height="60"
Fill="OrangeRed"
Grid.Row="1" Grid.Column="1"/>
<Rectangle Width="100" Height="60"
Fill="OrangeRed"
Grid.Row="1" Grid.Column="2"/>
- 现在运行应用程序,您将在屏幕上看到以下 UI:

它是如何工作的...
Grid通过创建行和列在单元格中工作。<Grid.RowDefinitions>和<Grid.ColumnDefinitions>定义了Grid的结构。它包含行和列的集合。在这里,我们使用RowDefinition和ColumnDefinition创建了两个行和三个列(2x3矩阵)。
当我们将矩形放置在Grid内时,我们通过指定行号和列号来定位它们,使用附加属性Grid.Row和Grid.Column。由于索引位置从0(零)开始,第一个放置在第一个单元格中的矩形具有行索引 = 0 和列索引 = 0。同样,第六个/最后一个矩形的定位为Row=1和Column=2。
您可以通过指定绝对值、百分比值(星号大小)或自动大小来设置RowDefinition的Height和ColumnDefinition的Width。在上面的例子中,我们使用了星号大小来定义行和列的维度。
绝对值使用整数来定义固定的高度/宽度。星号大小是一个基于相对的因子,类似于百分比值。当您将高度/宽度标记为*时,它将在填充所有其他固定和自动大小的行/列之后尽可能多地占用空间。当您指定Auto时,它将占用包含的控件所需的空间。
还有更多...
关于星号大小值还有更多要了解的。当有两行或两列的高度/宽度定义为*时,它们将通过按比例分配来占用可用空间。因此,在上面的例子中,两个行中的每一个都占用了 50%的可用空间。同样,三个列平均占用了总共 100%的可用空间。
您也可以使用n*来定义它们。例如,如果一个Grid包含两行,其中一行的高度定义为2*,另一行定义为8*,它们将分别占用 20%和 80%的可用空间。让我们用一个简单的例子来看看。
在窗口内创建一个Grid,并将其ShowGridLines属性设置为True,以便在屏幕上显示网格线。默认情况下,它设置为False。现在将整个Grid分为五个列。考虑以下 XAML 代码:
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="Auto" MinWidth="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions>
</Grid>
第二列的宽度设置为Auto,这意味着它将占用与包含元素宽度相同的空间。当所述列内部不包含任何元素时,这将具有0(零)宽度。您可以指定MinWidth来提供最小值。
第五列具有固定的宽度为40。第二列和第四列的宽度将首先计算,因为它们分别包含自动宽度和固定宽度。
在前面的例子中,其他三个列现在将基于可用空间进行计算,并按2:1:3的比例计算。示例中的第三列将占据六分之一的空间。第一列和第四列将根据第三列的宽度占据2x和3x的宽度。
一旦运行此 UI,你将看到以下输出。现在调整窗口大小以查看如何根据给定的输入动态调整大小:

创建可调整大小的网格
在 WPF 中创建可调整大小的Grid是可能的。你可以使用<GridSplitter/>标签创建一个splitter控件,用户可以使用它来调整特定列的大小。让我们考虑以下 XAML 代码:
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="40"/>
</Grid.ColumnDefinitions>
<GridSplitter Grid.Column="1" Width="5"/>
</Grid>
在这个例子中,GridSplitter控件被放置在第二列。当你运行应用程序时,你将在第二列中看到一个垂直线,你可以拖动它来调整网格列的大小,如下面的截图所示:

在多行和多/或列中跨越元素
并非必须将一个项目放置在单个单元格中。你可以将它跨越多个行和/或列。附加属性Grid.RowSpan允许你将元素跨越两个或多个网格行单元格。同样,Grid.ColumnSpan允许你将元素跨越两个或多个网格列。你可以使用其中一个或两个。
考虑以下代码片段,其中矩形跨越两个行和两个列,从(0,0)单元格位置开始:
<Rectangle Fill="OrangeRed"
Grid.Row="0" Grid.Column="0"
Grid.RowSpan="2" Grid.ColumnSpan="2" />
当你运行这个时,你将看到以下输出:

在均匀单元格中放置元素
就像 Windows Presentation Foundation 中的Grid布局系统一样,UniformGrid控件也提供了类似的布局系统,但只有一点不同,即行和列的大小相同。它根据行和列的数量,将布局平均分成相同大小的单元格。
在这个菜谱中,我们将通过一个简单的例子来学习UniformGrid布局。
准备工作
让我们创建一个示例应用程序来演示UniformGrid控件。打开你的 Visual Studio IDE,创建一个名为CH03.UniformGridDemo的新 WPF 应用程序项目。
如何做到这一点...
现在执行以下步骤:
-
从解决方案资源管理器中,打开
MainWindow.xaml页面。 -
用以下 XAML 代码替换现有的
Grid面板:
<UniformGrid>
<Label Content="Cell 1" Background="Yellow" />
<Label Content="Cell 2" Background="YellowGreen" />
<Label Content="Cell 3" Background="Orange" />
<Label Content="Cell 4" Background="OrangeRed" />
</UniformGrid>
- 构建项目并运行应用程序。你将在屏幕上看到以下输出:

- 现在关闭应用程序,并在相同的
UniformGrid中添加几个更多的Label控件,如下所示:
<Label Content="Cell 5" Background="Violet" />
<Label Content="Cell 6" Background="DeepSkyBlue" />
<Label Content="Cell 7" Background="SkyBlue" />
- 再次运行应用程序,你会看到行和列的数量会自动改变以适应新的元素,如下面的截图所示:

它是如何工作的...
当你开始在 UniformGrid 控件内放置控件时,它会自动计算所需的单元格数量以容纳添加的控件。基于此,它将可用空间划分为行和列,以顺序定位子元素。
当需要放置更多控件时,它再次将空间划分为额外的相等行和列,如第二个示例所示。
还有更多...
UniformGrid 为我们提供了许多属性,以自定义 UI。我们现在将讨论一些最重要的属性。
设置行和列数
UniformGrid 在设置行数和列数上没有限制。你可以通过分配 Rows 和 Columns 属性来设置数字。例如,以下 XAML 只会将元素渲染为单行,因为我们分配了 Rows="1":
<UniformGrid Rows="1">
<Label Content="Cell 1" Background="Yellow" />
<Label Content="Cell 2" Background="YellowGreen" />
<Label Content="Cell 3" Background="Orange" />
<Label Content="Cell 4" Background="OrangeRed" />
<Label Content="Cell 5" Background="Violet" />
<Label Content="Cell 6" Background="DeepSkyBlue" />
<Label Content="Cell 7" Background="SkyBlue" />
</UniformGrid>
以下示例将产生以下输出:

如果你设置 Columns="2",所有元素都将重新定位到两列,但跨越多行。你还可以组合这两个属性。
定义 UniformGrid 的第一个单元格
UniformGrid 面板默认将第一个元素放置在第一个单元格中(行=0,列=0),但它也允许显式设置单元格位置。第一个单元格的位置必须在第一行,索引从 0(零)开始。
以下示例演示了如何通过分配 FirstColumn 属性来设置第一个元素的位置:
<UniformGrid Columns="4" FirstColumn="2">
<Label Content="Cell 1" Background="Yellow" />
<Label Content="Cell 2" Background="YellowGreen" />
<Label Content="Cell 3" Background="Orange" />
<Label Content="Cell 4" Background="OrangeRed" />
<Label Content="Cell 5" Background="Violet" />
<Label Content="Cell 6" Background="DeepSkyBlue" />
<Label Content="Cell 7" Background="SkyBlue" />
</UniformGrid>
当你运行前面的示例时,你将在屏幕上看到以下输出,其中单元格 1 标签位于第三列(索引位置是 2):

从右到左填充元素
UniformGrid 中填充元素的默认行为是 从左到右。但你可以以 从右到左 的方式填充它们。为此,将 FlowDirection 属性设置为 RightToLeft(默认为 LeftToRight),如以下代码片段所示:
<UniformGrid FlowDirection="RightToLeft">
<Label Content="Cell 1" Background="Yellow" />
<Label Content="Cell 2" Background="YellowGreen" />
<Label Content="Cell 3" Background="Orange" />
<Label Content="Cell 4" Background="OrangeRed" />
<Label Content="Cell 5" Background="Violet" />
<Label Content="Cell 6" Background="DeepSkyBlue" />
<Label Content="Cell 7" Background="SkyBlue" />
</UniformGrid>
当你运行前面的代码时,你将看到一个类似于以下截图的 UI:

使用 WrapPanel 自动重新定位控件
WPF 中的 WrapPanel 与 StackPanel 类似,但它不会将项目堆叠成单行;而是根据可用空间将项目换行。它看起来也像 UniformGrid 控件,但它具有根据项目尺寸的奇数单元格大小。
在本食谱中,我们将更详细地介绍 WrapPanel 以及如何使用它重新定位控件。
准备工作
要开始,打开 Visual Studio IDE 并创建一个名为 CH03.WrapPanelDemo 的新项目。确保在创建项目时选择 WPF 应用程序模板。
如何做到这一点...
让我们通过一个简单的例子来在WrapPanel中添加几个按钮。按照以下步骤设计 UI:
-
从 Visual Studio 解决方案资源管理器中打开
MainWindow.xaml页面。 -
将现有的
Grid面板替换为WrapPanel控件,并将其Orientation属性设置为Horizontal。 -
添加一些不同大小的按钮控件。窗口中的整个 XAML 将类似于以下代码:
<WrapPanel Orientation="Horizontal">
<Button Content="Button 1" Margin="4"
Width="100" Height="30"/>
<Button Content="Button 2" Margin="4"
Width="100" Height="30"/>
<Button Content="Button 3" Margin="4"
Width="100" Height="30"/>
<Button Content="Button 4" Margin="4"
Width="208" Height="30"/>
<Button Content="Button 5" Margin="4"
Width="100" Height="30"/>
<Button Content="Button 6" Margin="4"
Width="60" Height="30"/>
<Button Content="Button 7" Margin="4"
Width="60" Height="30"/>
<Button Content="Button 8" Margin="4"
Width="180" Height="30"/>
</WrapPanel>
- 现在构建项目并运行应用程序。您将在屏幕上看到以下输出:

- 调整应用程序 UI 的大小,看看按钮如何在屏幕内放置。
它是如何工作的...
WrapPanel通过在一条线上堆叠子元素来工作。一旦这一行满了,不能再添加更多元素,它就会在那里换行,并在下一行添加新元素并继续。与UniformGrid不同,WrapPanel没有固定列宽。因此,可以根据可用空间放置项目。
我们作为WrapPanel子元素添加的按钮控件将堆叠在第一行。当它无法在同一行容纳时,它会换行以给下一个元素留出空间。
WrapPanel的Orientation属性决定了您是想水平还是垂直堆叠它们。
还有更多...
在前面的示例中,我们已经看到WrapPanel内的项目都有它们各自的大小提及。您也可以通过设置ItemWidth和ItemHeight属性来为所有项目设置一个特定值,如下面的代码片段所示:
<WrapPanel Orientation="Vertical"
ItemWidth="100" ItemHeight="30">
<Button Content="Button 1" Margin="4" />
<Button Content="Button 2" Margin="4" />
<Button Content="Button 3" Margin="4" />
<Button Content="Button 4" Margin="4" />
<Button Content="Button 5" Margin="4" />
<Button Content="Button 6" Margin="4" />
</WrapPanel>
在这种情况下,您不需要为每个子元素单独指定大小。当您运行前面的代码时,您将看到类似于以下输出的输出:

在堆叠中放置控件
WPF 中另一个简单且有用的布局面板是StackPanel。它几乎与WrapPanel一样工作,但有一个区别,它不能将子元素换行。所有添加到其中的项目要么水平堆叠,要么垂直堆叠。
StackPanel使用原生或相对大小来测量其子元素,通过按顺序布局项目来简化布局过程。
然而,当使用比例大小或自动大小的时候,Grid会使用复杂的子元素组合。因此,它使得Grid布局在测量过程和布局过程执行时速度较慢到中等。
因此,在可能的情况下,StackPanel比Grid面板更可取,以减少渲染开销。
在这个示例中,我们将通过一个非常简单的例子来学习StackPanel的工作原理。
准备工作
要开始,让我们打开 Visual Studio 并创建一个名为CH03.StackPanelDemo的新 WPF 应用程序项目。
如何实现...
在解决方案资源管理器中,导航到项目并执行以下步骤以创建包含几个按钮控件的 StackPanel 示例 UI:
-
首先,打开
MainWindow.xaml文件。 -
在
Window标签内,将默认的Grid替换为以下 XAML 代码:
<StackPanel>
<StackPanel Orientation="Horizontal">
<Button Content="Button 1" Margin="4" />
<Button Content="Button 2" Margin="4" />
<Button Content="Button 3" Margin="4" />
<Button Content="Button 4" Margin="4" />
</StackPanel>
<StackPanel Orientation="Vertical">
<Button Content="Button 5" Margin="4" />
<Button Content="Button 6" Margin="4" />
<Button Content="Button 7" Margin="4" />
<Button Content="Button 8" Margin="4" />
</StackPanel>
</StackPanel>
- 让我们构建并运行应用程序。你将看到以下输出:

工作原理...
第一个 StackPanel 用于容纳多个内嵌 StackPanel,默认垂直堆叠。第一个内嵌 StackPanel 控件包含按钮 1 - 按钮四。这些按钮将水平堆叠,因为我们设置了面板的 Orientation 属性为 Horizontal。
第二个内嵌 StackPanel 包含按钮 5 - 按钮八,垂直堆叠,因为我们设置了 Orientation 属性为 Vertical。
与默认方向为 Horizontal 的 WrapPanel 不同,StackPanel 的默认方向设置为 Vertical。
更多...
StackPanel 默认拉伸其子元素,但你也可以控制其拉伸方式。在垂直方向的 StackPanel 中,你可以将子元素的 HorizontalAlignment 属性设置为 Left、Center、Right 或 Stretch,如下面的代码所示:
<StackPanel Orientation="Vertical">
<Button Content="Button (Left)" Margin="4"
HorizontalAlignment="Left"/>
<Button Content="Button (Center)" Margin="4"
HorizontalAlignment="Center"/>
<Button Content="Button (Right)" Margin="4"
HorizontalAlignment="Right"/>
<Button Content="Button (Stretch)" Margin="4"
HorizontalAlignment="Stretch" />
</StackPanel>
上述代码示例将给出以下输出:

同样,你可以为放置在水平方向的 StackPanel 中的子元素分配 VerticalAlignment 属性。此属性包含以下值——Top、Center、Bottom 和 Stretch。
在 Canvas 中定位控件
Canvas 是 WPF 中的另一个简单面板,它允许你将子元素放置在相对于 Canvas 的特定坐标位置。它公开了四个附加属性:Left、Right、Top 和 Bottom,用于处理控件的位置。
这个示例将帮助你理解 Canvas 面板中子元素的位置。
准备中
让我们打开 Visual Studio 实例,创建一个名为 CH03.CanvasDemo 的新 WPF 应用程序项目。
如何做...
执行以下步骤以创建一个简单的 Canvas 面板,其中包含一些标签控件,并将它们定位到特定的坐标位置:
-
打开解决方案资源管理器并导航到项目。
-
打开
MainWindow.xaml文件,并用以下行替换默认的Grid:
<Canvas>
<Label Width="100" Height="60"
Background="GreenYellow"
Canvas.Left="70" Canvas.Top="40"
Content="(70, 40)"
FontSize="20" FontWeight="Bold"/>
<Label Width="100" Height="60"
Background="YellowGreen"
Canvas.Left="220" Canvas.Top="90"
Content="(220, 90)"
FontSize="20" FontWeight="Bold"/>
</Canvas>
- 构建并运行应用程序。它将显示以下屏幕:

- 现在调整窗口大小并观察标签的位置。
工作原理...
Canvas.Left 属性允许你指定子元素与 Canvas 左边缘的距离。Canvas.Top 属性允许你指定子元素与 Canvas 顶部的距离。
类似地,Canvas.Right 和 Canvas.Bottom 属性允许你分别指定相对于右边缘和底部的相对位置。
如前例所示,第一个标签放置在坐标位置(70, 40),而第二个元素放置在坐标位置(220, 90)。如果您调整窗口大小,子元素的位置不会改变。
需要注意的是,子元素上的垂直和水平对齐方式不起作用。此外,如果您设置了Left属性,则Right属性不起作用。同样,如果您设置了Top属性,则Bottom属性不起作用。
还有更多...
在一个Canvas面板中放置的控件的Z-顺序决定了该控件是在另一个重叠控件的上方还是下方。您可以使用Canvas.ZIndex属性来调整Z-顺序的位置。
默认情况下,画布上第一个元素的ZIndex从0(零)开始,并且每次您在画布上添加一个新元素时,它都会逐渐增加1。但在特殊情况下,当您想将一个重叠的控件置于顶部时,请将其ZIndex设置得高于与之重叠的最后一个元素的ZIndex。
使用 Border 包裹 UI 元素
WPF 中的Border控件用作Decorator,您可以使用它来围绕另一个控件绘制边框。由于 WPF 面板不支持在其边缘添加边框,因此使用Border控件来实现相同的效果。
本食谱将指导您如何为一个控件添加边框。您也可以使用相同的概念来装饰放置在面板内部的多个控件,通过将面板包裹在一个Border中来实现。
准备工作
以一个示例开始,让我们首先创建一个新的项目。打开 Visual Studio 并创建一个名为CH03.BorderDemo的 WPF 应用程序项目。
如何操作...
执行以下简单步骤以在TextBlock周围添加边框:
-
打开您的 WPF 项目的
MainWindow.xaml文件。 -
现在将默认的
Grid替换为StackPanel。 -
在其中添加几个由 Border 包裹的 TextBlock。以下是完整的 XAML 代码:
<StackPanel Margin="10">
<Border BorderBrush="OrangeRed"
BorderThickness="2"
Margin="10 4" Padding="10">
<TextBlock Text="Text surrounded by border"/>
</Border>
<Border BorderBrush="OrangeRed"
BorderThickness="2"
CornerRadius="20"
Margin="10 4" Padding="10">
<TextBlock Text="Text surrounded by border,
having corner radius = 20"/>
</Border>
<Border BorderBrush="OrangeRed"
BorderThickness="2"
CornerRadius="5"
Background="Yellow"
Margin="10 4" Padding="10">
<TextBlock Text="Text surrounded by border,
having a Yellow background and rounded border"
TextWrapping="Wrap"/>
</Border>
<Border BorderBrush="OrangeRed"
BorderThickness="4 0"
CornerRadius="5"
Margin="10 4" Padding="10">
<TextBlock Text="Text surrounded by two-side border"/>
</Border>
</StackPanel>
- 让我们运行应用程序。您将看到以下输出:

工作原理...
BorderThickness属性接受一个整数值来绘制控件周围的边框。BorderBrush属性为其添加颜色。您可以使用SolidColorBrush、GradientColorBrush或任何其他画笔类型。第一个Border控件为文本添加了一个细的2px边框。
在第二个示例中,CornerRadius属性已设置为20,以在Border控件的角落周围添加 20 度的曲线。
第三个示例有一个带有背景画笔的边框来包裹TextBlock控件。您可以将BorderThickness、BorderBrush和Background属性结合起来以实现这种外观。注意小角落半径为5度!
在第四个示例中,我们为文本的两边提供了一个边框。BorderThickness 的值可以是 1、2 或 4 个双精度值。四个双精度值(BorderThickness="5, 3, 5, 4" 或 BorderThickness="5 3 5 4")按顺序描述了 Left、Top、Right 和 Bottom 边。
当你提供两个双精度值(BorderThickness="5, 3" 或 BorderThickness="5 3")时,第一个值描述 Left 和 Right;第二个值分别描述 Top 和 Bottom。为了在所有边上提供相同厚度的厚度,只需将一个双精度值分配给属性(BorderThickness="5")。
创建可滚动面板
ScrollViewer 控件在 WPF 应用程序中启用滚动功能,并帮助您托管其他控件。当有更多内容可供显示,但可视区域小于该内容时,使用 ScrollViewer 帮助用户滚动内容。
在本教程中,我们将学习如何在 WPF 应用程序中使用 ScrollViewer。
准备中
让我们打开 Visual Studio 并创建一个名为 CH03.ScrollViewerDemo 的项目。请确保基于 WPF 应用程序模板创建项目。
如何操作...
使用 ScrollViewer 包围面板或控件是一个快速步骤。执行以下步骤以向 image 控件添加滚动功能:
-
在项目中添加一个名为
demoImage.jpg的图片。 -
从解决方案资源管理器打开
MainWindow.xaml文件。 -
现在将现有的
Grid替换为ScrollViewer。 -
添加一个指向
demoImage.jpg文件的图片,如下所示:
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Image Source="demoImage.jpg" />
</ScrollViewer>
- 运行应用程序,您将看到以下窗口,其中包含一个位于
ScrollViewer内的图片:

- 使用滚动条左右和/或上下滚动以查看整个图像。
工作原理...
ScrollViewer 暴露了两个主要属性—HorizontalScrollBarVisibility 和 VerticalScrollBarVisibility。两者都代表一个名为 ScrollBarVisibility 的枚举,有四个值:
-
可见: 当属性设置为
ScrollBarVisibility.Visible时,滚动条将始终可见。 -
隐藏: 当属性设置为
ScrollBarVisibility.Hidden时,滚动条将不会在屏幕上可见,用户将无法滚动以查看完整内容。 -
禁用: 当设置为
ScrollBarVisibility.Disabled时,滚动条将被禁用。 -
自动: 这通常用于仅在需要时使滚动滑块可见。为此,将属性设置为
ScrollBarVisibility.Auto。
使用 DockPanel 垂直控件
DockPanel 使在屏幕的左侧、右侧、顶部或底部停靠 UI 元素变得更容易。这通常很有用,尤其是在你想将窗口分成特定区域时。例如,状态栏始终保持在窗口底部,而菜单或工具栏位于窗口的最顶部。
这个菜谱将帮助您学习如何在应用程序窗口中停靠子元素。
准备工作
让我们从一个新的项目开始。打开 Visual Studio 并创建一个基于可用 WPF 应用程序模板的项目,命名为 CH03.DockPanelDemo。
如何操作...
执行以下步骤以添加一个带有几个标签停靠的 DockPanel:
-
从解决方案资源管理器导航到项目并打开
MainWindow.xaml。 -
将现有的
Grid面板替换为DockPanel控件。 -
现在向其中添加五个标签并将它们停靠在窗口的各个侧面。
-
这里是完整的 XAML 代码供参考:
<DockPanel>
<Label Content="Button (DockPanel.Dock='Right')"
Background="YellowGreen"
Margin="4" Padding="4"
DockPanel.Dock="Right"/>
<Label Content="Button (DockPanel.Dock='Top')"
Background="GreenYellow"
Margin="4" Padding="4"
DockPanel.Dock="Top"/>
<Label Content="Button (DockPanel.Dock='Bottom')"
Background="SkyBlue"
Margin="4" Padding="4"
DockPanel.Dock="Bottom"/>
<Label Content="Button (DockPanel.Dock='Left')"
Background="Orange"
Margin="4" Padding="4"
DockPanel.Dock="Left"/>
<Label Content="Button (None)"
Background="Pink"
Margin="4" Padding="4"/>
</DockPanel>
- 让我们运行应用程序。您将看到标签位于窗口的不同侧面,如下面的截图所示:

工作原理...
DockPanel.Dock 属性根据相对顺序确定元素的位置。该属性是 Dock 枚举类型,并接受以下值——Dock.Left、Dock.Right、Dock.Top 和 Dock.Bottom。如果您不指定该属性,则默认情况下,第一个元素将停靠在左侧,其他元素将占用剩余空间。
在前面的示例中,标签按照以下顺序添加到 DockPanel 中,分别具有 DockPanel.Dock 属性设置为 Right、Top、Bottom 和 Left。最后一个标签没有指定任何 Dock 属性,因此它占用剩余空间以适应其内部。
更多...
在 DockPanel 中,停靠顺序最为重要。如果您更改我们之前创建的示例的顺序,您将注意到 DockPanel 如何更改添加的标签的位置。
使用 ViewBox 缩放 UI 元素
当您构建应用程序时,您不知道应用程序将运行的系统的屏幕分辨率。如果您在设计 UI 时考虑到小或标准分辨率,则 UI 控件在高分辨率显示器上看起来会非常小。如果您反过来,考虑到大屏幕,则在低分辨率显示器上执行时,用户将看不到屏幕的部分。
因此,需要创建一个自动缩放机制,该机制将处理不同的屏幕分辨率。ViewBox 是 WPF 中一个非常流行的控件,它可以帮助您根据大小缩放内容以适应可用空间。当您调整父元素的大小时,它将自动转换内容以按比例缩放。
让我们通过一个简单的示例来了解它是如何工作的。
准备工作
打开您的 Visual Studio IDE 并创建一个名为 CH03.ViewBoxDemo 的新 WPF 应用程序项目。
如何操作...
请执行以下步骤:
-
从解决方案资源管理器打开
MainWindow.xaml文件。 -
设置
Window的较小尺寸。让我们将其高度设置为120,宽度设置为400。 -
将现有的
Grid面板替换为ViewBox。 -
使用以下方式在内部添加文本,使用
TextBlock控件:
<Viewbox>
<TextBlock Text="This is a text, inside a ViewBox"
Margin="10"/>
</Viewbox>
- 运行应用程序。你会看到以下输出:

- 现在调整窗口大小,你会看到文本会根据窗口的大小自动缩放:

它是如何工作的...
ViewBox为你提供了一种根据屏幕分辨率自动调整窗口内容的方式。当你调整ViewBox大小时,它会自动调整内容的大小和相对位置,以适应屏幕。
在前面的示例中,窗口的大小设置为400x120。窗口有一个包含文本字符串的TextBlock控件,被包裹在一个ViewBox中。当你调整窗口大小时,内容也会通过应用缩放变换来调整大小。
但如果ViewBox窗口的宽高比不合适,你会在内容的左侧和右侧或顶部和底部看到空白区域。
还有更多...
ViewBox控件提供了两个属性来拉伸内容。这些是Stretch和StretchDirection。当你没有指定ViewBox的Stretch属性时,它使用Stretch的默认值,即Uniform。
当Stretch属性设置为Uniform,且ViewBox不匹配内容的宽高比时,它会在其顶部和底部或左侧和右侧添加白色边框。它可以是顶部和底部或左侧和右侧:
<Viewbox Stretch="Uniform">
<TextBlock Text="This is a text, inside a ViewBox"
Margin="10"/>
</Viewbox>
当设置为Fill时,它会导致内容完全填充空间而不遵守宽高比。因此,你可能会在 UI 中看到扭曲:
<Viewbox Stretch="Fill">
<TextBlock Text="This is a text, inside a ViewBox"
Margin="10"/>
</Viewbox>
当你将Stretch属性设置为UniformToFill时,它保持原始的宽高比并完全填充窗口。你不会在 UI 中看到任何扭曲:
<Viewbox Stretch="UniformToFill">
<TextBlock Text="This is a text, inside a ViewBox"
Margin="10"/>
</Viewbox>
如果你不想调整内容的大小,将Stretch属性设置为None。当你将其设置为None,并调整窗口以放大时,内容不会缩放,并将保持其原始状态,周围有空白:
<Viewbox Stretch="None">
<TextBlock Text="This is a text, inside a ViewBox"
Margin="10"/>
</Viewbox>
ViewBox的StretchDirection属性用于告诉ViewBox根据Stretch属性拉伸内容。当Stretch属性设置为None时,StretchDirection属性没有效果。
当StretchDirection设置为UpOnly或DownOnly时,内容将根据ViewBox的大小向上或向下调整大小。当设置为Both时,内容将在两个方向上调整大小。
创建标签布局
为了在窗口布局中容纳更多内容,通常使用标签式用户界面。它们允许用户在一个窗口中打开多个页面。例如,大多数最近的网络浏览器使用标签界面,允许用户在一个窗口中同时打开多个网页。
WPF 提供了TabControl来创建标签布局。在这个菜谱中,我们将学习标签界面的基础知识,通过一个简单的示例让你了解它是如何工作的。
准备中
要开始,请确保你已经打开了 Visual Studio IDE。现在创建一个基于可用 WPF 应用程序项目模板的新项目,命名为 CH03.TabControlDemo。
如何操作...
让我们创建 UI 接口以托管一个包含几个标签项的非常基本的标签控制。执行以下步骤:
-
从解决方案资源管理器窗口中,打开
MainWindow.xaml文件。 -
在默认的
Grid面板内,添加一个TabControl,其中包含两个TabItem控件,如下面的代码所示:
<Grid>
<TabControl>
<TabItem Header="Tab 1">
<TextBlock Text="You have selected 'Tab 1'"
FontSize="30" Margin="4"/>
</TabItem>
<TabItem Header="Tab 2">
<TextBlock Text="You have selected 'Tab 2'"
FontSize="30" Margin="4"/>
</TabItem>
</TabControl>
</Grid>
-
现在运行此应用程序,你将看到以下 UI,其中包含两个标签页:
![图片]()
-
关闭应用程序并返回到 XAML 编辑器,在
TabControl内添加另一个TabItem。让我们更改标题的模板以包含除纯文本之外的 UI 元素。在第二个标签之后复制以下 XAML:
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<Ellipse Width="10" Height="10"
Fill="Green" Margin="0 1 8 0"/>
<TextBlock Text="Tab 3"/>
</StackPanel>
</TabItem.Header>
<Border Background="ForestGreen"
Margin="4">
<TextBlock Text="You have selected 'Tab 3'"
FontSize="30" Foreground="White"/>
</Border>
</TabItem>
- 现在再次运行应用程序,并导航到第三个标签页。你将在屏幕上看到以下 UI:
![图片]()
它是如何工作的...
TabControl 从 Selector 继承,为你提供了一个 ItemsControl 来托管它内部的元素。你只能托管 TabItem 控件,实际上它们是 HeaderedContentControl,为每个项目提供了一个 Header。
Header 属性是对象类型,这将允许你在其中放置任何内容,无论是纯文本还是不同的 UI 元素。
在前面的例子中,前两个 TabItem 控件包含作为标题的纯文本,而第三个 TabItem 包含许多不同的 UIElement 以给其标题一个定制的样式。当你从一个标签页切换到另一个标签页时,你会看到其关联的内容,你可以通过其 Content 属性以编程方式访问。
在面板中动态添加/删除元素
到目前为止,我们已经看到了如何在 Panel 控件中添加静态元素/内容。但这并不总是有用的,尤其是在你从后端检索数据并在 UI 中填充,或者根据用户交互动态填充时。
本食谱将讨论这个主题。由于所有面板在添加/删除元素时表现相似,只是在定位上略有不同,我们将通过一个简单的 Canvas 来演示。
准备工作
开始编码之前,让我们首先创建一个 WPF 应用程序项目。打开 Visual Studio 并创建一个名为 CH03.DynamicPanelDemo 的新项目。
如何操作...
让我们在窗口内添加一个 Canvas 面板,并在用户点击 Canvas 面板时在当前光标位置动态添加正方形。执行以下步骤:
-
打开
MainWindow.xaml页面,并将默认的Grid面板替换为Canvas。 -
给它起个名字。在我们的例子中,让我们将其命名为
canvasPanel。 -
为画布面板设置背景并为其注册一个
MouseLeftButtonDown事件。以下是完整的 XAML 代码,仅供参考:
<Window x:Class="CH03.DynamicPanelDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/
2006/xaml/presentation"
Title="Dynamic Panel Demo"
Height="300" Width="500">
<Canvas x:Name="canvasPanel"
Background="LightGoldenrodYellow"
MouseLeftButtonDown="OnMouseLeftButtonDown"/>
</Window>
-
现在打开其关联的代码后置文件
MainWindow.xaml.cs并实现该事件。或者,你可以将光标放在事件名称上方并按 F12 生成事件并直接导航到它。 -
在
OnMouseLeftButtonDown事件实现内部,获取当前光标位置并将元素放置在画布上用户点击的相同位置。以下是代码实现:
private void OnMouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
var mousePosition = e.GetPosition(canvasPanel);
var square = new Rectangle
{
Width = 50,
Height = 50,
Fill = new SolidColorBrush(Colors.Green),
Opacity = new Random().NextDouble()
};
// set the position of the element
Canvas.SetLeft(square,
mousePosition.X - square.Width / 2);
Canvas.SetTop(square,
mousePosition.Y - square.Height / 2);
// add the element on the Canvas
canvasPanel.Children.Add(square);
}
-
让我们运行应用程序。你会看到一个空白窗口,其背景颜色与我们在
Canvas上设置的相同。 -
随机点击
Canvas区域,你会在屏幕上看到正方形弹出,位置与你左键点击Canvas的位置相同。UI 将如下所示:![]()
-
要从正方形中删除元素,让我们在 XAML 中的
Canvas面板中注册一个MouseRightButtonDown事件。关闭正在运行的应用程序,并将MainWindow.xaml页面的全部内容替换为以下内容:
<Window x:Class="CH03.DynamicPanelDemo.MainWindow"
xmlns=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Dynamic Panel Demo"
Height="300" Width="500">
<Canvas x:Name="canvasPanel"
Background="LightGoldenrodYellow"
MouseLeftButtonDown="OnMouseLeftButtonDown"
MouseRightButtonDown="OnMouseRightButtonDown"/>
</Window>
- 现在导航到
MainWindow.xaml.cs文件以添加相关事件实现。在类内部添加以下代码片段:
private void OnMouseRightButtonDown(object sender,
MouseButtonEventArgs e)
{
if (e.Source is UIElement square)
{
canvasPanel.Children.Remove(square);
}
}
-
再次运行应用程序,并在
Canvas内随机点击以添加正方形。 -
一旦正方形放置到位,右键点击它们以查看被点击的正方形从面板中消失。
它是如何工作的...
每个面板都暴露一个名为 Children 的属性,用于存储 UIElement 集合作为 UIElementCollection。要动态地向 UIElementCollection 添加一个元素,请使用其 Add 方法;要删除一个元素,请将其传递给其 Remove 方法。
在前面的示例中,当用户在 Canvas 上左键点击时,e.GetPosition 方法提供了相对于点击面板的点击坐标位置(X、Y)。Canvas.SetLeft 和 Canvas.SetTop 方法用于将创建的元素相对于面板定位,然后将其添加到面板中。
同样,要删除面板中的元素,请使用 e.Source 属性来检索用户右键点击的元素。如果它不是 null,则通过调用 Remove 方法将其从 Canvas 中删除。
更多...
坐标位置用于在 Canvas 面板中放置元素。当你想要在 Grid 中放置一个项目时,放置它时设置 Row 和 Column。对于 StackPanel、WrapPanel 和 UniformGrid 面板,你不需要指定任何其他属性,因为它们将自动堆叠。
以下示例展示了如何在 Grid 中动态添加一个元素,在由 Row 和 Column 索引指定的特定单元格位置:
// set the Row and Column to place the element
Grid.SetRow(element, rowIndex);
Grid.SetColumn(element, columnIndex);
// add the element to the Grid
gridPanel.Children.Add(element);
如果你想要将元素跨越多行和多列,你可以通过调用 Grid.SetRowSpan 和 Grid.SetColumnSpan 方法来实现,如下面的代码所示:
Grid.SetRowSpan(element, noOfRowsToSpan);
Grid.SetColumnSpan(element, noOfColumnsToSpan);
实现拖放功能
当你想为用户提供丰富的体验时,你可能想使用拖拽和放置功能。你也许还希望在应用程序中添加拖拽功能以访问本地资源并将其上传到服务器。
在本教程中,我们将通过一个简单的示例学习 WPF 中拖拽实现的 basics。
准备工作
打开 Visual Studio 并创建一个名为 CH03.DragAndDropDemo 的新 WPF 应用程序。
如何实现...
让我们执行以下步骤,在窗口内创建一些元素,并从其中一个面板拖拽到另一个面板:
-
首先,打开
MainWindow.xaml并将现有的Grid替换为StackPanel。设置其Orientation属性为Horizontal。 -
在其中添加两个
WrapPanel并设置它们的Width、Margin、ItemHeight和ItemWidth属性。 -
给这两个面板起个名字。让我们将第一个 wrap panel 命名为
sourcePanel,第二个 wrap panel 命名为targetPanel。我们将在代码中使用这些名称来访问它们。 -
在第一个 wrap panel 中添加一些标签。设置它们的
Content、Background和其他文本格式化属性。以下是完整的标记代码:
<StackPanel Orientation="Horizontal">
<WrapPanel x:Name="sourcePanel"
ItemHeight="60" ItemWidth="100"
Width="200" Margin="4"
Background="LightGoldenrodYellow">
<Label Content="Item 1"
Background="Olive" Margin="4"
Foreground="White" FontSize="22" />
<Label Content="Item 2"
Background="Olive" Margin="4"
Foreground="White" FontSize="22" />
<Label Content="Item 3"
Background="Olive" Margin="4"
Foreground="White" FontSize="22" />
<Label Content="Item 4"
Background="Olive" Margin="4"
Foreground="White" FontSize="22" />
<Label Content="Item 5"
Background="Olive" Margin="4"
Foreground="White" FontSize="22" />
</WrapPanel>
<WrapPanel x:Name="targetPanel"
ItemHeight="60" ItemWidth="100"
Width="200" Margin="4"
Background="OldLace">
</WrapPanel>
</StackPanel>
- 如果你运行此应用程序,你将在屏幕上看到两个面板。如下面的截图所示,其中一个面板将包含五个标签(项目 1 - 项目 5),另一个将为空:
![截图]()
如果你现在从左侧面板拖拽任何元素并尝试将其放置到右侧面板,你会发现它不起作用。我们还没有添加拖拽支持。
- 要将拖拽支持添加到第一个 wrap panel (
sourcePanel),在 XAML 中注册其MouseLeftButtonDown事件属性,如下所示:
<WrapPanel x:Name="sourcePanel"
ItemHeight="60" ItemWidth="100"
Width="200" Margin="4"
Background="LightGoldenrodYellow"
MouseLeftButtonDown="OnDrag">
- 在 XAML 中注册的
OnDrag事件需要在文件的代码后端实现。打开MainWindow.xaml.cs并添加以下事件实现,这将向sourcePanel添加拖拽支持:
private void OnDrag(object sender, MouseButtonEventArgs e)
{
if (e.Source is UIElement draggedItem)
{
DragDrop.DoDragDrop(draggedItem,
draggedItem,
DragDropEffects.Move);
}
}
-
现在我们需要启用第二个 wrap panel (
targetPanel) 作为可放置的目标,并将其AllowDrop属性设置为True。 -
同时注册其
Drop事件属性,以便我们可以执行drop操作。以下是第二个面板的完整标记:
<WrapPanel x:Name="targetPanel"
ItemHeight="60" ItemWidth="100"
Width="200" Margin="4"
Background="OldLace"
AllowDrop="True"
Drop="OnDrop">
<!-- This is the DROP Target -->
</WrapPanel>
- 现在我们需要实现
OnDrop事件体以执行所需的drop操作。再次导航到MainWindow.xaml.cs并添加以下代码:
private void OnDrop(object sender, DragEventArgs e)
{
var draggedData = e.Data;
if (draggedData.GetData(draggedData.GetFormats()[0])
is UIElement droppedItem)
{
sourcePanel.Children.Remove(droppedItem);
targetPanel.Children.Add(droppedItem);
}
}
- 现在我们运行应用程序。屏幕上会出现相同的界面,有两个面板。第一个面板(左侧)将包含一些元素。将光标置于其中一个元素上,点击它将其拖拽到另一个面板(右侧),并在那里释放。你会看到该元素将从第一个面板移除并添加到右侧面板,如下面的截图所示:

工作原理...
AllowDrop="True"属性将面板准备为启用拖放。当您通过点击元素开始拖动时,OnDrag事件中编写的DragDrop.DoDragDrop方法将启动拖放操作。它将第一个参数作为依赖对象的引用,即正在拖动的数据源。第二个参数是包含正在拖动数据的对象。最后一个参数是一个值,用于指定操作的最终效果(DragDropEffects)。
在前面的示例中,当元素被拖放到目标时,从DragEventArgs参数值检索到的拖动数据首先从源中移除,然后添加到目标。
还有更多...
根据您的拖放需求,您可以通过指定DragDropEffects枚举值的正确值来更改效果。效果可以分为六种类型:
-
无: 当指定时,目标将不接受任何数据,光标将变为不可用图标
![图片]()
-
复制: 当指定时,数据将被复制到目标,并且在目标上的
drop操作期间,光标将看起来如下所示:![图片]()
-
移动: 当指定时,源数据将被移动到目标。在
drop操作期间,光标将变为以下样式:![图片]()
-
链接: 当指定时,源数据将与目标链接。在目标上的
drop操作期间,光标将变为以下样式:![图片]()
-
滚动: 当指定时,它定义了滚动是否即将在目标元素上开始或当前正在发生。
-
全部: 当指定时,数据在从源移除后将被复制并滚动到目标。
与数据绑定一起工作
在本章中,我们将介绍以下食谱:
-
与 CLR 属性和 UI 通知一起工作
-
与依赖属性一起工作
-
与附加属性一起工作
-
将数据绑定到对象
-
将数据绑定到集合
-
元素到元素的数据绑定
-
在
DataGrid控件中排序数据 -
在
DataGrid控件中分组数据 -
在
DataGrid控件中过滤数据 -
使用静态绑定
-
使用值转换器
-
使用多值转换器
第五章:简介
数据绑定 是一种在应用程序的 UI 和业务逻辑之间建立连接的技术,以便在它们之间实现适当的数据同步。虽然您可以直接从代码后端访问 UI 控件以更新其内容,但数据绑定由于其自动通知系统已成为更新 UI 层的首选方式。
要使数据绑定在 WPF 应用程序中工作,绑定双方都必须向对方提供更改通知。数据绑定的源属性可以是 .NET CLR 属性或依赖属性,但目标属性必须是依赖属性,如下所示:

数据绑定通常在 XAML 中使用 {Binding} 标记扩展来完成。在本章中,我们将通过探索一些食谱来了解更多关于 WPF 数据绑定机制的信息。
与 CLR 属性和 UI 通知一起工作
CLR 属性只是围绕私有变量的包装,以暴露获取器和设置器来检索和分配变量的值。您可以在数据绑定中使用这些常规 CLR 属性,但默认情况下无法自动进行 UI 通知,除非您创建通知机制。
在本食谱中,我们将学习如何使用 CLR 属性进行数据绑定,然后学习如何从代码中触发通知以在值更改时自动更新 UI。
准备工作
要开始使用常规 CLR 属性进行数据绑定,请打开您的 Visual Studio IDE 并创建一个名为 CH04.NotificationPropertiesDemo 的新 WPF 应用程序项目。
如何做到这一点...
执行以下步骤以创建向 UI 发送通知的 CLR 属性:
-
打开
MainWindow.xaml文件并给窗口一个名字。例如,通过在Window标签中添加以下语法来命名当前窗口window:x:Name="window"。 -
现在将默认的
Grid划分为几行和几列。将以下 XAML 标记复制到您的Grid面板中:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
- 一旦网格被划分为行和列,让我们在它里面添加一些文本和按钮控件。将这些控件放置在适当的单元格中,如下面的代码所示:
<!-- Row 0 -->
<TextBlock Text="Your department"
Grid.Row="0" Grid.Column="0"/>
<TextBlock Text=":"
Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Department, ElementName=window}"
Margin="0 2"
Grid.Row="0" Grid.Column="2"/>
<!-- Row 1 -->
<TextBlock Text="Your name"
Grid.Row="1" Grid.Column="0"/>
<TextBlock Text=":"
Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Center"/>
<TextBox Text="{Binding PersonName, ElementName=window, Mode=TwoWay}"
Margin="0 2"
Grid.Row="1" Grid.Column="2"/>
<!-- Row 3 -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Grid.Row="3" Grid.Column="0"
Grid.ColumnSpan="3">
<Button Content="Submit"
Margin="4" Width="80"
Click="OnSubmit"/>
<Button Content="Reset"
Margin="4" Width="80"
Click="OnReset"/>
</StackPanel>
- 现在打开
MainWindow.xaml.cs文件背后的代码,并在其中添加两个名为Department和PersonName的 CLR 属性。第一个属性(Department)始终返回一个常量字符串,而第二个属性(PersonName)可以接受用户的值。以下是完整的代码:
public string Department { get { return "Software Engineering"; } }
private string personName;
public string PersonName
{
get { return personName; }
set { personName = value; }
}
- 在代码后置类中添加以下事件实现:
private void OnSubmit(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello " + PersonName);
}
private void OnReset(object sender, RoutedEventArgs e)
{
PersonName = string.Empty;
}
- 现在构建并运行应用程序。如图所示,在
TextBox中输入你的名字并点击提交按钮。系统会向用户显示一个包含输入名字的消息:

- 现在,点击重置按钮并观察行为。尽管代码已经编写为使用空字符串设置属性,但 UI 没有被修改:

- 要在关联属性发生变化时向 UI 发送通知,你需要实现
System.ComponentModel命名空间中存在的INotifyPropertyChanged接口。打开MainWindow.xaml.cs文件,并添加如下定义的INotifyPropertyChanged接口:
public partial class MainWindow : Window, INotifyPropertyChanged
- 你需要添加以下
using命名空间声明来解决构建问题:
using System.ComponentModel;
- 在类内部添加以下
PropertyChanged事件实现:
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
//in C# 7.0 and above
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
//prior to C# 7.0
//var handler = PropertyChanged;
//if (handler != null)
//{
// handler(this, new PropertyChangedEventArgs(propertyName));
//}
}
- 现在通知框架在属性值发生变化时更新 UI。修改现有的
PersonName属性实现,以调用OnPropertyChanged事件,并按如下方式传递属性名:
private string personName;
public string PersonName
{
get { return personName; }
set
{
personName = value;
OnPropertyChanged("PersonName");
}
}
如果你使用的是 C# 6 及以上版本,你可以通过使用 nameof 运算符来移除硬编码的字符串。
-
再次构建并运行应用程序。在输入框中输入你的名字并点击提交按钮。你会看到一个消息框,其中提到了输入的名字。
-
关闭消息框并点击重置按钮。你会看到
TextBox中的文本被初始化为空字符串:

它是如何工作的...
在前面的示例中,Department 属性与 TextBlock 控件进行了数据绑定,因此相关的 TextBlock 显示了属性返回的文本。同样,PersonName 属性与 TextBox 控件进行了数据绑定。由于数据绑定已设置为 TextBlock 的 Text 属性(使用 TwoWay 模式),当用户在 UI 中更改它时,它会自动更新相关的属性。
因此,当你点击提交按钮时,OnSubmit 事件被触发,并且它直接读取 PersonName 属性,而不是通过访问 TextBox 控件的 Text 属性从 UI 中获取文本。
当你点击重置按钮时,OnReset 事件被触发,并将 PersonName 属性设置为空字符串。但是 UI 没有改变。这是因为 CLR 属性没有在值发生变化时自动更新 UI 的通知机制。
为了克服这一点,WPF 使用 INotifyPropertyChanged 接口,该接口定义了一个 PropertyChanged 事件,以自动将 UI 通知推送到 UI 线程以更新元素。在示例中,当你设置 PersonName 属性时,OnPropertyChanged 事件从属性 setter 触发,并通知 UI PersonName 已被修改。然后 UI 根据属性值设置值。
更多内容...
数据绑定可以是单向的(源 > 目标或目标 > 源)或双向的(源 < > 目标),称为 模式,并定义在四个类别中:
-
一次性: 这种类型的数据绑定模式会导致源属性初始化目标属性。绑定生成后,不会触发任何通知。你应该在源数据不发生变化的地方使用这种类型的数据绑定。
-
单向: 这种类型的绑定会导致源属性自动更新目标属性。这里不可能反向操作。例如,如果你想在 UI 中根据代码后端或业务逻辑中的某些条件显示标签/文本,你需要使用
单向数据绑定,因为你不需要从 UI 更新属性。 -
双向: 这种类型的绑定是双向数据绑定,其中源属性和目标属性都可以发送更新通知。这适用于用户可以更改 UI 中显示的值的可编辑表单。例如,
TextBox控件的Text属性支持这种类型的数据绑定。 -
单向到源: 这是另一种单向数据绑定,它会导致目标属性更新源属性(
单向绑定的反向)。在这里,UI 向上下文发送通知,如果上下文发生变化则不会生成通知。
这里有一个简单的图解,描述了各种数据绑定模式的工作原理:

与依赖属性一起工作
WPF 提供了一组服务,可用于扩展 CLR 属性以提供额外的优势,例如在生态系统中提供自动 UI 通知。要实现依赖属性,类必须继承自 DependencyObject 类。
CLR 属性直接从类的私有成员读取,而依赖属性存储在基类提供的键值字典中。由于依赖属性仅在属性更改时存储属性,因此它使用的内存较少,访问速度更快。
要在 .cs 文件中轻松创建依赖属性,请使用 propdp 代码片段。在继承自 DependencyObject 的任何类文件中,输入 propdp 后跟 TAB 键以生成其结构。使用 TAB 键进行导航,并更改类型、名称、所有者和元数据详细信息。
在本食谱中,我们将学习如何使用依赖属性来自动通知 UI 属性值已更改,从而减少从INotifyPropertyChanged接口定义PropertyChanged事件的负担。
准备工作
让我们打开 Visual Studio IDE 并创建一个名为CH04.DependencyPropertyDemo的项目。确保您已选择 WPF 应用程序类型作为项目模板。我们将使用我们在上一个食谱中创建的相同示例。
如何操作...
执行以下步骤以创建依赖属性,将其绑定到 UI,并从代码中发送通知:
- 从解决方案资源管理器中,打开
MainWindow.xaml页面,并使用我们在上一个示例中使用的相同 UI 设计。复制以下 XAML 标记,并将其替换为MainWindow.xaml文件的内容:
<Window x:Class="CH04.DependencyPropertyDemo.MainWindow"
x:Name="window"
Title="Dependency Properties Demo" Height="150"
Width="300">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Row 0 -->
<TextBlock Text="Your department"
Grid.Row="0" Grid.Column="0"/>
<TextBlock Text=":"
Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Department,
ElementName=window}"
Margin="0 2"
Grid.Row="0" Grid.Column="2"/>
<!-- Row 1 -->
<TextBlock Text="Your name"
Grid.Row="1" Grid.Column="0"/>
<TextBlock Text=":"
Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Center"/>
<TextBox Text="{Binding PersonName,
ElementName=window, Mode=TwoWay}"
Margin="0 2"
Grid.Row="1" Grid.Column="2"/>
<!-- Row 3 -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Grid.Row="3" Grid.Column="0"
Grid.ColumnSpan="3">
<Button Content="Submit"
Margin="4" Width="80"
Click="OnSubmit"/>
<Button Content="Reset"
Margin="4" Width="80"
Click="OnReset"/>
</StackPanel>
</Grid>
</Window>
- 现在打开代码隐藏文件,并在类内部添加以下 CLR 属性。由于这里的值始终是常量,我们不需要将其作为依赖属性:
public string Department
{
get { return "Software Engineering"; }
}
-
现在,在类内部,输入
propdp并按TAB键两次。这将创建属性系统的结构。默认情况下,int将被突出显示。将其替换为string。 -
再次按TAB键,将属性名称从
MyProperty重命名为PersonName。 -
再次按TAB键将焦点移至
Register方法的ownerclass名称参数。将其重命名为拥有者的类名。在我们的情况下,它是MainWindow。 -
再次按TAB键将焦点移至属性元数据。在这里,您可以设置属性的默认值。默认情况下,选择
0(零)。将其更改为string.Empty。这是我们的依赖属性PersonName的完整实现:
public string PersonName
{
get { return (string)GetValue(PersonNameProperty); }
set { SetValue(PersonNameProperty, value); }
}
public static readonly DependencyProperty PersonNameProperty =
DependencyProperty.Register("PersonName",
typeof(string), typeof(MainWindow),
new PropertyMetadata(string.Empty));
- 让我们在
MainWindow类内部添加以下事件实现,用于提交和重置按钮:
private void OnSubmit(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello " + PersonName);
}
private void OnReset(object sender, RoutedEventArgs e)
{
PersonName = string.Empty;
}
- 由于代码已更改,让我们构建并运行应用程序。您将在屏幕上看到应用程序窗口弹出。在提供的输入框中输入一个名称,然后点击提交。将显示消息框,包括输入的文本:

- 点击重置按钮。这将清除输入框(
TextBox控件)内的文本:

它是如何工作的...
在依赖属性中,获取器和设置器的工作方式不同。而不是从其私有字段(CLR属性)返回或设置值,依赖属性从其基类DependencyObject调用GetValue(DependencyProperty)或SetValue(DependencyProperty, value)。在我们的示例中,依赖属性的名称是PersonNameProperty。
DependencyProperty类的静态Register方法接受一些参数来创建依赖属性。它接受的第一个参数是属性的实际名称。第二个参数是属性的类型,第三个是所有者类型,基本上是依赖属性将要创建的类名。它接受的下一个参数是元数据信息,您可以在其中分配属性的默认值。以下是完整的代码:
public static readonly DependencyProperty PersonNameProperty =
DependencyProperty.Register("PersonName",
typeof(string),
typeof(MainWindow),
new PropertyMetadata(string.Empty));
当您从 XAML 设置值时,通过提供与属性的绑定,它设置您可以从可访问位置选择的值。同样,当您从代码设置值时,它会自动通知 UI 已进行更改,并在 UI 中执行相同的更改。因此,它减少了实现INotifyPropertyChanged接口及其关联的PropertyChanged事件的开销。
更多内容...
Register方法的属性元数据可以接受一到三个参数。第一个是我们在前面看到的默认值。第二个是PropertyChangedCallback,当属性的值有效值更改时,属性系统会调用它。第三个是CoerceValueCallback,当属性系统对属性调用System.Windows.DependencyObject.CoerceValue方法时,会调用它。
大多数情况下,属性元数据使用一到两个参数创建,定义默认值和属性更改回调。让我们通过一个示例来学习如何编写它:
public string PersonName
{
get { return (string)GetValue(PersonNameProperty); }
set { SetValue(PersonNameProperty, value); }
}
public static readonly DependencyProperty PersonNameProperty =
DependencyProperty.Register("PersonName", typeof(string),
typeof(MainWindow), new PropertyMetadata(string.Empty,
OnPropertyChangedCallback));
private static void OnPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
在这里,每当您更改属性的值时,都会触发OnPropertyChangedCallback事件。您可以根据事件触发进一步采取行动。您还可以通过访问DependencyObject "d"从回调事件中调用其他非静态成员。
您还可以在提交给属性系统之前验证依赖属性。Register方法的第五个参数接受一个委托,称为ValidateValueCallback。您可以实现它来验证依赖属性的有效值。如果值已正确验证,它将返回true;如果没有,它将被视为无效并返回false。
与附加属性一起工作
附加属性是一种依赖属性,旨在用作全局属性类型,并且可以在任何对象上设置。它没有传统的属性包装器,但仍可用于接收值更改的通知。与依赖属性不同,附加属性不是定义在它们被使用的同一类中。
使用附加属性的主要目的是允许不同的子元素指定父元素中定义的属性的唯一值。例如,您可以在 Grid 面板的任何子元素中使用 Grid.Row、Grid.Column。同样,Canvas.Left、Canvas.Top 附加属性用于 Canvas 面板的任何子元素。
在本食谱中,我们将学习如何创建一个 Attached 属性,并从不同的类中执行操作。
准备工作
首先,创建一个名为 CH04.AttachedPropertyDemo 的新项目,基于 WPF 应用程序项目类型。
如何操作...
现在,执行以下步骤以创建名为 SelectOnFocus 的 Attached 属性,并将其应用于 TextBox 控件,当启用时,将使用 TAB 键在焦点改变时选择文本:
-
打开解决方案资源管理器,右键单击项目,然后通过以下步骤添加一个新类:在“添加”菜单中选择“类...”。给这个类命名为
TextBoxExtensions。 -
打开
TextBoxExtensions.cs文件,并在类文件中添加以下using命名空间:
using System.Windows;
using System.Windows.Controls;
-
在类体内部,输入
propa并按 TAB 键两次。这将创建附加依赖属性的架构,并将键盘焦点移动到property类型,默认为int。将其更改为bool。 -
再次按 TAB 键选择
MyProperty并将其重命名为SelectOnFocus。 -
再次按 TAB 键选择
ownerclass并将其更改为TextBoxExtensions。 -
按 TAB 键设置属性元数据。将默认值设置为
false。将PropertyChangedCallback参数设置为OnSelectOnFocusChanged。以下是完整的代码,包括回调事件:
public static bool GetSelectOnFocus(DependencyObject obj)
{
return (bool)obj.GetValue(SelectOnFocusProperty);
}
public static void SetSelectOnFocus(DependencyObject obj,
bool value)
{
obj.SetValue(SelectOnFocusProperty, value);
}
public static readonly DependencyProperty SelectOnFocusProperty
= DependencyProperty.RegisterAttached("SelectOnFocus",
typeof(bool),
typeof(TextBoxExtensions),
new PropertyMetadata(false, OnSelectOnFocusChanged));
private static void OnSelectOnFocusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox)
{
textBox.GotFocus += (s, arg) =>
{
textBox.SelectAll();
};
}
}
- 现在打开
MainWindow.xaml文件,将现有的 XAML 内容替换为以下内容:
<Window x:Class="CH04.AttachedPropertyDemo.MainWindow"
Title="Attached Property Demo"
Height="150" Width="340">
<StackPanel Margin="15">
<TextBox Text="Normal TextBox Control"
Width="200" Height="30"
Margin="4"/>
<TextBox Text="Select On Focus: Enabled"
extensions:TextBoxExtensions.SelectOnFocus="True"
Width="200" Height="30"
Margin="4"/>
</StackPanel>
</Window>
-
现在,构建并运行应用程序。
-
专注于第一个文本框。默认情况下,它不会有任何选择。按 TAB 键将焦点移动到第二个文本框。文本框的全部文本将被突出显示。再次按 TAB 键将焦点移回第一个文本框。由于所提到的附加属性仅添加到第二个文本框,因此不会进行选择。
它是如何工作的...
通过调用 DependencyProperty.Register 方法注册依赖属性,而通过调用 DependencyProperty.RegisterAttached 方法注册附加属性。它接受四个参数——属性的真正名称、属性类型、所有者类型和属性元数据。
当您将属性设置为控件时,作为一个附加属性(在我们的例子中为 extensions:TextBoxExtensions.SelectOnFocus="True"),在 XAML 中,它将在实例加载期间将其注册到 WPF 属性系统,并触发在 RegisterAttached 方法中定义的 PropertyChangedCallback。在上面的例子中,将调用 OnSelectOnFocusChanged 事件,它将在关联的 TextBox 控件上注册 GotFocus 事件以执行文本选择。
与特定的控件如 TextBox 不同,您可以使用 UIElement 来泛化关联。这样,您可以通过在 XAML 中注册附加属性将其应用于任何控件。
对象数据绑定
到目前为止,我们已经学习了如何使用 INotifyPropertyChanged 接口创建 CLR 属性;我们还了解了一个具有简单数据类型的依赖属性。有许多情况下,您需要将某个类/模型的对象绑定到 UI 并显示其关联的属性。
在本食谱中,我们将学习如何进行对象数据绑定,以向用户展示和检索信息。
准备工作
让我们打开 Visual Studio 实例,创建一个名为 CH04.ObjectBindingDemo 的新项目。确保您选择了正确的 WPF 应用程序项目类型。
如何实现...
执行以下步骤以创建模型和依赖属性,并将数据绑定到 UI 控件,以便当底层数据发生变化时,它自动反映在 UI 中:
-
首先,我们需要创建一个数据模型。从解决方案资源管理器中,右键单击项目并导航到菜单项添加 | 类... 创建一个名为
Person.cs的类文件。 -
将
Person类的内容替换为以下三个属性:
public class Person
{
public string Name { get; set; }
public string Blog { get; set; }
public int Experience { get; set; }
}
- 再次转到解决方案资源管理器,双击打开
MainWindow.xaml.cs文件。创建一个名为PersonDetails的依赖属性,并将其数据类型设置为Person。同时,将其默认值设置为null,如下所示:
public Person PersonDetails
{
get { return (Person)GetValue(PersonDetailsProperty); }
set { SetValue(PersonDetailsProperty, value); }
}
public static readonly DependencyProperty PersonDetailsProperty =
DependencyProperty.Register("PersonDetails",
typeof(Person),
typeof(MainWindow),
new PropertyMetadata(null));
- 在
InitializeComponent()方法调用之后,在MainWindow类的构造函数内部,初始化PersonDetails属性并将其设置为所选类的DataContext,如下所示:
PersonDetails = new Person
{
Name = "Kunal Chowdhury",
Blog = "http://www.kunal-chowdhury.com",
Experience = 10
};
DataContext = PersonDetails;
-
现在,后端代码已经准备好了,让我们打开
MainWindow.xaml文件来设计 UI 并使用我们的模型进行数据绑定。 -
将现有的
Grid面板替换为以下 XAML 标记:
<StackPanel Margin="10">
<TextBlock Margin="0 0 0 20"
TextWrapping="Wrap">
<Run Text="{Binding Name}"/> blogs at <Hyperlink NavigateUri="{Binding Blog}"><Run Text="{Binding Blog}"/></Hyperlink>, and has <Run Text="{Binding Experience}"/> years of experience.
</TextBlock>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Enter years of experience:"/>
<TextBox Text="{Binding Experience, Mode=TwoWay}"
Margin="10 0" Width="50"/>
</StackPanel>
</StackPanel>
- 现在编译项目并运行应用程序。您将看到以下 UI:

它是如何工作的...
应用程序的 UI 有两个 TextBlock 控件来表示数据,以及一个 TextBox 来获取用户的输入。在第一个 TextBlock 控件中,我们使用了多个 <Run/> 命令来绑定来自 Person 类的数据值,以及其他静态文本和一个 Hyperlink 来创建链接。UI 类的数据绑定到 DataContext,在我们的例子中是 PersonDetails。绑定到 UI 的属性来自 Person 类,它是 PersonDetails 依赖属性的 数据类型。
TextBox控件绑定到Experience属性,该属性再次绑定到第一个TextBlock的第三个Run命令。因此,它在两个地方都显示10。现在将TextBox控件的值更改为15并按TAB键更改焦点。这将触发TextBox的TextChanged事件并修改名为Experience的底层属性。由于其性质,通知将自动发送到 UI,并且TextBlock控件将更新如下:

数据绑定到集合
正如我们学习了如何使用对象数据绑定在 UI 上显示单个对象,让我们从将数据对象集合绑定到 UI 以显示所有记录给用户开始。我们将在本食谱中讨论它。
准备工作
打开一个 Visual Studio 实例,创建一个名为CH04.CollectionBindingDemo的新项目。确保您使用 WPF 应用程序项目模板。
如何做...
执行以下步骤以创建集合数据模型并将其绑定到 UI,使用DataGrid控件:
-
在解决方案资源管理器中,右键单击项目。从上下文菜单中,导航到添加 | 类... 创建一个名为
Employee.cs的类文件。 -
打开
Employee.cs文件,并将类实现替换为以下代码:
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
}
- 导航到
MainWindow.xaml.cs文件,并在类中添加以下using语句以定义ObservableCollection:
using System.Collections.ObjectModel;
- 在
MainWindow类的实现中,创建一个名为Employees的依赖属性,类型为ObservableCollection<Employee>,如下所示:
public ObservableCollection<Employee> Employees
{
get { return (ObservableCollection<Employee>)GetValue(EmployeesProperty); }
set { SetValue(EmployeesProperty, value); }
}
public static readonly DependencyProperty EmployeesProperty =
DependencyProperty.Register("Employees",
typeof(ObservableCollection<Employee>),
typeof(MainWindow),
new PropertyMetadata(null));
- 现在,就在构造函数中
InitializeComponent()方法调用之后,编写以下代码块:
Employees = new ObservableCollection<Employee>
{
new Employee
{
FirstName = "Kunal", LastName ="Chowdhury",
Department="Software Division"
},
new Employee
{
FirstName = "Michael", LastName ="Washington",
Department="Software Division"
},
new Employee
{
FirstName = "John", LastName ="Strokes",
Department="Finance Department"
},
};
dataGrid.ItemsSource = Employees;
- 现在打开
MainWindow.xaml文件,并在默认的Grid面板内创建一个DataGrid控件。创建三个列并将它们的值绑定到Employee对象的FirstName、LastName和Department属性。确保将DataGrid的AutoGenerateColumns属性设置为False。以下是完整的 XAML 标记:
<Grid>
<DataGrid x:Name="dataGrid"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}"/>
<DataGridTextColumn Header="Department"
Binding="{Binding Department}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
- 现在构建项目并运行应用程序。您将看到以下屏幕,以及
DataGrid中的数据:

它是如何工作的...
当您将对象集合绑定到DataGrid时,它会为集合中每个对象创建数据网格行。列定义了对象公开的属性。
当DataGrid的AutoGenerateColumns属性设置为True(默认值)时,它会根据属性列表自动创建列。在这个例子中,我们将AutoGenerateColumns属性设置为False并显式定义了各个列。使用这种方法,您可以定义要显示或隐藏的列。一旦将集合设置到DataGrid的ItemsSource属性,它就会相应地填充行和列。
还有更多...
你也可以在 XAML 中定义绑定。为此,首先打开 MainWindow.xaml.cs 并删除行 dataGrid.ItemsSource = Employees;。现在,转到 MainWindow.xaml 文件并给窗口一个名字(x:Name="window")。现在,设置 DataGrid 控件的 ItemsSource 属性,如此处所述:
<DataGrid ItemsSource="{Binding Employees, ElementName=window}"
让我们再次运行应用程序,通过构建项目。你将在屏幕上看到相同的输出。
元素到元素的数据绑定
在最后几个教程中,我们学习了如何进行对象到元素的数据绑定。尽管这很常见,但你可能需要在同一 XAML 页面内进行元素到元素的数据绑定,以减少代码背后的额外代码行。在本教程中,我们将学习如何做到这一点。
准备工作
首先,启动你的 Visual Studio IDE 并创建一个新的 WPF 应用程序项目。将其命名为 CH04.ElementToElementBindingDemo。
如何做到这一点...
现在执行以下步骤以使用 TextBlock 和 Slider 控件设计 UI。然后我们将滑块控件的值绑定到 TextBlock 的 FontSize 属性:
- 打开
MainWindow.xaml页面,将默认的Grid面板替换为以下 XAML 标记:
<Grid>
<TextBlock FontSize="{Binding Value,
ElementName=fontSizeSlider}"
Margin="4"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Run Text="Font Size:"/>
<Run Text="{Binding Value,
ElementName=fontSizeSlider}"/>
</TextBlock>
<Slider x:Name="fontSizeSlider"
Minimum="10" Maximum="40" Value="20"
LargeChange="5"
VerticalAlignment="Bottom"
Margin="10"/>
</Grid>
-
现在构建项目并运行它。你将在屏幕上看到应用程序 UI,其中包含一个
TextBlock和Slider控件。 -
现在增加或减少滑块值,以查看 UI 中的变化,如图所示:

它是如何工作的...
当你拖动滑块的滑块时,它会增加或减少滑块控件(在我们的例子中为 fontSizeSlider)的值。TextBlock 控件的 FontSize 属性直接绑定到滑块的值。因此,当你拖动滑块时,根据值,它会增加或减少字体大小。
同样,TextBlock 有几个 Run 命令。其中一个 Run 命令的 Text 属性也绑定到滑块值,因此,你可以看到屏幕上的数字(滑块的当前值)作为字体大小。
在 DataGrid 控件中排序数据
DataGrid 控件用于以表格格式显示多个记录。行和列用于显示数据。除了其他常用功能外,WPF DataGrid 控件还提供默认的排序功能。你也可以自定义它以编程方式处理。在本教程中,我们将学习如何将排序功能添加到 DataGrid 并按需触发它。
准备工作
要开始本教程,打开你的 Visual Studio 编辑器并创建一个新的 WPF 应用程序项目,命名为 CH04.DataGridSortDemo。
如何做到这一点...
执行以下操作以创建数据模型、填充它并将其绑定到 UI 中的 DataGrid。稍后,添加一个 CheckBox 控件来自定义排序功能:
- 首先,在解决方案资源管理器上右键单击,通过右键单击上下文菜单项添加 | 类...创建一个名为
Employee.cs的新类文件,并在其中添加一些属性:
public class Employee
{
public string ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Department { get; set; }
}
- 打开
MainWindow.xaml.cs文件,并添加一个类型为ObservableCollection<Employee>的依赖属性Employees。确保添加以下命名空间,System.Collections.ObjectModel和System.ComponentModel,以解决所需的类:
public ObservableCollection<Employee> Employees
{
get { return (ObservableCollection<Employee>) GetValue(EmployeesProperty); }
set { SetValue(EmployeesProperty, value); }
}
public static readonly DependencyProperty
EmployeesProperty =
DependencyProperty.Register("Employees",
typeof(ObservableCollection<Employee>),
typeof(MainWindow),
new PropertyMetadata(null));
- 在
MainWindow类的构造函数中,初始化Employees集合如下:
Employees = new ObservableCollection<Employee>
{
new Employee
{
ID = "EMP0001",
FirstName = "Kunal", LastName = "Chowdhury",
Department = "Software Division"
},
new Employee
{
ID = "EMP0002",
FirstName = "Michael", LastName = "Washington",
Department = "Software Division"
},
new Employee
{
ID = "EMP0003",
FirstName = "John", LastName = "Strokes",
Department = "Finance Department"
},
new Employee
{
ID = "EMP0004",
FirstName = "Ramesh", LastName = "Shukla",
Department = "Finance Department"
}
};
-
现在打开
MainWindow.xaml页面,将默认的Grid面板替换为StackPanel。在其内部添加一个DataGrid控件,并给它一个名字(比如说,dataGrid)。将其AutoGenerateColumns属性设置为False。 -
创建四个类型为
DataGridTextColumn的数据网格列,并使用从Employee模型公开的属性创建数据绑定。以下是 XAML 代码:
<StackPanel>
<DataGrid x:Name="dataGrid"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="EMP ID"
Binding="{Binding ID}"/>
<DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}"/>
<DataGridTextColumn Header="Department"
Binding="{Binding Department}"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
- 现在,由于数据网格已经就位,将
Employees集合分配给数据网格的ItemsSource属性。你可以在MainWindow.xaml.cs文件中这样做,就在Employees集合初始化之后:
dataGrid.ItemsSource = Employees;
- 如果你现在运行应用程序,你将看到一个
DataGrid控件,其中包含我们添加到集合中的记录。你将能够通过点击列标题来对记录进行排序,这是控件默认的功能:

- 现在,我们需要在 UI 中添加一个
CheckBox控件来按需切换排序。让我们为Department列做这个。在StackPanel中,在DataGrid控件之后添加以下CheckBox:
<CheckBox x:Name="sortByDepartment"
Content="Sort by Department"
HorizontalAlignment="Right"
Margin="10"
Click="OnSortByDepartment"/>
- 再次导航到
MainWindow.xaml.cs文件,并在类中添加以下事件:
private void OnSortByDepartment(object sender,
RoutedEventArgs e)
{
var cvs =
CollectionViewSource.GetDefaultView(dataGrid.ItemsSource);
if (cvs != null && cvs.CanSort)
{
cvs.SortDescriptions.Clear();
if (sortByDepartment.IsChecked == true)
{
cvs.SortDescriptions.Add(
new SortDescription("Department",
ListSortDirection.Ascending));
}
}
}
- 现在再次运行应用程序。你将看到一个新复选框,位于数据网格下方。切换选择(勾选状态)并观察 UI 上的行为:

它是如何工作的...
一旦OnSortByDepartment事件被触发,它就会获取数据网格的默认视图,并将SortDescription添加到默认视图实例的SortDescriptions属性中。SortDescription将属性名称作为第一个参数。它定义了你想要添加排序功能的列。第二个参数是ListSortDirection,可以是Ascending或Descending。
这不仅限于单个SortDescriptor。根据你的需求,你可以添加更多。在任何时候,当你想要从应用的排序描述中重置视图时,你可以在视图中调用SortDescriptions.Clear()方法(在我们的例子中,它是cvs)。
在 DataGrid 控件中分组数据
DataGrid控件还允许你通过字段名对记录进行分组。在这个菜谱中,我们将学习如何使用PropertyGroupDescription实现这个功能。
准备工作
让我们从创建一个名为CH04.DataGridGroupDemo的新项目开始。确保在创建项目时选择 WPF 应用程序模板。
如何做到这一点...
执行以下步骤,在DataGrid中显示记录时创建分组:
-
在项目中创建
Employee模型类,并公开一些属性,就像我们在 在 DataGrid 控件中排序数据 菜单中分享的那样。 -
在
MainWindow.xaml.cs文件中创建相同的依赖属性(Employees,类型为ObservableCollection<Employee>)并使用一些数据记录填充集合。 -
现在,打开
MainWindow.xaml文件,并添加属性x:Name="window"以给Window命名,这样我们就可以执行元素到元素的数据绑定。 -
将默认的
Grid面板替换为StackPanel,并在其中添加一个DataGrid控件。 -
将
DataGrid的ItemsSource属性设置为绑定Employees集合,该集合从代码后端作为依赖属性公开:
ItemsSource="{Binding Employees, ElementName=window}"
-
将数据网格的
AutoGenerateColumns设置为False,因为我们打算手动添加列。 -
如以下 XAML 片段所示,将四个列添加到数据网格中。
-
还在
DataGrid之后添加一个CheckBox控件,以便能够按部门名称对记录应用分组。以下是完整的 XAML 代码:
<StackPanel>
<DataGrid x:Name="dataGrid"
ItemsSource="{Binding Employees,
ElementName=window}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="EMP ID"
Binding="{Binding ID}"/>
<DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}"/>
<DataGridTextColumn Header="Department"
Binding="{Binding Department}"/>
</DataGrid.Columns>
</DataGrid>
<CheckBox x:Name="groupByDepartment"
Content="Group by Department"
HorizontalAlignment="Right"
Margin="10"
Click="OnGroupByDepartment"/>
</StackPanel>
- 由于我们将在
DataGrid记录上添加分组,我们需要设计分组样式。在DataGrid内部添加以下片段:
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,5"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
GroupItem}">
<Expander IsExpanded="True">
<Expander.Header>
<TextBlock Text="{Binding
Path=Name}"
Margin="5,0,0,0"/>
</Expander.Header>
<Expander.Content>
<ItemsPresenter />
</Expander.Content>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
- 现在,我们需要添加
OnGroupByDepartment事件实现。打开MainWindow.xaml.cs并添加以下代码:
private void OnGroupByDepartment(object sender,
RoutedEventArgs e)
{
var cvs =
CollectionViewSource.GetDefaultView(dataGrid.ItemsSource);
if (cvs != null && cvs.CanGroup)
{
cvs.GroupDescriptions.Clear();
if (groupByDepartment.IsChecked == true)
{
cvs.GroupDescriptions.Add(
new PropertyGroupDescription("Department"));
}
}
}
-
现在运行应用程序。您将看到 UI 中包含一个带有一些记录的
DataGrid。 -
点击复选框,选择按部门分组,并观察其行为:

- 取消选中复选框,将视图恢复到其原始状态。
它是如何工作的...
当您触发 OnGroupByDepartment 事件时,它检索 DataGrid 默认视图的实例并将分组描述应用到它上。分组基于传递给 PropertyGroupDescription 的属性名称,如下所示:
cvs.GroupDescriptions.Add(
new PropertyGroupDescription("Department"));
根据这一点,分组样式应用于数据网格。模板包含一个名为要分组列名的 Expander 控件作为 Header:
<Expander IsExpanded="True">
<Expander.Header>
<TextBlock Text="{Binding Path=Name}" Margin="5,0,0,0"/>
</Expander.Header>
<Expander.Content>
<ItemsPresenter />
</Expander.Content>
</Expander>
现在,您可以展开或折叠组,并应用排序或过滤来深入数据。这有助于轻松找到正确的记录。
还有更多...
您还可以修改 Expander Header 以显示组内的记录数。ItemCount 属性可以用来显示记录数。修改 Expander.Header,如下所示,以自定义它:
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" Margin="5,0,0,0"/>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="5,0,0,0"
Text="{Binding Path=ItemCount}"/>
<TextBlock Text=" Item(s)"/>
</StackPanel>
</StackPanel>
</Expander.Header>
现在重新构建并运行应用程序。一旦窗口加载,点击复选框按部门名称分组记录。观察展开器中的项目数量,如图所示:

在 DataGrid 控件中过滤数据
当我们在 DataGrid 中显示大量记录时,用户通常很难在网格中搜索和找到特定的记录。在这种情况下,您可能希望提供额外的功能来过滤记录到特定的搜索词。
在本食谱中,我们将学习如何在 DataGrid 控件中添加一个搜索框以过滤记录。
准备工作
让我们在 Visual Studio IDE 中创建一个名为 CH04.DataGridFilterDemo 的 WPF 应用程序项目。
如何做...
现在执行以下步骤以添加附加到网格记录的搜索功能:
-
一旦创建项目,在项目中添加一个新的
Employee模型类并公开一些属性,就像我们在 在 DataGrid 控件中排序数据 食谱中分享的那样。 -
在
MainWindow.xaml.cs文件中创建相同的依赖属性(Employees,类型为ObservableCollection<Employee>)并在集合中填充一些数据记录。 -
现在打开
MainWindow.xaml文件并添加属性x:Name="window"以给Window命名,这样我们就可以执行元素到元素的数据绑定。 -
将默认的
Grid面板替换为StackPanel。 -
现在在根
StackPanel内插入以下水平的StackPanel,包含一个TextBlock和一个TextBox:
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="4 8">
<TextBlock Text="Filter records: "/>
<TextBox x:Name="searchBox" Width="100"
TextChanged="OnFilterChanged"/>
</StackPanel>
- 添加一个
DataGrid控件,其中包含四个列。将AutoGenerateColumns设置为False并将ItemsSource属性的数据绑定到Employees集合(ItemsSource="{Binding Employees, ElementName=window}")。以下是完整的代码供参考:
<DataGrid x:Name="dataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Employees,
ElementName=window}">
<DataGrid.Columns>
<DataGridTextColumn Header="EMP ID"
Binding="{Binding ID}"/>
<DataGridTextColumn Header="First Name"
Binding="{Binding FirstName}"/>
<DataGridTextColumn Header="Last Name"
Binding="{Binding LastName}"/>
<DataGridTextColumn Header="Department"
Binding="{Binding Department}"/>
</DataGrid.Columns>
</DataGrid>
- 现在导航到
MainWindow.xaml.cs文件,并添加以下代码块以实现OnFilterChanged事件,该事件在searchBox中任何文本更改时被触发:
private void OnFilterChanged(object sender,
TextChangedEventArgs e)
{
var cvs =
CollectionViewSource.GetDefaultView(dataGrid.ItemsSource);
if (cvs != null && cvs.CanFilter)
{
cvs.Filter = OnFilterApplied;
}
}
private bool OnFilterApplied(object obj)
{
if(obj is Employee emp)
{
var searchText = searchBox.Text.ToLower();
return
emp.Department.ToLower().Contains(searchText) ||
emp.FirstName.ToLower().Contains(searchText) ||
emp.LastName.ToLower().Contains(searchText);
}
return false;
}
- 让我们构建项目并运行应用程序。你将在屏幕上看到以下 UI:

- 现在通过在文本框中输入一些搜索词来过滤记录。让我们输入
Finance作为关键词并查看行为:

- 如果你更改搜索词以从记录中执行以下操作,它将仅过滤出这些记录。
它是如何工作的...
当你输入一个搜索词时,它会触发事件 OnFilterChanged 并检索 DataGrid 的默认视图。它暴露了一个名为 Filter 的属性,这是一个谓词。在我们的例子中,我们在 Filter 属性上分配了谓词 OnFilterApplied,当被调用时,它会将术语与 Department、FirstName、LastName 进行比较,如果找到匹配项则返回 true。基于 boolean 值,它显示相应的记录。
使用静态绑定
通常,我们在应用程序中使用静态属性。随着 WPF 4.5 的推出,Microsoft 提供了在执行数据绑定时使用 XAML 标记中的静态属性的选择。在本食谱中,我们将学习如何创建此类绑定。这些绑定在后续的食谱中使用 Converters、Styles 和 Templates 时非常有用。
准备工作
让我们从创建一个名为 CH04.StaticBindingDemo 的新项目开始。打开你的 Visual Studio IDE 并选择 WPF 应用程序项目作为项目模板。
如何做...
一旦创建了项目,请执行以下步骤来学习静态绑定:
- 打开
MainWindow.xaml页面,并在Grid面板内添加一个Label。给它一个背景颜色(比如说,OrangeRed),然后运行应用程序。这是我们最常用来在行内写入硬编码值的方式:
<Label Background="OrangeRed"
Content="Kunal Chowdhury"
FontSize="25"
Width="300" Height="60"
Padding="10" Margin="10"/>
- 现在,让我们将其更改为从系统定义的颜色设置背景颜色。为此,我们需要使用
{x:Static}标记扩展来访问静态属性。以下是代码将如何更改:
<Label Background="{x:Static
SystemColors.ControlDarkBrush}"
Content="Kunal Chowdhury"
FontSize="25"
Width="300" Height="60"
Padding="10" Margin="10"/>
- 你也可以访问在 XAML 页面内定义的本地资源,或者在集中定义的
ResourceDictionary中定义的资源。让我们在同一页面的Window下定义一个颜色:
<Window.Resources>
<SolidColorBrush Color="GreenYellow"
x:Key="myBrush"/>
</Window.Resources>
- 向标签添加一个
Foreground属性,以分配其前景颜色。让我们将其绑定到我们之前定义的静态资源(myBrush)。以下是代码供参考:
<Label Background="{x:Static
SystemColors.ControlDarkBrush}"
Foreground="{StaticResource myBrush}"
Content="Kunal Chowdhury"
FontSize="25"
Width="300" Height="60"
Padding="10" Margin="10"/>
- 现在,让我们构建并运行应用程序。你将看到类似于以下截图的颜色,其中背景将具有浅灰色(基于设置到系统
ControlDarkBrush的颜色),前景将具有黄绿色:

它是如何工作的...
标记扩展是一个从 System.Windows.Markup.MarkupExtension 派生的类,并实现了一个名为 ProvideValue 的单方法。在这个例子中,我们使用了由 System.Windows.Markup.StaticExtension 类实现的 {x:Static} 标记扩展,它允许你访问静态属性。
同样,{StaticResource} 用于访问在 XAML 中定义的资源(颜色、画刷、转换器等)。
使用值转换器
当你想在两个类型不兼容的属性之间进行数据绑定时,转换器非常有用。在这种情况下,你需要一段代码来在源和目标之间建立桥梁。这段代码被定义为 值转换器。
IValueConverter 接口用于创建值转换器,并包含两个名为 Convert 和 ConvertBack 的方法:
-
Convert(...):当源更新目标对象时被调用
-
ConvertBack(...):当目标对象更新源对象时被调用
在这个菜谱中,我们将学习如何创建值转换器并在数据绑定中使用它们。
准备工作
让我们从创建一个新的 WPF 项目开始。命名为 CH04.ConverterDemo。
如何做...
要开始值转换器,请执行以下步骤:
-
从解决方案资源管理器中打开
MainWindow.xaml文件。 -
将现有的
Grid替换为以下 XAML 标记,它包含一个CheckBox和一个Rectangle,它们位于StackPanel内:
<StackPanel Orientation="Horizontal"
VerticalAlignment="Top"
Margin="20">
<CheckBox x:Name="chkBox"
Content="Show/Hide Box"/>
<Rectangle Fill="Red" Margin="80 0 0 0"
Width="150" Height="50"
Visibility="{Binding IsChecked,
ElementName=chkBox}"/>
</StackPanel>
Rectangle的Visibility属性与CheckBox控件的IsChecked属性之间存在数据绑定。如果你构建并运行应用程序,当你改变复选框的选中状态时,你将看到 UI 中没有明显的可见变化:

-
由于
Visibility属性不接受boolean值,因此Rectangle默认情况下始终可见。现在我们将添加转换器到它,这将自动将值从bool转换为Visibility。 -
让我们在项目中创建一个新的类文件。将其命名为
BoolToVisibilityConverter。 -
打开
BoolToVisibilityConverter.cs文件,并添加以下命名空间——System、System.Globalization、System.Windows和System.Windows.Data作为using语句。 -
现在,将类标记为
public并实现IValueConverter接口。 -
在类内部添加以下两个代码块:
public object Convert(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
return value is bool val && val ? Visibility.Visible :
Visibility.Collapsed;
}
public object ConvertBack(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
throw new NotImplementedException();
}
-
现在,转到
MainWindow.xaml文件,添加以下 XMLNS 命名空间,以便我们可以将转换器声明为窗口资源: -
在
Window标签内,添加以下标记以声明我们创建的转换器:
<Window.Resources>
<converters:BoolToVisibilityConverter
x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
- 现在,在
Rectangle的Visibility属性的绑定语法中,将转换器关联为StaticResource,如下面的代码片段所示:
Visibility="{Binding IsChecked, ElementName=chkBox, Converter={StaticResource BoolToVisibilityConverter}}"
-
完成此操作后,构建项目并运行应用程序。
-
默认情况下,复选框将处于未选中状态,矩形将不会在屏幕上可见。将复选框的状态更改为选中,观察矩形将变为可见。再次取消选中复选框将再次隐藏矩形:

它是如何工作的...
使用值转换器(IValueConverter接口)将一个值转换为另一个值。这些值可以是相同类型或不同类型,但需要一些无法声明式实现的转换。由于它们是编写在代码中的,因此通常非常强大,因为它们具有更多的逻辑来控制功能。
转换器的实例通常在 XAML 页面上创建,并声明为资源。然后通过使用带有Converter属性的绑定表达式将其设置到控件上。
每当源属性发生变化时,转换器通过Convert方法返回不同的结果。在双向绑定模式下,会调用ConvertBack方法,其中源和目标被反转。在单向绑定中,不需要实现ConvertBack,通常我们将它的主体设置为返回一个异常,如下所示——throw new NotImplementedException();。
还有更多...
您可以通过使用转换器参数来扩展转换器的功能。让我们修改Convert方法,以利用名为parameter的参数并根据其值反转可见性。
要这样做,请打开BoolToVisibilityConverter.cs并修改类实现,如下面的代码片段所示:
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
var val = (bool) value;
if (parameter is string param &&
param.ToString().Equals("inverse")) { val = !val; }
return val ? Visibility.Visible: Visibility.Collapsed;
}
public object ConvertBack(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
throw new NotImplementedException();
}
}
现在,打开MainWindow.xaml文件,并修改Rectangle的Visibility属性的绑定,使其具有ConverterParameter=inverse,如下所示:
<Rectangle Fill="Red" Margin="80 0 0 0"
Width="150" Height="50"
Visibility="{Binding IsChecked, ElementName=chkBox,
Converter={StaticResource BoolToVisibilityConverter},
ConverterParameter=inverse}"/>
让我们构建并运行应用程序。您将看到,这次,当复选框未选中时,矩形将默认可见。现在将复选框的状态更改为选中,您将看到矩形在屏幕上变得可见:

当然,您可以根据业务需求更改实现和 ConverterParameter 的值,并使用相同的转换器类在不同的条件下返回不同的值。
您还可以使用 .NET Framework 提供的 BooleanToVisibilityConverter。您可以在此处了解更多关于此转换器的信息:msdn.microsoft.com/en-us/library/system.windows.controls.booleantovisibilityconverter(v=vs.110).aspx。
使用多值转换器
当您想根据相同或不同类型的多值更改目标值时,您需要使用多绑定。这是通过使用多值转换器(IMultiValueConverter 接口)来完成的。
在这个菜谱中,我们将构建一个示例演示,学习如何使用多绑定和多值转换器。
准备工作
打开您的 Visual Studio IDE 并创建一个名为 CH04.MultiValueConverterDemo 的新项目,基于 WPF 应用程序项目模板。
如何做到这一点...
一旦创建项目,请按照以下步骤设计 UI 并在多个元素之间进行多绑定:
-
从解决方案资源管理器中打开
MainWindow.xaml页面。 -
在默认的
Grid面板内,创建一些行和列,以便我们可以将元素定位在特定的单元格中。让我们将Grid分为五行和三列:
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="90"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
- 在
Grid面板内,插入以下 XAML 代码片段,在窗口内添加一些标签和输入框:
<TextBlock Text="Firstname:"
Grid.Column="0" Margin="2 0"/>
<TextBlock Text="Middle:"
Grid.Column="1" Margin="2 0"/>
<TextBlock Text="Lastname:"
Grid.Column="2" Margin="2 0"/>
<TextBlock Text="Fullname:"
Grid.Row="2" Grid.ColumnSpan="3"
Margin="2 0"/>
<TextBox x:Name="firstName"
Grid.Row="1" Grid.Column="0"
Margin="2 0"/>
<TextBox x:Name="middleName"
Grid.Row="1" Grid.Column="1"
Margin="2 0"/>
<TextBox x:Name="lastName"
Grid.Row="1" Grid.Column="2"
Margin="2 0"/>
<TextBox x:Name="fullName"
Grid.Row="3" Grid.ColumnSpan="3"
Margin="2 0">
</TextBox>
- 构建项目并运行应用程序。您将在屏幕上看到四个输入框及其相关的标签,如下所示:

-
让我们关闭应用程序并返回到解决方案资源管理器。在项目中创建一个名为
FullNameConverter的新类。 -
打开
FullNameConverter.cs文件并在其上实现IMultiValueConverter。 -
在类文件中定义以下
using命名空间—System、System.Globalization和System.Windows.Data。 -
现在在类内部添加以下两个方法,这些方法实现了
IMultiValueConverter接口中定义的方法:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return string.Format("{0} {1} {2}", values[0], values[1],
values[2]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return value.ToString().Split(' ');
}
-
现在导航到
MainWindow.xaml页面,并添加以下 XMLNS 命名空间,以便转换器可以从 XAML 标记中访问: -
现在将转换器添加到窗口资源中。为此,在
Window标签内,添加以下标记以通过键名定义实例:
<Window.Resources>
<converters:FullNameConverter
x:Key="FullNameConverter"/>
</Window.Resources>
- 现在,在
fullName文本框的Text属性中,定义多绑定以将属性绑定到三个TextBox控件的Text属性。以下是代码:
<TextBox x:Name="fullName"
Grid.Row="3"
Grid.ColumnSpan="3"
Margin="2 0">
<TextBox.Text>
<MultiBinding Converter="{StaticResource
FullNameConverter}">
<Binding ElementName="firstName"
Path="Text" Mode="TwoWay"/>
<Binding ElementName="middleName"
Path="Text" Mode="TwoWay"/>
<Binding ElementName="lastName"
Path="Text" Mode="TwoWay"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
- 一旦完成绑定,构建项目并运行应用程序。你将在屏幕上看到相同的用户界面。在
FirstName、Middle和Lastname字段中输入一些字符串。观察Fullname字段中的值:

- 同样,将
Fullname字段更改为包含三个字符串。完成操作后按一下TAB键,并观察其他三个字段——FirstName、Middle和Lastname的值。
它是如何工作的...
当你在MultiBinding中使用类型为IMultiValueConverter的转换器时,它将Binding标签定义的值作为一个对象数组传递给Convert方法。在我们前面的例子中,我们将三个字符串值(firstName、middleName和lastName)传递给Convert方法。该方法然后将这些字符串连接起来形成一个单一的字符串,这就是Fullname字段的输出字符串,因为绑定是通过其Text属性进行的。
同样,当我们更改Fullname字段的值时,绑定转换器触发的ConvertBack方法返回了分割的字符串。根据绑定顺序,这些值自动分配到相应的字段——FirstName、Middle和Lastname。
使用自定义控件和用户控件
在本章中,我们将涵盖以下食谱:
-
创建自定义控件
-
自定义自定义控件的模板
-
从自定义控件公开属性
-
从自定义控件公开事件
-
使用行为扩展控件的功能
-
创建用户控件界面
-
从用户控件公开事件
-
自定义 XMLNS 命名空间
第六章:简介
自定义控件是一个松散耦合的控件,它在从 System.Windows.Controls.Control 类派生的类中定义。根据你的需求,你也可以从不同的自定义控件派生它。

自定义控件的 UI 通常定义在 resource 文件中的 资源字典 内。我们可以为自定义控件创建主题,并在各种项目中非常容易地重用它们:
通常,自定义控件被编译成一个 dll 集合,可以非常容易地在多个地方重用。你对它的代码有完全的控制权,因此它为你提供了更多的灵活性来扩展行为。一旦你在项目中构建并添加了对自定义控件的引用,你就可以在 Visual Studio 控件工具箱中找到它,这将允许你将控件拖放到 XAML 设计视图中并开始使用它。
在另一方面,用户控件实际上是你为了控制项目特定的 UI 而派生的自定义控件。它从 System.Windows.Controls.UserControls 类派生,这个类基本上是从 System.Windows.Controls.Control 继承而来的:

通常,用户控件被放置在 XAML 页面中,与它的代码后部分紧密绑定。你可以直接从代码后部访问它的 UI 元素并执行一些特定操作。
一个需要注意的点是你不能为用户控件创建主题支持,但你可以通过为其子控件、自定义控件创建主题来对其样式化。此外,一旦你在某个项目中创建了一个用户控件 UI,你无法在其他项目中更改它。
在本章中,我们将学习如何创建自定义控件和用户控件,然后根据需要对其进行自定义。
创建自定义控件
在使用自定义控件之前,你需要知道如何创建自定义控件以及如何将它们添加到任何 XAML 页面。在本教程中,我们将首先学习这些基本操作。
准备工作
让我们打开 Visual Studio IDE 并创建一个新的 WPF 应用程序项目,命名为 CH05.SearchControlDemo。
如何做到这一点...
执行以下步骤以创建你的第一个自定义控件,它将包含一个文本输入框和一个按钮来构建搜索控件。最后,我们将将其添加到应用程序窗口中:
-
一旦项目创建完成,在项目上右键单击,从解决方案资源管理器中,然后从上下文菜单中选择“添加 | 新项...”。屏幕上将会弹出一个新的对话框窗口。
-
Inside the Add New Item dialog window, expand the Installed | Visual C# | WPF tree item, from the left navigation panel, and select Custom Control (WPF) from the right screen:

-
给自定义控件起一个名字(比如说,
SearchControl.cs)并点击添加以创建它。这将创建一个名为SearchControl.cs的类文件在项目内部,以及一个包含Generic.xaml文件的文件夹(命名为Themes)。 -
打开
Generic.xaml文件,其中将包含为我们创建的自定义控件的Style。这由 Visual Studio IDE 在从默认模板创建自定义控件时自动生成。以下是默认的Style:
<Style TargetType="{x:Type local:SearchControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:
SearchControl}">
<Border Background="{TemplateBinding
Background}"
BorderBrush="{TemplateBinding
BorderBrush}"
BorderThickness="{TemplateBinding
BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
- 现在将控件的先前
Style替换为以下一个,它包含一个输入框和一个按钮,作为Grid内部的控件模板:
<Style TargetType="{x:Type local:SearchControl}">
<Setter Property="Height" Value="26"/>
<Setter Property="Width" Value="150"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
local:SearchControl}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="PART_TextBox"
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<Button x:Name="PART_Button"
Content="Search"
Grid.Column="1"
Margin="2" Padding="8 2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
- 现在打开
MainWindow.xaml页面,并添加以下 XMLNS 命名空间:
7. Now, inside the default `Grid` panel, add the custom control that we just created, and optionally set its `Height` and `Width` properties:
<controls:SearchControl Height="30"
Width="180"/>
8. That's all! Our first custom control has been created and added to the `MainWindow` of the application. Let's build and run the application. You will see the following UI on the screen:

We have just added the UI of our custom control here and hence no functionality related to search will work. We will enhance the functionalities in the next recipes.
How it works...
When you first create a custom control in a project, Visual Studio creates a folder named `Themes`, and places a file named `Generic.xaml`. This file contains all the styles and templates of the custom controls, by default. When you add more custom controls inside the same project, the `Generic.xaml` file gets updated with the styles of the new controls.
The property called `TargetType` defines the type of the control for which we are going to create the style. In the preceding example, `<Style TargetType="{x:Type local:SearchControl}">` defines the style of the custom control called `SearchControl`. To change the UI of the control, we need to update the same style.
The `<ControlTemplate TargetType="{x:Type local:SearchControl}">` defines the template of the control, which generally resides inside the `Style`.
The `Setter` properties inside the `Style` define the default value of various properties of the said control. In the preceding example, we have defined the default value of the `Height` and `Width` properties. You can add additional property values.
There's more...
Before going further with the custom controls, you need to learn and understand some other points related to them. Let's discuss them in the following sections.
XMLNS attribute declaration
When the custom control is present within the same project where you are going to use it, you need to add the XMLNS attribute in the following way:
This is the same way we added it in the preceding example. The `clr-namespace` defines the namespace where the controls are available. A single namespace can have one or more controls.
When the custom control is present in a different project to the one where you are going to add it, you need to add the XMLNS attribute in the following way:
Here, the `clr-namespace` defines the namespace of the controls, whereas the `assembly` defines the fully qualified name of the assembly where the control is present.
Default styling
When you create a custom control, all the default properties of its base class, `Control`, gets assigned to it. You can use `TemplateBinding` to bind the data to a specific control. For example, to change the background color of the input box based on the `Background` property set on the control level, you need to create the template binding in the following way:
<TextBox x:Name="PART_TextBox"
Grid.Column="0"
Margin="2"
Background="{TemplateBinding Background}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
Now, when you change the color of the control, it will change the color of the said input box. Set a color to the `Background` property of our search control inside the `MainWindow.xaml` and observe the change.
Toolbox integration
When you create a custom control within the same project and/or reference a `dll` containing any custom control, you will be able to utilize the Visual Studio Toolbox to drag and drop the control directly to the XAML/designer view.
After creating the control or adding the control library in a project, you need to build it first. Now, open any XAML page and navigate to the Visual Studio Toolbox. You will be able to find the control, as demonstrated here:

Now you can drag it to the place where you want to add the said control.
Customizing the template of a custom control
The development of a custom control always requires its default template to be changed to give it a proper look and make it ready for theming support. That starts with the customization of the template and its default values.
In this recipe, we will learn how to change the template and use `TemplateBinding` to create a relation with its property values.
Getting ready
To get started, launch Visual Studio, create a project, and add a new custom control in it. For this demonstration, we will be using the existing project, `CH05.SearchControlDemo`, that we created in the previous recipe. So, let's open the project.
How to do it...
As we want to customize the template of the custom control to have a proper template binding, perform the following steps:
1. Open the `Generic.xaml` file, which is present under the `Themes` folder of the project.
2. Now, scroll down to the definition of the `ControlTemplate` as we need to customize the look and feel of it.
3. Search for the `TextBox` control named `PART_TextBox`, and set its `Background`, `BorderBrush`, `BorderThickness`, and `Foreground` properties to have a binding with the control's default properties.
4. Similarly, set the `Background` and `Foreground` property of the button (`PART_Button`) to the same properties of the control, by using template binding. Here's the complete code of the modified control template:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="PART_TextBox"
Grid.Column="0"
Margin="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding
BorderBrush}"
BorderThickness="{TemplateBinding
BorderThickness}"
Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<Button x:Name="PART_Button"
Content="搜索"
Grid.Column="1"
Margin="2" Padding="4 2"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
</Grid>
5. You can also assign default values to your control templates, by using the `<Setter/>` tag. You will need to add it inside the `<Style/>` definition. To add default values to the `Height`, `Width`, `Background`, `BorderBrush`, `BorderThickness`, and `Foreground` properties, add the following code block inside the `Style` tag:
6. Once done, compile your project and run it. You will see the following screen, where the background of the `TextBox` and `Button` controls are painted with `AliceBlue` color. Similarly, the other styles are applied as per the default values specified:

7. You can override the default style values in your application, where you are using the control. To do this, open the `MainWindow.xaml` file and add a custom `Background` color, `BorderBrush`, `Foreground`, and `BorderThickness` to the control as follows:
<controls:SearchControl
Background="#2200FF00"
BorderThickness="2"
BorderBrush="黄绿色"
Foreground="绿色"/>
8. Now, if you build and run the application, you will notice the UI changed as per the custom value that you specified directly to the control:

How it works...
`TemplateBinding` is a type of binding used mainly while working with templates. This allows you to replace the visual tree of controls for a completely fresh look and feel, based on the theme or style that you want to use. It also helps you to reference the parent control, read its properties, and apply its values.
When you apply a template binding to a control, present in the `ControlTemplate` of the parent control, it first checks whether the property is present to the parent control. If it is not present, it throws an XAML syntax error.
If it finds the property, it checks whether the value is supplied from the place where the custom control has been used. If it finds no reference, it applies the default value to the property.
Exposing properties from the custom control
Most of the time, while using custom controls, we need to expose additional properties based on the requirement. In this recipe, we will demonstrate exposing dependency properties from the custom control and binding the record to the UI.
Getting ready
Let's extend our previous project to perform these steps. To get started, launch Visual Studio and open the project `CH05.SearchControlDemo`.
How to do it...
Once the project has been opened, perform the following steps to create a dependency property named `SearchTerm` and bind it with the control UI:
1. Let's open the `SearchControl.cs` to create a dependency property. Inside the class definition, type `propdp` and press the *TAB* key twice to create the property structure. By default, it generates `MyProperty` of type `int`.
2. Change the property type from `int` to `string` and press *TAB*.
3. Rename `MyProperty` to `SearchTerm` and press *TAB* again.
4. Now change `ownerclass` to `SearchControl` and press *TAB*.
5. Pass `string.Empty` as the default value to the `PropertyMetaData`.
6. Once these preceding steps are done, your property is ready to use. Now open the `Generic.xaml` page to create the binding to the UI control.
7. Inside the template of the control, find the `TextBox` named `PART_TextBox`.
8. Now, add the `Text` property to it, by using `TemplateBinding`. You will see the dependency property (`SearchTerm`) listed in the XAML IntelliSense, as shown here:

9. Let's complete the template binding as follows:
<TextBox x:Name="PART_TextBox"
Grid.Column="0"
Margin="2"
Text="{TemplateBinding SearchTerm}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding
BorderThickness}"
Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
10. Now navigate to the `MainWindow.xaml` page and add the `SearchTerm` property to the control that we have already added:

11. Set some sample strings to it:
<controls:SearchControl
SearchTerm="我的搜索词"
Background="#2200FF00"
BorderThickness="2"
BorderBrush="黄绿色"
Foreground="绿色"/>
12. Build the project and run it. You will see that the string that you assigned to the `SearchTerm` property of the control, actually assigned the value to the search `TextBox`:

How it works...
Template binding works only with the dependency properties. When you assign a value to the dependency property, it automatically updates the child control where you have created the binding. In our example, when you assign a value to the `SearchTerm` property, it sets the value to the textbox (`PART_TextBox`) control's `Text` property and thus you can see the value provided to it.
Exposing events from a custom control
When you build any custom control, you need to expose additional events, based on the child controls and functionality that you want to expose to the user. In this recipe, we will learn how to expose a custom event from a custom control and perform a specific operation using it.
Getting ready
Let's start with the existing project that we have already used in the previous recipes. Launch the IDE and open the `CH05.SearchControlDemo` project inside Visual Studio.
How to do it...
In this recipe, we will create a public event from the `SearchControl`, so that we can subscribe to the `PART_Button` button event and fetch the user-entered text. To do so, follow perform the following steps:
1. From Solution Explorer, create a new class named `SearchEventArgs`, inside the project.
2. Extend the `SearchEventArgs` class from the `EventArgs` and expose a public property (`SearchTerm`) of type `string`. Here's the class implementation:
public class SearchEventArgs : EventArgs
{
public string SearchTerm { get; set; }
}
3. Now open the `SearchControl.cs` file. We need to create a delegate and event inside it. Let's add the following inside the class implementation:
public delegate void OnSearchClick(object sender,
SearchEventArgs e);
public event OnSearchClick SearchButtonClick;
4. The next task is to associate the button click event with the custom event that we have just created. Pass the `SearchTerm` to the custom event as an argument. To do this, copy the following code inside the `SearchControl` class:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (GetTemplateChild("PART_Button") is Button
searchButton)
{
searchButton.Click +=
OnSearchButtonClicked_Internal;
}
}
private void OnSearchButtonClicked_Internal(object sender,
RoutedEventArgs e)
{
SearchButtonClick?.Invoke(this, new SearchEventArgs {
SearchTerm = SearchTerm });
}
5. Open the `Generic.xaml` page and perform a slight change to the `Text` property binding of the search `TextBox`. Instead of template binding, let's perform a normal data binding, passing a relative source to it. As we need to take input from the user, we will set the binding mode to `TwoWay`. Here's the XAML code:
<TextBox x:Name="PART_TextBox"
Grid.Column="0"
Margin="2"
Text="{Binding SearchTerm, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
}
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderBrush}"
BorderThickness}"
Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
6. Once this is done, open the `MainWindow.xaml` and register the `SearchButtonClick` event of the `SearchControl`:
<controls:SearchControl
SearchTerm="my search term"
Background="#2200FF00"
BorderThickness="2"
BorderBrush="GreenYellow"
Foreground="Green"
SearchButtonClick="OnSearchButtonClicked"/>
7. Open the code behind `MainWindow.xaml.cs` and modify the event implementation to show a message box to the user, with the text that we passed as a search term. You can find it as `e.SearchTerm`, as passed to the event argument. Here's the code for your reference:
private void OnSearchButtonClicked(object sender,
SearchEventArgs e)
{
MessageBox.Show("您搜索了: " +
e.SearchTerm + """);
}
8. That's all! Let's build the application and run it. As we already have a default value set to the control, click on the Search button. You will see a message box with the default search term. Now, change the value to have a different search term. To do so, click on the `TextBox` control and replace the string. Now, click on the Search button once again, which will show the new search term inside the message box. Here's a screenshot of the same operation:

How it works...
When the application loads with the control on the UI, the first thing that it does is to load its defined template and call the `OnApplyTemplate()` method. `OnApplyTemplate()` is a virtual method present inside the `System.Windows.FrameworkElement` class, which gets invoked when application code or internal processes call the `System.Windows.FrameworkElement.ApplyTemplate()`.
As you can see in the `OnApplyTemplate()` method implementation, it finds out the template child named `PART_Button` using the `GetTemplateChild` method call, and registers its associated `Click` event:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (GetTemplateChild("PART_Button") is Button searchButton)
{
searchButton.Click += OnSearchButtonClicked_Internal;
}
}
The `Click` event then invokes the custom event (`SearchButtonClick`), passing the `SearchTerm` as `SearchEventArgs`. Now, when you click on the button in the application UI, it fires the `OnSearchButtonClicked_Internal` event and bubbles up to the application UI. The `OnSearchButtonClicked` event handler then triggers due to its subscription to the custom event and performs the operation. In our case, it shows a message to the user with the search term passed to the search box.
Extending the functionality of a control using behavior
**Behavior** is a concept to extend the functionality of a control using a reusable component. These components can be attached to any control or a specific type of control to provide designers with the flexibility to design complex user interactions without writing any additional code.
In this recipe, we will learn how to create a behavior and the way to apply it to a control without writing extra code in the code-behind file.
Getting ready
To get started, we need to open the Visual Studio IDE. Create a new project called `CH05.ControlBehaviorDemo`, based on the WPF application template.
How to do it...
Let's start by creating a component which will add a Size Grow effect to a `TextBlock` control when hovering over with the mouse cursor. To do this, perform the following steps:
1. To create and/or use behaviors in an application, you will need to set up the project to have a reference to the `System.Windows.Interactivity.dll` assembly file. To do this, right-click on the project and click Add | Reference... from the context menu.
2. From the Reference Manager dialog, search for `interactivity` to find the System.Windows.Interactivity assembly in the list of assemblies. Select the latest version, as shown in the following screenshot, and click OK. Make sure you verify the added reference in the project:

3. Now open the `MainWindow.xaml` page and add a `TextBlock` control inside the default `Grid`. Assign a string to its `Text` property:
<TextBlock Text="悬停以增大大小!"
HorizontalAlignment="Center"
VerticalAlignment="Center">
</TextBlock>
4. If you run the application now, it will have a text in the window. Hovering your mouse on top of that will not have any effect. For that, we need to create the behavior and register it with the `TextBlock` control.
5. Let's create a new class, called `GrowTextBehavior`, inside the project.
6. Mark the class as `public` and extend it from the `Behavior` class. As we are going to create this component for `TextBlock` control, we will extend the class from `Behavior<TextBlock>`, as shown here:
public class GrowTextBehavior : Behavior
7. You will need to add the `System.Windows.Interactivity` namespace as a `using` statement to resolve the class declaration. Alternatively, you can resolve the namespace by clicking the light bulb and selecting using System.Windows.Interactivity;, as shown in the following screenshot:

8. Add a `public` property inside the class to take dynamic input of the size to grow by. Give it a name:
public int GrowBySize { get; set; }
9. Now, inside the class, type `override` and enter a space. From the list of overridable methods, select `OnAttached` and hit the *Enter* key. This will override the `OnAttached()` method inside the class.
10. Similarly, override the method `OnDetaching()` inside the class.
11. Inside `OnAttached()`, register the `MouseEnter` and `MouseLeave` events for the associated object, which is a `TextBlock` in our case. Similarly, inside the `OnDetaching()`, unregister the preceding two events. Here's the code that you may like to take as reference:
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.MouseEnter +=
AssociatedObject_MouseEnter;
AssociatedObject.MouseLeave +=
AssociatedObject_MouseLeave;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.MouseEnter -=
AssociatedObject_MouseEnter;
AssociatedObject.MouseLeave -=
AssociatedObject_MouseLeave;
}
12. Now it's time to write our logic to grow and shrink the size of the associated `TextBlock` control on mouse over and mouse leave events, respectively. To do so, add the following code block inside the class:
private void AssociatedObject_MouseLeave(object sender,
MouseEventArgs e)
{
AssociatedObject.FontSize -= GrowBySize;
}
private void AssociatedObject_MouseEnter(object sender,
MouseEventArgs e)
{
AssociatedObject.FontSize += GrowBySize;
}
13. That ends the implementation of the behavior component for our `TextBlock` control. Now it's time to register it with the control in the UI. To do so, open the `MainWindow.xaml` again and add the following XMLNS namespace declaration:
14. Now modify the `TextBlock` control to register the association with the behavior component that we created. Replace the existing markup with the following:
<TextBlock Text="悬停以增大大小!"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<i:Interaction.Behaviors>
<b:GrowTextBehavior GrowBySize="10"/>
</i:Interaction.Behaviors>
</TextBlock>
15. Let's build the application and run it. You will see a text message in the application window. Hover over it to see the growing effect on its size:

16. Take your mouse away from the text to see how it moves back to the original state.
How it works...
The property, `AssociatedObject`, returns the object to which the `System.Windows.Interactivity.Behavior` is attached. In our case, it's the `TextBlock` control passed as `Behavior` of type `T` (`Behavior<TextBlock>`), which is associated in the XAML code block, as mentioned here:
<TextBlock Text="悬停以增大大小!"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<i:Interaction.Behaviors>
<b:GrowTextBehavior GrowBySize="10"/>
</i:Interaction.Behaviors>
When the association happens between the control and the component, it registers the two events (`MouseEnter` and `MouseLeave`) in our example. Now, when you hover over the mouse on top of the `TextBlock`, it gets the dynamic association of the events and triggers them. This way, it gets notification of the event and performs based on the logic specified.
You can now associate this behavior to any number of controls of type `TextBlock`, without writing additional code behind. The XAML designers can easily pick and associate it with the controls that they want to provide a grow effect on mouse hover.
If you want to associate the behavior to any control, you need to extend it from `Behavior`, instead of `Behavior<TextBlock>`. Similarly, to change the type to any other specific control (let's say, `Label`), change the `T` to `Label` as shown here—`Behavior<Label>`.
Creating a User Control interface
Typically, a User Control is a group of elements and controls joined together to create a reusable component. This is often used to show the same UI in multiple places, either on the same window or in a different window.
In this recipe, we will learn how to create a User Control interface with all its typical features.
Getting ready
Get started by creating a new project. Open the Visual Studio IDE and create a new project based on the WPF application template. Name it `CH05.UserControlDemo`.
How to do it...
To demonstrate the complete use of User Control, we will be creating a color mixer control, exposing some properties from it and binding data using converters. Perform the following simple steps:
1. Once the project has been created, add a new User Control element inside the project. To do this, right-click on the project and select Add | User Control... from the context menu entry.
2. From the Add New Item dialog, select User Control (WPF) as the template to create a blank User Control. Name the control `ColorMixer`. Click on the Add button to create a User Control file called `ColorMixer.xaml`:

3. Once the User Control has been created, open the code-behind file (`ColorMixer.xaml.cs`) and add a property `SelectedColor` of type `Color` inside it. Give it a default color (let's say, `Colors.OrangeRed`):
public Color SelectedColor
{
get { return (Color)GetValue(SelectedColorProperty); }
set { SetValue(SelectedColorProperty, value); }
}
public static readonly DependencyProperty
SelectedColorProperty =
DependencyProperty.Register("SelectedColor",
typeof(Color), typeof(ColorMixer),
new PropertyMetadata(Colors.OrangeRed));
4. Let's open the `ColorMixer.xaml` file to provide a UI to the control. We will be adding four `TextBox` controls to assign the color in RGB mode (Red, Green, Blue, and Alpha) and a `Border` to show the output from the RGB mixer.
5. First, give the User Control a name, so that we can easily set its `DataContext` to access its code-behind properties. To do this, add the attribute `x:Name="userControl"` to the `UserControl` tag.
6. Set the `DataContext` of the `Grid` to have an element binding. Add the following attribute inside the `Grid` tag:
DataContext="{Binding ElementName=userControl}"
7. Now let's divide the default `Grid` panel into a few rows and columns. Copy the following row and column definitions inside the `Grid` tag to create the structure:
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
8. Now it's time to add the controls inside the `Grid` to create the UI layout of our User Control. Add a `Border` at the first cell of the `Grid` and bind its `Background` property with the `SelectedColor` property of the User Control that we have created:
<Border BorderThickness="1" BorderBrush="Gray"
Grid.Row="0" Grid.Column="0">
<Border.Background>
<SolidColorBrush Color="{Binding SelectedColor}"/>
</Border.Background>
9. Now add a `StackPanel` inside the second cell of the `Grid`, which is `Row=0`, `Column=1`. Add four `TextBox` controls and their associated labels inside the panel. Here's the XAML block, which you can copy and place just after the `Border` control:
<StackPanel Orientation="Vertical"
Grid.Row="0" Grid.Column="1"
Margin="8 4">
<TextBlock Text="R:"/>
<TextBox Width="100" />
<TextBlock Text="G:"/>
<TextBox Width="100" />
<TextBlock Text="B:"/>
<TextBox Width="100" />
<TextBlock Text="A:"/>
<TextBox Width="100" />
10. As the basic UI design is ready, let's place the User Control inside the application window. Open `MainWindow.xaml` and add the following XMLNS attribute to the `Window` tag:
11. Now replace the default `Grid` panel with a `StackPanel`, so that we can place multiple controls in a stack.
12. Place `<local:ColorMixer />` inside the `StackPanel` and run the application. You will see the following UI on the screen:

13. The main advantage of a User Control is its ease of use. Creating many instances of it is easy, and there is full design-time support in Visual Studio. Similar to the preceding point, if you place multiple controls of the `ColorMixer` instance inside the `StackPanel`, you will see multiple copies in the UI. Let's not do it, but if you want to try, replace the entire `StackPanel` with the following code block and check out how it places the controls:
<StackPanel Orientation="Horizontal"
Margin="4">
<local:ColorMixer />
<local:ColorMixer />
14. Now let's add the bindings to the `TextBox` controls with the `SelectedColor` property. As the type of the property is `Color`, we will need to create a value converter. So, right-click on the project and add a class by following the context menu path Add | Class..., name it `ColorToByteConverter`, and hit OK.
15. As we need to access the class from the XAML, we will need to mark it as `public`.
16. Now inherit the class from `IValueConverter`, to make it a value converter. Click on the lightbulb icon, as shown in the following screenshot, and resolve the namespace. Alternatively, you can add the `using` statement to resolve the `System.Windows.Data` namespace:

17. Now click on the light bulb again and implement the interface. It will add two methods, called `Convert` and `ConvertBack`, inside the class, as shown in the following screenshot:

18. Replace the `Convert` method with the following code block, which will break the specified color into an RGBA byte value:
public object Convert(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
if (value is Color color &&
parameter is string parameterValue) //C# 7.x syntax
{
oldColor = color;
switch (parameterValue)
{
case "r":
return color.R;
case "g":
return color.G;
case "b":
return color.B;
default:
return color.A;
}
}
return value;
}
19. Similarly, replace the `ConvertBack` method to construct the `Color` object from the RGBA values, which you can enter by the `TextBox.Text` property:
public object ConvertBack(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
var color = oldColor;
var colorValue = System.Convert.ToByte(value);
if (parameter is string parameterValue) //C# 7.x syntax
{
switch (parameterValue)
{
case "r":
color.R = (byte)colorValue;
break;
case "g":
color.G = (byte)colorValue;
break;
case "b":
color.B = (byte)colorValue;
break;
default:
color.A = (byte)colorValue;
break;
}
}
oldColor = color;
return color;
}
20. Once this is done, open the `ColorMixer.xaml` file and add the following XMLNS attribute under the `UserControl` tag:
21. Now create a `<UserControl.Resources>` tag inside the `UserControl` element and register the value converter as a resource. Here's the code that you can copy inside the `UserControl` tag:
<UserControl.Resources>
<local:ColorToByteConverter
x:Key="ColorToByteConverter"/>
</UserControl.Resources>
22. Let's modify the `Text` property of all the four `TextBox` controls to have a `TwoWay` data binding with the `SelectedColor` property, and associate them with the converter that we have added. Pass the proper parameter to the converter, as `ConverterParameter`, as mentioned in the following code. You can copy the following code and replace the existing `StackPanel`:
<StackPanel Orientation="Vertical"
Grid.Row="0" Grid.Column="1"
Margin="8 4">
<TextBlock Text="R:"/>
<TextBox Width="100"
Text="{Binding SelectedColor, Converter={StaticResource ColorToByteConverter}, ConverterParameter=r, Mode=TwoWay}"/>
<TextBlock Text="G:"/>
<TextBox Width="100"
Text="{Binding SelectedColor, Converter={StaticResource ColorToByteConverter}, ConverterParameter=g, Mode=TwoWay}"/>
<TextBlock Text="B:"/>
<TextBox Width="100"
Text="{Binding SelectedColor, Converter={StaticResource ColorToByteConverter}, ConverterParameter=b, Mode=TwoWay}"/>
<TextBlock Text="A:"/>
<TextBox Width="100"
Text="{Binding SelectedColor, Converter={StaticResource ColorToByteConverter}, ConverterParameter=a, Mode=TwoWay}"/>
23. At the end, build the project and run the application. You will see the following UI on the screen, where the rectangular `Border` control has an `OrangeRed` background and the associated `TextBox` control has the RGBA byte value of the color:

24. Now modify the values of the `TextBox` controls to have a range (`0`–`255`) between `0` to `255` and press *TAB* to reflect the change in the UI. Let's replace the values, Red by `120`, Green by `75`, Blue by `200`, and Alpha by `77`, as shown in the following screenshot, which will result in a light violet background color for the `Border` control:

How it works...
A User Control wraps the UI with appropriate properties and events to make it a reusable component. In this User Control, named `ColorMixer`, we created a dependency property called `SelectedColor` of type `Color`. The user (the developer or the designer) can also set a default value to it, by accessing the property, while adding it to the application UI.
The `Text` property of the `TextBox` controls, inside the `ColorMixer.xaml`, is bound with the `SelectedColor` property. As the types of `Text` and `SelectedColor` properties are different, we required the value converter here.
`ConverterParameter` is used to define whether we need to break the R, G, B, or A value of the color composition. The `Convert` method of the converter class breaks the color according to the parameter and returns, which gets displayed in the appropriate `TextBox` control:
switch (parameterValue)
{
case "r":
return color.R;
case "g":
return color.G;
case "b":
return color.B;
default:
return color.A;
}
Similarly, when you modify the value in the `TextBox`, due to its `TwoWay` binding mode, the `ConvertBack` method of the converter triggers. This constructs the color object based on the values available in all the `TextBox` controls and returns, which gets filled in the `SelectedColor` property and reflects in the `Background` property of the `Border` control.
Exposing events from a User Control
In the previous recipe, we learned about User Control, how to create it, and how to expose a dependency property and utilize it. In this recipe, we will learn how to expose events from a User Control, as you will need it in most cases.
Getting ready
Let's open the same project, `CH05.UserControlDemo`, inside the Visual Studio to proceed with this recipe.
How to do it...
To demonstrate the usage of the event, we will add two buttons inside our `ColorMixer` User Control and expose the `OK` and `Cancel` button events from it. To implement the same, perform the following steps:
1. Open the `ColorMixer.xaml` file and add the following `StackPanel` inside the `Grid`, which will place it at row index `1` and column index `0`. The panel consists of two buttons with labels `OK` and `Cancel:`
<StackPanel Orientation="Horizontal"
Grid.Row="1" Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4 10 4 4"
HorizontalAlignment="Right">
<Button Content="OK" Margin="4"
Width="50" Click="OnOkClicked"/>
<Button Content="Cancel" Margin="4"
Width="50" Click="OnCancelClicked"/>
2. Open the `ColorMixer.xaml.cs` file and register the button click events (`OnOkClicked` and `OnCancelClicked`) inside it.
3. Inside the `ColorMixer` class, register the following two delegates and events to handle the OK and Cancel button events from outside the control:
public delegate void OnOkButtonClick(object sender,
EventArgs e);
public delegate void OnCancelButtonClick(object sender,
EventArgs e);
public event OnOkButtonClick OkButtonClick;
public event OnCancelButtonClick CancelButtonClick;
4. Now update the `OK` button and the `Cancel` button event handlers to route the event to the place where the control has been used. Here's the code to replace the button click event implementations:
private void OnOkClicked(object sender, RoutedEventArgs e)
{
OkButtonClick?.Invoke(sender, e);
}
private void OnCancelClicked(object sender, RoutedEventArgs e)
{
CancelButtonClick?.Invoke(sender, e);
}
5. To register the associated events, in the application window, open the `MainWindow.xaml` file and register the `OkButtonClick` and `CancelButtonClick` events as follows:
<local:ColorMixer OkButtonClick="OnOkClicked"
CancelButtonClick="OnCancelClicked"/>
6. Navigate to the `MainWindow.xaml.cs` file to implement the associated event handlers. As shared in the following code, show a message box to the UI from the event implementation:
private void OnOkClicked(object sender, EventArgs e)
{
MessageBox.Show("确定按钮点击");
}
private void OnCancelClicked(object sender, EventArgs e)
{
MessageBox.Show("取消按钮点击");
}
7. Let's compile the project and run the application. You will see two buttons on the UI. Click on the OK and Cancel buttons to see the output:

How it works...
When you hit the OK button in the application window, it triggers the event associated with the button click. In our case, it's the `OnOkClicked` event, inside the `ColorMixer` class. It then routes the event to the custom event `OkButtonClick`, which gets caught in the originating place. It's the `OnOkClicked` event listener in our `MainWindow.xaml`.
Similarly, when you click on the Cancel button, it raises the `Click` event inside the `ColorMixer` class and then routes to the `MainWindow`. If the association is present, it gets called. In our case, it's the `OnCancelClicked` handler in `MainWindow` which triggers the message box.
Customizing the XMLNS namespace
XAML namespace is an extension of XML namespace and conventionally written as `xmlns` in XAML pages. It is used in all the XAML-related technologies to refer to the assemblies and/or namespaces within the XAML page.
Till now, we have seen how to add the XMLNS attribute entry in XAML to refer to custom controls, User Controls, converters, behaviors, and so on, but all that used an assembly/namespace system to define the entry.
For local declaration, we use the `clr-namespace:[namespace]` format, as shown in the following code:
For declarations from a different assembly, we use the `clr-namespace:[namespace];assembly=[assembly]` format, as shown in the following code:
In this recipe, we will learn how to customize the namespace to give a URL representation.
Getting ready
Let's get started by creating a project called `CH05.NamespaceCustomizationDemo`. In this example, you can either choose a WPF application template or a WPF class library template.
How to do it...
Perform the following steps steps to proceed:
1. Let's create two folders, called `Behaviors` and `Converters`, inside the project.
2. Now create one or more behaviors and converters in the respective folders. These will have `CH05.NamespaceCustomizationDemo.Behaviors` and `CH05.NamespaceCustomizationDemo.Converters` as the namespace for all the behaviors and converters in the respective modules.
3. To create the URL schema for the namespace representation, open the `AssemblyInfo.cs` file present in the `Properties` folder of each project.
4. Now, to create the schema to represent the namespace of the behaviors (`CH05.NamespaceCustomizationDemo.Behaviors`), let's add the following inside the file:
[assembly: XmlnsPrefix("http://schemas.kunal-chowdhury.com/xaml/behaviors", "behaviors")]
[assembly: XmlnsDefinition("http://schemas.kunal-chowdhury.com/xaml/behaviors", "CH05.NamespaceCustomizationDemo.Behaviors")]
5. Similarly, to define the URL schema for the converters (`CH05.NamespaceCustomizationDemo.Behaviors`), add the following inside the same file:
[assembly: XmlnsPrefix("http://schemas.kunal-chowdhury.com/xaml/converters", "converters")]
[assembly: XmlnsDefinition("http://schemas.kunal-chowdhury.com/xaml/converters", "CH05.NamespaceCustomizationDemo.Converters")]
6. Navigate to the `MainWindow.xaml` file. To add the XMLNS declaration, you can write `` instead of `.` ``
7. It is a similar case for all the declarations that you have made in the `AssemblyInfo.cs` file to represent the namespace as a URL schema.
How it works...
The `XmlnsPrefix` attribute defines the prefix name that you suggest using in the XAML, while declaring the module namespace. Though it is optional to use the same prefix name, while using the Visual Studio IntelliSense, it automatically adds it.
When you define the XML namespace as URL format, it has multiple benefits over the traditional representation:
* If you follow the same structure, it is easy to remember.
* When you are using custom libraries, you don't have to write the complete namespace and assembly every time in each file. Thus, uses of `` can be reduced to `.` ``
* You can define the prefix, so that you can follow the same convention in all the files while defining the XMLNS attribute.
使用样式、模板和触发器
在本章中,我们将涵盖以下菜谱:
-
创建控制的
Style -
基于另一个
Style创建控制的Style -
自动应用
Style到控件 -
编辑任何控件的模板
-
创建属性触发器
-
创建多触发器
-
创建数据触发器
-
创建多数据触发器
-
创建事件触发器
第七章:简介
当为应用程序设计用户界面时,你需要确保控件在应用程序中的外观和感觉的一致性。例如,如果你正在使用按钮,它们应该看起来相同——相似的颜色、相同的边距等等。
样式 是包含 Setter 属性的对象,为元素和控件提供一系列设置。样式还提供控件模板,用于自定义控件模板以具有独特的外观和感觉。
在 Win32/WinForms 模型中,控件的外观和行为是紧密捆绑的;但在 WPF 世界中,控件模板是通过使用面向设计师的工具在 XAML 中创建的,并且这应用于样式以产生类似的外观。你也可以从不同的样式中继承样式。
在本章中,我们将讨论样式、模板、触发器以及它们与所应用控件的关联。
创建控制样式
样式为你提供了一个方便的方法,可以在单个对象内分组一组属性和触发器,并将其应用到元素上。你可以选择性地将其应用到一组控件,或者根据控件类型自动将其应用到所有控件。
在这个菜谱中,我们将从按钮的默认样式开始,设置其各种样式属性以赋予它新的外观。然后我们将选择性地应用它以设置多个按钮控件的样式。
准备工作
让我们从创建一个名为 CH06.ControlStyleDemo 的新项目开始。确保你基于 WPF 应用程序模板创建项目。
如何做...
在这个菜谱中,我们将从在应用程序窗口内创建两个按钮开始。然后我们将为按钮创建一个样式并将其应用到这两个控件上。按照以下步骤尝试自己操作:
-
从解决方案资源管理器中打开
MainWindow.xaml并将现有的Grid面板替换为StackPanel。 -
将
StackPanel的Orientation属性设置为Vertical,这样我们就可以垂直堆叠子控件。 -
现在向其中添加几个按钮并分配内容。这是我们的包含两个按钮的
StackPanel标记:
<StackPanel Orientation="Vertical"
Margin="10">
<Button Content="Click Here"/>
<Button Content="Click Here"/>
</StackPanel>
- 构建并运行应用程序。你将看到以下 UI:

-
关闭应用程序并返回到
MainWindow.xaml页面。在Window标签内添加<Window.Resources></Window.Resources>以在其中添加按钮样式。 -
将以下样式复制到资源中,以定义一个名为
ButtonBaseStyle的样式,用于我们的按钮控件:
<Style x:Key="ButtonBaseStyle"
TargetType="{x:Type Button}">
<Setter Property="Height"
Value="30"/>
<Setter Property="MinWidth"
Value="180"/>
<Setter Property="FontSize"
Value="16"/>
<Setter Property="HorizontalAlignment"
Value="Center"/>
<Setter Property="Padding"
Value="8 0"/>
</Style>
- 现在通过添加属性
Style="{StaticResource ButtonBaseStyle}"将定义的样式应用到两个按钮上。以下是代码,供您参考:
<StackPanel Orientation="Vertical"
Margin="10">
<Button Content="Click Here"
Style="{StaticResource ButtonBaseStyle}"/>
<Button Content="Click Here"
Style="{StaticResource ButtonBaseStyle}"/>
</StackPanel>
- 完成这些后,构建项目,再次运行应用程序。你会看到按钮现在形状正确,文本和边缘之间有一些填充。此外,字体大小已增加,正如样式中所定义的。现在看起来是这样的:

- 让我们在样式中添加一些额外的
Setter属性。我们现在将定义一个4px的边距、一个手形光标和一个边框,如下所示:
<Setter Property="Margin"
Value="4"/>
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="BorderThickness"
Value="2"/>
- 这是到目前为止我们构建的完整样式:
<Window.Resources>
<Style x:Key="ButtonBaseStyle"
TargetType="{x:Type Button}">
<Setter Property="Height"
Value="30"/>
<Setter Property="MinWidth"
Value="180"/>
<Setter Property="FontSize"
Value="16"/>
<Setter Property="HorizontalAlignment"
Value="Center"/>
<Setter Property="Padding"
Value="8 0"/>
<Setter Property="Margin"
Value="4"/>
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="BorderThickness"
Value="2"/>
</Style>
</Window.Resources>
- 让我们编译项目并再次运行应用程序。现在您将看到一个更好的 UI,按钮控件有适当的样式,如下面的截图所示:

它是如何工作的...
当你创建一个Style对象时,你设置了一组Setter对象到它上面,以定义各种属性来改变控件的外观和感觉。这可能包括高度、宽度、位置、对齐方式、颜色、字体、控件模板、触发器等等。
FrameworkElement类公开了一个Style属性,可以通过Style对象填充。样式始终作为资源构建,正如您在我们示例中的<Window.Resources>标签内看到的那样。它包含一个x:Key属性,该属性定义了样式的名称/键。通过使用此Key,您可以从作用域内的任何其他资源/控件执行绑定。Style对象的TargetType属性通常被设置,这使得Style适用于该类型,这可以是任何类型,甚至是自定义控件的类型。
在本例中,应用样式作用于Button对象。尝试将其应用于其他元素类型将导致运行时异常。
还有更多...
你可以省略定义Style的TargetType,但要使其工作,你必须使用完全限定名称来定义属性。例如,前面的Style可以像下面这样编写,以获得相同的结果:
<Style x:Key="ButtonBaseStyle">
<Setter Property="Button.Height"
Value="30"/>
<Setter Property="Button.MinWidth"
Value="180"/>
<Setter Property="Button.FontSize"
Value="16"/>
<Setter Property="Button.HorizontalAlignment"
Value="Center"/>
<Setter Property="Button.Padding"
Value="8 0"/>
<Setter Property="Button.Margin"
Value="4"/>
<Setter Property="Button.Cursor"
Value="Hand"/>
<Setter Property="Button.BorderThickness"
Value="2"/>
</Style>
由于这使得属性名称冗余,为了定义一个限定名称,人们更喜欢使用第一个带有TargetType定义的名称。那么,第二种声明类型有什么用呢?是的,这个问题是合理的。使用这种样式,通过指定属性的完全限定名称,你可以定义一个针对各种类型的控件(在这些控件中可用该属性)的样式。
需要注意的一点是,如果你明确地为控件定义了一个属性,它将覆盖Style中定义的属性值。
基于另一个样式创建控件样式
样式支持继承。这意味着,你可以从一个Style派生出一个Style。这可以通过使用BasedOn属性来完成,该属性必须指向另一个要继承的Style。在本教程中,我们将学习如何根据另一个相同类型的Style创建一个按钮控制的Style。
准备工作
让我们从创建一个名为 CH06.StyleInheritanceDemo 的项目开始。为此,打开您的 Visual Studio 实例,并基于 WPF 应用程序模板创建一个项目。
如何做到这一点...
按照以下步骤创建一个按钮控件的基样式,然后从中派生出不同的按钮样式:
-
打开
MainWindow.xaml文件,并在Window标签内创建一个<Window.Resources></Window.Resources>部分。 -
现在,在窗口资源内添加以下样式定义,这是我们之前在本章的另一个配方中讨论过的:
<Style x:Key="ButtonBaseStyle"
TargetType="{x:Type Button}">
<Setter Property="Height"
Value="30"/>
<Setter Property="MinWidth"
Value="180"/>
<Setter Property="FontSize"
Value="16"/>
<Setter Property="HorizontalAlignment"
Value="Center"/>
<Setter Property="Padding"
Value="8 0"/>
<Setter Property="Margin"
Value="4"/>
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="BorderThickness"
Value="2"/>
</Style>
- 将默认的
Grid替换为以下具有四个按钮控制的StackPanel,这些按钮具有我们创建的相同样式:
<StackPanel Orientation="Vertical"
Margin="10">
<Button x:Name="baseButton"
Content="Base Button Style"
Style="{StaticResource ButtonBaseStyle}"/>
<Button x:Name="redButton"
Content="Red Button Style"
Style="{StaticResource ButtonBaseStyle}"/>
<Button x:Name="greenButton"
Content="Green Button Style"
Style="{StaticResource ButtonBaseStyle}"/>
<Button x:Name="blueButton"
Content="Blue Button Style"
Style="{StaticResource ButtonBaseStyle}"/>
</StackPanel>
-
构建项目并运行它。您将看到以下 UI,其中所有按钮控件都应用了相同的样式!
![图片]()
-
为了演示
Style继承,让我们创建另一个基于基Style的Style。给它一个新的Key名称,RedButtonStyle,将TargetType设置为Button,并添加一个新的属性BasedOn="{StaticResource ButtonBaseStyle}"以创建继承。 -
向新创建的样式添加一些额外的
Setter值,以定义其边框、背景和前景颜色。以下是RedButtonStyle的标记:
<Style x:Key="RedButtonStyle"
TargetType="{x:Type Button}"
BasedOn="{StaticResource ButtonBaseStyle}">
<Setter Property="BorderBrush"
Value="DarkRed"/>
<Setter Property="Foreground"
Value="White"/>
<Setter Property="Background"
Value="OrangeRed"/>
</Style>
- 现在,将
redButton的Style属性更改为指向RedButtonStyle:
<Button x:Name="redButton"
Content="Red Button Style"
Style="{StaticResource RedButtonStyle}"/>
-
让我们再次运行应用程序,它将具有以下 UI,其中第二个按钮将具有红色背景和白色字体颜色!
![图片]()
-
现在,添加另外两个基于
ButtonBaseStyle的样式,分别命名为GreenButtonStyle和BlueButtonStyle。 -
将它们的
BorderBrush、Foreground和Background属性分别设置为绿色和蓝色。为此,请在<Window.Resources>标签内复制以下样式:
<Style x:Key="GreenButtonStyle"
TargetType="{x:Type Button}"
BasedOn="{StaticResource ButtonBaseStyle}">
<Setter Property="BorderBrush"
Value="ForestGreen"/>
<Setter Property="Foreground"
Value="ForestGreen"/>
<Setter Property="Background"
Value="GreenYellow"/>
</Style>
<Style x:Key="BlueButtonStyle"
TargetType="{x:Type Button}"
BasedOn="{StaticResource ButtonBaseStyle}">
<Setter Property="BorderBrush"
Value="DarkSlateBlue"/>
<Setter Property="Foreground"
Value="DarkSlateBlue"/>
<Setter Property="Background"
Value="SkyBlue"/>
</Style>
- 要应用前面的样式,按照以下方式修改
greenButton和blueButton的Style属性:
<Button x:Name="greenButton"
Content="Green Button Style"
Style="{StaticResource GreenButtonStyle}"/>
<Button x:Name="blueButton"
Content="Blue Button Style"
Style="{StaticResource BlueButtonStyle}"/>
- 这是整个
StackPanel的代码片段,现在它将包含四个按钮。其中第一个按钮遵循基本样式,而其他三个按钮分别遵循新的红色、绿色和蓝色按钮样式:
<StackPanel Orientation="Vertical"
Margin="10">
<Button x:Name="baseButton"
Content="Base Button Style"
Style="{StaticResource ButtonBaseStyle}"/>
<Button x:Name="redButton"
Content="Red Button Style"
Style="{StaticResource RedButtonStyle}"/>
<Button x:Name="greenButton"
Content="Green Button Style"
Style="{StaticResource GreenButtonStyle}"/>
<Button x:Name="blueButton"
Content="Blue Button Style"
Style="{StaticResource BlueButtonStyle}"/>
</StackPanel>
- 是时候构建项目并运行应用程序了。现在,当应用程序启动时,它将具有以下 UI,但具有独特的样式。如前所述,按钮的颜色将根据我们为不同样式设置的值来设置!
![图片]()
它是如何工作的...
继承样式可以具有额外的 Setter 属性来设置,或者它可以为已由基 Style 设置的属性提供不同的值。在上面的示例中,RedButtonStyle、GreenButtonStyle 和 BlueButtonStyle 继承自第一个(ButtonBaseStyle),并向其添加了 BorderBrush、Foreground 和 Background 设置器属性。
自动应用样式到控件
在前两个菜谱中,我们学习了如何创建样式,并通过使用x:Key名称将它们应用到控件上。在同一个应用程序内手动为大量控件分配样式并不总是可行的。因此,我们需要将其自动应用到特定窗口作用域内的所有元素或整个应用程序中。
例如,我们可能希望同一应用内的所有按钮都拥有相同的样式和感觉。这使得创建新的按钮更加容易,因为开发者/设计师不需要知道应用哪种样式。如果自动样式配置正确,它将使工作流程更加顺畅。
让我们通过一个简单的例子看看这是如何实现的。
准备工作
要开始这个菜谱,打开您的 Visual Studio 实例,创建一个名为CH06.StyleUsageDemo的新 WPF 应用程序项目。
如何操作...
按照以下步骤为按钮控件创建样式,并将它们应用到同一窗口内的控件上,然后跨整个应用程序应用:
- 打开
MainWindow.xaml,并将现有的Grid替换为以下StackPanel,其中包含四个按钮控件:
<StackPanel Orientation="Vertical"
Margin="10">
<Button Content="Red Button Style"/>
<Button Content="Red Button Style"/>
<Button Content="Red Button Style"/>
<Button Content="Red Button Style"/>
</StackPanel>
- 在
Window标签内创建一个<Window.Resources></Window.Resources>部分,并在其中添加以下样式:
<Style TargetType="{x:Type Button}">
<Setter Property="Height"
Value="30"/>
<Setter Property="MinWidth"
Value="180"/>
<Setter Property="FontSize"
Value="16"/>
<Setter Property="HorizontalAlignment"
Value="Center"/>
<Setter Property="Padding"
Value="8 0"/>
<Setter Property="Margin"
Value="4"/>
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="BorderThickness"
Value="2"/>
<Setter Property="BorderBrush"
Value="DarkRed"/>
<Setter Property="Foreground"
Value="White"/>
<Setter Property="Background"
Value="OrangeRed"/>
</Style>
-
在解决方案资源管理器中,右键单击项目。从上下文菜单中选择“添加 | 窗口...”,以打开添加新项对话框窗口。
-
输入名称为
SecondaryWindow,然后点击添加。这将创建项目内的SecondaryWindow.xaml和SecondaryWindow.xaml.cs文件。 -
打开
SecondaryWindow.xaml文件,将Grid替换为相同的StackPanel以创建 UI,其中包含四个按钮。以下是您需要复制的标记:
<StackPanel Orientation="Vertical"
Margin="10">
<Button Content="Red Button Style"/>
<Button Content="Red Button Style"/>
<Button Content="Red Button Style"/>
<Button Content="Red Button Style"/>
</StackPanel>
- 现在,导航到
App.xaml文件,并删除如以下屏幕截图所示的StartupUri="MainWindow.xaml"属性。

- 现在,转到其代码隐藏文件,即
App.xaml.cs,并在类实现中插入以下代码块以创建MainWindow和SecondaryWindow的实例以显示在屏幕上:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
new MainWindow().Show();
new SecondaryWindow().Show();
}
-
完成此操作后,编译您的项目,并运行应用程序。
-
如以下屏幕截图所示,您将在屏幕上看到两个窗口。一个窗口(
MainWindow)将应用按钮控件的样式,而另一个窗口(SecondaryWindow)将保持默认的样式和感觉:

-
现在关闭应用程序并导航到
MainWindow.xaml文件。复制那里的样式,并删除/注释掉整个Window.Resources部分。 -
现在打开
App.xaml文件,并将复制的内文粘贴到Application.Resources标签内,如下所示:
<Application.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Height"
Value="30"/>
<Setter Property="MinWidth"
Value="180"/>
<Setter Property="FontSize"
Value="16"/>
<Setter Property="HorizontalAlignment"
Value="Center"/>
<Setter Property="Padding"
Value="8 0"/>
<Setter Property="Margin"
Value="4"/>
<Setter Property="Cursor"
Value="Hand"/>
<Setter Property="BorderThickness"
Value="2"/>
<Setter Property="BorderBrush"
Value="DarkRed"/>
<Setter Property="Foreground"
Value="White"/>
<Setter Property="Background"
Value="OrangeRed"/>
</Style>
</Application.Resources>
- 让我们构建并运行应用程序。您现在将看到样式被应用到两个窗口上。以下是相同的屏幕截图:

它是如何工作的...
当您创建一个未指定 x:Key 值的样式时,自动样式会起作用。任何未显式设置其样式的元素将自动获得它。
在前面的示例中,我们在两个窗口(MainWindow 和 SecondaryWindow)中都有按钮,并且没有手动将任何样式应用到它们中,但 MainWindow 中的控件仍然获得了红色按钮的样式,因为该窗口中的 Style 创建时没有指定任何键 (<Style TargetType="{x:Type Button}">)。
对于 SecondaryWindow,我们没有定义任何 Style 元素,因此它应用了按钮的默认样式。
当我们将 Style 定义移动到 App.xaml 中的 Application.Resources 标签时,它将 Style 注册到应用程序级别。现在,当您运行应用程序时,两个窗口都将从应用程序资源接收样式,并且所有类型的 Button 控件将自动应用该样式。
如果一个元素希望恢复其默认样式,它可以将其 Style 属性设置为 null。这通常在 XAML 中写作 {x:Null}。
编辑任何控件的模板
WPF 允许您自定义任何控件的模板。使用 Visual Studio,您可以轻松地编辑任何模板以满足您的需求。在本例中,我们将讨论如何编辑 ProgressBar 控件的模板。
准备工作
让我们从创建一个名为 CH06.ControlTemplateDemo 的项目开始。确保在创建项目时选择正确的 WPF 应用程序模板。
如何操作...
按照以下步骤编辑进度条模板:
-
打开
MainWindow.xaml文件,并将默认的Grid控件替换为垂直的StackPanel。 -
在
StackPanel内添加两个ProgressBar控件,并设置它们的Height、Width和Value属性,如这里所示:
<StackPanel Orientation="Vertical">
<ProgressBar Height="30"
Margin="10"
Value="40"/>
<ProgressBar Height="30"
Margin="10"
Value="60"/>
</StackPanel>
- 如果您运行应用程序,您将看到应用程序窗口包含两个进度条控件。这两个控件都将应用默认样式。以下是相同截图:

-
现在,我们将为
ProgressBar控件创建一个自定义模板并将其应用到第二个进度条上。为此,在Window标签内添加以下标记,以在Window.Resources下定义模板。 -
确保您设置了正确的
TargetType并为其分配一个x:Key名称:
<Window.Resources>
<ControlTemplate TargetType="{x:Type ProgressBar}"
x:Key="ProgressBarTemplate">
<Grid>
<Rectangle x:Name="PART_Track"
Fill="AliceBlue"/>
<Rectangle x:Name="PART_Indicator"
StrokeThickness="0"
HorizontalAlignment="Left">
<Rectangle.Fill>
<LinearGradientBrush
EndPoint=".08,0"
SpreadMethod="Repeat">
<GradientStop
Offset="0"
Color="Green" />
<GradientStop
Offset=".8"
Color="Green" />
<GradientStop
Offset=".8"
Color="Transparent" />
<GradientStop
Offset="1"
Color="Transparent" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<TextBlock FontSize="20"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Window.Resources>
- 现在,通过添加
Template="{StaticResource ProgressBarTemplate}"属性值将模板应用到第二个控件上。完成此操作后,XAML 将如下所示:
<StackPanel Orientation="Vertical">
<ProgressBar Height="30"
Margin="10"
Value="40"/>
<ProgressBar Height="30"
Margin="10"
Value="60"
Template="{StaticResource
ProgressBarTemplate}"/>
</StackPanel>
- 让我们再次运行应用程序。您将看到第二个控件应用了我们的自定义模板,看起来如下:

工作原理...
当您将Template="{StaticResource ProgressBarTemplate}"分配给控件时,它将应用模板到相关控件。进度条控件包含在其模板中定义的两个主要部分,它们是PART_Track和PART_Indicator。第一个用于定义控件的基本轨道,而第二个用于定义进度指示器。
在我们的模板中,我们将LinearGradientBrush分配给PART_Indicator矩形的Fill颜色,以设计以条形格式显示的进度指示。使用GradientStop定义所选颜色的Offset,如下所示:
<LinearGradientBrush EndPoint=".08,0"
SpreadMethod="Repeat">
<GradientStop Offset="0"
Color="Green" />
<GradientStop Offset=".8"
Color="Green" />
<GradientStop Offset=".8"
Color="Transparent" />
<GradientStop Offset="1"
Color="Transparent" />
</LinearGradientBrush>
现在,当应用程序运行时,由于LinearGradientBrush的重复行为(SpreadMethod="Repeat"),堆叠的条形将根据值在控件中展开。
更多内容...
记住控件默认模板的主体并不容易。也不可能记住每个定义为PART_Name的控件部分。Visual Studio 提供了一个轻松修改模板的方法。
要做到这一点,右键单击控件并遵循以下截图所示的上下文菜单条目编辑模板 | 编辑副本...:

这将打开一个对话框窗口,以指定您想要创建样式的文件。如果您选择应用程序,它将创建在Application.Resources标签下,并且可以在整个应用程序中访问。
如果您选择此文档,它将创建在Window.Resources标签下:

从这个屏幕,您还可以选择创建隐式或显式样式。选择应用全部以创建隐式样式,该范围内的所有控件都将获得相同的样式。在另一种情况下,给它一个键名称。一旦您点击确定,它将在相同的 XAML 中创建默认模板。您可以根据需求进行自定义。
不要移除任何由PART_定义的模板的PART控件,因为这些控件在内部需要它们。
创建属性触发器
触发器允许您在满足某些条件时更改属性值。它还可以通过允许您动态更改控件的外观和/或行为,而无需在代码后类中编写额外的代码,来根据属性值执行操作。
最常见的触发器是属性触发器,它可以通过 XAML 中的<Trigger>元素简单地定义。当拥有控件的特定属性更改以匹配指定值时,它将触发。
在本食谱中,我们将通过合适的示例学习属性触发器。
准备工作
打开您的 Visual Studio 实例,创建一个名为CH06.PropertyTriggerDemo的新 WPF 应用程序项目。
如何操作...
要使用属性触发器,我们将在这个示例中使用一个Label控件并触发系统在鼠标悬停时改变其各种属性。遵循以下简单步骤:
- 打开
MainWindow.xaml页面并在网格内添加以下Label控件:
<Grid>
<Label Content="Hover over the text"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
-
在
Window标签内,创建一个Window.Resources标签来保存Label控件的风格。在资源内创建一个Style并将其TargetType设置为Button。 -
在样式内添加以下触发器:
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="FontSize"
Value="30"/>
<Setter Property="Foreground"
Value="Red"/>
<Setter Property="Background"
Value="LightYellow"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
RenderingBias="Performance"
BlurRadius="1"/>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
- 这是包含对
Label控件触发器的完整样式,当鼠标悬停时将改变所提到的属性:
<Window.Resources>
<Style TargetType="{x:Type Label}">
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="FontSize"
Value="30"/>
<Setter Property="Foreground"
Value="Red"/>
<Setter Property="Background"
Value="LightYellow"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
RenderingBias="Performance"
BlurRadius="1"/>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
- 现在,构建项目并运行它。你将看到应用程序窗口上的文本标签上有一个悬停效果。将鼠标悬停在文本上以查看屏幕上的效果,如图所示:
![]()
它是如何工作的...
Label的样式创建了一个在鼠标悬停时触发的Trigger,通过检查IsMouseOver="True"属性值。当条件满足时,它将设置器属性设置为定义在其下的属性。
当条件变为false时,设置器逻辑上被移除,将属性还原到其原始值。这意味着不需要提供一个相反的触发器。
创建一个多触发器
并非必须使用触发器仅基于单一条件执行动作。有时你需要创建一个由多个条件组合而成的触发器,如果所有条件都满足,则激活整个触发器。这就是多触发器的作用。让我们看看如何创建一个多触发器。
准备工作
打开你的 Visual Studio IDE 并创建一个名为CH06.MultiTriggerDemo的新 WPF 应用程序。
如何操作...
在以下步骤中,我们将构建一个简单的应用程序,它将创建并执行一个基于某些条件的多触发器,并改变TextBox控件的Foreground和Background属性:
-
打开
MainWindow.xaml文件。 -
将默认的
Grid面板替换为垂直的StackPanel。 -
在面板内添加两个
TextBox控件并设置它们的Text属性以表示一些文本。以下是本例中我们将使用的 XAML:
<StackPanel>
<TextBox Text="Focus your cursor here"
FontSize="20"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="30"
Margin="4"/>
<TextBox Text="Focus your cursor here"
FontSize="20"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="30"
Margin="4"/>
</StackPanel>
- 现在,在窗口资源(
Window.Resources)下创建一个针对TextBox的Style:
<Style TargetType="{x:Type TextBox}">
</Style>
- 创建一个基于一个或多个条件的
MultiTrigger样式触发器,并应用设置器,如下所示:
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsEnabled"
Value="True" />
<Condition Property="IsKeyboardFocused"
Value="True" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Foreground"
Value="Green" />
<Setter Property="Background"
Value="LightGreen" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
- 让我们执行应用程序并关注
TextBox控件以查看其行为:

它是如何工作的...
这几乎就像属性触发器,但在这里它用于在多个属性更改上设置动作,并在MulitTrigger.Conditions内的所有条件都满足时执行它。MultiTrigger对象包含这些Condition对象集合。
在这个例子中,我们有一个与TextBox控件关联的MultiTrigger。当控件启用并获得键盘焦点时,它更改其Foreground和Background属性。当其中任何一个为false时,它将属性值返回到其原始状态。
创建数据触发器
正如其名所示,数据触发器将属性值应用于对已绑定到UIElement的Data执行一系列操作。这由<DataTrigger>元素表示。
在这个菜谱中,我们将学习如何创建一个对底层数据起作用的触发器。
准备工作
让我们从创建一个新的 WPF 项目开始。打开 Visual Studio 并创建一个名为CH06.DataTriggerDemo的项目。
如何操作...
按照以下简单步骤创建一个数据触发器,该触发器将根据单选按钮的选择更改Label的Background和Content属性:
-
从解决方案资源管理器中,打开
MainWindow.xaml文件。 -
让我们将
Grid面板分为两列:
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
- 现在在列
0(零)插入一个150x100维度的Label,并将其Foreground属性设置为White:
<Label Width="150"
Height="100"
Grid.Column="0"
Foreground="White"
FontSize="20"
BorderBrush="Gray"
BorderThickness="1"/>
- 在列
1插入一个垂直的StackPanel,并在其中添加三个单选按钮。确保设置它们的名称和GroupName。x:Name属性用于定义控件名称,GroupName="colors"用于定义单选按钮的一个组。以下是完整的 XAML 标记:
<StackPanel Grid.Column="1"
Margin="10">
<RadioButton x:Name="rdoRed"
GroupName="colors"
Content="Red (#FFFF0000)"/>
<RadioButton x:Name="rdoGreen"
GroupName="colors"
Content="Green (#FF00FF00)"/>
<RadioButton x:Name="rdoBlue"
GroupName="colors"
Content="Blue (#FF0000FF)"/>
</StackPanel>
- 在窗口的
Window.Resources标签内,创建一个针对Label控件的Style:
<Style TargetType="{x:Type Label}">
</Style>
- 在
Style内插入以下触发器。<Style.Triggers>包含三个绑定到复选框控件的DataTrigger:
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=rdoRed,
Path=IsChecked}"
Value="True">
<Setter Property="Content"
Value="Red"/>
<Setter Property="Background"
Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=rdoGreen,
Path=IsChecked}"
Value="True">
<Setter Property="Content"
Value="Green"/>
<Setter Property="Background"
Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=rdoBlue,
Path=IsChecked}"
Value="True">
<Setter Property="Content"
Value="Blue"/>
<Setter Property="Background"
Value="Blue"/>
</DataTrigger>
</Style.Triggers>
- 当触发器准备就绪时,让我们构建项目并运行它。更改单选按钮选择并观察其工作情况,如图所示:

它是如何工作的...
当你点击第一个单选按钮(rdoRed)时,它触发第一个数据触发器,因为它满足rdoRed控件的IsChecked属性并修改Setter属性——Content和Background。
类似地,当你将选择更改为第二个或第三个单选按钮时,相应的DataTrigger将触发并更新Label控件,根据Setter属性。
创建多数据触发器
多数据触发器与数据触发器相同,唯一的区别在于你可以在MultiDataTrigger.Conditions中定义的多个条件下设置属性值。属性值在MultiDataTrigger.Setters中定义。
让我们了解在这个菜谱中多数据触发的用法。
准备工作
要开始使用多数据触发器,让我们首先创建一个名为CH06.MultiDataTriggerDemo的项目。确保选择正确的项目模板。
如何操作...
按照以下步骤创建一个包含两个复选框和一个按钮的 UI,然后根据复选状态应用多数据触发器来启用/禁用按钮:
- 让我们从用
StackPanel替换Grid开始,其中包含两个复选框(chkLicense和chkTerms)控件和一个按钮:
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<CheckBox x:Name="chkLicense"
Content="Yes, I accept license agreement" />
<CheckBox x:Name="chkTerms"
Content="Yes, I accept Terms & Conditions" />
<Button HorizontalAlignment="Center"
Margin="0,20,0,0"
FontSize="20"
Content="Register">
</Button>
</StackPanel>
- 现在,修改
Button以公开其样式,如下所示:
<Button HorizontalAlignment="Center"
Margin="0,20,0,0"
FontSize="20"
Content="Register">
<Button.Style>
</Button.Style>
</Button>
- 在其中添加以下按钮样式,它包含一个
MultiDataTrigger来启用/禁用按钮:
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled"
Value="False"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding
ElementName=chkLicense,
Path=IsChecked}"
Value="True" />
<Condition Binding="{Binding
ElementName=chkTerms,
Path=IsChecked}"
Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled"
Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
- 现在运行应用程序,屏幕上将有两个复选框和一个按钮。更改复选框控件的选择以查看行为:

它是如何工作的...
多数据触发器基于设置给它的条件工作,它作用于底层数据。在我们的例子中,我们有一个包含两个条件的MultiDataTrigger。
根据条件,如果两个复选框控件都被选中,它将触发并启用按钮,通过将IsEnabled属性设置为True。当任何前面的条件不满足时,它将自动将IsEnabled属性设置回上一个值,在我们的例子中是False。
创建事件触发器
到目前为止,我们已经看到了属性触发器和数据触发器,它们基于比较属性与值来工作。在本例中,我们将学习事件触发器,它在发生路由事件时触发。
准备工作
在您的 Visual Studio IDE 中,创建一个名为CH06.EventTriggerDemo的新项目,基于 WPF 应用程序模板。
如何做到这一点...
按照以下步骤在TextBlock控件上创建一个简单的事件触发器:
- 打开
MainWindow.xaml并在Grid内添加以下TextBlock:
<TextBlock Text="Hover here"
FontSize="30"
Opacity="0.2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock.Style>
</TextBlock.Style>
</TextBlock>
- 将以下包含
EventTrigger的样式添加到TextBlock.Style属性:
<Style TargetType="TextBlock">
<Style.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:0.500"
Storyboard.TargetProperty="FontSize"
To="50" />
<DoubleAnimation
Duration="0:0:0.500"
Storyboard.TargetProperty="Opacity"
To="1.0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:0.500"
Storyboard.TargetProperty="FontSize"
To="30" />
<DoubleAnimation
Duration="0:0:0.500"
Storyboard.TargetProperty="Opacity"
To="0.2"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
- 构建应用程序并运行它。将鼠标悬停在文本上方,您会看到文本的字体大小逐渐增大,可见性变为
100%:

它是如何工作的...
事件触发器通常用于在关联的FrameworkElement的路由事件引发时执行操作。这主要用于动画,以控制在某个 UI 事件引发时的外观和感觉。
在此示例中,当您将鼠标悬停在TextBlock控件上时,MouseEnter事件会触发,并触发EventTrigger,这是我们已在 XAML 中定义的。然后它将文本动画化以具有更大的字体大小和更高的不透明度,以提供更大、更明显的可见内容。
当MouseLeave事件触发时,它会减小字体大小并将控件的不透明度降低到20%。关于Storyboard动画的更多内容将在第八章,与动画一起工作中讨论。
使用资源和 MVVM 模式
在本章中,我们将介绍以下配方:
-
在 WPF 应用程序中使用二进制资源
-
从另一个程序集使用二进制资源
-
在代码中访问二进制资源
-
在 WPF 中使用静态逻辑资源
-
在 WPF 中使用动态逻辑资源
-
管理逻辑资源
-
使用用户选择的颜色和字体
-
使用 MVVM 模式构建应用程序
-
在 WPF 应用程序中使用路由命令
第八章:简介
虽然二进制资源在任何应用程序中都发挥着至关重要的作用,但 WPF 还提供了一种不同类型的资源,称为 逻辑资源。这些逻辑资源是可以在整个应用程序中共享的对象,并且可以在多个程序集之间访问。这些资源可以分为两种类型,静态逻辑资源和动态逻辑资源。
另一方面,MVVM(模型-视图-视图模型)是一种保持 UI 和代码之间分离的模式,它为设计师和开发者提供了在单个窗口上工作的灵活性,而不依赖于彼此。
在本章中,我们将首先介绍二进制资源、逻辑资源,然后继续学习使用 MVVM 模式构建应用程序。我们还将介绍如何使用 RoutedCommands 在 WPF 应用程序中演示 命令设计模式,它可以从多个位置调用。
在 WPF 应用程序中使用二进制资源
二进制资源是添加到项目中并为其定义了 Build Action 的字节数据块。通常,这些是应用程序所需的图像、徽标、字体、文件等,它们与应用程序捆绑在一起。
在本配方中,我们将学习如何在 WPF 应用程序中使用二进制资源。
准备工作
要开始,打开您的 Visual Studio IDE 并创建一个名为 CH07.BinaryResourceDemo 的新项目。确保您选择 WPF 应用作为项目模板。
如何操作...
按以下步骤将图像作为二进制资源添加到 WPF 应用程序中,并将它们加载到应用程序窗口中:
- 右键单击项目以添加一个新文件夹。按照上下文菜单路径添加 | 新文件夹。将新创建的文件夹重命名为
Images:

- 现在,右键单击 Images 文件夹以添加一些图像。从上下文菜单条目中选择添加 | 已存在项... 并添加您选择的两个图像。在这个例子中,我们添加了两个现有图像,
image1.png和image2.png,以供演示:

-
从解决方案资源管理器中,右键单击
image1.png并转到其属性。将图像的构建操作设置为资源,这是默认设置:![]()
-
现在,从解决方案资源管理器中右键单击
image2.png,并转到其属性。将其构建操作设置为内容。 -
将“复制到输出目录”更改为“始终复制”:
![]()
-
从解决方案资源管理器中打开
MainWindow.xaml,并将Grid替换为水平StackPanel。 -
现在将两个图像插入
StackPanel中,并将它们的Source属性分别设置为Images/image1.png和Images/image2.png:
<StackPanel Orientation="Horizontal">
<Image Source="Images/image1.png"
Width="150"
Margin="8"/>
<Image Source="Images/image2.png"
Width="150"
Margin="8"/>
</StackPanel>
-
构建项目并运行应用程序。你将在屏幕上看到以下 UI:
![图片]()
-
现在转到项目的 bin | Debug 目录。你会看到一个名为 Images 的文件夹,其中包含我们定义为
Build Action = Content和Copy to Output Directory = Copy Always的图像(image2.png)。 -
现在将
image2.png替换为不同的图像。 -
现在直接从 bin | Debug 文件夹中运行应用程序,而不是重新编译项目。观察屏幕上的输出。你会看到第二个图像现在指向我们放置在 bin | Debug | Images 文件夹中的新图像:
![图片]()
它是如何工作的...
当Build Action设置为Resource时,文件作为资源存储在编译的程序集中。在我们的例子中,image1.png在项目二进制文件中被设置为Resource,这使得在部署应用程序时实际的图像文件变得不必要。
当Build Action设置为Content时,资源不会被包含在程序集内。为了使其对应用程序可用,需要将Copy to Output Directory设置为Copy Always或Copy if Newer。
这使得当资源需要经常更改且不希望重新构建时更为合适。如果资源在输出目录中不可用,在执行时将渲染一个空白图像。如果资源较大且不是总是需要的,最好将其留给生成的程序集。
还有更多...
在插入图像到 XAML 时,我们通常使用相对 URI(在我们的例子中是Images/image1.png),因为它相对于应用程序。你也可以更详细地将其指定为pack://application:,,,/Images/image1.png,这通常用于从代码后端分配图像源。
你也可以使用 Visual Studio 编辑器来指定图像源。为此,在 XAML 设计视图中的图像上右键单击,并转到其属性。从属性面板中,点击下拉箭头,如以下截图所示,从列表中选择所需的图像:

使用来自另一个程序集的二进制资源
不必在将使用资源的同一程序集中定义资源。有时,根据需要,二进制资源在一个程序集(通常是一个类库)中定义,并在另一个程序集中使用。
WPF 提供了一种统一的方式来访问在其他程序集中定义的资源。为了使用它,我们需要使用 pack URI 方案。在本教程中,我们将学习如何使用来自另一个程序集的二进制资源。
准备工作
让我们从创建一个名为 CH07.RemoteBinaryResourceDemo 的新项目开始。确保在创建此项目时选择 WPF 应用程序模板。
如何做到这一点...
按照以下步骤创建一个类库来定义二进制资源,并从我们已创建的应用程序中使用它:
- 在同一解决方案中创建另一个项目。让我们将其命名为
CH07.ResourceLibrary,并确保您选择类库 (.NET Framework) 作为项目模板:

-
删除自动生成的类文件
Class1.cs。 -
现在右键单击项目
CH07.ResourceLibrary并创建一个名为Images的新文件夹。 -
现在右键单击新创建的文件夹,并将现有的图像(在我们的情况下是
image1.png)添加到该文件夹中。 -
然后右键单击图像(
image1.png)并导航到其属性窗格。 -
如前一个示例所示,将其构建操作更改为
Resource。编译项目CH07.ResourceLibrary以确保构建成功。 -
从解决方案资源管理器中,右键单击名为
CH07.RemoteBinaryResourceDemo的其他项目,并通过上下文菜单中的“添加 | 引用...”选项添加此项目中类库的引用。 -
从引用管理器对话框窗口中,导航到项目并选择我们创建的类库(CH07.ResourceLibrary)。如图所示,完成后点击确定。这将把我们的类库添加到应用程序项目中:

- 现在,从解决方案资源管理器中,导航到
CH07.RemoteBinaryResourceDemo项目的MainWindow.xaml文件,并在Grid中添加以下图像:
<Image Source="/CH07.ResourceLibrary;component/
Images/image1.png"/>
- 让我们编译解决方案并运行应用程序。您将看到应用程序窗口启动时带有一个图像,该图像位于不同的程序集。以下是基于我们的演示应用程序的截图:

它是如何工作的...
当您使用引用的程序集时,WPF 打包 URI 会将其识别为 /AssemblyReference;component/ResourceName 格式。在上面的示例中,AssemblyReference 是程序集的名称,在我们的情况下是 CH07.ResourceLibrary,而 ResourceName 是相对于项目组件的资源完整路径。
还有更多...
AssemblyReference 还可能包括版本和/或公钥标记(如果程序集是强命名的)。版本通过在其前缀为 v, 来表示,如下面的示例所示:
/<AssemblyName>;v<VersionNo>;<Token>;component/<ResourcePath>
"/CH07.ResourceLibrary;v1.0;3ca44a7f7ca54f49;component/Images/image1.png"
这不适用于标记为 Build Action 为 Content 的资源。要使用它,我们需要使用带有 siteOfOrigin 基的完整打包 URI,如下所示:
<Image Source="pack://siteOfOrigin:,,,/Images/image1.png" />
请注意,当使用 siteOfOrigin 时,Visual Studio 设计器窗口将无法加载图像,但在运行时这将正常工作。
在代码中访问二进制资源
在 XAML 中访问二进制资源非常简单,但有一个选项可以从代码背后读取二进制资源。在这个菜谱中,我们将学习如何在代码中读取二进制资源并将其设置在 UI 中。我们将使用图像作为示例。
准备工作
打开你的 Visual Studio IDE。让我们从创建一个名为 CH07.BinaryResourceFromCodeDemo 的新 WPF 项目开始。
如何做到这一点...
按照以下步骤读取图像文件,将其嵌入为 Resource 并在 UI 中显示:
-
首先,在项目中创建一个名为
Images的文件夹,并在其中添加一个图像。让我们将其命名为image1.png。 -
通过导航到解决方案资源管理器打开
MainWindow.xaml文件。 -
在
Grid面板内添加一个图像标签并命名为img:
<Grid>
<Image x:Name="img" />
</Grid>
- 打开
MainWindow.xaml.cs文件,并在类的构造函数中,在InitializeComponent()调用之后,从图像的资源流中创建streamResourceInfo。以下是获取流信息的代码:
var streamResourceInfo = Application.GetResourceStream(new Uri("Images/image1.png", UriKind.RelativeOrAbsolute));
- 现在我们需要从该流创建
BitmapImage的实例。复制以下内容并将streamResourceInfo.Stream传递给BitmapImage的StreamSource属性:
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = streamResourceInfo.Stream;
bitmapImage.EndInit();
bitmapImage.Freeze();
- 现在将
bitmapImage实例设置为图像的Source属性:
img.Source = bitmapImage;
- 这是访问流并将其分配给图像源的完整代码:
public MainWindow()
{
InitializeComponent();
var streamResourceInfo = Application.GetResourceStream(
new Uri("Images/image1.png",
UriKind.RelativeOrAbsolute));
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.StreamSource = streamResourceInfo.Stream;
bitmapImage.EndInit();
bitmapImage.Freeze();
img.Source = bitmapImage;
}
- 完成后,构建项目并运行它。你会看到分配的图像已加载到应用程序窗口中:
![图片]()
它是如何工作的...
静态方法 Application.GetResourceStream 提供了一种使用其相对 URI 访问资源的方法,它返回一个 StreamResourceInfo 对象。StreamResourceInfo 对象的 Stream 属性提供了对实际二进制数据的访问,这些数据已被转换为 BitmapImage 实例,并设置为图像源属性。
为了使 Application.GetResourceStream 方法正常工作,资源必须在 Build Action 中标记为 Resource。
如果资源已在 Build Action 属性中标记为 Content,则应使用 Application.GetContentStream 方法来获取资源流。
在 WPF 中使用静态逻辑资源
WPF 中的逻辑资源是可以跨 Visual Tree 的某个部分或整个应用程序共享和重用的对象。这些可以是颜色、画笔、几何图形、样式或任何其他由 .NET 框架或开发者定义的 .NET 对象(int、string、List<T>、T 等)。这些对象通常放置在 ResourceDictionary 中。
在这个菜谱中,我们将学习如何使用绑定键 StaticResource 来使用逻辑资源。
准备工作
确保 Visual Studio 正在运行。基于 WPF 应用程序模板创建一个名为 CH07.StaticResourceDemo 的项目。
如何做到这一点...
按照以下步骤创建逻辑资源并在应用程序窗口中使用它:
-
打开
MainWindow.xaml文件并将Grid替换为一个水平的StackPanel。 -
在
StackPanel内部插入一个Border控件。将其Height和Width属性分别设置为80和150:
<Border Height="80"
Width="150"
Margin="8">
</Border>
- 让我们在
Border控件中添加一个背景颜色。我们将使用线性渐变画笔来装饰背景颜色。让我们按照以下方式修改它:
<Border Height="80"
Width="150"
Margin="8">
<Border.Background>
<LinearGradientBrush>
<GradientStop Offset="0"
Color="LightYellow"/>
<GradientStop Offset="0.2"
Color="Yellow"/>
<GradientStop Offset=".5"
Color="YellowGreen"/>
<GradientStop Offset="1"
Color="Green"/>
</LinearGradientBrush>
</Border.Background>
</Border>
-
将与上一背景相同的边框复制并粘贴到同一面板内部。现在
StackPanel将具有两个具有相同属性的边框控件。 -
让我们构建项目并运行它。你会看到两个带有漂亮的渐变颜色的矩形形状作为背景:
![图片]()
-
如你所见,我们添加了相同的背景画笔两次来表示颜色;有时这很难管理,并且不必要地增加了 XAML 代码以及复杂性。为了解决这个问题,我们现在可以将画笔对象作为逻辑资源移动,并在同一窗口或整个应用程序中访问它。让我们将其移动到窗口级别。在窗口资源(
Window.Resources)内插入LinearGradientBrush并为其分配一个键(myLinearBrush):
<Window.Resources>
<LinearGradientBrush x:Key="myLinearBrush">
<GradientStop Offset="0"
Color="LightYellow"/>
<GradientStop Offset="0.2"
Color="Yellow"/>
<GradientStop Offset=".5"
Color="YellowGreen"/>
<GradientStop Offset="1"
Color="Green"/>
</LinearGradientBrush>
</Window.Resources>
- 现在从两个控件中移除背景定义,并用之前提到的画笔的绑定替换它。由于它是在资源中定义的,我们将使用
{StaticResource}来访问它。以下是面板内边框控件的修改版本:
<StackPanel Orientation="Horizontal">
<Border Height="80"
Width="150"
Margin="8"
Background="{StaticResource myLinearBrush}"/>
<Border Height="80"
Width="150"
Margin="8"
Background="{StaticResource myLinearBrush}"/>
</StackPanel>
- 让我们再次运行应用程序。你会看到相同的背景应用于矩形形状的边框控件。在这种情况下,我们只使用了一个画笔的定义。
它是如何工作的...
每个从FrameworkElement派生的 UI 元素都有一个名为Resources的属性,其类型为ResourceDictionary。因此,每个元素都可以与其相关联资源。在 XAML 中,我们需要定义x:Key属性来访问资源,无论是从 XAML 还是从代码隐藏文件中。
在我们的例子中,我们将myLinearBrush定义为Window的ResourceCollection的元素。因此,它将可以被同一窗口内的任何控件访问。如果你将定义移到StackPanel内部,它将可以在该面板内访问:
<Window.Resources>
<LinearGradientBrush x:Key="myLinearBrush">
<GradientStop Offset="0"
Color="LightYellow"/>
<GradientStop Offset="0.2"
Color="Yellow"/>
<GradientStop Offset=".5"
Color="YellowGreen"/>
<GradientStop Offset="1"
Color="Green"/>
</LinearGradientBrush>
</Window.Resources>
要在 XAML 中使用此资源,我们需要使用标记扩展,{StaticResource},以及提供的资源键,Background="{StaticResource myLinearBrush}",这将在这两者之间创建绑定。
还有更多...
从代码隐藏文件中管理逻辑资源是可能的。你可以调用FindResource方法,并传递资源键给它,以获取资源的实例。以下是如何查找名为myLinearBrush的资源的方法:
var resource = FindResource("myLinearBrush") as Brush;
你也可以通过编程方式向集合中添加或删除资源。调用Resources.Add和Resources.Remove方法来添加或删除特定资源,如下面的代码片段所示:
Resources.Add("myBrush", new SolidColorBrush(Colors.Red));
Resources.Remove("myBrush");
由于Resources属性基本上是一个Dictionary对象,确保在执行任何操作之前,例如Add/Remove,检查指定的键是否已经存在。
在 WPF 中使用动态逻辑资源
在前面的菜谱中,我们学习了如何使用StaticResource标记扩展使用逻辑资源。在本菜谱中,我们将学习如何使用DynamicResource标记扩展,并了解它们之间的区别。
准备工作
通过创建一个新的项目开始。打开 Visual Studio IDE 并创建一个名为CH07.DynamicResourceDemo的新 WPF 应用程序项目。
如何操作...
按照以下步骤使用逻辑资源动态修改资源值:
-
打开
MainWindow.xaml文件并将Grid替换为StackPanel。 -
在
StackPanel内部添加一个边框并设置其尺寸。 -
在面板内部添加另一个
StackPanel并在其中添加一组三个单选按钮。将它们标记为Red、Green和Blue。以下是完整的 XAML 代码:
<StackPanel Orientation="Horizontal">
<Border Height="80"
Width="150"
Margin="8"/>
<StackPanel Margin="10">
<RadioButton GroupName="colorGroup"
Content="Red"
Margin="4"/>
<RadioButton GroupName="colorGroup"
Content="Green"
IsChecked="True"
Margin="4"/>
<RadioButton GroupName="colorGroup"
Content="Blue"
Margin="4"/>
</StackPanel>
</StackPanel>
- 现在向窗口资源添加一个
LinearGradientBrush并将其键名设置为myLinearBrush。添加一些GradientStop来定义一个漂亮的渐变画笔,如下所示:
<Window.Resources>
<LinearGradientBrush x:Key="myLinearBrush">
<GradientStop Offset="0"
Color="LightYellow"/>
<GradientStop Offset="1"
Color="Green"/>
</LinearGradientBrush>
</Window.Resources>
- 是时候将定义的画笔绑定到
Border控件上了。修改 XAML 以在它们之间创建一个StaticResource绑定,如下所示:
<Border Height="80"
Width="150"
Margin="8"
Background="{StaticResource myLinearBrush}"/>
- 为所有三个单选按钮注册
Checked事件,这样我们就可以在状态改变时执行一些更改:
<StackPanel Orientation="Horizontal">
<Border Height="80"
Width="150"
Margin="8"
Background="{StaticResource myLinearBrush}"/>
<StackPanel Margin="10">
<RadioButton GroupName="colorGroup"
Content="Red"
Margin="4"
Checked="OnRedRadioChecked"/>
<RadioButton GroupName="colorGroup"
Content="Green"
IsChecked="True"
Margin="4"
Checked="OnGreenRadioChecked"/>
<RadioButton GroupName="colorGroup"
Content="Blue"
Margin="4"
Checked="OnBlueRadioChecked"/>
</StackPanel>
</StackPanel>
- 导航到
MainWindow.xaml.cs并为所有单选按钮的Checked事件添加以下实现:
private void OnRedRadioChecked(object sender,
RoutedEventArgs e)
{
var brush = Resources["myLinearBrush"];
if (brush is LinearGradientBrush lBrush)
{
lBrush = new LinearGradientBrush
{
GradientStops = new GradientStopCollection
{
new GradientStop
(Colors.LightGoldenrodYellow, 0),
new GradientStop(Colors.Red, 1)
}
};
Resources["myLinearBrush"] = lBrush;
}
}
private void OnGreenRadioChecked(object sender,
RoutedEventArgs e)
{
var brush = Resources["myLinearBrush"];
if (brush is LinearGradientBrush lBrush)
{
lBrush = new LinearGradientBrush
{
GradientStops = new GradientStopCollection
{
new GradientStop(Colors.LightYellow, 0),
new GradientStop(Colors.Green, 1)
}
};
Resources["myLinearBrush"] = lBrush;
}
}
private void OnBlueRadioChecked(object sender,
RoutedEventArgs e)
{
var brush = Resources["myLinearBrush"];
if (brush is LinearGradientBrush lBrush)
{
lBrush = new LinearGradientBrush
{
GradientStops = new GradientStopCollection
{
new GradientStop(Colors.LightBlue, 0),
new GradientStop(Colors.Blue, 1)
}
};
Resources["myLinearBrush"] = lBrush;
}
}
-
一旦完成这些操作,运行应用程序。你会看到一个带有三个单选按钮的矩形。默认情况下,绿色单选按钮将被选中。将选择更改为红色或蓝色以观察行为。你会看到颜色始终为绿色,无论选择如何!
![图片]()
-
让我们关闭应用程序并导航回
MainWindow.xaml。 -
将
StaticResource更改为DynamicResource,如下面的代码片段所示:
<Border Height="80"
Width="150"
Margin="8"
Background="{DynamicResource myLinearBrush}"/>
- 现在,再次运行应用程序。默认情况下,绿色将被选中,矩形将具有绿色渐变背景。将选择更改为红色或蓝色以观察颜色变化!
![图片]()
它是如何工作的...
当你将逻辑资源作为StaticResource绑定时,它会在构造时触发绑定。另一方面,DynamicResource标记扩展仅在需要时动态绑定资源。
在前面的示例中,当我们将资源注册到Border控制的Background属性作为StaticResource时,即使我们在选择单选按钮时用新对象替换了资源,我们也没有在 UI 中看到变化。但是当我们将绑定更改为DynamicResource时,变化会自动反映出来。这是因为动态资源绑定会在对象更改时自动刷新。但是这与静态资源绑定不同,因为它始终引用旧对象。
更多内容...
当指定的x:Key对象不存在时,StaticResource绑定会在设计时抛出错误。另一方面,DynamicResource不会抛出任何异常,并且会显示为空白。稍后,当它找到Key时,它会将自己与该资源绑定。
应该在大多数情况下使用StaticResource,除非需要动态替换资源。DynamicResource应该由可以轻松交换资源的主题使用。
在复杂的 UI 上拥有大量的DynamicResource可能会影响 UI 的性能。 wherever possible,将它们标记为StaticResource。
管理逻辑资源
单个应用程序中可能存在多种类型的逻辑资源,将它们放置在单个 XAML 文件(例如,App.xaml)中在维护时会增加问题。为了解决这个问题,你可以将不同类型的资源分别放入它们各自的文件中,并在App.xaml中引用它们。
在这个菜谱中,我们将通过一个简单的示例学习如何管理这些逻辑资源。虽然这将是通过单个文件展示的,但你也可以创建单独的文件并引用它们。
准备工作
假设你已经打开了 Visual Studio,现在创建一个名为CH07.ManagingLogicalResourceDemo的新 WPF 应用程序项目。
如何操作...
按照以下简单步骤创建单独的资源文件并在应用程序中引用它们:
-
由于我们想要创建一个单独的资源文件,我们需要创建一个类型为资源字典的文件。在解决方案资源管理器中,右键单击项目节点并创建一个名为
Themes的新文件夹。 -
现在右键单击“主题”文件夹,并从上下文菜单中选择添加 | 资源字典...:

- 在“添加新项”对话框中,确保选择了资源字典(WPF)模板。将其命名为
Brushes.xaml,然后点击添加:

- 从解决方案资源管理器打开新创建的文件
Brushes.xaml,并在ResourceDictionary元素内添加以下具有x:Key名为myLinearBrush的LinearGradientBrush。你可以在ResourceDictionary内添加多个元素以拥有资源集合。确保为每个元素分配一个唯一的键名:
<LinearGradientBrush x:Key="myLinearBrush">
<GradientStop Offset="0"
Color="Yellow"/>
<GradientStop Offset="1"
Color="OrangeRed"/>
</LinearGradientBrush>
- 打开
MainWindow.xaml并将Grid替换为以下标记,以便在内部有一个Border控件。设置元素的大小并将Background属性绑定到我们创建的myLinearBrush:
<Grid>
<Border Height="100"
Width="280"
Margin="8"
Background="{DynamicResource myLinearBrush}"/>
</Grid>
-
如果你现在运行应用程序,你将看不到窗口内的任何元素,因为文件映射尚未创建。由于我们有
DynamicResource绑定,你不会看到任何错误。 -
让我们关闭应用程序并打开
App.xaml文件。 -
在
Application.Resources内部添加一个名为ResourceDictionary的元素。在这个元素内部,创建另一个名为ResourceDictionary.MergedDictionaries的元素,并加载我们创建的ResourceDictionary。以下是它的样子:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="Themes/Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
- 现在再次运行应用程序。你将在应用程序窗口中看到一个矩形形状的边框控件,它具有我们创建在
Brushes.xaml文件中的漂亮渐变颜色。以下是应用程序窗口的截图!![应用程序窗口截图]()
它是如何工作的...
ResourceDictionary 可以使用其 MergedDictionaries 属性(ResourceDictionary.MergedDictionaries)加载一个或多个资源字典,它是一个集合。不一定需要引用其他资源字典,但它也可以有自己的资源:
<Application.Resources>
<ResourceDictionary>
<SolidColorBrush Color="Red" x:Key="redBrush" />
<SolidColorBrush Color="Green" x:Key="greenBrush" />
<SolidColorBrush Color="Blue" x:Key="blueBrush" />
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="Themes/SolidBrushes.xaml" />
<ResourceDictionary
Source="Themes/GradientBrushes.xaml" />
<ResourceDictionary Source="Themes/Fonts.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
ResourceDictionary 元素的 Source 属性必须指向 ResourceDictionary 的位置。如果该位置在子文件夹中,则必须包含该子文件夹。
还有更多...
当存在两个或更多来自多个合并字典且具有相同键名的资源时,它不会抛出任何错误或异常。相反,它将加载元素树中最后添加的 Resource Dictionary 中的资源。
使用用户选择的颜色和字体
有时,在应用程序 UI 中使用系统主题是有用的,以便在操作系统和应用程序之间同步颜色和字体流。在这些情况下,我们可以动态加载这些值并将它们应用到我们的 UI 元素上。这是通过访问 SystemColors 和 SystemFonts 类中的某些特殊资源键来实现的。在本教程中,我们将学习如何使用它们。
准备工作
让我们从创建一个名为 CH07.SystemResourcesDemo 的新项目开始。确保从可用列表中选择 WPF 应用程序模板。
如何操作...
我们现在将构建一个使用系统颜色和字体的应用程序。按照以下步骤进行:
- 打开
MainWindow.xaml文件,并在Grid面板内插入以下Rectangle:
<Rectangle Height="100"
Width="300"/>
- 我们希望用桌面刷的颜色填充矩形。添加以下标记来填充矩形的背景颜色
Fill="{DynamicResource {x:Static SystemColors.DesktopBrushKey}}"。现在 XAML 将看起来如下所示:
<Rectangle Fill="{DynamicResource {x:Static SystemColors.DesktopBrushKey}}"
Height="100"
Width="300"/>
-
让我们现在运行应用程序。您将看到矩形已选择一个背景颜色。这是基于您在系统中为
DesktopBrush选择的设置!![图片]()
-
为了确认这一点,右键单击您的桌面并选择个性化。如果您正在使用 Windows 10,您将看到设置应用导航到背景设置页面。检查背景设置中选定的颜色以及应用到您应用程序的颜色。两者都将相同:

- 让我们从背景设置中选择不同的颜色。您将看到颜色将自动应用到您的应用程序上:

-
现在选择加号符号(自定义颜色)以选择调色板中默认颜色的另一种颜色!
![图片]()
-
如此所示,为您的桌面选择一个自定义背景颜色并点击完成!
![图片]()
-
现在检查应用程序窗口。您将看到在设置应用中选定的颜色已应用到矩形背景上。导航到您的桌面,同样的颜色也将应用到那里!
![图片]()
它是如何工作的...
向资源绑定提供一个string类型的键名不是强制的。您也可以向绑定提供一个静态对象。在这个例子中,我们使用了静态值SystemColors.DesktopBrushKey与{x:Static}标记扩展结合使用:
Fill="{DynamicResource {x:Static SystemColors.DesktopBrushKey}}"
如同我们之前在菜谱中学到的动态资源绑定,这个例子也遵循了同样的方法,因此您可以看到选定的颜色已自动应用到矩形上。
在SystemColors类下有许多静态键,您可以在设计中引用它们。这通常在您希望应用程序与操作系统的主题保持同步时非常有用。
更多内容...
就像SystemColors一样,我们还有SystemFonts类,它公开了与字体相关的静态属性。您可以从系统调色板定义FontFamily、FontSize和FontWeight样式,如下所示:
<TextBlock FontFamily="{DynamicResource {x:Static SystemFonts.CaptionFontFamily}}"
FontSize="{DynamicResource {x:Static SystemFonts.CaptionFontSizeKey}}"
FontWeight="{DynamicResource {x:Static SystemFonts.CaptionFontWeightKey}}"
Text="Hello World!"/>
使用 MVVM 模式构建应用程序
MVVM代表模型、视图和ViewModel,这是一种促进将GUI(图形用户界面)与业务逻辑分离的模式。这意味着设计师和开发者可以一起工作,而不会遇到任何麻烦。
在这个模式中,模型是帮助通过 ViewModel 显示在视图中的数据。在这个菜谱中,我们将学习如何创建一个 MVVM 应用程序,将 ViewModel 中的属性公开给相关视图,并在 XAML 代码后文件中不编写任何代码的情况下显示记录。
准备工作
让我们打开 Visual Studio IDE 并创建一个新的项目,命名为CH07.MVVMDemo,基于 WPF 应用程序模板。
如何操作...
一旦项目创建完成,按照以下步骤构建符合 MVVM 标准的项目(非强制),并使用 MVVM 模式构建一个示例演示:
-
每个 WPF 应用程序项目都有一个
MainWindow.xaml。从解决方案资源管理器中,让我们删除默认文件。 -
在项目中创建三个名为
Models、Views和ViewModels的文件夹。这只是为了创建所有代码文件的正确结构。 -
现在,右键单击
Views文件夹,通过上下文菜单路径添加 | 窗口...创建一个新的Window,并将其命名为MainWindow.xaml。 -
打开
App.xaml文件并修改StartupUri以指向正确的文件。如图所示,将StartupUri更改为ViewsMainWindow.xaml:

-
打开
MainWindow.xaml文件并替换Grid为DockPanel。 -
在
Dock内部添加两个StackPanel并设计 UI,如下所示:
<DockPanel Margin="10">
<StackPanel Orientation="Vertical"
DockPanel.Dock="Left">
<ListBox Width="180" Height="110">
</ListBox>
<TextBlock>
</TextBlock>
</StackPanel>
<StackPanel Orientation="Vertical"
Margin="4 0"
DockPanel.Dock="Right">
<TextBlock Text="Firstname"/>
<TextBox Text=""/>
<TextBlock Text="Lastname"/>
<TextBox Text=""/>
<Button Content="Add"
Margin="0 8"/>
</StackPanel>
</DockPanel>
- 如果你现在运行应用程序,你会看到应用程序窗口看起来像这样:

- 现在,右键单击
Models文件夹并创建一个名为UserModel.cs的类文件,并将类修改为具有两个类型为string的属性。如图所示,将它们命名为Firstname和Lastname:
public class UserModel
{
public string Firstname { get; set; }
public string Lastname { get; set; }
}
-
右键单击
ViewModels文件夹并添加另一个类文件。将其命名为MainWindowViewModel.cs。 -
打开
MainWindowViewModel.cs文件并在其中添加以下命名空间:
using CH07.MVVMDemo.Models;
using System.Collections.ObjectModel;
using System.ComponentModel;
- 现在,从
System.ComponentModel命名空间下的INotifyPropertyChanged接口继承MainWindowViewModel类。
public class MainWindowViewModel : INotifyPropertyChanged
- 如我们所知,
INotifyPropertyChanged接口公开了PropertyChanged事件处理程序;我们需要在类内部注册该处理程序。将以下代码复制以实现接口:
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
- 完成此操作后,在
ViewModel内部创建两个属性。一个命名为SelectedUser,类型为UserModel,另一个命名为UserCollection,类型为ObservableCollection<UserModel>。确保在两个设置器中调用OnPropertyChanged(str)方法,以便自动将值更改报告给 UI。以下是我们在演示中将要引用的属性:
private UserModel m_selectedUser;
public UserModel SelectedUser
{
get { return m_selectedUser; }
set
{
m_selectedUser = value;
OnPropertyChanged("SelectedUser");
}
}
private ObservableCollection<UserModel> m_userCollection;
public ObservableCollection<UserModel> UserCollection
{
get { return m_userCollection; }
set
{
m_userCollection = value;
OnPropertyChanged("UserCollection");
}
}
- 在 ViewModel 的构造函数中,使用一些示例数据初始化
UserCollection属性:
public MainWindowViewModel()
{
UserCollection = new ObservableCollection<UserModel>
{
new UserModel
{
Firstname = "User", Lastname = "One"
},
new UserModel
{
Firstname = "User", Lastname = "Two"
},
new UserModel
{
Firstname = "User", Lastname = "Three"
},
new UserModel
{
Firstname = "User", Lastname = "Four"
},
};
}
-
当
viewmodel准备就绪,拥有我们需要的所有属性时,让我们将其与视图关联起来作为其DataContext。你可以从代码后端或从 XAML 本身进行此操作。由于我们的目标是使代码后端尽可能小,让我们从 XAML 开始。打开MainWindow.xaml并添加以下XMLNS条目,以便我们可以访问我们创建的viewmodel: -
在
Window.Resources标签内,将我们的viewmodel作为资源添加,并定义为x:Key="ViewModel",如下所示:
<Window.Resources>
<viewmodels:MainWindowViewModel x:Key="ViewModel"/>
</Window.Resources>
- 由于
viewmodel已注册为资源,将DockPanel的DataContext设置为我们定义的ViewModel。绑定需要使用{StaticResource}标记扩展来完成。以下是它的样子:
<DockPanel DataContext="{StaticResource ViewModel}"
Margin="10">
- 现在将
ListBox控件的ItemsSource和SelectedItem属性设置为与viewmodel内部的属性进行数据绑定。
<ListBox Width="180" Height="110"
ItemsSource="{Binding UserCollection}"
SelectedItem="{Binding SelectedUser}">
- 类似地,将
TextBlock的DataContext属性设置为SelectedUser并创建数据绑定,如图所示,以显示选定的用户全名:
<TextBlock DataContext="{Binding SelectedUser}">
<Run Text="Selected:"/>
<Run Text="{Binding Firstname}"/>
<Run Text="{Binding Lastname}"/>
</TextBlock>
-
现在让我们运行这个应用程序。你将看到以下 UI,其中
ListBox控件中的值将显示为model类的完全限定名称:![图片]()
-
为了解决这个问题,我们需要创建
ListBox的DataTemplate。将ListBox.ItemTemplate定义如下,以有一个包含用户全名的TextBlock,通过连接Firstname和Lastname属性:
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Firstname}"/>
<Run Text="{Binding Lastname}"/>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
- 一旦完成,应用程序 UI 的 XAML 代码将类似于以下内容:
<StackPanel Orientation="Vertical"
DockPanel.Dock="Left">
<ListBox Width="180" Height="110"
ItemsSource="{Binding UserCollection}"
SelectedItem="{Binding SelectedUser}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Firstname}"/>
<Run Text="{Binding Lastname}"/>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock DataContext="{Binding SelectedUser}">
<Run Text="Selected:"/>
<Run Text="{Binding Firstname}"/>
<Run Text="{Binding Lastname}"/>
</TextBlock>
</StackPanel>
-
现在让我们运行应用程序。你将在
ListBox中看到正确的值:![图片]()
-
选择
ListBox中的任何项,以在TextBox中查看选定的用户名。更改选择以自动更新 UI:![图片]()
它是如何工作的...
MVVM 由三个重要部分组成——Model、View 和 ViewModel。Model 代表数据;View 是实际的 UI,它显示模型的有关部分;ViewModel 是将所需数据分发给视图的机制。ViewModel 基本上公开属性和命令,并维护视图的相关状态。
如果我们将 MVVM 模式与自行车(如下面的截图所示)进行比较,自行车的 Body 是 View,Fuel 是 Model,自行车的 Engine 是 ViewModel,它通过燃烧/使用模型(Fuel)来移动 View(自行车车身):

在我们的应用程序中,我们使用了 DataContext 来定义 View 和 ViewModel 之间的绑定,然后我们使用它来访问属性。如果你现在导航到 MainWindow.xaml.cs 文件,你将看不到除了代码后置类的构造函数之外的其他代码。
在 MVVM 模式下,我们的目的是保持代码后置文件(MainWindow.xaml.cs)的代码尽可能少(或更少),以便减少 UI 和代码之间的直接绑定。请注意,MVVM 不是一个框架,但在使用此模式时,你可以创建一个框架。例如,来自 GalaSoft 的 MVVMLight (www.mvvmlight.net) 提供了一个完全定制的框架,你可以在应用程序中使用它,以保持开发无烦恼。
在 WPF 应用程序中使用路由命令
路由命令用于在元素层次结构中导航路径。这个过程也被称为冒泡和隧道。RoutedCommand 类实现了 ICommand 接口,并允许将输入手势,如鼠标输入和键盘快捷键,附加到目标。
在本配方中,我们将通过一个简单的示例学习如何使用路由命令。
准备工作
要处理这个配方,我们将使用之前的 MVVM 示例应用程序。启动你的 Visual Studio IDE 并打开项目 CH07.MVVMDemo。在这个例子中,我们将使用 RoutedCommand 来处理添加按钮的点击事件。
如何实现...
按照以下简单步骤将路由命令注册到按钮点击并执行操作:
-
从解决方案资源管理器中,右键单击项目节点并创建一个名为
Commands的文件夹。 -
在“命令”文件夹上右键单击,并按照“添加 | 类...”上下文菜单路径创建一个名为
RoutedCommands.cs的新类。 -
在类实现内部,声明一个类型为
RoutedCommand的静态成员,并将其命名为AddCommand。以下是代码实现:
public class RoutedCommands
{
public static RoutedCommand AddCommand =
new RoutedCommand();
}
- 添加以下命名空间以解析
RoutedCommand类:
using System.Windows.Input;
- 完成这些后,导航到
MainWindowViewModel.cs文件,该文件位于ViewModels文件夹下,并添加一个名为NewUserDetails的UserModel类型属性。我们将使用此属性将 UI 中存在的TextBox控件的Text属性进行绑定。属性实现如下:
private UserModel m_newUserDetails;
public UserModel NewUserDetails
{
get { return m_newUserDetails; }
set
{
m_newUserDetails = value;
OnPropertyChanged("NewUserDetails");
}
}
- 现在,在我们的 ViewModel 构造函数内部,初始化
NewUserDetails属性:
public MainWindowViewModel()
{
UserCollection = new ObservableCollection<UserModel>
{
new UserModel
{
Firstname = "User", Lastname = "One"
},
new UserModel
{
Firstname = "User", Lastname = "Two"
},
new UserModel
{
Firstname = "User", Lastname = "Three"
},
new UserModel
{
Firstname = "User", Lastname = "Four"
},
};
NewUserDetails = new UserModel();
}
- 导航到
MainWindow.xaml,它位于Views文件夹下。按照以下方式修改 XAML,为StackPanel设置DataContext并与TextBox控件创建数据绑定。确保将数据绑定模式设置为TwoWay,否则代码将无法接收到从 UI 收到的更新值:
<StackPanel Orientation="Vertical"
Margin="4 0"
DockPanel.Dock="Right"
DataContext="{Binding NewUserDetails}">
<TextBlock Text="Firstname"/>
<TextBox Text="{Binding Firstname, Mode=TwoWay}"/>
<TextBlock Text="Lastname"/>
<TextBox Text="{Binding Lastname, Mode=TwoWay}"/>
<Button Content="Add"
Margin="0 8"/>
</StackPanel>
- 现在将以下
XMLNS属性添加到 XAML 页面,以便我们可以访问CH07.MVVMDemo.Commands命名空间下的类:
9. What next? We need to create the command binding under the `Window` tag. Add the following XAML code block inside the `Window` tag:
<Window.CommandBindings>
<CommandBinding Command="{x:Static commands:RoutedCommands.AddCommand}"
CanExecute="CanExecute_AddCommand"
Executed="Execute_AddCommand"/>
</Window.CommandBindings>
10. Register the `CanExecute` and `Executed` events, named `CanExecute_AddCommand` and `Execute_AddCommand,` respectively, inside the code-behind class file, which is `MainWindow.xaml.cs` in our case.
11. Navigate back to the `MainWindow.xaml` and associate the command with the `Button` control, as follows:
<Button Content="Add"
Margin="0 8"
Command="{x:Static commands:RoutedCommands.AddCommand}"/>
12. The complete markup changes will look like this:
<StackPanel Orientation="Vertical"
Margin="4 0"
DockPanel.Dock="Right"
DataContext="{Binding NewUserDetails}">
<TextBlock Text="Firstname"/>
<TextBox Text="{Binding Firstname, Mode=TwoWay}"/>
<TextBlock Text="Lastname"/>
<TextBox Text="{Binding Lastname, Mode=TwoWay}"/>
<Button Content="Add"
Margin="0 8"
Command="{x:Static commands:RoutedCommands.AddCommand}"/>
13. Now open the `MainWindow.xaml.cs` file and create a member variable of type `MainWindowViewModel`. Name it `ViewModel` and initialize it as `null`. This will be used to store the reference of the ViewModel from the window resources:
private MainWindowViewModel ViewModel = null;
14. Inside the constructor, grab the associated `ViewModel` reference from the `Resources`:
public MainWindow()
{
InitializeComponent();
ViewModel = Resources["ViewModel"] as
MainWindowViewModel;
if (ViewModel == null)
{
throw new NullReferenceException("ViewModel
can't be NULL");
}
}
15. The `CanExecute_AddCommand` event passes an argument of type `CanExecuteRoutedEventArgs`. It contains a property named `CanExecute`, which is responsible for holding a `boolean` value, indicating whether the `System.Windows.Input.RoutedCommand` associated with this event can be executed on the command target. As we have associated the `AddCommand` with the button, `e.CanExecute = true` will enable the button. In other cases, it will be disabled. So, let's modify the `CanExecute_AddCommand` event to implement this logic:
private void CanExecute_AddCommand(object sender,
CanExecuteRoutedEventArgs e)
{
if (ViewModel != null)
{
var userDetails = ViewModel.NewUserDetails;
e.CanExecute =
!string.IsNullOrWhiteSpace(userDetails.Firstname) &&
!string.IsNullOrWhiteSpace(userDetails.Lastname);
}
}
16. Once that has been done, we need to implement the `Execute` command. Modify the `Execute_AddCommand` event handler, as follows:
private void Execute_AddCommand(object sender,
ExecutedRoutedEventArgs e)
{
ViewModel.UserCollection.Add(ViewModel.NewUserDetails);
ViewModel.SelectedUser = ViewModel.NewUserDetails;
ViewModel.NewUserDetails = new Models.UserModel();
}
17. Let's run the application now. You will see that the Add button is disabled. This is because, as per our logic, the `e.CanExecute` property has been set to `false` as both the `TextBox` fields are empty:

18. Enter some strings into both the `TextBox` fields and press the *TAB* key. It will automatically enable the button control, as follows:

19. Click on Add, which will add the entered value to the collection and reset the `TextBox` fields. As soon as it resets the fields to empty, the button will automatically become disabled until the user fills the fields again:

How it works...
The `RoutedCommand` class falls under the `System.Windows.Input` namespace, and provides two methods named `CanExecute` and `Execute`. The `CanExecute` method indicates whether the command is available, whereas the `Execute` method executes the command.
The `RoutedCommand` objects are basically empty shells and can't contain the implementation. For this to work, they look for a `CommandBinding` object from a target element that indicates the handler of the command. It registers the `CanExecute` and `Execute` methods to fire when the command associates with any control.
For example, in this demonstration, the `AddCommand` associated with the `Button` control has a `CommandBinding`, which denotes its `CanExecute` and `Execute` handler as `CanExecute_AddCommand` and `Execute_AddCommand`. When the button fires the `Click` event, it routes to the command binding to execute the associate command interface.
与动画一起工作
在本章中,我们将介绍以下食谱:
-
在渲染时缩放元素
-
在渲染时旋转元素
-
在渲染时倾斜元素
-
在渲染时移动元素
-
对多个变换进行分组
-
创建基于属性的动画
-
创建基于路径的动画
-
创建基于关键帧的动画
-
为动画添加缓动效果
第九章:简介
Windows Presentation Foundation (WPF) 因其丰富的 图形用户界面 (GUI) 和布局功能而闻名,这使得您能够创建令人惊叹的桌面应用程序。只需通过动画化 UI 元素、变换、屏幕过渡等,就可以使用动画来创建吸引人的 用户界面 (UI)。
在本章中,我们将学习如何使用故事板创建动画。我们首先从帮助您理解各种变换的食谱开始,例如ScaleTransform、RotateTransform、SkewTransform和TranslateTransform。然后我们将继续学习各种类型的动画,例如基于属性的动画、基于路径的动画和基于关键帧的动画。
最后,我们将学习 WPF 4 中引入的各种缓动函数,这些函数可用于在您的线性动画上创建缓动效果,使其看起来非线性。
在渲染时缩放元素
ScaleTransform用于水平或垂直缩放(拉伸或缩小)对象。ScaleX属性用于指定沿 X 轴拉伸或缩放对象的程度,而ScaleY属性用于指定沿 Y 轴拉伸或缩放对象的程度。使用CenterX和CenterY属性,操作基于指向特定坐标点的中心点。
在这个食谱中,我们将学习如何使用缩放变换拉伸或缩小元素。
准备工作
首先,打开您的 Visual Studio 实例,创建一个名为CH08.ScaleTransformDemo的新 WPF App 项目。
如何做到这一点...
按照以下步骤将Image控件添加到应用程序 UI 中,并应用ScaleTransform以缩放图像:
-
从解决方案资源管理器中,右键单击项目节点,创建一个新的文件夹。将其命名为
Images。 -
现在,右键单击
Images文件夹,并从您的系统中添加一个现有的图片。将其命名为image1.png:

-
导航到
MainWindow.xaml页面,并将默认的Grid替换为水平StackPanel。 -
在
StackPanel内部,添加以下Grid,包含两个图像控件。两个图像控件都应该指向Images/image1.png图像文件。第二个图像将设置一个变换以将图像缩放到 80%,如下面的代码片段所示:
<Grid>
<Image Height="300" Width="260"
Margin="4" Opacity="0.2"
Source="Images/image1.png"/>
<Image Height="300" Width="260"
Margin="4"
Source="Images/image1.png">
<Image.RenderTransform>
<ScaleTransform ScaleX="0.8"
ScaleY="0.8"/>
</Image.RenderTransform>
</Image>
</Grid>
- 让我们在
StackPanel内添加一个额外的Grid,使用以下 XAML 标记,其中两个图片缩放至 50%,并分别标记缩放中心位置为 (0,0) 和 (100,100):
<Grid Margin="110 0 0 0">
<Image Height="300" Width="260"
Margin="4" Opacity="0.2"
Source="Images/image1.png">
<Image.RenderTransform>
<ScaleTransform ScaleX="0.5"
ScaleY="0.5"
CenterX="0"
CenterY="0"/>
</Image.RenderTransform>
</Image>
<Image Height="300" Width="260"
Margin="4"
Source="Images/image1.png">
<Image.RenderTransform>
<ScaleTransform ScaleX="0.5"
ScaleY="0.5"
CenterX="100"
CenterY="100"/>
</Image.RenderTransform>
</Image>
</Grid>
- 现在让我们运行应用程序并检查屏幕上各种图片的缩放行为。
它是如何工作的...
RenderTransform 属性可以帮助你为任何 UI 元素设置运行时转换。在这个例子中,我们使用了 ScaleTransform 来缩放应用程序窗口上的图片。
当你运行应用程序时,第一个是默认图片,透明度设置为 20%,而第二个图片则缩放至 80%,透明度为 100%。ScaleX 和 ScaleY 属性用于缩放元素,它使用小数表示比例。例如,0.8 表示 80%,而 1.2 表示缩放 120%:

对于第三张和第四张图片,两者都缩放至 50%。但是,正如你在 UI 上看到的那样,这些图片的位置不同。CenterX 和 CenterY 属性用于设置缩放中心位置。第三张图片的缩放中心设置为 (0,0),而第四张图片的中心位置设置为 (100,100):
<ScaleTransform ScaleX="0.5"
ScaleY="0.5"
CenterX="100"
CenterY="100"/>
在渲染时旋转元素
当你在运行时想要旋转一个元素时,使用 RotateTransform。它围绕由 CenterX 和 CenterY 表示的中心位置旋转元素,角度由 Angle 属性指定的度数。
让我们学习如何使用 RotateTransform 在指定角度旋转 UI 元素。在这个菜谱中,我们将讨论这一点。
准备工作
打开 Visual Studio 并创建一个名为 CH08.RotateTransformDemo 的新项目。确保在创建项目时选择 WPF App 模板。
如何做到这一点...
按照此处提到的步骤应用旋转到 Image 控件:
-
从解决方案资源管理器中,右键单击项目节点并创建一个新的文件夹。将其命名为
Images。 -
现在,右键单击
Images文件夹并从你的系统中添加一个现有图片。命名为image1.png。 -
打开
MainWindow.xaml文件并将现有的Grid替换为一个水平的StackPanel。 -
在
StackPanel内插入以下 XAML 标记来添加两个图片到应用程序窗口。第一个图片的透明度设置为 20%,第二个图片将RotateTransform设置为 45 度角:
<Image Height="300" Width="260"
Margin="4" Opacity="0.2"
Source="Images/image1.png"/>
<Image Height="300" Width="260"
Margin="4"
Source="Images/image1.png">
<Image.RenderTransform>
<RotateTransform Angle="45"/>
</Image.RenderTransform>
</Image>
-
让我们在
StackPanel内添加一个额外的Grid。 -
将另外两张图片添加到新的
Grid面板中。将两张图片的RenderTransform属性设置为具有RotateTransform,角度为45度。 -
如以下 XAML 片段所示,使用
CenterX和CenterY属性设置图像旋转的中心位置。在这个演示中,我们将设置 (0,0) 和 (30,30) 作为相应图像的旋转中心:
<Grid Margin="80 0 0 0">
<Image Height="300" Width="260"
Margin="4" Opacity="0.2"
Source="Images/image1.png">
<Image.RenderTransform>
<RotateTransform Angle="45"
CenterX="0"
CenterY="0"/>
</Image.RenderTransform>
</Image>
<Image Height="300" Width="260"
Margin="4"
Source="Images/image1.png">
<Image.RenderTransform>
<RotateTransform Angle="45"
CenterX="30"
CenterY="30"/>
</Image.RenderTransform>
</Image>
</Grid>
- 完成此操作后,构建项目并运行它。你将在屏幕上看到四张图像,如下面的截图所示:

它是如何工作的...
RotateTransform 允许你通过 Angle 属性指定的一定角度旋转一个元素。在第一张图像中,没有应用任何转换,因此它将显示默认效果。如果你与其他屏幕上的图像进行比较,第二张图像是顺时针旋转了 45 度。第三张和第四张图像也是以 45 度的角度旋转,但有一些细微的差别。
对于第三张图像,旋转是在中心位置(0,0)进行的。对于第四张图像,它是在中心位置(30,30)进行的。以下是差异的展示:

渲染时倾斜元素
在 WPF 平台上,SkewTransform 用于剪切一个元素,使其在 2D 平面上通过添加深度来获得 3D 视觉效果。AngleX 和 AngleY 属性用于指定 X 轴和 Y 轴的倾斜角度,而 CenterX 和 CenterY 属性用于指定中心点的 X 和 Y 坐标。
在本教程中,我们将学习如何将倾斜变换应用于图像。
准备工作
要开始,打开你的 Visual Studio IDE 并创建一个名为 CH08.SkewTransformDemo 的新项目,基于 WPF 应用程序模板。
如何做到这一点...
让我们在应用程序窗口中添加一些图像,并在特定的角度和中心位置上对这些图像应用倾斜。按照以下步骤操作:
-
从解决方案资源管理器中,右键单击项目节点并创建一个新的文件夹。将其命名为
Images。 -
现在右键单击
Images文件夹并从你的系统中添加一个现有图像。将其命名为image1.png。 -
打开
MainWindow.xaml文件并将现有的Grid替换为一个水平的StackPanel。 -
在
StackPanel内插入以下Grid以显示两张图像。第一张图像的透明度设置为 20%,而另一张图像将在 X 和 Y 轴上应用一个50度和5度的倾斜。为了设置这些,请使用AngleX和AngleY属性,如下所示:
<Grid>
<Image Height="300" Width="260"
Margin="4" Opacity="0.2"
Source="Images/image1.png"/>
<Image Height="300" Width="260"
Margin="4"
Source="Images/image1.png">
<Image.RenderTransform>
<SkewTransform AngleX="50"
AngleY="5"/>
</Image.RenderTransform>
</Image>
</Grid>
- 在
StackPanel内再添加一个Grid,并在新的Grid内插入两张图像。将SkewTransform应用于两张图像的 X 和 Y 轴,分别为30度和5度。对于其中一张图像,将倾斜中心位置设置为(0,0),而对于另一张图像,通过指定CenterX和CenterY属性将其设置为(200,-100),如下所示:
<Grid Margin="200 0 0 0">
<Image Height="300" Width="260"
Margin="4" Opacity="0.2"
Source="Images/image1.png">
<Image.RenderTransform>
<SkewTransform AngleX="30"
AngleY="5"
CenterX="0"
CenterY="0"/>
</Image.RenderTransform>
</Image>
<Image Height="300" Width="260"
Margin="4" Opacity="1.0"
Source="Images/image1.png">
<Image.RenderTransform>
<SkewTransform AngleX="30"
AngleY="5"
CenterX="200"
CenterY="-100"/>
</Image.RenderTransform>
</Image>
</Grid>
- 让我们运行应用程序。你将在屏幕上看到图像,如下所示:

它是如何工作的...
当将 AngleX 和 AngleY 设置为 SkewTransform 时,相关元素将分别从 Y 轴和 X 轴逆时针倾斜到指定的角度,该角度以度为单位。
CenterX属性用于设置转换中心的X坐标,而CenterY属性用于设置转换中心的Y坐标。在前面的例子中,当我们指定CenterX和CenterY到图片上时,它改变了以(200,-100)坐标点为中心的倾斜位置,如下面的截图所示:

在渲染时移动元素
TranslateTransform用于在 2D 界面中将一个元素从一个位置移动到另一个位置。X和Y属性用于将元素移动到X和Y轴。在本教程中,我们将学习如何将这种转换应用于一个元素。
准备工作
打开 Visual Studio 并创建一个基于 WPF 应用程序模板的项目,命名为CH08.TranslateTransformDemo。
如何操作...
按照以下简单步骤,通过X和Y属性指定的坐标位置移动一个图片:
-
在进行这项工作之前,我们需要将一个图片文件添加到项目中。从解决方案资源管理器中,右键点击项目节点并创建一个新文件夹。将其命名为
Images。 -
现在右键点击
Images文件夹,并从您的系统中添加一个现有的图片。将其命名为image1.png。 -
打开
MainWindow.xaml文件,并在Grid面板内添加两个图片。第一个图片的透明度为 30%。对于第二个图片,在由X和Y属性指定的(300,80)位置添加一个TranslateTransform,如下面的截图所示:
<Grid VerticalAlignment="Top"
HorizontalAlignment="Left">
<Image Height="300" Width="260"
Margin="4" Opacity="0.3"
Source="Images/image1.png"/>
<Image Height="300" Width="260"
Margin="4"
Source="Images/image1.png">
<Image.RenderTransform>
<TranslateTransform X="300"
Y="80"/>
</Image.RenderTransform>
</Image>
</Grid>
- 就这样!让我们构建并运行应用程序。
工作原理...
当您运行应用程序时,您将在屏幕上看到两个图片。第一个图片,其不透明度为 20%,放置在窗口的左侧。第二个图片,放置在第一个图片上方,已移动到坐标点(300,80),如下面的截图所示:

要设置沿X轴的平移距离,使用TranslateTransform的X属性,这里为300。同样,要设置沿Y轴的平移距离,使用TranslateTransform的Y属性。在我们的例子中为80。
组合多个转换
不必将单个转换应用于单个元素。您可以使用<TransformGroup></TransformGroup>标签将多个转换组合到它上面。在本教程中,我们将学习如何组合多个转换。
准备工作
要开始,请打开 Visual Studio 并创建一个基于 WPF 应用程序模板的新项目,命名为CH08.GroupedTransformsDemo。
如何操作...
让我们按照以下步骤将两个图片添加到应用程序窗口中,并将第二个图片翻转以产生反射效果。这将通过将多个转换组合到该图片上完成:
-
首先,我们需要向项目中添加一个图片。为此,在项目根目录下创建一个名为
Images的文件夹。 -
右键单击“图像”文件夹,并将现有图像添加到其中。命名为
image1.png,它将在 XAML 中作为Images/image1.png可访问。 -
从解决方案资源管理器中,导航到
MainWindow.xaml文件。 -
将现有的
Grid面板替换为水平StackPanel。 -
在其中插入两个
Image控件,并将它们的名称设置为originalImage和flippedImage。 -
现在将两个控件的控制图像源设置为
Images/image1.png,然后设置它们的大小。这将使 XAML 看起来如下:
<StackPanel Orientation="Horizontal"
Margin="10">
<Image x:Name="originalImage"
Source="Images/image1.png"
Height="200" Width="250"/>
<Image x:Name="flippedImage"
Source="Images/image1.png"
Height="200" Width="250"/>
</StackPanel>
- 运行应用程序,将得到以下输出:

-
关闭应用程序并返回到
MainWindow.xaml文件。 -
现在我们将翻转第二个图像(
flippedImage)以产生反射效果。为此,首先将Image控件的RenderTransformOrigin设置为0.5,0.5。 -
现在添加
<Image.RenderTransform>以添加转换标记。在这种情况下,因为我们将要添加多个转换,所以在其中添加一个<TransformGroup>标签。 -
让我们在
<TransformGroup>标签内添加ScaleTransform、SkewTransform、RotateTransform和TranslateTransform,以翻转图像。这是Image的RenderTransform将看起来:
<Image x:Name="flippedImage"
Source="Images/image1.png"
Height="200" Width="250"
RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="1" ScaleX="-1"/>
<SkewTransform AngleY="0" AngleX="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform/>
</TransformGroup>
</Image.RenderTransform>
</Image>
完成更改后,你的 XAML 将看起来如下代码:
<StackPanel Orientation="Horizontal"
Margin="10">
<Image x:Name="originalImage"
Source="Images/image1.png"
Height="200" Width="250"/>
<Image x:Name="flippedImage"
Source="Images/image1.png"
Height="200" Width="250"
RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="1" ScaleX="-1"/>
<SkewTransform AngleY="0" AngleX="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform/>
</TransformGroup>
</Image.RenderTransform>
</Image>
</StackPanel>
- 让我们构建项目并再次运行应用程序。你看到了什么?有一个由翻转第二个图像创建的第一图像的反射。以下是输出截图:

它是如何工作的...
它通过在 <TransformGroup> 标签内定义转换标记来实现。在我们的例子中,我们应用了 ScaleTransform,这创建了一个翻转效果。我们在这里应用的其余其他转换使用默认值。你可以修改它们的值,并检查这在 UI 中的效果。
更多内容...
Visual Studio 提供了一种简单的方法来向任何 UI 元素添加转换。从设计视图中,选择要应用转换的元素,并导航到其属性面板。在这里,你可以找到一个标题为“转换”的展开面板。这用于设置 XAML 中可用的各种转换的值。
如以下截图所示,你可以定义 TranslateTransform、RotateTransform、ScaleTransform、SkewTransform 和 Flip。每个选项卡/部分都包含它可以接受的不同值:

创建基于属性的动画
基于属性的动画用于在指定的时间内将依赖属性从一个值更改为另一个值。在命名空间 System.Windows.Media.Animation 下存在各种动画类,包括 DoubleAnimation、ColorAnimation 和 PointAnimation。这些用于根据正在动画化的属性类型创建动画。
在这个食谱中,我们将学习如何创建基于属性的动画。请记住,只有依赖属性可以在动画期间进行修改。
准备工作
要开始使用这个食谱,我们首先创建一个项目。打开 Visual Studio IDE 并创建一个基于 WPF 应用程序模板的项目,命名为 CH08.PropertyBasedAnimationDemo。
如何做到这一点...
在这个演示中,我们将向应用程序窗口添加一个正方形框。在鼠标悬停时,我们将运行一个故事板来更改框的大小和颜色,然后在鼠标离开时将其重置到初始值。按照以下步骤操作:
-
从解决方案资源管理器,导航到
MainWindow.xaml文件。 -
在 XAML 文件中,您将找到一个默认放置的
Grid面板。让我们在其中添加一个Rectangle控件,并将其Height和Width属性设置为100以使其看起来像一个正方形。 -
给矩形命名为
squareBox,这样我们就可以从我们的Storyboard中识别它。 -
向
Rectangle的背景添加一个SolidColorBrush。设置画笔的颜色并将其命名为squareBoxFillBrush。以下是一个 XAML 片段:
<Grid>
<Rectangle x:Name="squareBox"
Height="100"
Width="100">
<Rectangle.Fill>
<SolidColorBrush x:Name="squareBoxFillBrush"
Color="Black"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
- 由于我们需要向
Rectangle的MouseEnter和MouseLeave事件添加Storyboard动画,让我们使用触发器来控制这些。如图所示,向我们的Rectangle控件添加一个<Rectangle.Triggers></Rectangle.Triggers>元素:
<Grid>
<Rectangle x:Name="squareBox"
Height="100"
Width="100">
<Rectangle.Fill>
<SolidColorBrush x:Name="squareBoxFillBrush"
Color="Black"/>
</Rectangle.Fill>
<Rectangle.Triggers>
</Rectangle.Triggers>
</Rectangle>
</Grid>
-
由于我们将触发动画在
MouseEnter和MouseLeave事件上,请在我们添加的<Rectangle.Triggers></Rectangle.Triggers>元素内添加一个EventTrigger。 -
现在展开触发器以包含
Actions以开始一个Storyboard动画。按照以下方式修改您的 XAML 标记:
<Rectangle.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Rectangle.Triggers>
- 在
MouseEnter事件的Storyboard动画中,我们将更改squareBox矩形控件的大小和颜色。通过使用DoubleAnimation,我们将更改矩形的Height和Width属性,并通过使用ColorAnimation我们将更改Fill颜色。按照以下方式更新Storyboard:
<Storyboard>
<DoubleAnimation Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Height"
To="200"/>
<DoubleAnimation Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Width"
To="400"/>
<ColorAnimation
Storyboard.TargetName="squareBoxFillBrush"
Storyboard.TargetProperty="Color"
To="OrangeRed"
Duration="0:0:1"/>
</Storyboard>
- 类似地,向
Rectangle控件添加另一个EventTrigger,在MouseLeave事件上触发另一个Storyboard以重置大小和颜色。新的标记将如下所示:
<EventTrigger RoutedEvent="MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Height"
To="100"/>
<DoubleAnimation
Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Width"
To="100"/>
<ColorAnimation
Storyboard.TargetName="squareBoxFillBrush"
Storyboard.TargetProperty="Color"
To="Black"
Duration="0:0:1"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
- 现在运行应用程序。您将看到一个黑色背景的正方形,如下面的截图所示:

- 将鼠标悬停在正方形上。这将使正方形变为矩形,并将颜色更改为
OrangeRed。检查大小和颜色的过渡,这将有一个漂亮的动画:

- 现在将鼠标光标从矩形移出。现在会发生什么?矩形将通过一个漂亮的动画重置为正方形。此外,背景颜色将从
OrangeRed更改为Black。
它是如何工作的...
可以通过构造适当的动画类型、指定属性,然后在要动画化的元素上调用BeginStoryboard来手动创建动画。这些属性必须是依赖属性,你想要在动画对象上动画化。
在这个例子中,当MouseEnter事件触发时,以下Storyboard动画将运行。DoubleAnimation和ColorAnimation接受附加属性Storyboard.TargetName和Storyboard.TargetProperty,这使得Storyboard能够在运行时更改目标元素的指定属性:
<Storyboard>
<DoubleAnimation Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Height"
To="200"/>
<DoubleAnimation Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Width"
To="400"/>
<ColorAnimation Storyboard.TargetName="squareBoxFillBrush"
Storyboard.TargetProperty="Color"
To="OrangeRed"
Duration="0:0:1"/>
</Storyboard>
From和To属性用于将属性从指定的值更改为另一个值。尽管设置From字段是可选的,但你需要指定To字段,以便Storyboard能够产生变化。在先前的例子中,动画将从其初始值改变Height、Width和Color。
你也可以为动画设置一个TimeSpan来设定过渡发生的时间。你可以使用Duration属性来设置值。在先前的例子中,从Black颜色过渡到OrangeRed颜色将需要1秒。
类似地,当MouseLeave事件触发时,以下负责重置值的Storyboard将触发,它将To字段设置为其初始值。当Storyboard运行时,你将在屏幕上看到漂亮的过渡动画:
<Storyboard>
<DoubleAnimation Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Height"
To="100"/>
<DoubleAnimation Storyboard.TargetName="squareBox"
Storyboard.TargetProperty="Width"
To="100"/>
<ColorAnimation Storyboard.TargetName="squareBoxFillBrush"
Storyboard.TargetProperty="Color"
To="Black"
Duration="0:0:1"/>
</Storyboard>
这些是在大多数动画类型中都会找到的一些常见属性:
-
From: 它用于指示动画的起始值。如果你省略了From字段,它将使用依赖属性的当前值。 -
To: 这是动画的目标值,你应该填写它。如果你省略它或放入当前值,该动画将没有效果。 -
Duration: 这是动画的持续时间。除了hh:mm:ss.ms格式的TimeSpan类型值之外,它还可以包含两个特殊值——Duration.Automatic(默认值)和Duration.Forever。当你指定Duration.Forever时,它将无限期运行。在 XAML 中,hh:mm:ss.ms格式通常使用。 -
FillBehavior: 它表示动画结束时动画的行为。默认值FillEnd要求保持最后一个动画值;在动画之前使用的上一个值将没有效果。另一个值Stop将销毁动画并将属性还原到没有动画的值。 -
BeginTime: 当你想要在动画开始前设置延迟时,你可以使用此属性来定义延迟时间。 -
AutoReverse: 如果你想要动画自动反转,在动画结束后,你可以将其设置为true。当启用时,动画的总持续时间将有效加倍。 -
SpeedRatio: 它允许你加快或减慢动画持续时间。 -
RepeatBehavior:此属性指定动画结束后要重复的次数或时间。当将AutoReverse设置为true时,这通常很有用。
创建路径动画
除了我们在上一个菜谱中学到的基于属性的动画外,WPF 还支持沿由PathGeometry指定的路径运行的路径动画。
在这个菜谱中,我们将学习如何使用PathGeometry来沿路径动画化一个元素。
准备工作
让我们从创建一个新的 WPF 应用程序项目开始。命名为CH08.PathBasedAnimationDemo。
如何做...
在这个演示中,我们将使用一个圆形在按钮点击时进行动画。动画将基于由一组几何坐标指定的路径执行。让我们按照以下步骤构建它:
-
从解决方案资源管理器中,导航到
MainWindow.xaml文件。 -
文件中会存在一个默认的
Grid面板。让我们通过指定以下行定义将其分为两行:
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
-
让我们在第一行内放置一个
Canvas面板。添加一个Ellipse,其Height="30"和Width="30"以形成圆形。给它命名为circle。 -
设置
Ellipse的填充颜色并将其放置在画布的(100,100)坐标位置。以下是完整的标记供您参考:
<Canvas Grid.Row="0">
<Ellipse x:Name="circle"
Height="30"
Width="30"
Canvas.Left="100"
Canvas.Top="100"
Fill="OrangeRed"/>
</Canvas>
- 我们将使用这个
Ellipse在画布中沿着路径进行动画。为此,我们需要定义一个PathGeometry。要做到这一点,请在Window标签内作为Resources添加以下内容,以定义PolyLineSegment点作为坐标集合:
<Window.Resources>
<PathGeometry x:Key="animationPath">
<PathFigure IsClosed="True"
StartPoint="100,100">
<PolyLineSegment Points="150,150 400,200 300,50 200,200 100,100 400,100 50,50 400,150 100,250, 100,50" />
</PathFigure>
</PathGeometry>
</Window.Resources>
- 让我们在
Window内添加一个Button控件,它将被用来触发动画。将按钮包裹在一个水平StackPanel中,并将其放置在Grid的第二行中:
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="10">
<Button Content="Animate"
Width="100">
</Button>
</StackPanel>
- 现在我们需要捕获
Button.Click事件。为此,我们需要一个EventTrigger来定义针对Button控件。一旦触发器被触发,动作就是开始一个Storyboard来执行动画。让我们修改Button控件以设置此触发器以启动故事板。以下是参考代码:
<Button Content="Animate"
Width="100">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard x:Name="Animate"
AutoReverse="True">
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
-
现在是时候给我们在
Button.Click事件处理器中添加的Storyboard添加一些路径动画了。让我们使用DoubleAnimationUsingPath来绑定我们添加到Window.Resources标签中的PathGeometry。 -
将
Storyboard.TargetName设置为circle,将Storyboard.TargetProperty设置为(Canvas.Left)和(Canvas.Top)以在 X 和 Y 轴上创建动画。以下是代码:
<DoubleAnimationUsingPath Duration="0:0:5"
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
PathGeometry="{StaticResource animationPath}"
Source="X"/>
<DoubleAnimationUsingPath Duration="0:0:5"
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Top)"
PathGeometry="{StaticResource animationPath}"
Source="Y"/>
- 让我们构建应用程序并运行它。您将看到以下带有圆形和按钮的 UI:

- 点击动画按钮并观察圆形的位置。您将在屏幕上看到一个漂亮的动画流程:

它是如何工作的...
基于路径的动画使用PathGeometry作为路径来创建动画。在我们的示例中,我们将其定义在Window.Resources标签下作为animationPath,它表示一个 2D 界面中的路径,作为一个坐标点的集合。请参阅以下代码片段:
<PathGeometry x:Key="animationPath">
<PathFigure IsClosed="True"
StartPoint="100,100">
<PolyLineSegment Points="150,150 400,200 300,50 200,200 100,100 400,100 50,50 400,150 100,250, 100,50" />
</PathFigure>
</PathGeometry>
我们在故事板动画中使用的DoubleAnimationUsingPath使用Canvas.Left和Canvas.Top作为沿X和Y轴动画的目标属性。当Storyboard播放时,目标元素从一个坐标点移动到另一个坐标点,在两点之间有一个平滑的动画。
创建基于关键帧的动画
WPF 中的关键帧动画允许您使用超过两个目标值来动画化一个元素,并控制动画的插值方法。关键帧动画没有From/To属性,我们可以用它们来设置其目标值。
动画的目标值使用关键帧对象来描述,您需要将它们添加到动画的KeyFrames集合中。当动画运行时,它会在您指定的关键帧之间进行转换。
在本食谱中,我们将学习如何创建基于关键帧的动画,并将其用于我们的应用程序。
准备工作
我们首先需要创建一个项目。打开 Visual Studio IDE,并基于 WPF 应用程序模板创建一个名为CH08.KeyFrameBasedAnimationDemo的新项目。
如何操作...
让我们按照以下步骤创建一个基于关键帧的动画:
-
打开
MainWindow.xaml文件。 -
在
Grid内部添加两行,通过指定RowDefinitions:
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
-
在
Grid的第 0 行添加一个Canvas面板。 -
在画布内插入一个
Ellipse,并将其Height和Width属性设置为30,以便它显示为一个圆圈。 -
为椭圆提供名称,并将其定位到
Canvas面板上的(50,100)坐标位置,然后用OrangeRed颜色填充背景。 -
在一个水平
StackPanel内部插入一个Button控件,并将面板放置在第二行。以下是为此演示生成的 UI 的完整 XAML:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Canvas Grid.Row="0">
<Ellipse x:Name="circle"
Height="30"
Width="30"
Canvas.Left="50"
Canvas.Top="100"
Fill="OrangeRed"/>
</Canvas>
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Center"
Margin="10">
<Button Content="Animate"
Width="100">
</Button>
</StackPanel>
</Grid>
-
按下按钮时,我们需要围绕应用程序窗口动画化圆圈。为此,我们将使用一个
EventTrigger。定义一个Button.Click事件的触发器,并将其操作设置为开始一个故事板。 -
将
Storyboard的AutoReverse属性设置为True。以下是当用户触发按钮点击事件时启动故事板的代码:
<Button Content="Animate"
Width="100">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard x:Name="Animate"
AutoReverse="True">
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
</Button>
-
在故事板内部,我们需要定义一个基于关键帧运行的动画。这是通过在
DoubleAnimationUsingKeyFrames元素内部添加一个或多个LinearDoubleKeyFrame(s)来完成的。在Storyboard定义内部插入两个DoubleAnimationUsingKeyFrames实例。 -
将
DoubleAnimationUsingKeyFrames的Storyboard.TargetName属性设置为circle。 -
将
AutoReverse设置为True,将RepeatBehavior设置为Forever。 -
对于第一个
DoubleAnimationUsingKeyFrames,将Storyboard.TargetProperty设置为(Canvas.Left)。对于另一个,将其设置为(Canvas.Top)。 -
通过向
DoubleAnimationUsingKeyFrames添加一个或多个LinearDoubleKeyFrame实例来定义关键帧。设置它们的KeyTime和Value。以下是完整的代码:
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
AutoReverse="True"
RepeatBehavior="Forever">
<LinearDoubleKeyFrame Value="50"
KeyTime="0:0:0" />
<LinearDoubleKeyFrame Value="450"
KeyTime="0:0:1" />
<LinearDoubleKeyFrame Value="450"
KeyTime="0:0:3" />
<LinearDoubleKeyFrame Value="250"
KeyTime="0:0:5" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Top)"
AutoReverse="True"
RepeatBehavior="Forever">
<LinearDoubleKeyFrame Value="100"
KeyTime="0:0:0" />
<LinearDoubleKeyFrame Value="200"
KeyTime="0:0:1" />
<LinearDoubleKeyFrame Value="50"
KeyTime="0:0:3" />
<LinearDoubleKeyFrame Value="150"
KeyTime="0:0:5" />
</DoubleAnimationUsingKeyFrames>
- 完成后,让我们运行应用程序。您将在屏幕上的(
50,100)坐标点看到一个圆。还有一个标记为动画的按钮,如下面的截图所示:

- 点击动画按钮以启动定义的故事板。查看圆的运动和速度:

它是如何工作的...
当关键帧动画开始时,它将按照它们根据KeyTime属性定义的顺序遍历指定的关键帧。如果不存在时间0(初始点)的关键帧,动画将在目标属性的当前值和集合中定义的第一个关键帧的Value之间创建过渡。
如果动画的Duration设置为Automatic或设置为最后一个关键帧的时间,则动画结束。
在前面的演示中,第一个关键帧(在时间0)将动画的输出值设置为Canvas.Left="50"和Canvas.Top="100"。在下一个关键帧(在1 sec时),输出值设置为坐标点(450,200),您将看到在(50,100)和(450,200)点之间的平滑过渡。同样,在第三秒和第四秒,圆从(450,200)过渡到(450,50),然后过渡到坐标点(250,150)。
由于定义的Storyboard具有设置为True的AutoReverse属性,动画将有一个反向过渡,将圆从终点(250,150)移动到初始起点(50,100),经过坐标点(450,50)和(450,200)。
更多内容...
基于关键帧的动画类类型不仅限于DoubleAnimationUsingKeyFrames。您可以使用以下任何关键帧动画类来构建您的故事板:
-
布尔值:
BooleanAnimationUsingKeyFrames -
字节:
ByteAnimationUsingKeyFrames -
颜色:
ColorAnimationUsingKeyFrames -
十进制:
DecimalAnimationUsingKeyFrames -
双精度浮点数:
DoubleAnimationUsingKeyFrames -
Int16:
Int16AnimationUsingKeyFrames -
Int32:
Int32AnimationUsingKeyFrames -
Int64:
Int64AnimationUsingKeyFrames -
矩阵:
MatrixAnimationUsingKeyFrames -
对象:
ObjectAnimationUsingKeyFrames -
点:
PointAnimationUsingKeyFrames -
四元数:
QuaternionAnimationUsingKeyFrames -
矩形:
RectAnimationUsingKeyFrames -
3D 旋转:
Rotation3DAnimationUsingKeyFrames -
单精度浮点数:
SingleAnimationUsingKeyFrames -
字符串:
StringAnimationUsingKeyFrames -
大小:
SizeAnimationUsingKeyFrames -
厚度:
ThicknessAnimationUsingKeyFrames -
3D 向量:
Vector3DAnimationUsingKeyFrames -
向量:
VectorAnimationUsingKeyFrames
为动画添加缓动效果
基于属性的动画是线性的,而基于关键帧的动画是非线性的,用于创建贝塞尔插值。但创建此类效果并不容易。为了克服这一点,WPF 4 引入了缓动函数,将线性动画转换为非线性动画,并为这些动画对象添加一些缓动效果。
在本食谱中,我们将通过一个合适的示例学习如何做到这一点。
准备工作
要开始向动画添加缓动效果,让我们打开 Visual Studio 并创建一个名为CH08.EasingEffectDemo的新项目。在创建项目时选择 WPF 应用程序模板。
如何操作...
让我们按照以下步骤创建具有各种缓动效果的动画:
-
从解决方案资源管理器中打开
MainWindow.xaml文件。 -
通过对现有
Grid面板应用ColumnDefinition将其分为两列:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
-
现在,在
Grid内部放置一个Canvas面板,并将其Grid.Column属性设置为0(零)。 -
在画布内部添加一个
Ellipse(命名为circle),并将其Height和Width属性设置为80以使其看起来是圆形的。设置其Fill颜色属性,并将其定位到(150,80)位置。以下是代码片段:
<Canvas Grid.Column="0">
<Ellipse x:Name="circle"
Height="80"
Width="80"
Fill="OrangeRed"
Canvas.Left="150"
Canvas.Top="80"/>
</Canvas>
-
现在在
Grid内部添加一个垂直的StackPanel,并将其Grid.Column属性设置为1。 -
在
StackPanel内部添加三个单选按钮(GroupName="AnimationSelector"),并在RadioButton.Checked事件触发时添加一个Storyboard动画。 -
添加一个简单的
DoubleAnimation来水平移动圆圈,通过将其Storyboard.TargetProperty设置为(Canvas.Left)。 -
现在将动画扩展以添加缓动效果。插入一个
<DoubleAnimation.EasingFunction></DoubleAnimation.EasingFunction>属性来保存我们即将添加的效果。 -
让我们在三个单选按钮上添加一个
BackEase效果。此类效果表示一个在动画开始之前稍微收缩运动并在指定路径中动画化的缓动函数,表示为以下函数——f(t) = t3 - t * a * sin(t * pi)。将函数的Amplitude属性设置为0.3,将EasingMode属性分别设置为EaseIn、EaseOut和EaseInOut。完整的代码如下:
<StackPanel Grid.Column="1"
Margin="10">
<RadioButton GroupName="AnimationSelector"
Content="BackEase - EaseIn"
Margin="4">
<RadioButton.Triggers>
<EventTrigger
RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseIn"
Amplitude="0.3"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
<RadioButton GroupName="AnimationSelector"
Content="BackEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger
RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseInOut"
Amplitude="0.3"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
<RadioButton GroupName="AnimationSelector"
Content="BackEase - EaseOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger
RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<BackEase EasingMode="EaseOut"
Amplitude="0.3"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
</StackPanel>
- 让我们运行应用程序。您将在屏幕上看到以下 UI:

- 现在更改单选按钮的选择,并观察添加到圆形对象动画中的效果。
它是如何工作的...
动画缓动将一个函数应用于动画值,以将线性动画转换为非线性动画。一个由EasingMode属性定义的模式选项允许您设置何时应用缓动函数。这可以是开始时(EaseIn)、结束时(EaseOut)或两者(EaseInOut)。
在前面的示例中,我们为动画定义了一个具有不同缓动模式的BackEase函数,当单选按钮的Checked事件触发时,它将进行动画处理。
下面的图表展示了不同EasingMode值的BackEase效果:

还有更多...
它不仅限于BackEase函数,还可以是 WPF 中定义的 11 个内置缓动函数中的任何一个。完整的列表如下:
-
BackEase -
BounceEase -
CircleEase -
CubicEase -
ElasticEase -
ExponentialEase -
PowerEase -
QuadraticEase -
QuarticEase -
QuinticEase -
SineEase
所有这些列出的缓动函数都源自抽象类EasingFunctionBase,该类实现了IEasingFunction接口。它包含一个Ease方法并添加了EasingMode属性,该属性指示函数是否应用于动画的开始(EaseIn)、结束(EaseOut)或两者(EaseInOut)。
让我们修改现有的 UI,向动画添加更多内置的缓动函数。为了演示这一点,我们将在StackPanel内添加 10 个更多的单选按钮,并将缓动函数应用于每个单选按钮,如以下章节所述。
BounceEase
这种类型的函数为目标创建一个动画弹跳效果。可以使用Bounces和Bounciness属性来控制弹跳。Bounces属性表示弹跳次数,而Bounciness属性定义了弹跳动画的弹跳程度。Bounciness的值越低,弹跳动画越高;Bounciness的值越高,动画的弹跳越低。
在以下示例中,让我们将BounceEase函数应用于DoubleAnimation以创建弹跳效果。让我们在StackPanel内添加以下RadioButton:
<RadioButton GroupName="AnimationSelector"
Content="BounceEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<BounceEase EasingMode="EaseInOut"
Bounces="2"
Bounciness="2"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
下面的图表展示了不同EasingMode值的BounceEase效果:

CircleEase
这代表了一个创建动画,使用圆形函数加速/减速的缓动函数,表示为以下函数f(t) = 1 - sqrt(1 - t2)。
让我们在StackPanel内添加以下RadioButton以创建具有圆形缓动效果的动画:
<RadioButton GroupName="AnimationSelector"
Content="CircleEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
下面的图表展示了不同EasingMode值的CircleEase效果:

CubicEase
这将创建一个使用公式f(t) = t3加速/减速的动画,其中EasingMode可以通过设置EaseIn、EaseOut或EaseInOut的值来应用于控制加速、减速或两者,以控制动画的开始(EaseIn)、结束(EaseOut)或两者(EaseInOut)。
让我们在StackPanel内添加以下RadioButton以创建具有加速CubicEase函数的动画:
<RadioButton GroupName="AnimationSelector"
Content="CubicEase - EaseIn"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseIn"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
下面的图表展示了不同EasingMode值的CubicEase效果:

ElasticEase
正如其名所示,它代表一个创建动画的缓动函数,该动画类似于弹簧来回振荡直到静止。Oscillations 属性可以用来获取/设置目标在动画目的地来回滑动的次数。Springiness 属性可以用来定义弹簧的刚度。Springiness 的值越小,表示作用中的弹簧越硬。
为了演示,让我们在 StackPanel 内添加以下 RadioButton 来创建一个具有 Oscillations="3" 和 Springiness="1" 的 ElasticEase 动画:
<RadioButton GroupName="AnimationSelector"
Content="ElasticEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<ElasticEase EasingMode="EaseInOut"
Oscillations="3"
Springiness="1"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
以下图表展示了 EasingMode 的不同值,用于 ElasticEase 效果:

ExponentialEase
这种类型的缓动函数通过指数公式 f(t) = [[e(at) - 1] / [e(a) - 1]] 创建动画,加速/减速。Exponent 属性用于确定动画的插值;而 EasingMode 属性用于加速和减速目标控件的动画。
为了演示这一点,在 StackPanel 内添加以下 RadioButton 控件,这将创建一个具有插值值 5 的减速指数缓动效果:
<RadioButton GroupName="AnimationSelector"
Content="ExponentialEase - EaseOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode="EaseOut"
Exponent="5"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
以下图表展示了 EasingMode 的不同值,用于 ExponentialEase 效果:

PowerEase
它定义了一个缓动函数,该函数通过公式 f(t) = tp 创建动画,其中 p 等于 Power 属性的值。与其他缓动函数一样,你可以添加一个缓动模式来指定动画是加速还是减速。
在这个演示中,将以下定义 PowerEase 缓动函数的 RadioButton 添加到指定的 DoubleAnimation 中:
<RadioButton GroupName="AnimationSelector"
Content="PowerEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<PowerEase EasingMode="EaseInOut"
Power="12"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
你可以使用 PowerEase 来替代 QuadraticEase [f(t) = t²],CubicEase [f(t) = t³],QuarticEase [f(t) = t⁴],和 QuinticEase [f(t) = t⁵] 类型的缓动函数。
以下图表展示了 EasingMode 的不同值,用于 PowerEase 效果:

QuadraticEase
它通过公式 f(t) = t² 创建一个加速/减速的动画。你可以通过指定 Power="2" 来使用 PowerEase 创建相同的行为。在这个例子中,我们将学习如何将 QuadraticEase 函数添加到动画中。在定义的 StackPanel 内添加以下标记:
<RadioButton GroupName="AnimationSelector"
Content="QuadraticEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<QuadraticEase
EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
以下图表展示了 EasingMode 的不同值,用于 QuadraticEase 效果:

QuarticEase
与QuadraticEase类似,你也可以定义QuarticEase来创建一个使用公式f(t) = t4进行加速/减速的动画。你可以通过指定Power="4"使用PowerEase来创建相同的行为。让我们在我们的StackPanel中添加以下标记来定义具有所述缓动函数的动画:
<RadioButton GroupName="AnimationSelector"
Content="QuarticEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<QuarticEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
以下图表展示了QuarticEase效果的EasingMode的不同值:

QuinticEase
如果你想要将QuinticEase效果添加到你的缓动函数中,请将其添加到你的动画中。它使用公式f(t) = t5进行加速/减速。你可以通过指定Power="5"使用PowerEase来创建相同的效果。将以下RadioButton添加到我们的StackPanel中,以定义具有QuinticEase缓动函数的动画:
<RadioButton GroupName="AnimationSelector"
Content="QuinticEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
以下图表展示了QuinticEase效果的EasingMode的不同值:

SineEase
这代表一个创建动画的缓动函数,该动画使用正弦公式f(t) = [1 - [sin(1 - t) * [pi / 2]]]进行加速和/或减速。将EasingMode属性添加到StackPanel中,以加速和/或减速效果。让我们添加以下代码:
<RadioButton GroupName="AnimationSelector"
Content="SineEase - EaseInOut"
Margin="4">
<RadioButton.Triggers>
<EventTrigger RoutedEvent="RadioButton.Checked">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation
Storyboard.TargetName="circle"
Storyboard.TargetProperty="(Canvas.Left)"
To="350">
<DoubleAnimation.EasingFunction>
<SineEase EasingMode="EaseInOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</RadioButton.Triggers>
</RadioButton>
以下图表展示了SineEase效果的EasingMode的不同值:

一旦你准备好了,让我们构建项目并运行它。你现在将看到以下 UI,其中在右侧面板内包含额外的单选按钮:

更改单选按钮的选择,以查看与每个缓动函数关联的动画。
使用 WCF 服务
在本章中,我们将介绍以下食谱:
-
创建 WCF 服务
-
自托管 WCF 服务
-
在 IIS 服务器上托管 WCF 服务
-
在 WPF 应用程序中集成 WCF 服务
第十章:简介
在现代世界,企业应用程序是面向消费者的企业的关键。用户通过一个或多个设备连接到外部世界。为了成功做到这一点,业务需要共享服务,这些服务可以被所有这样的设备消费。
面向服务的架构(SOA)是企业遵循的设计原则,用于定义明确的服务,使用一组通用的合同。这些服务中的每一个都可以独立于其他服务进行修改,并由外部世界消费。
Windows Communication Foundation(WCF)是一个用于构建面向服务应用程序的框架。使用 WCF,您可以从一个端点异步发送数据/消息到另一个端点。您可以在 IIS 或直接在应用程序中托管服务端点。通过此服务端点传递的消息可以是作为 XML 发送的单一字符或单词,也可以是复杂的二进制数据流。
WCF 已被广泛接受为创建 Web 服务的标准,它支持多种协议和端点。在 WCF 中,有三个重要的事情需要您记住;这些事情通常被称为 WCF 的 ABC。WCF 端点的 ABC 定义了以下元素:
-
A 代表 Address,它指定服务所在的位置。这通常遵循 URL 格式,如
schema://domain[:port]/path,例如http://www.kunal-chowdhury.com:8080/Services、https://www.kunal-chowdhury.com:8050/Services或http://192.168.0.1/Services。 -
B 代表 Binding,这基本上是一组元素,对应于通道堆栈中位于传输和协议通道中的元素,用于定义在服务端和客户端如何处理消息。
-
C 代表 Contract,这仅仅是客户端和服务器之间关于通过通道传递的结构(数据合同)和内容(消息合同)的协议。
在本章中,我们将学习如何创建 WCF 服务,托管它们,并将它们集成到 WPF 应用程序中,以便对定义的端点进行服务调用。由于本书不是关于 WCF 的,我们只会讨论基本概念,以帮助您入门。
请确保 ASP.NET 和 WCF 已正确安装并注册。为了确认,请打开 Visual Studio 2017 安装程序,并确保 ASP.NET 和 Web 开发工作负载以及 Windows Communication Foundation 组件已经安装。
如果它们还没有被选中,请选择它们,并修改安装:

创建 WCF 服务
WCF 服务是一个用于处理业务交易的安全服务,它向他人提供当前数据,通过将使用 Windows Workflow Foundation 实现的工作流作为 WCF 服务公开。它提供了一个单一的编程模型,以利用功能创建一个针对所有分布式技术的统一解决方案。这意味着您可以编写一次服务,并通过任何格式(默认为 SOAP)在任意传输协议(即 HTTP、TCP、MSMQ、命名管道等)上公开不同的端点以交换消息。
SOAP(简单对象访问协议)是首选模型之一,其中服务器和客户端之间的通信是通过基于 XML 的数据进行。
在本食谱中,我们将了解 数据合同、数据成员、服务合同、操作合同,在创建和连接到 WCF 服务时需要考虑这些内容。当服务引用被引入应用程序项目时,开发者只需配置具有适当端点地址的服务。让我们通过创建一个简单的、基本的 WCF 服务来演示这一点。
准备工作
要开始,以管理员权限打开 Visual Studio IDE。在将服务部署到服务器时,这通常很有用。
如何操作...
按照以下步骤创建一个简单的 WCF 服务,我们将在本章后面将其集成到 WPF 应用程序中:
- 首先,创建一个名为
EmployeeService的新项目。在创建项目时使用 WCF 服务应用程序模板。您可以在 WCF 模板类别下找到它,如下面的截图所示:

-
Visual Studio 默认情况下,在项目内部创建了三个服务文件(
IService1.cs、Service1.svc和Service1.svc.cs)。由于我们将从头开始创建自己的服务,从解决方案资源管理器中,让我们删除这三个文件:![图片]()
-
让我们在项目节点内部创建两个文件夹,并将它们命名为
DataModels和Services。这是可选的,但保持代码文件组织良好是一个好主意:

-
现在,右键单击 DataModels 文件夹,并按照上下文菜单中的“添加 | 类...”选项创建一个名为
Employee的新类文件。 -
在
Employee.cs文件的类实现中,添加一些类型为string的公共属性,并分别命名为ID、FirstName、LastName和Designation。 -
将属性
[DataContract]设置在类级别,以指定该类型定义或实现了数据合同,并且可以被序列化器(如System.Runtime.Serialization.DataContractSerializer)序列化。 -
将属性
[DataMember]设置在您希望成为数据合同一部分的属性上,并通过System.Runtime.Serialization.DataContractSerializer标记为可序列化。 -
您需要解析命名空间
System.Runtime.Serialization,以便使用DataContract和DataMember属性:

- 这是完整的代码:
using System.Runtime.Serialization;
namespace EmployeeService.DataModels
{
[DataContract]
public class Employee
{
[DataMember]
public string ID { get; set; }
[DataMember]
public string FirstName { get; set; }
[DataMember]
public string LastName { get; set; }
[DataMember]
public string Designation { get; set; }
}
}
-
现在,右键单击“服务”文件夹,并按照上下文菜单项“添加 | 新项...”创建一个新的服务定义。
-
从“添加新项”对话框中选择 WCF 服务作为模板。给它一个名称(在我们的例子中是
EmployeeService),然后点击添加按钮,如下面的截图所示:

-
这将在
Services文件夹下创建三个文件:IEmployeeService.cs、EmployeeService.svc和EmployeeService.svc.cs:![图片]()
-
从解决方案资源管理器导航到
IEmployeeService.cs文件,并添加以下using命名空间声明:
using EmployeeService.DataModels;
using System.Collections.Generic;
using System.ServiceModel;
- 现在将类定义替换为以下代码片段,它将包含三个操作合约
GetEmployeeByID、GetEmployees和InsertEmployee。将接口标记为[ServiceContract],并将方法标记为[OperationContract]。以下是代码片段供参考:
[ServiceContract]
public interface IEmployeeService
{
[OperationContract]
Employee GetEmployeeByID(string empID);
[OperationContract]
List<Employee> GetEmployees();
[OperationContract]
void InsertEmployee(Employee employee);
}
- 现在,从解决方案资源管理器导航到
EmployeeService.svc.cs文件,并创建一个类型为List<Employee>的static成员变量。让我们称它为m_employees,它将用作我们演示应用程序的静态数据源:
private static List<Employee> m_employees = new List<Employee>();
- 让我们实现接口
IEmployeeService,如下所示:

- 修改方法定义以执行按名称/功能进行的操作。让我们修改它们,它们将看起来像这样:
public class EmployeeService : IEmployeeService
{
private static List<Employee> m_employees = new List<Employee>();
public Employee GetEmployeeByID(string empID)
{
return m_employees.First(emp => emp.ID.Equals(empID));
}
public List<Employee> GetEmployees()
{
return m_employees;
}
public void InsertEmployee(Employee employee)
{
m_employees.Add(employee);
}
}
- 就这样!您的 WCF 服务
EmployeeService现已准备好托管,以便应用程序可以消费它。要检查服务是否可以正常运行,请构建项目,然后在解决方案资源管理器中右键单击EmployeeService.svc文件,并点击“在浏览器中查看(浏览器名称)”,在我们的例子中是“在浏览器中查看(Firefox)”:

- 这将启动服务并显示消息“服务已在服务器上托管”。
需要注意的是,如果您从 Visual Studio 运行服务,它将需要管理员权限来打开指定的端口并托管服务。如果您尚未提供管理员权限,请使用“以管理员身份运行”重启 Visual Studio。
- 一旦服务已在
localhost上托管,这将加载浏览器窗口中的 SVC 文件,它将看起来如下截图所示,这告诉我们服务正在正常运行且没有问题:

- 每个服务都提供了一个 Web 服务描述语言(WSDL),它定义了包括元数据在内的公共接口,类似于 接口定义语言(IDA)。点击链接以生成服务的 WSDL。如果您的浏览器没有在屏幕上显示生成的 WSDL,请复制链接,并在 Internet Explorer 中运行它,这将给出以下 XML 输出:

它是如何工作的...
在这个简单的 WCF 服务中,我们使用了少量属性。让我们更深入地了解每一个。
DataContract 属性
数据契约是客户端和服务之间的一种正式协议,抽象地描述了要交换的数据。在 WCF 中,这是序列化对象并将其准备好在客户端和服务之间传递的最常见方式。这是通过使用[DataContract]属性标记类来实现的。
值得注意的是,序列化并不局限于与类名和/或类中的属性名完全匹配。您只需简单地使用DataContract和DataMember属性来定义它们在序列化中的名称。例如,考虑以下代码片段:
[DataContract (Name = "Employee")]
public class EmployeeModel { ... }
在前面的代码片段中,尽管类名为EmployeeModel,但由于使用了属性中的Name属性进行名称映射,该类将以Employee名称进行序列化。
DataMember 属性
与此相反,[DataMember]属性指定成员是数据契约的一部分,并且可以通过DataContractSerializer进行序列化。在定义数据成员属性时,您可以使用以下属性:
-
Name:它定义了数据成员的名称 -
Order:它设置成员的序列化和反序列化顺序 -
TypeId:它为派生类中的此属性设置一个唯一的标识符 -
IsRequired:此属性获取或设置一个值,指示在反序列化期间成员必须存在 -
EmitDefaultValue:当定义时,此属性值指定是否序列化数据成员的默认值
您应该将[DataMember]属性与[DataContract]属性一起应用,以标识类型中属于数据契约的成员。
ServiceContract 属性
[ServiceContract]属性用于定义提供服务的接口。一个服务应该至少有一个由[ServiceContract]属性装饰的服务契约。以下属性可以与ServiceContractAttribute一起使用:
-
ConfigurationName:它指定了配置文件中服务元素的名称。 -
Name:这指定了 WSDL 元素中契约的名称。 -
Namespace:这指定了 WSDL 元素中契约的命名空间。 -
SessionMode:这指定了契约是否需要支持会话的绑定。它可以有以下三个值之一:Allowed(指定契约支持会话)、NotAllowed(指定契约不支持会话)和Required(指定契约不需要会话)。 -
CallbackContract:此属性指定双工会话中的返回契约。 -
ProtectionLevel:这指定了操作在运行时所需的消息级安全。它可以有三种类型:None(仅简单身份验证)、Sign(签名数据以帮助确保数据完整性)和EncryptAndSign(加密并签名数据以确保传输数据的完整性和机密性)。 -
HasProtectionLevel:这表示是否已显式设置ProtectionLevel属性。
OperationContract 属性
[OperationContract] 属性用于定义服务合同的方法。它放置在您希望包含在服务合同中的方法上。以下属性可以用来控制操作的架构:
-
Action:此属性指定了唯一标识操作的动作用户。 -
ReplyAction:这指定了操作回复消息的动作。 -
AsyncPattern:这表示操作可以异步调用。 -
ProtectionLevel:这指定了操作在运行时所需的消息级安全。它可以有三种类型之一——None(仅简单身份验证)、Sign(签名数据以帮助确保数据完整性)和EncryptAndSign(加密并签名数据以确保传输数据的完整性和机密性)。 -
HasProtectionLevel:这表示是否已显式设置ProtectionLevel属性。 -
IsOneWay:此属性指示操作由一个输入消息组成,没有关联的输出消息。 -
IsInitiating:这指定了此操作是否可以是会话中的初始操作。 -
IsTerminating:这指定了在操作完成后,WCF 是否尝试终止当前会话。
自托管 WCF 服务
要使用 WCF 服务,您需要在一个运行时环境中托管它,这样服务宿主就可以监听来自客户端的请求,将请求直接导向服务,并将响应发送回客户端。使用宿主,您可以启动和停止服务。
如果您想自托管一个服务,您必须创建 System.ServiceModel.ServiceHost 类的一个实例,并使用端点进行配置。这可以通过代码或配置文件完成。一旦宿主准备就绪,任何客户端都可以通过指定的 URL 访问服务。
自托管可以在任何托管应用程序中完成,例如控制台应用程序、Windows 服务、Windows 窗体应用程序或 Windows Presentation Foundation(WPF)应用程序。在本菜谱中,我们将学习如何在控制台应用程序中自托管 WCF 服务并执行它。
准备工作
要开始,让我们以管理员权限启动 Visual Studio。现在,打开我们之前在上一道菜谱中创建的项目 CH09.EmployeeService。确保项目构建成功,并且服务在浏览器中正确启动。记下服务 URL 以供参考,我们将在本菜谱的后续部分使用它。
如何操作...
让我们按照以下步骤创建一个自托管的控制台应用程序:
-
首先,在解决方案内添加一个类型为控制台应用程序的新项目,并将其命名为
CH09.SelfHostingDemo。 -
现在,右键单击“引用”节点,并添加服务的项目引用(
CH09.EmployeeService):

-
还需将
System.ServiceModel的程序集引用添加到控制台应用程序项目中。 -
从“解决方案资源管理器”导航到
Program.cs文件。 -
在类文件内添加以下
using命名空间:
using CH09.EmployeeService.Services;
using System;
using System.ServiceModel;
using System.ServiceModel.Description;
- 现在我们需要定义服务 URL,以便我们可以从宿主访问它。在
Program.cs类文件中创建一个静态成员变量,如下所示:
private static Uri serviceUrl = new Uri(
"http://localhost:59795/Services/EmployeeService");
Program类包含一个静态的Main方法。将定义替换为以下代码块:
static void Main(string[] args)
{
// create Service Host
using (var serviceHost = new ServiceHost(
typeof(EmployeeService.Services.EmployeeService),
serviceUrl))
{
// add the service endpoint
serviceHost.AddServiceEndpoint(
typeof(IEmployeeService),
new BasicHttpBinding(), "");
serviceHost.Description.Behaviors.Add(
new ServiceMetadataBehavior
{
HttpGetEnabled = true
});
// start the Service host
serviceHost.Open();
Console.WriteLine("Service hosting time: " +
DateTime.Now.ToString());
Console.WriteLine();
Console.WriteLine("Service Host is running...");
Console.WriteLine("Press [Enter] key to stop the host...");
Console.ReadLine();
// close the Service host
serviceHost.Close();
}
}
-
构建解决方案,并运行控制台应用程序。您将在控制台输出窗口中看到以下输出
![图片]()
-
服务现在通过宿主进程进行托管。按Enter键停止服务。
它是如何工作的...
要托管服务,宿主应用程序使用System.ServiceModel命名空间中的ServiceHost类。它根据您实现的服务类型进行实例化。在上面的示例中,ServiceHost类创建了一个EmployeeService.Services.EmployeeService的对象,并在服务完成执行时将其从内存中移除。
如果您在快速查看窗口中检查ServiceHost对象,您会注意到该对象公开了几个属性。BaseAddress属性定义了服务的 URL,它维护一个运行时套接字监听器,监听为创建的服务打开的端口。一旦它收到任何请求,它就会解析传递给它的整个消息并调用服务对象。
这是一张快速查看窗口的截图,显示了ServiceHost对象公开的属性数量:

serviceHost.AddServiceEndpoint方法向托管服务添加一个具有指定契约、绑定和端点地址的服务端点。您可以根据需求使用任何绑定类型,但在这里我们使用了BasicHttpBinding来创建服务端点。
在BasicHttpBinding的情况下,传输 SOAP 消息。SOAP 消息包含一个定义良好的封装,其中包含消息的头部和体。当客户端调用服务时,ServiceHost类解析消息并通过创建上下文来调用服务。
要查看ServiceHost对象使用的端点,请在快速查看窗口中展开Description属性,并导航到Endpoints。展开服务的第一个端点,并检查其Address、Binding、Contract("ABC")属性。它看起来如下所示:

你可以看到,Address 指向服务的 BaseAddress,Binding 表示我们创建的 BasicHttpBinding,而 Contract 揭示了服务的 Name、ConfigurationName、ContractType、SessionMode、ProtectionLevel、HasProtectionLevel 以及其他属性。
当你准备好时,serviceHost.Open() 方法启动服务。它使通信对象从创建状态转换为打开状态。当你完成时,调用 serviceHost.Close() 方法停止服务。这将使通信对象从其当前状态转换为关闭状态。
如果你想使服务对象可重用,你可以在服务类中添加一个 ServiceBehavior 属性,如下所示:
[ServiceBehavior(InstanceContextMode =
InstanceContextMode.Single)]
public class EmployeeService : IEmployeeService
{
...
...
}
当你应用此属性时,它指定了服务合同实现的内部执行行为。指定的 InstanceContextMode 可以是三种类型之一:
-
PerSession: 每个会话都会创建一个新的
System.ServiceModel.InstanceContext对象。 -
PerCall: 在每个调用之前创建一个新的
System.ServiceModel.InstanceContext对象,并在调用之后回收。如果通道没有创建会话,此值的行为类似于PerCall。 -
Single: 只使用一个
System.ServiceModel.InstanceContext对象来处理所有传入的调用,并且在调用之后不会回收。如果不存在service对象,将创建一个新的。
还有更多...
如果你没有系统管理员权限,应用程序将因 System.ServiceModel.AddressAccessDeniedException 而崩溃,表示 HTTP 无法注册 URL。错误日志将如下所示:

如果你遇到此错误,以管理员权限运行应用程序。如果你直接从 Visual Studio 运行应用程序,请以管理员权限重新启动 Visual Studio。为此,右键单击 Visual Studio 图标并单击“以管理员身份运行”。
在 IIS 服务器中托管 WCF 服务
另一种托管 WCF 服务的方式是在 IIS(互联网信息服务)中。它需要一个具有 .svc 扩展名的物理文件来正确托管服务。与之前的配方不同,你不需要编写任何代码来创建 ServiceHost 实例。IIS 在托管服务时会自动为你创建它。
在本配方中,我们将学习如何将已创建的服务发布到 Windows 的 IIS 服务器中托管。
准备工作
要开始,以管理员权限启动 Visual Studio IDE。为此,右键单击图标并单击“以管理员身份运行”。现在打开我们之前创建的现有项目 CH09.EmployeeService。或者,你也可以打开解决方案。
要进一步操作,我们假设你已经熟悉 IIS 并了解如何使用 IIS 管理工具创建和管理 IIS 应用程序。
如何操作...
让我们按照以下步骤在 IIS 服务器中托管我们的服务:
- 首先,您需要检查您打算托管服务的系统上是否已经安装了IIS(互联网信息服务)。为此,打开控制面板,导航到“启用或关闭 Windows 功能”,如下面的截图所示:

-
从屏幕上弹出的“Windows 功能”对话框中,确保已选中“互联网信息服务”功能。如果没有,请选中它,然后点击“确定”。这将在该系统上安装 IIS 服务器。
-
现在,点击开始(
),输入inetmgr,然后点击“互联网信息服务(IIS)管理器”应用程序快捷方式来启动它。请确保默认网站正在运行。在接下来的步骤中,我们将在这个网站上部署我们的服务:

-
一旦 IIS 安装完成(如果尚未安装)并且 IIS 中的默认网站正在运行,导航回 Visual Studio。
-
从“解决方案资源管理器”中,右键单击服务项目(
CH09.EmployeeService)节点,然后点击发布:

- 这将在 Visual Studio 内部打开发布向导。导航到“发布”选项卡,选择发布模板为 IIS、FTP 等,然后点击发布按钮,如下所示:

-
这将打开发布对话框。请确保已选中连接选项卡:
-
选择Web Deploy作为发布方法类型。
-
输入服务器的名称。在我们的例子中,因为我们是在同一系统上部署,所以它将是
localhost。 -
输入我们打算部署服务的网站名称。在我们的例子中,它是
Default Web Site。如果我们想在网站内的特定 Web 应用中部署,请在网站名称后输入 Web 应用的名称。例如,要在Default Web Site内的MyAppWeb 应用中部署,这里的网站名称将是Default Web Site/MyApp。 -
输入您将要部署的 Web 服务器的用户名和密码。在我们的例子中,因为它是在
localhost上,我们不需要输入任何凭据。这两个字段将默认禁用。 -
点击验证连接以确认您输入的发布详情。如果成功,您将在“验证连接”按钮旁边看到一个绿色的勾号。
-
完成后,点击下一步以进入设置页面:
![]()
-
在“设置”页面内,选择“发布”作为配置。根据需要,可选地选择“文件发布选项”。
-
完成后,点击保存以开始发布:
![]()
-
一旦 Visual Studio IDE 构建了解决方案并完成了到所选网站的部署,导航回“互联网信息服务(IIS)管理器”应用程序(
inetmgr)。 -
刷新默认网站节点,现在将列出两个文件夹,分别命名为 bin 和 Services。单击 Services 文件夹,切换到内容视图。这将列出
EmployeeService.svc文件,它位于其中。以下是这个截图:

-
如前一个截图所示,右键单击
EmployeeService.svc文件,然后从上下文菜单中单击浏览。或者,您也可以单击右侧操作窗格中的浏览链接。 -
这将在浏览器窗口中打开服务 URL,如下所示:

它是如何工作的...
IIS 托管与 ASP.NET 集成,并使用进程回收、进程健康监控、基于消息的激活等功能。IIS 还提供集成管理性,使其成为企业级服务器。
要在 IIS 中托管服务,IIS 需要正确配置。对于 IIS 托管,不需要编写额外的代码。在 IIS 中托管的 WCF 服务在 IIS 应用程序中以 .svc 文件的形式表示。.svc 文件包含一个 WCF 特定的处理指令,即 @ServiceHost,它创建服务宿主并允许 WCF 服务的托管结构在接收到传入消息时激活:
<%@ ServiceHost
Language="C#"
Debug="true"
Service="CH09.EmployeeService.Services.EmployeeService"
CodeBehind="EmployeeService.svc.cs"
%>
Service 属性的值是服务实现的完全限定 CLR 类型名(在我们的例子中,它是 CH09.EmployeeService.Services.EmployeeService)。CodeBehind 属性定义了 .svc 文件背后的代码的相对路径,在我们的例子中是 EmployeeService.svc.cs。
当部署一个服务时,预编译的 .dll 文件被部署在应用程序的 bin 目录中,并且只有当类库的最新版本被部署时才会更新。
未编译的源文件被部署在应用程序的 App_Code 目录中。当应用程序收到第一个请求时,这些未编译的源文件会动态加载到内存中。对已部署源文件的任何更改都会导致整个应用程序被回收。当应用程序收到新的请求时,会自动进行新鲜的重编译。
在 WPF 应用程序中集成 WCF 服务
一旦创建了一个 WCF 服务,您可能希望将其集成到客户端应用程序中。但在那之前,您将不得不创建一个 WCF 客户端代理,这样您就可以通过 WCF 客户端代理与该服务进行通信。
在这个菜谱中,我们将学习如何创建代理客户端,并通过服务与客户端之间传递消息。
准备工作
在进入集成服务的步骤之前,我们需要创建一个客户端应用程序。打开您的 Visual Studio IDE,创建一个新的 WPF 项目。将其命名为 CH09.ClientDemo。
如何做...
按照以下步骤创建服务代理并在客户端应用程序中集成服务调用:
- 右键单击项目节点(
CH09.ClientDemo),然后按照上下文菜单路径添加 | 服务引用...,这将打开屏幕上的添加服务引用对话框:

- 在添加服务引用对话框中,在地址字段中输入服务 URL(
http://localhost:59795/Services/EmployeeService.svc),然后单击 Go 按钮:

-
这将解析服务地址并显示其详细信息。
-
如以下截图所示,将
EmployeeServiceReference作为服务代理的命名空间输入,然后单击确定:

-
这将在项目下创建服务代理作为已连接的服务:
![图片]()
-
构建项目以确保没有编译问题。
-
一旦构建成功,导航到
MainWindow.xaml.cs文件。 -
创建一个类型为
ObservableCollection<Employee>的依赖属性,并将其命名为Employees。属性实现如下:
public ObservableCollection<Employee> Employees
{
get
{
return (ObservableCollection<Employee>)
GetValue(EmployeesProperty);
}
set
{
SetValue(EmployeesProperty, value);
}
}
public static readonly DependencyProperty
EmployeesProperty =
DependencyProperty.Register(
"Employees",
typeof(ObservableCollection<Employee>),
typeof(MainWindow),
new PropertyMetadata(null));
-
现在,解析
Employee类的引用,这将添加CH09.ClientDemo.EmployeeServiceReference作为using命名空间:![图片]()
-
确保在类文件中添加以下
using命名空间:
using CH09.ClientDemo.EmployeeServiceReference;
using System.Collections.ObjectModel;
using System.Windows;
- 在类内部,创建以下代理客户端的静态实例,以便我们可以调用服务 API:
private static EmployeeServiceClient client =
new EmployeeServiceClient();
- 现在,在类内部添加以下两个方法,并确保将方法标记为
async:
private async void RefreshListAsync()
{
var result = await client.GetEmployeesAsync();
Employees = new ObservableCollection<Employee>(result);
}
private async void AddNewEmployeeAsync()
{
var employee = new Employee
{
ID = "EMP00" + (Employees.Count + 1),
FirstName = "User",
LastName = (Employees.Count + 1).ToString(),
Designation = "Software Engineer"
};
await client.InsertEmployeeAsync(employee);
}
-
从解决方案资源管理器中,导航到
MainWindow.xaml文件。 -
通过添加
x:Name="window"属性为Window实例命名。 -
将默认的
Grid分为两行,如下所示:
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
-
在
Grid面板的第一行中添加一个DataGrid,并在ItemsSource属性和Employees集合之间创建数据绑定。这将使用Employees集合中的值填充DataGrid。 -
设置
AutoGenerateColumns="False"、CanUserAddRows="False"和CanUserDeleteRows="False",如下所示:
<DataGrid ItemsSource="{Binding Employees,
ElementName=window}"
Grid.Row="0"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False">
<DataGrid.Columns>
</DataGrid.Columns>
</DataGrid>
- 由于我们已经要求
DataGrid不要自动生成列,因此我们需要根据需要手动创建它们。在这个演示中,我们将在DataGrid中仅显示ID、Name和Designation列。让我们添加以下列,其中,名称列将与Employee类的FirstName和LastName属性进行多绑定,以显示员工的完整姓名。以下是供您参考的代码:
<DataGrid.Columns>
<DataGridTextColumn Header="ID"
Width="80"
Binding="{Binding ID}"/>
<DataGridTextColumn Header="Name"
Width="200">
<DataGridTextColumn.Binding>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="FirstName"/>
<Binding Path="LastName"/>
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn Header="Designation"
Width="150"
Binding="{Binding Designation}"/>
</DataGrid.Columns>
- 在
Grid面板的第二行中,让我们添加一个包含两个按钮的水平StackPanel。将它们标记为刷新和添加。同时,公开两个按钮的Click事件:
<StackPanel Orientation="Horizontal"
Grid.Row="1"
Margin="8">
<Button Content="Refresh"
Margin="4"
Height="26"
Width="80"
Click="OnRefreshClicked"/>
<Button Content="Add"
Margin="4"
Height="26"
Width="80"
Click="OnAddClicked"/>
</StackPanel>
- 在
MainWindow.xaml文件的代码背后(即MainWindow.xaml.cs),为两个按钮编写Click事件实现。OnRefreshClicked事件将调用RefreshListAsync()方法以获取员工列表。OnAddClicked事件将调用AddNewEmployeeAsync()方法以调用服务插入新的员工记录,然后调用RefreshListAsync()方法从服务获取当前的员工列表:
private void OnRefreshClicked(object sender,
RoutedEventArgs e)
{
RefreshListAsync();
}
private void OnAddClicked(object sender,
RoutedEventArgs e)
{
AddNewEmployeeAsync();
RefreshListAsync();
}
-
让我们构建项目并运行应用程序。确保服务已经运行并且可访问。
-
你将在屏幕上看到以下应用程序 UI:
![图片]()
-
点击添加按钮。这将创建一个新的员工记录并将其传递给服务以存储在数据库中,在我们的案例中是静态的
m_employees实例。 -
在插入记录后,它将再次调用服务以获取新插入的详细信息并填充 UI 中的
DataGrid。点击添加按钮多次将增加记录数量并相应地填充DataGrid:![图片]()
它是如何工作的...
WCF 客户端代理可以通过使用SVCUtil.exe(服务模型元数据工具)手动生成。这是一个用于从服务元数据生成代码的命令行工具。以下命令可以用来生成代理代码:svcutil.exe <Service URL>。
如果你想要为我们之前创建的服务创建代理客户端,你可以在控制台窗口中输入以下命令:
svcutil.exe http://localhost:59795/Services/
EmployeeService.svc?wsdl
或者,你也可以从 Visual Studio 生成客户端代理。如前所述,添加服务引用功能会自动生成代理代码。一旦你点击插入服务地址后的“Go”按钮,对话框将显示指定地址上可用的服务列表。当你点击“OK”按钮时,它开始生成代码。
在我们的案例中,服务模型元数据工具和 Visual Studio 的添加服务引用对话框(你可以使用其中的任何一个)为我们生成以下 WCF 客户端类(EmployeeServiceClient),它继承自泛型System.ServiceModel.ClientBase<TChannel>类并实现了CH09.ClientDemo.EmployeeServiceReference.IEmployeeService接口:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute(
"System.ServiceModel", "4.0.0.0")]
public partial class EmployeeServiceClient : System.ServiceModel.ClientBase<CH09.ClientDemo.EmployeeServiceReference.IEmployeeService>, CH09.ClientDemo.EmployeeServiceReference.IEmployeeService
{
public EmployeeServiceClient() {
}
public EmployeeServiceClient(string
endpointConfigurationName)
: base(endpointConfigurationName) {
}
public EmployeeServiceClient(string
endpointConfigurationName,
string remoteAddress)
: base(endpointConfigurationName, remoteAddress) {
}
public EmployeeServiceClient(string
endpointConfigurationName,
System.ServiceModel.EndpointAddress remoteAddress)
: base(endpointConfigurationName, remoteAddress) {
}
public EmployeeServiceClient
(System.ServiceModel.Channels.Binding binding,
System.ServiceModel.EndpointAddress remoteAddress)
: base(binding, remoteAddress) {
}
public CH09.ClientDemo.EmployeeServiceReference.Employee
GetEmployeeByID(string empID) {
return base.Channel.GetEmployeeByID(empID);
}
public System.Threading.Tasks.Task<CH09.
ClientDemo.EmployeeServiceReference.Employee>
GetEmployeeByIDAsync(string empID) {
return base.Channel.GetEmployeeByIDAsync(empID);
}
public CH09.ClientDemo.EmployeeServiceReference.Employee[]
GetEmployees() {
return base.Channel.GetEmployees();
}
public System.Threading.Tasks.Task<CH09.
ClientDemo.EmployeeServiceReference.Employee[]>
GetEmployeesAsync() {
return base.Channel.GetEmployeesAsync();
}
public void InsertEmployee(CH09.ClientDemo.
EmployeeServiceReference.Employee employee) {
base.Channel.InsertEmployee(employee);
}
public System.Threading.Tasks.Task InsertEmployeeAsync
(CH09.ClientDemo.EmployeeServiceReference.Employee employee) {
return base.Channel.InsertEmployeeAsync(employee);
}
}
一旦创建服务代理,你就可以创建服务客户端的实例并调用服务的方法。在我们的例子中,我们创建了以下服务客户端实例并将其标记为static:
private static EmployeeServiceClient client =
new EmployeeServiceClient();
客户端对于服务公开的每个操作合约都有两种 API 方法类型。其中一个是同步方法,而另一个是异步方法。例如,你可以看到GetEmployees()和GetEmployeesAsync()方法,如以下截图所示:

当您想以同步方式调用服务时,请调用GetEmployees()方法。如果您想以异步模式操作,请调用GetEmployeesAsync()方法。同样,根据同步和异步模式,您可以通过选择GetEmployeeByID和GetEmployeeByIDAsync来根据 ID 获取员工详细信息。其他服务方法的情况也类似。
还有更多...
WCF 服务客户端可能会抛出一个或多个异常,您必须在您的代码中处理这些异常。其中一些最常见的异常包括:
-
SocketException:当远程主机强制关闭现有连接时可能会发生此异常 -
CommunicationException:当底层连接意外关闭时可能会发生此异常 -
CommunicationObjectAbortedException:当由于处理您的消息时出现错误、处理请求时超时或底层网络问题而导致套接字连接被中止时可能会发生此异常
调试和线程
在本章中,我们将介绍以下食谱:
-
启用 XAML 的 UI 调试工具
-
使用实时视觉树遍历 XAML 元素
-
使用实时属性浏览器检查 XAML 属性
-
从非 UI 线程更新 UI
-
为长时间运行的线程添加取消支持
-
使用后台工作组件
-
使用计时器定期更新 UI
第十一章:简介
当涉及到应用程序开发时,调试起着至关重要的作用。这是一个通过逐行遍历代码来快速查看程序当前状态的过程。在编写代码时,开发者开始调试他们的应用程序。有时,开发者甚至在编写第一行代码之前就开始调试,以便了解现有的逻辑。
Visual Studio 尽可能提供有关运行程序的详细信息,并帮助您在应用程序运行时更改一些值。作为一名开发者,您必须已经知道这一点。由于本书的重点是 Windows 表现基金会(WPF),我们将讨论使用 实时视觉树 和 实时属性浏览器进行 XAML UI 调试。
在本章的后面部分,我们将讨论 线程,并学习如何从非 UI 线程、后台工作进程和用于定期更新 UI 的计时器更新 UI 线程。
启用 XAML 的 UI 调试工具
要开始调试您的 XAML 应用程序 UI,您首先需要在 Visual Studio 中启用一些设置。如果设置被禁用,您将无法查看 实时视觉树 和 实时属性窗口,我们将在接下来的几个食谱中讨论。
这些设置在 Visual Studio 2017 中默认启用,但如果已禁用,此食谱将帮助您开始使用它。
准备工作
确保您已安装 Visual Studio 2017。打开它以开始设置更改。
如何操作...
按照以下步骤验证并启用 Visual Studio 2017 中 XAML 的 UI 调试工具:
- 在 Visual Studio IDE 中,导航到工具 | 选项...菜单,如图所示:

-
这将打开 Visual Studio 选项窗口。从该页面,导航到调试 | 通用部分。
-
选择标记为启用 XAML UI 调试工具的复选框,如果尚未启用,请将其打开:

-
一旦您打开调试 XAML 应用程序 UI 的功能,您将启用更多设置以使用 实时视觉树 和在调试器已附加时修改 XAML 属性。
-
从同一页面,选择其他标记为在实时视觉树中预览所选元素和显示应用程序中的运行时工具的复选框。
-
要能够在应用程序以调试模式运行时更改 XAML 元素及其属性,请检查“启用 XAML 编辑和继续”,如图所示。
-
点击“确定”以保存更改并重新启动调试过程以使更改生效。现在您将能够调试您的 XAML UI。
使用实时视觉树导航 XAML 元素
实时视觉树是一个调试工具,可以帮助您更轻松地执行 XAML 调试。使用它,您可以在运行时检查 XAML 并可视化布局以显示 UI 元素的对齐和空间。
基本上,实时视觉树为您提供了正在运行的 XAML 应用程序 UI 元素的树视图,并提供了每个容器内 XAML 元素数量的信息。如果界面从一个状态变为另一个状态,实时视觉树也会在运行时发生变化。
在本食谱中,我们将学习更多关于实时视觉树以及如何使用它来可视化 UI 上的实际控件渲染。
准备工作
要开始,打开 Visual Studio 2017 IDE 并创建一个名为 CH10.XamlDebuggingDemo 的新 WPF 项目。
如何操作...
按照此处提到的步骤创建我们的示例演示应用程序,然后学习如何在调试应用程序时使用实时视觉树导航 XAML 元素:
-
让我们先设计应用程序 UI。从解决方案资源管理器打开
MainWindow.xaml文件。 -
将默认的
Grid面板分成以下方式具有五行:
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
- 在
Grid内部,添加以下 XAML 代码块以创建一个包含几个文本块、文本框和按钮控件的登录屏幕。将它们放置在适当的行中,如下所示:
<TextBlock Text="Username:"
Grid.Row="0"
Margin="0 4 0 0"/>
<TextBlock Text="Password:"
Grid.Row="2"
Margin="0 4 0 0"/>
<TextBox x:Name="username"
Grid.Row="1"/>
<TextBox x:Name="password"
Grid.Row="3"/>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="4">
<Button Content="Login"/>
<Button Content="Cancel"/>
</StackPanel>
- 现在,在
<Window>标签内,添加<Window.Resources>并为TextBox和Button控件添加以下样式,以使它们看起来更合适:
<Window.Resources>
<Style TargetType="TextBox">
<Setter Property="Height"
Value="24"/>
</Style>
<Style TargetType="Button">
<Setter Property="Margin"
Value="4"/>
<Setter Property="Width"
Value="60"/>
<Setter Property="Height"
Value="30"/>
</Style>
</Window.Resources>
- 一旦 UI 准备就绪,让我们运行应用程序。你将在屏幕上看到以下输出:

-
现在,关闭应用程序并以调试模式运行。为此,可以点击Visual Studio 工具栏上的开始按钮 (
),或者导航到 Visual Studio 调试菜单并点击开始调试。 -
或者,您可以按键盘快捷键 F5 以调试模式运行应用程序。
-
一旦应用程序启动,您将在屏幕上看到以下输出,其中包含一个工具栏:

- 如果工具栏在 UI 中不存在,请导航到 Visual Studio 的调试选项,并启用“在应用程序中显示运行时工具”,如图所示。同时确保其他复选框(此处标记)已经勾选:

- 当应用程序以调试模式运行时,点击运行时工具栏上的第二个按钮以启用控件选择:

- 现在,将鼠标悬停在应用程序 UI 上的任何控件上,你将看到悬停控件上有一个红色虚线边框(就像IE 开发者工具一样),如图所示:

- 点击任何控件以在 Visual Studio 编辑器中打开实时视觉树。如果它不可见,导航回应用程序 UI,如图所示,点击运行时工具栏上的第一个按钮以启动实时视觉树对话框面板:

-
或者,你可以导航到 Visual Studio 2017 菜单调试 | 窗口 | 实时视觉树以打开此对话框窗口。
-
点击标记为密码的输入框。实时视觉树将显示视觉树中当前选中的视觉元素。查看以下截图:

- 让我们点击按钮控制(标记为登录),如图所示,在视觉树中会自动选择相应的按钮控制:

- 现在,点击按钮内的登录标签。你会看到
Button控制包含一个TextBlock元素,该元素位于ContentPresenter内部,被Border控制包裹:

它是如何工作的...
当你在调试模式下启动 WPF 应用程序时,浮动工具栏也会在屏幕上加载,这允许你轻松选择应用程序运行实例中的元素,并检查其在实时视觉树中的视觉元素。
浮动工具栏包含四个按钮——转到实时视觉树、启用选择、显示布局装饰器和跟踪焦点元素,如图所示:

在MainWindow.xaml中,我们只在StackPanel内添加了Button控件,但当你看到它在实时视觉树中时,你会注意到Button控件由其他 UI 元素组成以表示控件。它包含一个Border、一个ContentPresenter和一个TextBlock来可视化Button内容:

就这样,每个 UI 控件都由一个或多个 UI 元素组成,这些元素仅在视觉树中可见,并且当调试器附加到应用程序时可以通过实时视觉树进行检查。
请注意,这就是 XAML 控件在 UI 中实际渲染的方式。元素在视觉树中的层级越多,可能遇到的性能问题就越多。检测和消除视觉树中不必要的元素是实时视觉树调试窗口的主要优势之一。
Visual Studio 2017 还支持在实时视觉树窗口中修改所选元素,我们将在下一个菜谱中演示。
还有更多...
你还可以要求 XAML 调试器显示布局装饰器。当运行时调试工具在应用程序窗口中可见时,点击第三个按钮(如下面的截图所示),标题为显示布局装饰器。这将导致应用程序窗口在所选对象的边界上显示水平和垂直线,以便你可以看到它对齐的位置。它还显示显示边距的矩形:

当启用时,将鼠标悬停在应用程序窗口上的任何 UI 元素上或点击它。你会看到该控件的布局装饰器,如下面的截图所示:

使用 Live Property Explorer 检查 XAML 属性
在前面的配方中,我们学习了 Live Visual Tree,它通过检查视觉元素来获取运行中的 XAML 代码的实时视图。Visual Studio 2015 及以上版本还提供了一个 Live Property Explorer 窗口,允许你在运行时临时修改 XAML 属性以查看视觉效果。
在本配方中,我们将学习关于 Live Property Explorer 的内容。我们将使用 Visual Studio 2017 来演示它。
准备工作
让我们从创建一个演示项目开始。打开你的 Visual Studio 2017 实例,创建一个名为 CH10.LivePropertyExplorerDemo 的新项目。确保在创建项目时选择 WPF 应用程序模板。
如何操作...
按照以下步骤使用简单的按钮设计我们的应用程序 UI,然后利用 Live Property Explorer 在运行时查看和修改 XAML 属性:
-
从解决方案资源管理器中打开
MainWindow.xaml文件。 -
将 XAML 的内容替换为以下代码,以获得具有默认样式的基本
Button:
<Window x:Class="CH10.LivePropertyExplorerDemo.MainWindow"
Title="Live Property Explorer Demo" Height="150" Width="400">
<Grid>
<Button x:Name="myButton"
Content="Click here"/>
</Grid>
</Window>
- 让我们运行应用程序。你会看到按钮自动排列以覆盖整个应用程序。这是因为我们将按钮放置在一个
Grid中,并且没有指定其尺寸和边距,如下面的截图所示:

-
现在,关闭应用程序并以调试模式运行。
-
一旦应用程序以调试模式启动,导航到 Visual Studio 菜单—调试 | 窗口 | Live Property Explorer 以打开该探索器窗口。
-
Live Property Explorer 窗口将如下所示:

-
如前一个截图所示,Live Property Explorer 正在显示所选
Button控件的属性,命名为myButton。你会发现大多数属性都是禁用的。这是因为它们要么是从隐式/显式样式继承的,要么具有默认值。 -
要实验 UI 元素属性,你应该修改 Local 面板内的属性。
-
要从 Live Visual Tree 中覆盖所选元素(在我们的例子中是
myButton)的现有属性值,点击如下截图所示的“新建”按钮:

- 这将在面板中添加一个下拉列表,你可以从中选择要修改的属性。让我们从属性列表中选择“宽度”:

-
当你选择属性时,面板将填充适当的属性框以填充它。将宽度属性的值输入为
120。 -
注意运行中的应用程序窗口。窗口中的
Button控件将自动调整到 120 像素的宽度。 -
观察 XAML 设计器窗口中的实际元素。更改并未在 XAML 代码中执行:

-
让我们修改
Button控制的几个属性。单击“新建”按钮,并从属性列表中选择“高度”。将其值设置为30。 -
再次单击“新建”按钮,并从属性列表中选择“背景”。现在将其值输入为
Red。你也可以输入#FFFF0000以将红色应用到按钮背景。 -
一旦你进行了这些更改,查看运行中的应用程序窗口。新的高度和背景颜色已经应用到按钮上:

-
让我们再更改一些属性。在你的本地属性列表中添加
FontSize和Foreground属性。分别将它们的值设置为16和White。 -
检查应用程序窗口中的更改。现在它将看起来如下:

它是如何工作的...
实时属性浏览器只为你提供了在运行时想要修改的预览。基于此,你可以更改 XAML 视图或设计视图中的原始 UI 以进行永久更改。
如果你结束调试会话,你在 Live Property Explorer 窗口中进行的更改将不会保存,并且你会丢失这些更改。当你重新启动应用程序时,你会看到默认的新值。
这通常在你想要在运行时实时查看 Visual Tree 中任何元素的更改时很有用。
还有更多...
在调试模式下运行应用程序时,要永久设置任何 UI 元素的属性,请使用 XAML 代码视图或 XAML 设计器视图。运行中的应用程序将自动获取样式更改的更新。
在调试模式下运行应用程序时修改 XAML 代码,请确保在 Visual Studio 选项窗口的调试 | 通用部分中启用了“启用 XAML UI 调试工具”和“启用 XAML 编辑和继续”设置(已勾选)。
让我们在调试模式下再次运行应用程序,并直接在 XAML 视图中修改控件属性。一旦你进行了更改,检查运行中的应用程序,你会看到它已经根据修改后的数据进行了更新:

下面是按钮修改后的 XAML 代码,我们在前面的屏幕截图中使用了它。应用后,它将为按钮背景添加一个漂亮的线性渐变颜色:
<Button x:Name="myButton"
Content="Click here"
Height="30"
Width="200"
FontSize="18"
FontWeight="Bold"
Foreground="Red">
<Button.Background>
<LinearGradientBrush>
<GradientStop Color="#FFFF5454"
Offset="0"/>
<GradientStop Color="#FFFFF754"
Offset="0.3"/>
<GradientStop Color="#FFFFF754"
Offset="0.8"/>
<GradientStop Color="#FFFF5454"
Offset="1"/>
</LinearGradientBrush>
</Button.Background>
</Button>
从非 UI 线程更新 UI
在 WPF 中,UI 由一个称为UI 线程的单个线程管理,该线程创建一个窗口实例并处理该窗口的 UI 消息。这被称为消息泵。
当 UI 线程执行大量操作时,它会进入等待状态并停止处理进一步的 UI 消息。这会导致应用程序进入无响应模式,这通常被称为UI 冻结。
要解决这个问题,你需要将这个长时间运行的操作卸载到另一个线程。这会使 UI 线程保持空闲,并允许它执行 UI 更新并保持响应。
在这个菜谱中,我们将学习如何将长时间运行的过程卸载到线程池中的单独线程,并在执行完成后执行 UI 更新。
准备工作
让我们从创建一个 WPF 项目开始。命名为CH10.ThreadingDemo1。确保在创建项目时选择正确的 WPF App 模板。
如何做...
我们将创建一个简单的应用程序,该应用程序将计算一个数字范围内的奇数和偶数。这将在非 UI 线程上完成,一旦结果可用,我们将更新 UI。按照以下步骤操作:
-
从解决方案资源管理器中打开
MainWindow.xaml文件。 -
将现有的
Grid替换为以下简单的用户界面,以提供数字范围,并一个按钮来计算和显示结果:
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal"
Grid.Row="0"
Margin="4">
<TextBlock Text="From:"
Margin="4"/>
<TextBox x:Name="fromValue"
Text="100"
Width="100"
MaxLength="10"
Margin="4"/>
<TextBlock Text="To:"
Margin="4"/>
<TextBox x:Name="toValue"
Text="1000000000"
Width="100"
MaxLength="10"
Margin="4"/>
<Button x:Name="calculateButton"
Content="Calculate"
Margin="4"
Padding="8 2"
Click="OnCalculateClicked"/>
</StackPanel>
<TextBlock x:Name="oddResultBlock"
Grid.Row="1"
Text="Total odd numbers: 0"
Margin="4"/>
<TextBlock x:Name="evenResultBlock"
Grid.Row="2"
Text="Total even numbers: 0"
Margin="4"/>
</Grid>
- 导航到
MainWindow.xaml.cs文件,并添加以下两个成员变量以存储奇数和偶数的总数:
private int totalOdd = 0;
private int totalEven = 0;
- 现在,我们将创建一个方法来计算奇数和偶数。在
MainWindow类中,创建一个名为CalculateOddEven的方法,并实现如下代码片段所示的代码块:
private void CalculateOddEven(int from, int to)
{
for (int i = from; i <= to; i++)
{
if (i % 2 == 0) { totalEven++; }
else { totalOdd++; }
}
}
- 现在,我们需要调用这个方法。让我们实现按钮点击事件
OnCalculateClicked来调用CalculateOddEven方法并显示结果:
private void OnCalculateClicked(object sender,
RoutedEventArgs e)
{
totalOdd = 0;
totalEven = 0;
if (int.TryParse(fromValue.Text, out int from) &&
int.TryParse(toValue.Text, out int to))
{
calculateButton.IsEnabled = false;
CalculateOddEven(from, to);
oddResultBlock.Text = "Total odd numbers: " +
totalOdd;
evenResultBlock.Text = "Total even numbers: " +
totalEven;
calculateButton.IsEnabled = true;
}
}
- 运行应用程序并点击计算按钮。在长时间运行的操作进行时,UI 会冻结一段时间,因为它正在找出
100 - 1000000000范围内的奇数和偶数。一旦计算完成,它将解冻 UI 并显示结果:

-
要解决这个问题,我们应该将长时间运行的过程移动到不同的线程,以便 UI 线程保持响应。我们将使用线程池将过程移动到不同的线程。您也可以通过创建一个新的
Thread实例或使用Task来完成此操作。 -
在这个例子中,我们将使用线程池。这可以通过调用
ThreadPool.QueueUserWorkItem来实现,如下面的代码片段所示:
ThreadPool.QueueUserWorkItem(_ =>
{
CalculateOddEven(from, to);
});
- 现在,我们需要在操作完成后更新 UI。这不能在
ThreadPool.QueueUserWorkItem块外部完成,因为操作将在不同的线程上运行。同样,也不能直接在ThreadPool.QueueUserWorkItem块内部完成,因为更新应该在 UI 线程上执行。为了使这可行,可以在ThreadPool.QueueUserWorkItem块内部使用Dispatcher.BeginInvoke块,如下面的代码片段所示:
ThreadPool.QueueUserWorkItem(_ =>
{
CalculateOddEven(from, to);
Dispatcher.BeginInvoke(new Action(() =>
{
oddResultBlock.Text = "Total odd numbers: " +
totalOdd;
evenResultBlock.Text = "Total even numbers: " +
totalEven;
calculateButton.IsEnabled = true;
}));
});
- 让我们再次运行应用程序。点击 Calculate 按钮。您将观察到,在长时间运行的操作进行时,UI 仍然是响应的。
它是如何工作的...
WPF 中的每个元素都继承自DispatcherObject,因此 UI 线程始终与System.Windows.Threading.Dispatcher相关联。这就是为什么可以通过使用DispatcherObject.Dispatcher属性在任何时候访问Dispatcher对象的原因。
ThreadPool.QueueUserWorkItem会导致委托在 CLR 线程池上执行。因此,在该委托内部执行的操作永远不会在 UI 线程上执行。
一旦操作完成,并且您需要更新 UI,您必须从 UI 线程更新。对Dispatcher.BeginInvoke的调用会导致委托在 UI 线程上运行并做出必要的 UI 更改。
需要注意的一点是,调度器也可以从 UI 线程通过静态属性Dispatcher.CurrentDispatcher访问。
还有更多...
Dispatcher有两种调用方式——BeginInvoke和Invoke。我们已经看到了BeginInvoke的使用,它基本上会调用delegate并在delegate仍在 UI 线程上运行时返回以执行其他操作。
另一方面,Invoke操作不会返回,直到delegate在 UI 线程上完成其执行。
除非有特定的原因需要等待 UI 操作完成,否则BeginInvoke始终是首选。
Dispatcher维护一个需要在 UI 线程上处理的请求队列。这基本上是通过设置DispatcherPriority来处理的。默认优先级是DispatcherPriority.Normal,但您可以根据操作的重要性设置一个较低的或较高的优先级。
为长时间运行的线程添加取消支持
当您在另一个线程上执行长时间运行的过程时,为了在操作期间保持 UI 的响应性,您可能希望提供一个取消操作的功能。这可以按需完成。
在这个菜谱中,我们将学习如何将取消支持添加到之前菜谱中构建的现有长时间运行操作。
准备工作
我们将使用之前菜谱中使用的相同示例。您可以复制整个CH10.ThreadingDemo1项目文件夹,并给它一个新的名字,CH10.ThreadingDemo2。启动 Visual Studio,并在其中打开新的(CH10.ThreadingDemo2)项目。
如何做到这一点...
按照以下步骤更新现有项目,并在长时间运行过程中支持取消操作:
- 导航到
MainWindow.xaml文件,并修改 UI 以在其中包含一个Cancel按钮。在StackPanel内部添加以下按钮控件,并将其标签设置为Cancel:
<Button x:Name="cancelButton"
Content="Cancel"
IsEnabled="False"
Margin="4"
Padding="8 2"
Click="OnCancelClicked"/>
-
确保将其
IsEnabled属性设置为False。 -
现在,导航到
MainWindow.xaml.cs文件,并在类内部添加以下成员变量:
private CancellationTokenSource tokenSource = null;
- 在
Cancel按钮点击时,我们需要取消正在运行的操作。让我们修改OnCancelClicked事件,使其与以下代码片段执行相同的操作:
private void OnCancelClicked(object sender,
RoutedEventArgs e)
{
if (tokenSource != null)
{
tokenSource.Cancel();
tokenSource = null;
}
}
- 让我们导航到
CalculateOddEven方法并修改它以接受一个类型为CancellationToken的第三个参数:
private void CalculateOddEven(int from,
int to,
CancellationToken token)
- 在
CalculateOddEven方法的for循环内部,检查CancellationToken.IsCancellationRequested是否为true,如果是,则立即返回,并在将totalOdd和totalEven值设置为-1后立即返回。
for (int i = from; i <= to; i++)
{
if (token.IsCancellationRequested)
{
totalOdd = -1;
totalEven = -1;
return;
}
- 仅供参考,以下是
CalculateOddEven方法实现修改后的代码:
private void CalculateOddEven(int from,
int to,
CancellationToken token)
{
for (int i = from; i <= to; i++)
{
if (token.IsCancellationRequested)
{
totalOdd = -1;
totalEven = -1;
return;
}
if (i % 2 == 0) { totalEven++; }
else { totalOdd++; }
}
}
-
在
OnCalculateClicked事件实现中,我们需要进行一些更改。首先创建CancellationTokenSource的实例并将其分配给tokenSource变量。 -
然后,将实例传递给
CalculateOddEven方法作为第三个参数值。 -
然后,在
Dispatcher.BeginInvoke调用内部,修改代码以根据totalOdd和totalEven变量的值显示“操作已取消!”。只有当其中任何一个为-1时才显示消息。以下是完整的实现:
tokenSource = new CancellationTokenSource();
ThreadPool.QueueUserWorkItem(_ =>
{
CalculateOddEven(from, to, tokenSource.Token);
Dispatcher.BeginInvoke(new Action(() =>
{
if (totalOdd < 0 || totalEven < 0)
{
oddResultBlock.Text = "Operation canceled!";
evenResultBlock.Text = string.Empty;
}
else
{
oddResultBlock.Text = "Total odd numbers: " +
totalOdd;
evenResultBlock.Text = "Total even numbers: " +
totalEven;
}
calculateButton.IsEnabled = true;
cancelButton.IsEnabled = false;
}));
});
- 完成后,让我们运行应用程序。点击计算按钮以在线程池中的单独线程中启动进程:

- 当操作正在进行时,点击取消按钮。您会看到进程立即停止,并且在 UI 中显示“操作已取消!”消息:

- 让我们再次点击
Calculate按钮并等待进程结束。现在您能看到什么?它会在 UI 上显示奇数和偶数的总数。
它是如何工作的...
CancellationTokenSource表示一个可以取消的逻辑操作。CancellationTokenSource的Token属性提供了提供逻辑操作一部分的令牌对象。
当在CancellationTokenSource对象上调用Cancel()方法时,该源的所有分布式令牌的IsCancellationRequested属性都会设置为true。
在我们的示例中,CalculateOddEven方法内部的for循环轮询IsCancellationRequested属性,并将totalOdd和totalEven成员变量填充为-1,这可以用来理解已执行取消调用。基于该值,屏幕上会显示“操作已取消!”消息。
使用后台工作组件
在之前的菜谱中,我们使用了线程池在不同的线程中执行长时间运行的操作。从那里,我们必须通过将代码打包到 UI 线程来更新 UI,这需要额外的工作。
为了克服显式线程池和 UI 更新在 UI 线程上的打包,我们可以使用System.ComponentModel.BackgroundWorker类。它提供在后台线程上自动管理长时间运行操作。
在这个菜谱中,我们将使用BackgroundWorker来执行异步操作,而不会阻塞 UI 线程。
准备中
我们将使用之前菜谱中使用的相同示例。您可以复制整个CH10.ThreadingDemo1项目文件夹,并创建一个名为CH10.ThreadingDemo3的新文件夹。启动 Visual Studio 并打开新项目。
如何操作...
按照以下步骤使用后台工作线程,执行长时间运行的过程,并计算范围内的奇数和偶数:
-
从解决方案资源管理器,导航到
MainWindow.xaml.cs文件。 -
添加以下
using命名空间—System.ComponentModel,这样我们就可以使用BackgroundWorker类。 -
在
OnCalculateClicked事件内部,而不是调用ThreadPool来执行操作,创建BackgroundWorker类的实例。 -
注册工作线程事件
DoWork和RunWorkerCompleted。 -
通过传递数值范围作为参数调用后台工作线程的
RunWorkerAsync方法。参数接受对象,因此我们将使用Tuple<int, int>作为数据类型以简化。完整的代码如下:
private void OnCalculateClicked(object sender,
RoutedEventArgs e)
{
totalOdd = 0;
totalEven = 0;
if (int.TryParse(fromValue.Text, out int from) &&
int.TryParse(toValue.Text, out int to))
{
calculateButton.IsEnabled = false;
var worker = new BackgroundWorker();
worker.DoWork += OnWorker_DoWork;
worker.RunWorkerCompleted +=
OnWorker_WorkCompleted;
worker.RunWorkerAsync(new Tuple<int, int>(from, to));
}
}
- 让我们修改
OnWorker_DoWork事件实现,首先提取参数。然后,通过传递从参数中提取的值调用长时间运行的方法(CalculateOddEven):
private void OnWorker_DoWork(object sender,
DoWorkEventArgs e)
{
var argument = (Tuple<int, int>)e.Argument;
CalculateOddEven(argument.Item1, argument.Item2);
}
- 在
OnWorker_WorkCompleted事件实现中,释放BackgroundWorker实例,然后根据值更新 UI。以下是供您参考的代码:
private void OnWorker_WorkCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (sender is BackgroundWorker worker)
{
worker.RunWorkerCompleted -=
OnWorker_WorkCompleted;
worker.DoWork -= OnWorker_DoWork;
worker = null;
}
oddResultBlock.Text = "Total odd numbers: " +
totalOdd;
evenResultBlock.Text = "Total even numbers: " +
totalEven;
calculateButton.IsEnabled = true;
}
- 一旦完成这些操作,让我们运行应用程序。您将看到与我们在第一个示例中看到相同的应用程序 UI:

-
点击
Calculate按钮。您将观察到,在后台工作进程执行的同时,应用程序正在响应。 -
一旦执行完成,它将在 UI 中显示结果。
它是如何工作的...
BackgroundWorker公开事件以协调工作。当您调用RunWorkerAsync方法时,DoWork事件在线程池线程上触发。您可以将一个可选的Argument传递给RunWorkerAsync方法,该Argument可以在DoWork事件处理程序中的DoWorkEventArgs.Argument属性中检索。
由于DoWork事件处理程序在线程池线程上执行,因此在DoWork处理程序中访问 UI 控件将抛出Exception。因此,将 UI 的值作为参数传递给RunWorkerAsync方法。
当 DoWork 事件处理程序完成其执行时,BackgroundWorker 会引发 RunWorkerCompleted 事件。这个事件在 UI 线程上运行,因此你可以从这个事件处理程序中执行 UI 操作。如果你从 DoWork 处理程序中传递了任何值,你可以从这里从 RunWorkerCompletedEventArgs.Result 属性中检索它。
还有更多...
为了显示长时间运行的后台操作的当前进度指示,你可以在工作进程中引发 ProgressChanged 事件,并直接从这里更新 UI。ProgressChanged 处理程序在 UI 线程上运行,并且当从 DoWork 处理程序中调用 BackgroundWorker.ReportProgress(System.Int32) 时发生。为了使其工作,请确保已将工作线程的 WorkerReportsProgress 属性设置为 true。
你还可以检查 BackgroundWorker 是否正在运行异步操作。如果它正在运行后台操作,IsBusy 属性将返回 true。
如果你想取消正在运行的后台工作线程,可以调用工作线程的 CancelAsync() 方法来请求取消挂起的后台操作。如果 BackgroundWorker.WorkerSupportsCancellation 设置为 false,它将抛出 InvalidOperationException。
使用定时器定期更新 UI
经常需要定期更新用户界面的一部分。在这种情况下,定时器对象有助于保持 UI 的刷新。例如,在你的应用程序中,你可能想在 UI 的某个部分显示当前时间。为此,你可以使用定时器定期更新 UI,而无需创建不同的线程。
可以使用 System.Windows.Threading.DispatcherTimer 类将其集成到 Dispatcher 队列中,并可以在指定的时间间隔和优先级下进行处理。
在这个菜谱中,我们将使用 DispatcherTimer 类来实现一个定时器,每次指定的 Interval 达到时,它将执行其订阅的 Tick 事件。
准备中
打开 Visual Studio 并创建一个新的 WPF 应用程序项目。将其命名为 CH10.DispatcherTimerDemo。
如何实现...
按照以下步骤使用定时器创建数字时钟体验:
-
从解决方案资源管理器导航到
MainWindow.xaml页面。 -
将默认的
Grid分为三行,如下所示:
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
- 在
Grid.Row="0"位置添加一个TextBlock控件并将其对齐到中心:
<TextBlock x:Name="clock"
Grid.Row="0"
Text="00:00:00"
FontSize="80"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
- 在
Grid.Row="2"位置添加一个StackPanel,并在其中插入两个按钮。将按钮命名为startButton和stopButton。同时,为两个按钮分别注册Click事件为OnStartTimer和OnStopTimer:
<StackPanel Grid.Row="2"
Margin="10"
Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Button x:Name="startButton"
Content="Start"
Margin="4"
Height="26"
Width="100"
Click="OnStartTimer"/>
<Button x:Name="stopButton"
Content="Stop"
Margin="4"
Height="26"
Width="100"
IsEnabled="False"
Click="OnStopTimer"/>
</StackPanel>
-
现在,导航到
MainWindow.xaml.cs以添加逻辑背后的代码。 -
首先,在类文件中添加以下命名空间:
using System;
using System.Windows;
using System.Windows.Threading;
- 在类内部,声明一个私有的成员变量(
dispatcherTimer)的类型为DispatcherTimer:
private DispatcherTimer dispatcherTimer = null;
- 在类的构造函数内部,让我们创建
DispatcherTimer的实例,并在每1秒间隔后触发其Tick事件。以下是代码:
public MainWindow()
{
InitializeComponent();
dispatcherTimer = new DispatcherTimer();
dispatcherTimer.Interval = TimeSpan.FromSeconds(1.0);
dispatcherTimer.Tick += OnTimerTick;
}
- 现在,在
Tick事件实现内部,将TextBlock控件(clock)的Text属性设置为当前时间,格式为hh:mm:ss:
private void OnTimerTick(object sender,
EventArgs e)
{
clock.Text = DateTime.Now.ToString("hh:mm:ss");
}
- 当用户点击开始按钮时,
OnStartTimer事件处理程序将被触发。在其内部,让我们通过在dispatcherTimer实例上调用Start()方法来启动计时器。或者,你也可以将dispatcherTimer.IsEnabled属性设置为true来启动计时器:
private void OnStartTimer(object sender,
RoutedEventArgs e)
{
if (dispatcherTimer != null)
{
dispatcherTimer.Start();
startButton.IsEnabled = false;
stopButton.IsEnabled = true;
}
}
- 当处理程序
OnStopTimer被触发时,在点击停止按钮时,我们将调用dispatcherTimer实例的Stop()方法。在这里,你也可以将dispatcherTimer.IsEnabled属性作为停止计时器的另一种方法,但在这个情况下,你必须将其设置为false:
private void OnStopTimer(object sender,
RoutedEventArgs e)
{
if (dispatcherTimer != null)
{
dispatcherTimer.Stop();
startButton.IsEnabled = true;
stopButton.IsEnabled = false;
}
}
- 现在运行应用程序。你将在屏幕上看到以下输出:

- 观察 UI 中的文本,它被显示为
00:00:00。现在点击开始按钮。这将现在将文本更改为你的系统当前时间,并且每秒刷新一次:

-
当时间在 UI 上流逝时,在每秒间隔之后,点击停止按钮。这将导致屏幕上运行的计时器停止。
-
再次点击开始按钮将启动计时器并在屏幕上显示当前时间。UI 上显示的时间将每秒刷新一次。
它是如何工作的...
当你使用DispatcherTimer对象时,它代表一个绑定到 UI 线程的计时器。DispatcherTimer类的Interval属性表示计时器的周期,Tick事件将在该周期内触发,并持续计时,直到明确停止。
要启动计时器,你可以调用其Start()方法,或者将IsEnabled属性设置为true。同样,要停止一个计时器,你可以调用Stop()方法,或者将IsEnabled属性设置为false。
不要在Tick事件中执行任何长时间的操作,因为它在 UI 线程上运行。长时间运行的操作可能会阻止 UI 响应用户操作。
与 Win32 和 WinForm 的互操作性
在本章中,我们将介绍以下食谱:
-
在 WPF 应用程序中托管 WinForm 控件
-
在 WinForm 应用程序中托管 WPF 控件
-
从 WPF 应用程序调用 Win32 API
-
在 WPF 应用程序中嵌入 ActiveX 控件
第十二章:简介
术语互操作性描述了不同应用程序通过一组可交换的格式交换数据的能力。它是产品或系统的特性,其接口完全被理解,可以与其他产品或系统协同工作。
WPF 和 Windows 窗体提供了创建应用程序界面的两种不同架构。WindowsFormsHost和ElementHost类用于实现这两种架构之间的互操作能力。
类似地,WPF 提供了与 Win32 程序的互操作性,这些程序是用非托管 C++代码编写的:

在本章中,我们将从 WPF 和 WinForm 之间的互操作开始,演示如何在 WPF 应用程序中托管 WinForm 控件以及在 WinForm 应用程序中托管 WPF 控件。然后,我们将继续学习 WPF 和 Win32 之间的互操作性,接着是嵌入 WPF 中的 ActiveX 控件。
在 WPF 应用程序中托管 WinForm 控件
虽然 Windows 平台基础(WPF)提供了一套功能丰富的控件,但在某些情况下,你可能会遇到一些Windows 窗体(WinForm)控件在 WPF 中不可用的情况。在将 WinForm 应用程序移植到 WPF 时,也可能出现这样的情况,你除了重用现有的控件和/或窗体别无选择,因为重新实现将消耗大量精力。那么,在这种情况下需要做什么呢?
WPF 提供了一种方法来重用 Windows 窗体中的现有控件,并在其中托管它们(无论是在控件、窗口还是页面上)。这被称为两个平台之间的互操作性,因为它们提供了创建应用程序界面的两种不同架构。
System.Windows.Forms.Integration命名空间提供了使常见互操作场景成为可能的类,而WindowsFormsHost类提供了实现互操作的能力。
当在 WPF 中托管 Windows 窗体控件时实现两种技术之间的互操作,可能会出现以下适用场景:
-
一个或多个 WinForm 控件可以在 WPF 中托管
-
一个或多个复合控件可以托管在 WPF 元素中
-
一个或多个 ActiveX 控件也可以在 WPF 中托管
-
包含其他 WinForm 控件的 WinForm 容器控件也可以托管
-
你还可以托管一个主/详细窗体,其中 WPF 作为主窗体,WinForm 作为详细窗体,或者 WinForm 作为主窗体,WPF 作为详细窗体
需要注意的一点是,不支持多级混合控制。多级混合控制包含一种技术中的控制嵌入在另一种技术的控制中。
在这个菜谱中,我们将以 WinForm 的PropertyGrid控件为例,这个控件在 WPF 中不可用,并且将使用WindowsFormsHost控件在 WPF 窗口中托管它。
准备工作
让我们从创建一个新的 WPF 应用程序开始。打开您的 Visual Studio IDE,创建一个名为CH11.WinFormInWpfXamlDemo的新项目。确保选择 WPF App 作为项目模板。
如何做到这一点...
按照以下步骤在 WPF 应用程序窗口中托管 WinForm 控件并映射其属性:
-
从打开 WPF 应用程序窗口开始。从解决方案资源管理器中打开
MainWindow.xaml文件。 -
让我们将默认的
Grid面板拆分为两个列。第二列的宽度将基于其子元素,而第一列将容纳剩余的空间。在Grid内部添加以下 XAML 标记,以根据特定要求拆分它:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
- 在
Grid的第一个单元格(第 0 列)中放置一个TextBlock控件,命名为txtBlock,并将其Text属性设置为Hello World!:
<TextBlock x:Name="txtBlock"
Grid.Column="0"
Margin="8"
Text="Hello World!"/>
- 现在,在
TextBlock控件之后,添加一个<WindowsFormsHost> </WindowsFormsHost>元素。当添加后,这将抛出以下设计时错误信息——在 Windows Presentation Foundation (WPF)项目中不支持 WindowsFormsHost。这是因为解决WindowsFormsHost元素所需的功能程序集未在此项目中引用:

-
要在项目中添加依赖的程序集引用,请右键单击项目节点,并从上下文菜单中选择“添加 | 引用...”:
-
从引用管理器对话框窗口中,检查以下两个程序集引用(System.Windows.Forms 和 WindowsFormsIntegration),然后单击“确定”,这将向项目中添加引用:

-
检查 XAML 文件。前面的设计时错误现在将消失,因为所需的功能程序集引用已经建立。
-
将
<WindowsFormsHost>定位在第二列(Grid.Column="1")中,并将其Width属性设置为300。 -
现在,在
WindowsFormsHost元素内部,放置另一个类型为PropertyGrid的元素。 -
您需要为
PropertyGrid添加 XMLNS 命名空间,以便从System.Windows.Forms程序集解析。如图所示,单击灯泡图标,或简单地按CTRL +键,将所需的 XMLNS 条目添加到MainWindow.xaml文件中:

- 或者,您可以将以下 XMLNS 声明添加到
Window标签中:
xmlns:forms="clr-namespace:System.Windows.Forms;
assembly=System.Windows.Forms"
- 将
x:Name="propertyGrid"添加到PropertyGrid元素中,以使用名称定义它。这将在我们想要从代码中访问它时非常有用。以下是此处将使用的Grid的完整 XAML 标记:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="txtBlock"
Grid.Column="0"
Margin="8"
Text="Hello World!"/>
<WindowsFormsHost Width="300"
Grid.Column="1">
<forms:PropertyGrid x:Name="propertyGrid"/>
</WindowsFormsHost>
</Grid>
- 完成后,让我们运行应用程序。您将看到以下输出,包含文本和一个空的属性网格:

-
让我们导航到应用程序窗口背后的代码文件(
MainWindow.xaml.cs)。 -
在
MainWindow构造函数中的InitializeComponent()调用之后,添加以下行,propertyGrid.SelectedObject = txtBlock;,以设置我们已在 UI 中添加的属性网格的SelectionObject属性。此更改后,代码将如下所示:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
propertyGrid.SelectedObject = txtBlock;
}
}
- 让我们再次运行应用程序。这次您将看到属性网格包含一组属性,指向窗口中放置的
txtBlock控件:

- 向上滚动属性网格,并将网格内的
FontSize属性更改为40。这将立即影响我们已在 UI 中添加的文本的字体大小:

它是如何工作的...
PropertyGrid控件是.NET Framework 的一部分,它允许您浏览、查看和编辑一个或多个对象的属性。它使用反射来检索和显示任何对象或类型的属性。
反射是一种允许您在运行时检索类型信息的技术。
如果您使用 WinForm,您将能够从控件工具栏轻松使用PropertyGrid控件。但是,不幸的是,此控件在 WPF 中不可用。要在 WPF 应用程序中使用此控件,您需要使用 WPF 和 WinForm 的互操作性。为此,我们需要使用WindowsFormsHost类。
WindowsFormsHost类允许您在 WPF 页面上托管 Windows Forms 控件。它是System.Windows.Forms.Integration命名空间的一部分,并且可在WindowsFormsIntegration.dll程序集内部使用。这就是为什么我们必须在项目中引用System.Windows.Forms和WindowsFormsIntegration程序集。
WindowsFormsIntegration.dll程序集的默认位置是%programfiles%Reference AssembliesMicrosoftFrameworkv3.0,它与其他 WPF 程序集一起提供。
一旦在 WPF 窗口内成功托管 WinForm 控件,您就可以设置/获取其属性。在上面的示例中,我们将txtBlock控件(WPF 控件)分配给propertyGrid控件的SelectedObject属性(WinForm 控件)。因此,当您运行应用程序时,它使用反射来检索TextBlock控件(txtBlock)公开的所有属性,并将它们填充到PropertyGrid中,每个属性都设置为默认值。当您在运行时修改属性值时,它根据选择更改相关控件。因此,当您更改FontSize、Foreground和其他属性时,您可以在TextBlock的 UI 中看到变化。
还有更多...
尽管大多数属性与WindowsFormsHost一起工作,但在混合应用程序中使用时,Z 轴和转换有一些限制。默认情况下,WindowsFormsHost元素绘制在其他 WPF 元素之上,因此 Z 轴属性对该元素没有影响。
如果你想启用 Z 轴排序,将WindowsFormsHost的IsRedirected属性设置为True,并将CompositionMode属性设置为CompositionMode.Full或CompositionMode.OutputOnly。
由于 WinForm 控件不支持适当的缩放和旋转功能,WindowsFormsHost元素不会与其他 WPF 元素一起缩放或旋转。要启用这些转换功能,例如 Z 轴排序,将IsRedirected属性设置为True,并将CompositionMode属性设置为CompositionMode.Full或CompositionMode.OutputOnly。
在 WinForm 应用程序中托管 WPF 控件
由于 WPF 为应用程序提供了丰富的用户界面,你可能希望将其应用到现有的应用程序中。但是,当你有一个大型的 Windows Form 应用程序项目,你已经投入了大量资金,你不会愿意再次投资来废弃它,并完全用 WPF 重写整个项目。
在这种情况下,WPF 与 WinForms 的交互是理想的。使用此方法,你可以在表单中嵌入 WPF 控件,并在可能的情况下利用 WPF 的额外功能。
在前面的配方中,我们学习了如何在 WPF 应用程序中托管 WinForm 控件。但在这个配方中,我们将学习相反的过程,即如何在 Windows Forms 应用程序中托管WPF 复合控件。我们将通过遵循一些简单的指南步骤来学习这一点。你可以稍后扩展此过程以托管更复杂的应用程序和控件。
本指南将基本上分为两个逻辑部分。在第一部分,我们将构建一个 WPF UserControl,在第二部分,我们将将其托管在一个表单窗口中。
准备工作
在我们开始使用此配方在 Windows Form 中托管 WPF 控件之前,请确保 Visual Studio 正在运行。
如何操作...
让我们按照以下步骤创建一个 WPF 复合控件,并将其托管在 Windows Form 中:
-
首先,让我们创建一个WPF 用户控件库项目。为此,从解决方案资源管理器中,右键单击现有解决方案,并在上下文菜单中选择添加 | 新项目...。
-
选择 WPF 用户控件库(.NET Framework)作为项目模板,将其命名为
CH11.WpfUserControlLibrary,然后单击 OK 按钮,如图所示:

-
一旦 Visual Studio 创建了项目,你将在项目文件夹中找到一个名为
UserControl1.xaml的用户控件。从解决方案资源管理器中双击它以打开它。 -
将
UserControl1的默认Grid分为两列。将第一列设置为可伸缩的,以占用最大可用空间,并将第二列设置为Auto:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
- 在第一列中放置一个名为
searchBox的TextBox控件:
<TextBox x:Name="searchBox"
Grid.Column="0"
MinWidth="100"
Margin="4"/>
- 放置一个名为
searchButton的Button控件,并将其放置在Grid的第二列中。将其Content属性设置为搜索,并将其Click事件注册为OnSearchButtonClicked:
<Button x:Name="searchButton"
Content="Search"
Grid.Column="1"
Padding="8 2"
Margin="4"
Click="OnSearchButtonClicked"/>
- 这里是
Grid的完整 XAML 代码:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="searchBox"
Grid.Column="0"
MinWidth="100"
Margin="4"/>
<Button x:Name="searchButton"
Content="Search"
Grid.Column="1"
Padding="8 2"
Margin="4"
Click="OnSearchButtonClicked"/>
</Grid>
-
现在,按F7键导航到
UserControl1.xaml.cs文件的代码后端。 -
在类中插入以下事件实现:
private void OnSearchButtonClicked(object sender,
RoutedEventArgs e)
{
MessageBox.Show("You searched for: {" +
searchBox.Text + "}");
}
- 现在,是时候将创建的用户控件集成到 Windows 表单中了。为此,我们需要一个 WinForm 项目。让我们在解决方案中添加新的项目。为此,从解决方案资源管理器中,右键单击解决方案文件,从上下文菜单中选择添加 | 新项目...。在创建项目时使用以下 Windows Forms App (.NET Framework)模板。将其命名为
CH11.WpfInWinFormDemo并点击确定按钮:

-
一旦项目创建完成,您需要将 UserControl 库的引用添加到该项目中。为此,在
CH11.WpfInWinFormDemo项目的引用节点上右键单击,然后从上下文菜单中点击添加引用...: -
从引用管理器对话框中,如图所示,展开项目条目,选择所需的库项目(在我们的例子中是 CH11.WpfUserControlLibrary),然后点击确定。这将把库的引用添加到项目中:

-
还需要在项目中添加以下程序集引用—
PresentationCore、PresentationFramework、System.Xaml、WindowsBase和WindowsFormsIntegration。这些是使用 WPF 控件并托管它们的必需项。 -
重新构建解决方案并确保解决方案构建时没有错误。这一步也确保了库项目被编译并可在主项目中被发现。
-
从解决方案资源管理器中双击
Form1.cs文件以打开它。 -
现在,打开工具箱,如图所示,从 WPF 互操作性部分拖动 ElementHost 元素到表单(
Form1.cs)中:

- 展开 ElementHost 元素的箭头,以选择托管内容。如图所示,点击选择托管内容组合框并选择 UserControl1 以在其中托管:

- 或者,您也可以从工具箱中拖动 UserControl1。在这种情况下,Visual Studio 将添加
ElementHost并配置它以加载您拖到表单中的 UserControl。完成后,调整控件大小并将其放置在表单中:

-
现在再次重新构建解决方案并运行表单应用程序(
CH11.WpfInWinFormDemo)。您将在屏幕上看到一个表单窗口,其中包含我们创建的 WPF UserControl。它基本上由一个TextBox和一个Button组成。 -
在搜索框中输入一些文本并点击按钮。您将在屏幕上看到一个消息框弹出,其中包含您输入的文本:

工作原理...
要托管 WPF 复合控件,在 Windows 表单宿主应用程序中使用 ElementHost 对象。ElementHost 类是 System.Windows.Forms.Integration 命名空间的一部分,因此您需要在项目中引用 WindowsFormsIntegration.dll。
要在 Windows 表单中托管 WPF 元素,必须将其分配给 ElementHost 控件的 Child 属性。如果需要,使用 PropertyMap 属性将 ElementHost 和其宿主 WPF 元素之间的自定义映射分配。可选地,您可以使用布尔 BackColorTransparent 属性为托管元素设置透明背景。
从 WPF 应用程序调用 Win32 API
Windows 表达式基础和 Win32 插值可以作为不同的方法工作。您可以选择在 WPF 应用程序中托管 Win32 应用程序,在 Win32 应用程序中托管 WPF 应用程序,或者通过导入指定的系统 DLL 从 WPF 调用 Win32 API。这些方法在您已经在 Win32 应用程序上投入了大量资金,现在您想利用现有代码构建一个丰富的 WPF 应用程序时非常有用。
在本配方中,我们将学习如何从 WPF 调用 Win32 API。我们将使用一个简单的示例来启动浏览器窗口,然后从我们的 WPF 代码中激活/刷新浏览器窗口。
准备工作
开始创建 WPF 应用程序。打开您的 Visual Studio IDE,创建一个名为 CH11.Win32ApiCallDemo 的新项目。确保选择 WPF App (.NET Framework) 作为项目模板。
如何操作...
按照以下步骤从 WPF 应用程序调用 Win32 API:
-
首先,我们需要设置项目。一旦 Visual Studio 创建了项目,右键单击项目的“引用”节点。
-
选择上下文菜单中的“添加引用...”以添加程序集引用。
-
在引用管理器对话框中,搜索窗体,并从列表中选择 System.Windows.Forms。点击“确定”以添加引用:

-
现在,从解决方案资源管理器,导航到
MainWindow.xaml文件。 -
将现有的
Grid面板替换为以下标记,其中包含一个TextBox(address) 和三个Button控件 (goButton、bringToFrontButton和refreshButton):
<StackPanel Margin="10">
<TextBlock Text="Enter website URL:"
Foreground="Gray"
Margin="4 0"/>
<StackPanel Orientation="Horizontal">
<TextBox x:Name="address"
Text="http://www.kunal-chowdhury.com"
Width="250"
Margin="4"/>
<Button x:Name="goButton"
Content="Go..."
Padding="8 2"
Margin="4"
Click="OnGoClicked"/>
<Button x:Name="bringToFrontButton"
Content="BringToFront"
Padding="8 2"
Margin="4"
Click="OnBringToFrontClicked"/>
<Button x:Name="refreshButton"
Content="Refresh"
Padding="8 2"
Margin="4"
Click="OnRefreshClicked"/>
</StackPanel>
</StackPanel>
-
一旦 UI 准备就绪,就是时候创建按钮点击事件实现了。在 XAML 页面中按 F7 以导航到其代码隐藏部分。或者,您也可以从解决方案资源管理器打开
MainWindow.xaml.cs文件。 -
在文件的后端代码中,添加以下命名空间:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
- 现在,在
MainWindow类中,添加以下声明并确保DllImport属性和Process类是可发现的:
[DllImport("User32.dll")]
static extern int SetForegroundWindow(IntPtr hWnd);
private static Process process = new Process();
- 让我们实现
OnGoClicked事件处理器。将以下代码复制以使用指定的 URL 地址启动 Internet Explorer,在我们的例子中是http://www.kunal-chowdhury.com:
private void OnGoClicked(object sender,
RoutedEventArgs e)
{
goButton.IsEnabled = false;
process.StartInfo.FileName = "iexplore.exe";
process.StartInfo.Arguments = address.Text;
process.Start();
}
- 让我们实现
OnBringToFrontClicked事件处理器,以将启动的 Internet Explorer 窗口带到前台,如果它失去了焦点。将以下代码复制以获取进程实例的MainWindowHandle并调用 Win32 API 方法SetForegroundWindow:
private void OnBringToFrontClicked(object sender,
RoutedEventArgs e)
{
if (process != null)
{
var ptr = process.MainWindowHandle;
SetForegroundWindow(ptr);
}
}
- 现在,让我们添加刷新按钮的事件实现。在类文件中添加以下
OnRefreshClicked处理器以激活 Internet Explorer 窗口,然后调用键盘上的 F5 键来刷新该浏览器窗口:
private void OnRefreshClicked(object sender,
RoutedEventArgs e)
{
if (process != null)
{
IntPtr ptr = process.MainWindowHandle;
SetForegroundWindow(ptr);
SendKeys.SendWait("{F5}");
}
}
-
代码实现完成后,让我们运行应用程序。你将在屏幕上看到以下 UI:
![图片]()
-
由于应用程序的地址字段已经填充,点击“转到...”按钮。这将启动 Internet Explorer 并导航到指定的地址:

-
现在,点击应用程序窗口。这将使应用程序带到前台。
-
现在点击“带到前台”按钮,这将激活 Internet Explorer 并将其带到前台。
-
同样,点击应用程序窗口,然后点击刷新按钮。这次,Internet Explorer 将激活并刷新窗口的内容:

它是如何工作的...
当你点击应用程序窗口的“转到...”按钮时,这会创建一个新进程的 Internet Explorer (iexplore.exe) 窗口,并将指定的 URL 作为其 Arguments 传递给进程,一旦我们调用 process.Start() 方法。
当你点击“带到前台”按钮时,它会检索进程的主窗口句柄,并将其作为参数传递给 SetForegroundWindow Win32 API 方法。该 API 方法将线程带到前台并激活窗口。
只有满足以下条件之一时,进程才能设置前台窗口:
-
该进程本身是一个前台进程
-
它是由前台进程启动的
-
进程正在被调试
-
前台进程不是一个现代应用程序或启动屏幕
-
没有菜单是激活的
DllImport 属性表示具有该属性的函数是由一个未管理的 动态链接库 (DLL) 作为静态入口点公开的。在我们的例子中,是 User32.dll 文件。
当你点击“刷新”按钮时,就像“带到前台”按钮一样,它首先通过将其带到前台来激活 Internet Explorer 窗口。然后键盘输入被导向该窗口。
注意,如果用户正在另一个窗口上工作,应用程序不能强制将窗口带到前台。在这种情况下,窗口将在任务栏中闪烁以通知用户。
The SendKeys.SendWait("{F5}") 方法调用将给定的键(在我们的例子中是F5)发送到活动应用程序,然后等待消息被处理。因为我们在这里传递了F5键,它将调用浏览器的refresh方法。确保在项目中正确引用了System.Windows.Forms,以便SendKeys.SendWait方法能够工作。
在 WPF 应用程序中嵌入 ActiveX 控件
WPF 也支持ActiveX,您可以在 WPF 应用程序中轻松嵌入。这不是 WPF 特有的功能,但由于与 Windows Forms 的互操作性,它得以实现。WinForm 充当两个之间的中间层。
有几个 ActiveX 控件可以轻松嵌入到任何 WPF 应用程序中。在本教程中,我们将学习如何通过一些简单的步骤嵌入 ActiveX 控件。我们将使用随 Windows 一起提供的Microsoft 终端服务控件来演示。
准备工作
确保 Visual Studio 正在运行。创建一个新的 WPF 项目,并将其命名为CH11.ActiveXDemo。
如何操作...
按照以下步骤生成所需的Microsoft 终端服务 ActiveX控件库,并将其嵌入到我们的 WPF 应用程序中:
-
第一步是生成我们的 ActiveX 控件所需的库。这是为了获取相关类型的托管和 Windows Forms 兼容的定义。为此,打开Visual Studio 开发者命令提示符并导航到一个空文件夹(例如,
D:libs)。 -
现在,在命令提示符中,输入以下命令以生成终端服务 DLL 的管理定义:
aximp c:WindowsSystem32mstscax.dll
-
这将在同一文件夹中生成两个 DLL 文件,分别命名为
MSTSCLib.dll和AxMSTSCLib.dll(在我们的例子中是D:libs):![]()
-
让我们将这些 DLL 文件复制到我们的项目文件夹中。在项目的根文件夹内创建一个名为
libs的文件夹,并将两个文件复制到那里。 -
现在,将这些二进制文件的引用添加到我们的项目中。导航回 Visual Studio,从解决方案资源管理器中,右键单击“引用”节点。然后,从上下文菜单中点击“添加引用...”。
-
从引用管理器对话框窗口中,点击“浏览...”以添加引用。
-
选择 MSTSCLib.dll 和 AxMSTSCLib.dll,如以下截图所示,然后点击“添加”,这将把选择添加到引用管理器中:

-
在参考管理器对话框中搜索
forms,然后选择 System.Windows.Forms 和 WindowsFormsIntegration dlls。 -
点击“确定”以确认添加四个程序集文件的引用。
-
现在打开
MainWindow.xaml文件,并添加以下 XMLNS 属性到其中:
11. Replace the existing `Grid` panel with the following markup:
<WindowsFormsHost>
<lib:AxMsTscAxNotSafeForScripting
x:Name="terminal"
Height="500" Width="1000"/>
</WindowsFormsHost>
12. Go to the code behind the file by pressing the *F7* key. Alternatively, you can open `MainWindow.xaml.cs` from Solution Explorer.
13. Inside the constructor of the `MainWindow` class, add the following, just after the `InitializeComponent()` method call, and replace the IP with the one that you want to connect:
terminal.Server = "192.168.0.10";
terminal.Connect();
14. Now, run the application. You will see the terminal host launched in our WPF application embedded inside it, and pointing to the remote machine for which the IP address has been provided as the `terminal.Server` name. Here's how the application will look:

15. Within that application window, you can now log in to the system and access the desktop, files, and programs remotely.
How it works...
The ActiveX DLL for **Microsoft Terminal Services** (the `mstscax.dll` file) resides in the `%WINDIR%System32` directory. The **ActiveX Importer** (`AXIMP.EXE`), which is part of the **.NET Framework component** of the Windows SDK, generates two DLLs (`MSTSCLib.dll` and `AxMSTSCLib.dll`) from that ActiveX DLL.
The first DLL, `MSTSCLib.dll`, contains the managed definitions of the unmanaged interfaces, classes, structures, and enums, defined in the type library contained inside the ActiveX DLL (`mstscax.dll`). This is generally named with the library name from the original type library.
The second DLL, `AxMSTSCLib.dll`, is named the same but with an `Ax` prefix. This contains a Windows Forms control corresponding to each ActiveX class. The Windows Forms representation of the ActiveX control is added to `WindowsFormsHost`.
In our example, the `AxMsTscAxNotSafeForScripting` control is used in XAML, inside `WindowsFormsHost`, to perform the interaction. Its `Server` property, from the code behind the class, has been set to a simple string, pointing to the remote system's IP address or machine name, discoverable from the host.
When you are ready, the `Connect()` call to the instance of the terminal control (`AxMsTscAxNotSafeForScripting`) connects to the remote system. You can additionally provide `Domainname` , `Username`, and other properties to the terminal instance, before calling the `Connect()` method.


























































),输入





),或者导航到 Visual Studio 调试菜单并点击开始调试。

浙公网安备 33010602011771号