65.9K
CodeProject 正在变化。 阅读更多。
Home

文章零: 构建 UI 平台

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.45/5 (11投票s)

2005年2月4日

CPOL

8分钟阅读

viewsIcon

74265

downloadIcon

1132

在 C# 中构建 UI 平台

Article Selector

引言

在图形用户界面方面,富有远见的想法的流动本质上是

Vannevar Bush -> Douglas Englebart -> Alan Kay

有趣的是,在 2005 年,Vannevar 撰写关于 memex 的六十年后,Englebart 发明鼠标的四十年后,Kay 发明 GUI 的近三十年后,我们仍然在使用一个由这些伟人的稀释想法所诞生的 GUI 的黑盒实现。我们需要的是一个现代化的、面向对象的、高度集中的类库,可以根据需要进行组合以创建最先进的 UI。

这就是本文系列的宗旨,这是一个宏大的目标,所以让我们开始吧。

快速浏览一下现代 UI(至少与 C# 相关)的格局会发现

  • Avalon,哦是的,它来了
  • Xamlon,就在这里,我们鼓励您查看一下
  • VG.net,通过 100% 的托管代码绘制最先进的矢量图形
  • MyXaml,今天就可以使用 C# 进行声明式编程

本质上,您可以通过 Xamlon 在 C# 应用程序中绘制酷炫的矢量图形。您可以使用 MyXaml 将声明式 C# 代码转换为 XML。借助 VG.net,您可以通过 C# 包装类利用 GDI+ 来绘制矢量图形。

这些技术中的任何一种都可以帮助您改进应用程序,除了 Avalon,它仍处于 beta 阶段。但最终,我们又回到了起点:我们依赖的控件仍然被锁定在黑盒中,潜在的创新领域被完全阻碍。我们认为 C# 和 .NET Framework 足够强大,足以纠正这种情况。所以让我们直奔主题……

下一步

首先,我们需要一个简单的应用程序(称之为 `ApplicationZero`),以帮助我们扩展核心控件逻辑。这必然意味着命中测试、拖放、热控件检测(更多命中测试),最重要的是流畅、快速、*无闪烁*的绘制。这是 `ApplicationZero`

在这个应用程序中,您可以拖动实心矩形并将其放置在空框中。当鼠标悬停在实心矩形上时,它会改变颜色,并在拖动过程中保持该颜色。当实心矩形在拖动过程中与框相交时,框会改变颜色。将实心矩形放置在框内的任何位置都会使其精确地落在框内(“吸附”行为)。

对 `ApplicationZero` 的一些测试

  • 绘制实心矩形。
  • 绘制框。
  • 绘制实心矩形热点。
  • 绘制框热点。
  • 将实心矩形向左拖动一像素。
  • 将实心矩形拖动直到它与框相交。
  • 将实心矩形拖动到框上方并释放。

编写这些测试并不那么容易。我们如何知道实心矩形已正确绘制?我们如何模拟拖动?我们先来解决绘制问题。

绘制策略

为了实现*无闪烁*绘制,我们肯定需要缓冲绘制操作。也就是说,我们需要将控件绘制到 `Bitmap` 中,然后将位图的选定区域*闪现*到屏幕上。这种古老的技巧是由 Dan Inglis 在 70 年代 Xerox 的 Palo Alto 研究中心发明的。如果您想深入了解混合和 2D 图形,有许多优秀的资源可用,其中一些列在本文章的末尾。

现在,我们直接切入正题:C# 依靠 GDI+ 进行绘制。Dan Inglis 技术在 GDI+ 中的对应物是 `DrawImage` 方法,我们将使用它将 `offscreen` 位图的一部分绘制到 `Windows.Form` 的 `Graphics` 对象上。为了实现 `ApplicationZero` 中看到的炫酷效果(框出现在半透明矩形下方),GDI+ 使用 alpha 混合。Alpha 混合给 `DrawImage` 带来了严重的性能问题,因为透明度的错觉是通过组合两个图像的像素来创建的。在 GDI+ 中,这可能很慢:非常慢。为了让 `DrawImage` 高效工作,我们必须使用一种特殊的 `Bitmap`,它允许使用一种更简单(也更快)的组合算法(注意后续代码中的构造函数)。

让我们逐步实现可用的缓冲代码,我们有

创建缓冲区

using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;

Form form = new System.Windows.Form();

Bitmap offscreen = new Bitmap(form.ClientSize.Width, 
      form.ClientSize.Height, PixelFormat.Format32bppPArgb);

using (Graphics graphics = Graphics.FromImage(offscreen))
{
    graphics.Clear(Color.White);
}

绘制“控件”`offscreen`

using (Graphics graphics = Graphics.FromImage(offscreen))
{
    graphics.DrawRectangle(Brushes.Black, 10, 10, 100 , 100);
}

最后,将“控件”*闪现*到窗体上

using (Graphics graphics = form.CreateGraphics())
{
    graphics.DrawImage(offscreen, 10, 10, 100, 100);
}

这段代码虽然简单,但它体现了缓冲绘制技术的核心。有了这个策略,我们现在可以考虑测试了。那么我们应该如何验证控件是否正确绘制?事实证明,这是基于缓冲区的绘图的另一个好处——窗体的客户端区域的完整表示可以在 `offscreen` 位图中获得。采样该 `Bitmap` 的一小块区域,例如控件应该出现的位置,您就有了验证正确性的方法。

采样

注意我们说*采样*。我们将*采样*控件应该出现的位置。这意味着我们将只保存控件区域中一部分像素的值。为什么?为什么不在每次测试中保存整个 `offscreen` 位图?好问题。主要问题是整个 `offscreen` 位图通常过于精确。它是对测试结果的*过度指定*。例如,假设有一个多控件测试,它考虑了文本框和单选按钮之间的交互。这是一个压力测试,还包含其他控件。如果窗体上任何控件的绘制发生最轻微的变化,我们的测试框架就会在该测试上发出红色警报。

但那又怎样,让我们继续前进。下一个问题将是:研究红色警告,通过检查两个 `Bitmap` 来找出它们之间的差异(或差异),然后确定原因。这通常会导致与当前案例无关的研究,因为主 `Bitmap` 包含了窗体上所有控件的完整表示。现在,这并非全是坏事,有时通过研究“与当前案例无关”的事情,您可以发现一些非常棘手的错误。但真正的问题是,保存整个 `Bitmap` 可能导致所有测试因单个控件的任何轻微更改而全部失败。想让两千个案例通过位图比较器运行,手动研究每次更改的原因?这没什么用。

因此,采样被证明是一种更有效的方法,因为它允许我们专注于我们正在测试的那个控件,而忽略我们没有测试的控件。这降低了主测试集对系统更改的敏感性,并通常避免了在进行任何微小的绘制调整后,整个测试集都失败。

如果我们不使用位图比较器来隔离绘制结果中的更改,我们会使用什么?很高兴您问了。我们将使用一个采样查看器,它显示采样像素中的颜色,并大致按照像素在控件中的排列方式进行排列。并排放置两个这样的查看器(一个显示实时结果,另一个显示主结果),我们可以一目了然地识别差异。

所以,让我们增强我们的测试框架以支持这种新的测试风格。我们确实需要两样东西。一个托管测试的 Windows 窗体和一个带有采样查看器的 Monitor。下面显示了增强的测试框架。

出现在左上角的是 Windows 宿主窗体,由用例创建。在测试期间创建的所有控件都驻在此窗体中。我们新的用例祖类 `PaintCase` 有一个设置例程来创建窗体,以及一个拆卸例程来关闭窗体。在左下角是新的 `Monitor`,带有两个采样查看器——左侧是实时结果,右侧是主结果。已选择第二个用例,并且您可以看到颜色匹配。

让我们更仔细地看看采样器。

采样模式由控件每个角的三个对角线像素以及控件每条边中点的像素组成。对于我们在此处采样的控件,此技术绰绰有余。

编码 ApplicationZero

要达到这一点,显然我们必须进行一些编码。基本架构如下所示。

  • FormBlaster - 包含缓冲区并将缓冲区闪现到窗体
  • Control - 定义基本的“`paintable`”对象
  • ControlOverlay - 表示窗体的 `ClientArea`,包含所有子控件
  • Bounds - 声明一个单一的抽象例程:`Contains`,用于命中测试
  • Rectangle - 一个典型的矩形,实现了 `Contains`
  • Stone - `ApplicationZero` 中的实心、可拖动的矩形
  • StonePad - `ApplicationZero` 中的拖放目标

`Paint` 调用流程如下。

简单,简单,简单,是的。但这就是我们目前想要的。如果您像鹰一样观察,您可能会注意到 Case List 中的最后两个用例带有空的圆圈图标。这表明该用例没有主项。我们目前没有“热度”的实现,所以这些用例无法正确绘制。在下一篇文章(第一篇文章:拖放)中,我们将完成 `ApplicationZero`,并介绍命中测试和拖放。

链接

更新

  • 2005 年 2 月 4 日 - *UICaseBaseSource.zip* 包含关于如何使用 `UICaseBase` 为新用例集继续进行的详细注释
© . All rights reserved.