WPF 中的动画“AlarmBar”自定义控件
WPF 关于控件自定义的发布资源几乎全部集中在编辑 ControlTemplates 的本地副本上,而实现和交互实际的 Custom Control 库则需要使用一套不同的技术和引用语法。
引言
许多应用程序都包含某种“消息栏”来以美观且不显眼的方式向用户显示错误、警告、状态或其他类别的消息。虽然在 WPF 中实现基本功能(只需将 `TextBlock` 拉伸到窗口顶部)非常简单,但在生产环境中,您可能需要创建可在项目之间共享、具有微妙动画效果、支持简单 XAML 实例化语法的... ...
附加项目实现了一个“`AlarmBar`”自定义控件,支持“`Glass`”(玻璃)和“`Etched`”(蚀刻)视觉样式以及多种动画选项。消息会缓慢淡入,保持可见几秒钟,然后淡出(在实际应用中,支持的细节将单独写入某个日志)。
添加 `AlarmBar` 并自定义其外观和行为,只需几行 XAML,您就可以定义应用程序所需的任意数量的 `Alarm` 类别及其关联的自定义颜色方案。尝试将 `Button` 或其他无效的子元素添加到集合中,将触发即时(设计时)错误通知。

要显示 `Alarm`,只需指定 `Alarm` id 和消息文本
alarmBar.Display( “Error”, “Containment breach imminent. Run for your lives!” )
WPF 自定义控件项目结构
WPF 关于控件自定义的发布资源几乎全部集中在编辑 ControlTemplates 的本地副本上,而实现和交互实际的 Custom Control 库则需要使用一套不同的技术和引用语法。如果您是第一次接触 WPF Custom Controls,最好创建一个空的 WPF 客户端项目并按以下步骤操作,这样会更容易理解项目代码。请注意,一个通用的 Custom Control 将在 *Themes*/*generic.xaml* 中生成,您一定不要重命名或移动该文件。
-
使用 *Add\New Project\WPF Custom Control Library* 生成默认控件和模板。
-
从您的客户端项目添加一个对控件项目的引用,并重新生成解决方案。
-
在您的客户端窗口 XAML 文件顶部添加一行以导入控件命名空间
xmlns:custom="clr-namespace:ControlProjectName; assembly=ControlProjectAssemblyName"
-
在 WPF 窗口中添加控件实例,设置其背景颜色,然后运行应用程序
<custom:CustomControl1 Background="Red" Height="20" VerticalAlignment="Top" />
验证设计时 XAML 条目
要显示消息,`AlarmBar` 会引用其集合中某个 `Alarm` 对象的相关信息。构建此类控件时,您面临的第一个决定是选择一个要继承的基类,以便通过 XAML 支持向控件添加 `Alarm` 对象(并且*仅* `Alarm` 对象)。继承自 `ItemsControl` 将允许通过标记添加集合成员,但您无法在设计时验证添加(您可以通过手动迭代成员并检查其类型在运行时捕获错误,但这并不是一个令人满意的解决方法)。通过继承自 `ContentControl` 并指定 `Alarm` 作为支持的集合类型,当用户尝试向我们的集合添加无效的子元素时,会自动在设计时生成错误。请注意,尽管 `AlarmBar` 包含一个 `Alarm` 列表,但我们可能不想使用 `ListBox` 作为基类,因为我们实际上从未显示一个列表(技术上,我们显示与单个“活动”列表项相关的属性)。

偏好 XAML 而非过程代码
实现我们的控件几乎没有实际代码。我们只需定义包含绑定到几个 `DependencyProperties` 的显示元素的模板和样式。显示 `Alarm` 所需的过程代码包括将“`ActiveAlarm`”属性设置为 ID 与显示请求中 ID 匹配的集合成员,设置该 `Alarm` 对象的消息文本,以及启动 Storyboard 动画。
构成 `AlarmBar` 默认模板的关键组件是
- `TextBlock`,反映当前的 `Alarm` 颜色和实际的消息文本,并裁剪以符合控件的圆角半径设置。
- 可选的次要动画,例如动画“脉冲”,它将与“淡入/淡出”显示序列并行运行。
- 可选的 `static` 覆盖(目前仅用于创建“`Glass`”效果)。
次要动画和 `static` 覆盖元素的显示和行为是在单独的样式和模板中定义的,这些样式和模板根据“`VisualStyle`”(Visual Style)和“`AnimationStyle`”(Animation Style)`enum` 值“插入”到 `AlarmBar` 中(这些值将在下一节中进行内部转换)。
隐藏客户端的 ComponentResourceKeys
有两种方法(忽略反射等技术)供客户端代码引用外部程序集中的 WPF 逻辑资源。对于简单的资源 DLL,我们可以指定远程 XAML 文件的路径...
<ResourceDictionary Source="/SomeAssembly;component/SomeResourceDictionary.xaml" />
...并使用与本地相同的语法引用其资源。
MyProperty="{StaticResource SomeResourceIdString}"
自定义控件通常依赖于在特定 CLASS 上下文中标识的资源,使用 `ComponentResourceKeys`,必须从客户端使用以下语法变体之一引用
MyProperty="{DynamicResource {ComponentResourceKey TypeInTargetAssembly=
{x:Type custom:SomeClass}, ResourceId= SomeResourceIdString }}"
MyProperty="{DynamicResource {ComponentResourceKey {x:Type custom:SomeClass},
SomeResourceIdString }}"
MyProperty="{StaticResource {x:Static custom:SomeClass.PropertyToExposeSomeResource}}"
这要求客户端了解控件库内部定义的特定 `ComponentResourceKey string`,这并不总是可取的。我们的控件通过向客户端公开由 Intellisense 友好的 `enum` 值支持的属性,并在设置底层 `DependencyProperties` 之前将这些 `enum` 转换为 `ComponentResourceKeys` 来避免这种情况。为了消除额外的步骤,我们使用相同的 `string` 来定义 `enum` 值及其关联的 `ComponentResourceKey`。
杂项
XAML 文件组织
实际的 Custom Control Library 最终会包含许多不同的控件,将它们所有相关的标记都转储到一个 `generic.xaml` 文件中将导致无法维护的混乱。在我们的项目中,`generic.xaml` 会从第二个 XAML 文件中提取信息(目前有一个已知的 WPF 错误,在引用 *Themes* 文件夹内的同级 XAML 文件时需要指定完整路径)。我们还将颜色定义(即使只在一个地方使用)提取到一个单独的文件中。颜色在开发过程中往往会频繁更改,通过扫描越来越多的非平凡标记来挑选颜色值会极其繁琐。
动画 Storyboards
Storyboards 在启动的那一刻就被“冻结”了,导致尝试将动画的“To”或“From”属性绑定到动态更新的值失败,这就是为什么我们被迫使用 `static SystemParameters.FullPrimaryScreenWidth` 值作为我们“Blip”动画中的 `TranslateTransform` 目标。另一个选项可能是响应窗口大小调整事件显式重新加载动画。
启动动画的预期架构方法是让 `AlarmBar` 引发一个“`AlarmChanged`”`RoutedEvent`,该事件在模板中通过 `EventTrigger` 处理。虽然这在运行*单个*动画时效果很好,但在我们的情况下(有时必须在两个不同模板的上下文中并行运行两个动画),我无法使其正常工作。
Andy L. 的其他项目
历史
- 2008 年 7 月 22 日:初始发布