文章一: 在 C# 中构建 UI 平台 - 拖放






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

85461

2853
拖放的初始架构开始起飞。
引言
要解决拖动的问题,我们需要非常仔细地研究拖动序列。首先,鼠标指针会移动到一个控件上(使其“变热”)。然后,按下鼠标左键。接着,移动鼠标直到控件的某一部分与放置区域相交。最后,释放鼠标左键。这是标准的完美用户操作的拖动。如果用户非常用力地单击鼠标左键,导致鼠标在释放按钮之前移动了几个像素,怎么办?在内部这可能看起来像一次拖动,但实际上是一次单击。为了避免意外拖动的问题,我们需要抑制拖动的开始。这可以通过在移动了几个像素之后*才*开始拖动来实现。
另一种情况:用户开始拖动,但在放置之前单击了鼠标右键。这是什么意思?就我们而言,我们将把鼠标右键单击解释为一种求助的请求。拖动可能很危险(你有没有在 Windows 资源管理器中丢失过文件夹?),为了让用户对拖动感到放心,我们需要支持他们随时取消拖动的权利。拖动过程中右键单击可以很好地实现这一点。
拖动周期
让我们将所有这些综合起来形成一个目标序列:拖动周期。拖动周期将包括:
- 开始(按下鼠标左键并且鼠标移动了超过几个像素)。
- 拖动(按下鼠标左键移动鼠标)。
- 结束(释放鼠标左键)。
- 取消(拖动时按下鼠标右键)。
除了区分拖动的精确序列外,我们还需要识别不同类型的拖动。首先,我们有“复制”拖动,即复制被拖动项的副本,然后在窗体中移动它,直到放置。这里的用户概念模型是:拖动以创建某物。第二种拖动是“移动”拖动,即不创建副本,而是拖动项本身。在这里,我们将拖动用作重新排列项的一种方式;不创建新内容。这两种拖动都应该得到支持。
ApplicationZero 和一些测试
根据我们之前的文章,我们有 ApplicationZero
实心矩形可以拖动到框架上并放置。这是一个移动拖动。涵盖完整的拖动周期,我们有以下测试:
- 将鼠标放在实心矩形上(变热)。
- 将鼠标放在框架上(无反应)。
- 将实心矩形拖动到框架上(框架变热)。
- 将实心矩形拖动到框架上并放置(实心矩形移动)。
- 向左拖动一像素(由于抑制而无反应)。
- 向左拖动一个矩形宽度(开始拖动)。
- 向左拖动一个矩形宽度并右键单击(拖动被取消)。
- 拖动到框架上并右键单击(拖动被取消)。
你可能已经注意到,我们将上篇文章中未完成的案例移到了这个案例集中。将这些案例与拖动测试集一起出现似乎更合适。
拖动基础设施
通过增强我们的实现,我们有:
WindowsForm
- 一个标准的System.Windows.Form
。ControlSystem
– 平台的核心,将窗体事件路由给处理对象。FormOverlay
– 填充窗体客户区的控件的基类。ControlOverlay
– 所有标准控件的最终父类,位于所有其他覆盖层之下。DragOverlay
– 参与拖动的控件的最终父类,位于ControlOverlay
之上。Mouse
– 使用HitTester
来定位热点控件,并在存在热点控件的MouseTrap
方法时调用它们。HitTester
– 遍历控件复合体,找到最前面的控件。MouseTrap
– 处理给定控件的鼠标事件。DropSite
– 在窗体中定义一个目标区域,兼容的DragBots
可以放置在此区域。DragBot
– 在窗体中定义一个源区域,可以从此区域开始拖动。DragManifest
– 包含DragBots
和DropSites
,便于拖动。
拖动演练
新的 ControlSystem
类会挂钩标准 Windows 窗体的鼠标事件,并调用 Mouse
上的相应方法。Mouse
使用 HitTester
来查找给定鼠标事件的最前面控件。找到控件后,Mouse
会搜索其 MouseTrap
集合,查找与控件对应的陷阱。如果找到 MouseTrap
,则调用相应的鼠标方法。这是鼠标处理的基本流程。
通过创建 DragBot
、DropSite
并将它们添加到 DragManifest
来实现拖动。DragBot
将 DragTrap
部署到 Mouse
中。DragTrap
包含抑制实现(在鼠标按下左键移动超过三像素之前不会开始拖动)。
对于 ApplicationZero,为实心矩形创建了 DragBot
,为框架创建了 DropSite
。当 DragTrap
触发时,DragBot
开始移动实心矩形,检查 DragManifest
是否有兼容的 DropSite
。实心矩形与框架相交的瞬间,DragManifest
会返回一个 DropSite
。此时 DropSite
会改变框架的外观,指示这是一个合法的放置点。如果实心矩形被拖走,DropSite
会将框架恢复到“冷”外观。当实心矩形在框架上方释放时,DragBot
会向 DropSite
请求放置位置,然后相应地定位实心矩形。这就实现了拖动结束时的“吸附”效果。
有了这个基础设施,前八个测试现在都可以通过了。
通过创建不同的 DragBot
(FactoryDragBot
),我们可以将 ApplicationZero 的行为更改为工厂拖动。在这里,实心矩形在拖动开始时被克隆,然后拖动克隆体。通过复制移动拖动测试并更改 DragBot
的声明,可以轻松创建工厂拖动的测试。工厂拖动的绿色指示灯
调整:限制拖动
最基本的拖动行为现在已经就绪。在完成之前,让我们再添加一项:轴锁定。轴锁定可以轻松地将拖动限制在垂直线或水平线上。锁定本身的作用非常简单:它获取拖动操作的增量(表示为点),并对其进行修改以符合某个规则。为了将拖动限制在水平线或垂直线上,我们只需将拖动增量的 X 或 Y 部分置零。DragBot
负责动画拖动序列,并且似乎是安装约束(我们将称之为 Tweak
)的绝佳位置。要安装 Tweak
,我们将这样编写代码:
MoveDragBot dragBot = new MoveDragBot(solidRectangle);
ControlSystem.DragManifest.AddDragBot(dragBot);
dragBot.Tweak = new HorizontalDragTweak();
我们可以设计任意数量的拖动调整,实现吸附式行为等。现在,我们先坚持使用垂直和水平。我们的测试:
- 带水平
Tweak
的移动拖动。 - 带垂直
Tweak
的移动拖动。 - 带水平
Tweak
的工厂拖动。 - 带垂直
Tweak
的工厂拖动。
为了帮助我们手动检查调整,我们将在窗体上绘制一条线,指示拖动的有效方向。
在检查完每个案例的结果后,我们进行主控、验证,并在完全绿色(即测试全部通过)的情况下继续前进。
好了,ApplicationZero 的内容就到这里。在下一篇文章中,我们将使用多线程来动画化这些相同的测试。这种技术将帮助我们模拟“真实”用户,使所有未来的 UI 测试更加逼真……
题外话
当你审视拖动基础设施时,你可能会想:所有这些真的有必要吗?——看起来有些过度设计。对此我们说:你可能是对的。请记住,我们的目标是创建一个 UI 库,允许你组合元素并根据需要创建尖端的 UI。这是一个非常崇高的目标。正如开创性的设计模式著作中所述:“面向对象软件的设计很难,而设计可重用的面向对象软件则更难”。虽然这句话一点也不为过,但我们试图将任何给定的方法归结为结构和策略。也就是说,我们要么在定义某个东西,要么在操作某个东西。
在定义结构时,我们试图在需要扩展的领域声明极简的祖先类和接口。在不需要扩展的领域,我们通常会将其锁定,因为可扩展性总是会带来性能损失。在定义策略时,我们更倾向于可插入性。祖先类和接口可以很好地用于策略,但它们也可能是一个真正的噩梦。这通常是由于策略中涉及的时间。一个沉重的祖先类,它为你做了“一半”的工作,并提供了一些微小的抽象方法,让你可以在“恰到好处的时机”做事情,但随着系统的发展,它很快就会崩溃。例如,请看上面介绍的 Tweak
类。我们喜欢这个类,因为它目标明确,而且祖先类什么都不做。当你实现并插入一个调整时,你就完全控制了。这就是我们试图设计策略类的方式。
但是,你可能还是对的。随着我们继续实现各种拖动功能,我们将看到这个基础设施的实际效果。
下载次数
- UICaseBaseSource.zip - 34.6 KB。使用测试框架时的起始解决方案。
- UITestingFrameworkSource.zip - 48.1 KB。测试框架的源文件。
链接
- About Face Alan Cooper - (第一版,第 263 页)。
- Design Patterns Gamma, Helm, Johnson and Vlissides - (第 1 页)。