文章三: 在 C# 中构建 UI 平台 - 复合控件





4.00/5 (3投票s)
2005年2月16日
8分钟阅读

44093

943
使用 TranslateTransform 和 Clip 有效地绘制子控件。
我们的第一个控件
到目前为止,我们所使用的所有控件都是为了开发核心基础设施而创建的。也就是说,它们并不打算在平台的生产版本中使用。这些测试控件的简单性使我们能够以循序渐进的 TDD 方法来实现平台的关键区域。当我们开始考虑我们的第一个真正的控件:Container
时,几个问题很快就浮现出来。
绘制子控件
首先,当前实现的绘制方法对于 Container
中的子控件将无法正常工作。在每个控件的 Paint
方法中,至少有两个假设:
- 控件可以从 0,0 开始绘制。
- 控件不能绘制超出其大小定义的区域。
我们正在将内容离屏绘制到一个代表窗体客户端区域的位图中。为了满足第一个假设,我们应该为复合体中的每一层控件调用 Graphics.TranslateTransform
。TranslateTransform
移动原点,将任何给定的子控件置于上下文中。为了满足第二个假设,我们应该将 Graphics.Clip
设置为控件的大小。Clip
将画布限制在给定的大小(或区域)。例如,假设我们有一个 Form,其中 ControlOverlay
中有一个蓝色容器,而该蓝色容器又父级化了一个红色容器。
绘制过程将如下进行:
首先绘制 ControlOverlay
,并将其裁剪到窗体的客户端区域(此处显示为白色)。由于 ControlOverlay
位于原点 (0,0),因此不需要平移。
接下来,绘制蓝色容器,将其裁剪并平移到其大小和位置。在这里,我们将平移到 10, 10(蓝色容器的位置)。
最后绘制红色容器,再次将其裁剪并平移到 10, 10(红色容器的位置)。
嗯,这 *几乎* 如此容易。这里最重要的一个技术要点是,在准备子控件进行绘制时,我们必须考虑 *当前* 的裁剪区域。如果我们不这样做,将只使用子控件的边界,子控件将像完全可见一样进行绘制(实际上,子控件的部分或全部可能被父控件裁剪)。这段代码片段是:
Rectangle bounds = (Rectangle) ((IBounds) aControl).Bounds;
Rectangle intersection = Rectangle.Intersection(bounds, aGraphics.Clip);
if (!intersection.IsEmpty)
{
aGraphics.Clip = intersection;
aGraphics.TranslateTransform(bounds.Location);
aControl.Paint(aGraphics);
}
这个绘制方案的一些合理测试将是:
- 绘制蓝色容器。
- 将蓝色容器定位在左侧被裁剪的位置。
- 将蓝色容器定位在顶部被裁剪的位置。
- 将蓝色容器定位在右侧被裁剪的位置。
- 将蓝色容器定位在底部被裁剪的位置。
- 绘制带有红色子容器的蓝色容器。
- 将红色子容器定位在左侧被裁剪的位置。
- 将红色子容器定位在顶部被裁剪的位置。
- 将红色子容器定位在右侧被裁剪的位置。
- 将红色子容器定位在底部被裁剪的位置。
在创建并掌握了这些测试之后,我们就准备好进行命中测试了。
命中测试子控件
由于前几篇文章中介绍的测试控件位于 ControlOverlay
或 DragOverlay
(它们代表窗体的客户端区域),因此这些控件的边界始终相对于窗体本身。这使得命中测试非常简单。Container
及其容纳子控件的能力会立即打破这个实现。为什么?因为父级化到 Container
的控件的边界将以相对于该 Container
的像素表示,而不是相对于窗体。将这些控件的边界与窗体像素中的鼠标点进行比较将不起作用。下图显示了这一困境:
这里,蓝色容器位于 10, 10,大小为 100, 100。红色容器也位于 10, 10(尽管在蓝色容器内部),大小为 10, 10。假设鼠标移动点为 25, 25,HitTester
应该返回红色容器。然而,当前的实现将返回蓝色容器。为什么?因为 25, 25 超出了红色容器的“原生”边界(10, 10, 20, 20)。因此,在命中测试子控件之前,HitTester
必须根据其上方的父控件来缩小(或平移)鼠标移动点。在这种情况下,在命中测试红色容器之前,我们必须先将鼠标移动点缩小到 15, 15(根据蓝色容器的位置进行缩小),然后将这个新点与子控件的边界进行比较。
一些用于验证新的 HitTester
的测试
父控件热点,子控件热点
子控件热点左侧裁剪,子控件热点顶部裁剪
子控件热点右侧裁剪,子控件热点底部裁剪
在构建这些测试时,我们将使用 Player 驱动的案例。也就是说,上一篇文章中介绍的 UI 动画解决方案现在将成为我们声明和运行测试的标准方式,因为这是我们目前最现实和最彻底的测试方法。
实现这些类并运行测试后,我们又回到了绿色状态。
请注意,这些测试要求热点控件的颜色发生变化。在这里,蓝色容器从海军蓝变为蓝色,红色容器从栗色变为红色。为了获得这种行为,我们需要一种新的 MouseTrap
,即 HotTrap
。
ControlSystem
- 将Windows.Form
事件路由到处理对象(如Mouse
)。HotRefresher
- 将HotTrap
部署到Mouse
中,并在鼠标进入或离开时绘制关联的控件。Mouse
- 处理所有与鼠标相关的事件。MouseTrap
- 与控件相关联,响应特定鼠标事件(在本例中为OnMouseOver
)触发事件。HotTrap
- 每次鼠标进入、悬停或离开控件时都会触发一个事件。HitTester
- 查找给定点的最前端控件。Control
- 所有控件的基类。Container
- 可以包含其他控件的控件(如面板)。
要使容器显示为“热点”,我们首先必须将容器与 HotRefresher
相关联。
Container container = new Container();
HotRefresher hotRefresher = new HotRefresher(container, ControlSystem);
HotRefresher
将 HotTrap
部署到 ControlSystem.Mouse
中。当 HitTester
返回 Container
时(即鼠标已定位到 Container
上方),将检索 HotTrap
并调用其 Move
方法。HotTrap
将控件的 HotProperty
设置为 true
,从而触发一个事件。HotRefresher
处理该事件,通过刷新控件来绘制热点状态。当鼠标离开控件时,将使用相同的过程来绘制冷态。
构建复合体
Container
控件的出现揭示了平台中另一个需要改进的领域:控件复合体。ControlOverlay
目前位于复合体的底部,您可以像这样向其中添加控件:
Control anyControl = new Control();
Form.ControlSystem.ControlOverlay.Controls.Add(anyControl);
这里,Controls
属性是一个 ControlCollection
。将集合公开为属性是可以的,只要您愿意承担监听集合并响应其中发生的每一次更改的负担。伴随这项责任而来的是语法本身,它看起来很陌生,尤其是如果您习惯于像这样父级化控件:
Control anyControl = new Control();
anyControl.Parent = Form.ControlSystem.ControlOverlay;
这种语法不仅更容易理解,而且将复合体更改的处理隔离到一个属性设置器(即 set_Parent
)中。
显然,Parent
属性方法是两者中更好的选择。出于这个原因,我们将从 ControlOverlay
中删除 Controls
属性。现在这将破坏我们测试中的大量代码,因为几乎所有测试都涉及创建一个控件并将其放入 ControlOverlay
或 DragOverlay
。重写测试以使用 Parent
而不是 Controls.Add
并不是什么大问题。而且,测试仍然应该显示为绿色。测试结束时控件的状态不应因我们以不同的方式构建复合体而有所不同。这正是 TDD 应该做的:支持我们进行重构。
测试复合体与测试任何树形结构一样。我们希望确保添加和删除“叶子”和“分支”。
- 向
ControlOverlay
添加子控件。 - 向
ControlOverlay
添加子控件,然后将其删除。 - 向
Container
添加子控件。 - 向
Container
添加子控件,然后将其删除。 - 向
ControlOverlay
添加带有子控件的子控件。 - 向
ControlOverlay
添加带有子控件的子控件,然后将其删除。
为这些测试添加动画似乎没有必要,但它实际上为我们提供了很好的视觉反馈,因为复合体正在发生变化。
随着这些测试的完成,我们的控件复合体就绪了,我们拥有了更好的绘制技术、更好的命中测试技术,并准备好继续开发另一个控件:Label
。这(以及其他方面)将是下一篇文章的主题。
项目统计
有趣的是,在这一点上,我们的测试代码和平台代码大致相当。
下载次数
- UICaseBaseSource.zip - 37.7 KB。使用测试框架时的起始解决方案。
- UITestingFrameworkSource.zip - 56.6 KB。测试框架的源代码。