简介
长期以来,程序员们都使用 C 和 C++ 来开发 Windows GUI 应用程序。对于我们当中很多人来说,这一段历史可以追溯到 Windows 2.0 时期,那时,我们使用基于 C 的 16 位 Windows API,即便只是显示一个窗口,也需要编写数十行代码。幸运的是,随着时间的推移,抽象的级别越来越高,越来越好。在 1992 年,Microsoft 发行了 Programmer's Workbench,其中包括 Microsoft 基础类库 1.0 版。Microsoft 基础类库 1.0 版包含大约 60 个类,用于包装窗口化编程和绘制部分的 16 位 Windows API。到 2002 年为止,Microsoft 基础类库 7.0 版已经发展为超过 200 个类,并且,其用途已经扩展为能够提供 Microsoft Win32 API 的完整 C++ 对象模型替代物。
虽然 MFC 非常强大,但它也有很多缺点,比如它只是 Win32 API 外的一层薄薄的面板,并且对于很多程序员来说,它太过复杂,很难有效地使用。通常,要开发 Windows 应用程序,仍需要直接访问 Win32 API,特别是在对 Windows 应用程序所需的基本功能的要求不断提升的情况下。因此,要开发任何功能真正强大的 Windows GUI 应用程序,需要耗费大量时间精力。为了应对应用程序开发难度不断提高的状况,Microsoft 于 2002 年初发行了一个针对 Windows 平台的新编程环境。该环境称为 .NET 框架,它为开发人员提供了一个托管 应用程序运行库,以及称为 .NET 框架类库的大量库。.NET 框架可以管理内存和安全性,从而能够产生更为可靠的应用程序。.NET 框架类库提供了一个大型、资源丰富和统一的类库,任何 .NET 语言(包括 Micrisoft 为 .NET 程序员提供的 C++ 的托管扩展和 C++ 的托管版),都可以以相同的方式同等地访问该类库。作为 .NET 框架的一部分,Windows 窗体是一组用于构建客户端 Windows GUI 应用程序的类。
本文中,我们将深入了解如何使用 C++ 的托管扩展编写 Windows 窗体代码,先介绍如何从头开始编写,然后讲解如何使用 Microsoft Visual Studio .NET 2003 来完成这一任务。与此同时,我们将着重介绍 Windows 窗体的一些常用功能,如自动布局和数据绑定。最后,我们将把注意力集中到 Windows 窗体与 MFC 的比较以及在进一步使用托管扩展时,如何混合使用这两套工具。
什么是 Windows 窗体?
Windows 窗体是一个窗口化工具包,而不像 MFC 一样是完整的应用程序框架。事实上,相对于 Windows 窗体所提供的用于构建基于文档的独立应用程序的功能来说,MFC 提供的功能更多。例如,如果要生成一个文本编辑器,在 MFC 中,只需运行一个向导,选择适当的选项并编写若干行代码即可完成。仅仅是通过运行该向导得到的应用程序就包含了一个状态栏、一个工具栏(浮动),并实现了所有的 File、Edit 和 Help 菜单项,其中包括最近使用的文件列表和打印以及上下文相关的帮助,所有这些内容都包含在一个完全徽标兼容的单文档界面 (SDI)、多 SDI 或多文档界面 (MDI) 应用程序中。作为基于文档的应用程序框架,没有能够与 MFC 相匹敌的竞争者。
但是,程序员们现在倾向于构建更多基于 HTML 的应用程序,或者能够与业务对象或数据库服务器通信的应用程序,而不是基于文档的应用程序。.NET 框架和 Windows 窗体正是为这一目的而量身定做的。
这并不是说 Windows 窗体不能用来构建优秀的基于文档的应用程序。实际上,由于 Windows 窗体只是 .NET 框架所提供的超过 2000 个类中的一小部分,您所需要的内容很有可能 Windows 窗体并没有提供,而是位于该框架中的其他部分中。例如,Windows 窗体本身并不提供任何对象序列化支持,但是 .NET 框架类库的其余部分提供了多种序列化对象图的方法。
这是 MFC 和 Windows 窗体之间的主要区别。MFC 旨在替代基础 Win32 API,但这并不能阻止 Win32 API 增长。事实上,就像 MFC 随时间的不断增长一样,基础 OS 的功能最少增加了十倍。但是,Windows 窗体只是 Win32 的窗口化部分的替代物。.NET 框架类的其余部分负责替代其余的 Win32。当然,框架永远不可能替代整个 Win32 API,不过,由于在可预期的未来,大多数要添加到 Windows 中的新功能都要被添加到框架中,替代整个 Win32 API 将是一个未来的目标。
因此,虽然 Windows 窗体不能具有 MFC 的全部功能,但它的确提供了一组很强的功能,可以极大地便利客户端应用程序开发人员,这其中包括一些 MFC 完全不具备的功能。下面,我们将介绍如何从头开始构建应用程序,然后讲解 Visual Studio .NET 2003 为 Windows 窗体 C++ 程序员提供的工作效率改进功能。
从头开始创建 Windows 窗体
典型的 Windows 窗体应用程序有至少一个窗体。窗体就是一个窗口,也就是从 Windows 1.0 开始我们所看到的 Microsoft 用户界面单元。通常,Windows 窗体应用程序中的一个窗体是主窗体,意思是它是应用程序的生存周期内可能显示的所有其他窗体的父级或所有者。该窗体是显示主菜单以及工具栏、任务栏等的位置。主窗体结束时,应用程序也退出。
应用程序的主窗体可以是一个简单的消息框、一个对话框、一个 SDI 窗口、一个 MDI 窗口,或者更为复杂的控件,如 Visual Studio .NET 这样的应用程序中具有多个子窗口、工具窗口和浮动工具栏的窗体。
如果您的应用程序极为简单,可以使用充斥在任何窗口化系统中的简朴的消息框来实现它:
#using <mscorlib.dll
#using <System.dll
#using <System.Windows.Forms.dll
void __stdcall WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
long lpCmdLine,
int nCmdShow)
{
System::Windows::Forms::MessageBox::Show("Hello, Windows Forms");
}
作为 C++ 程序员,您必定非常熟悉 WinMain 入口点,在托管应用程序中,仍需要该入口点。在我们的第一个 Windows 窗体应用程序中,唯一一行实际的代码调用了 Windows::System::Forms::MessageBox 类的静态 Show 方法,这其实只是调用包含在 Window::Systems::Forms 命名空间 内的 MessageBox 类的静态方法的长写形式。.NET 框架类库中广泛使用了命名空间来将类、结构、枚举等类型分隔为不同的逻辑组。由于有数以千计的 Microsoft 雇员要从事添加 .NET 框架类库的工作,而且有数以百计的第三方组织要扩展该类库,同时有数以百万计的程序员正在尝试学习它,因此这种分隔是非常必要的。没有命名空间,就需要采用各种复杂的约定来唯一地命名各种对象(像现有的 Win32 API 一样)。
命名空间中的类型的定义位于 .NET 程序集 中。程序集是打包为 DLL 或 EXE 的托管类型的容器。#using 指令用于将程序集中的类型应用到您的应用程序中。例如,mscorlib 和 System 程序集提供了基本的 .NET 框架类型,如 int 和 string。System.Windows.Forms 程序集包含了 Windows 窗体类型。
使用 #using 将程序集应用到应用程序中后,就可以以前面讲到的长写形式引用类型,或者,可以使用标准的 C++ using namespace 语句来省略键入代码的工作:
#using <mscorlib.dll
#using <System.Windows.Forms.dll
using namespace System::Windows::Forms;
void __stdcall WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
long lpCmdLine,
int nCmdShow)
{
MessageBox::Show("Hello, Windows Forms");
}
因此,虽然这个简单的示例很有效地演示了最基本的 .NET 框架和 C++ 的托管扩展的概念,但它并不能很好地演示典型的 Windows 窗体程序。对于真实的应用程序,将需要一个 Form 类(或从 Form 派生的类)的实例,如下所示:
void __stdcall WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
long lpCmdLine,
int nCmdShow)
{
Form* form = new Form();
...
}
form 变量引用了托管类型的一个实例。托管对象是由 .NET 框架的公共语言运行库 (CLR) 处理的,它们的生存周期由垃圾回收器控制的,该回收器在一定的时间取消分配内存。因此,C++ 程序员无需显式地删除托管类型,但是同样不能指望在任何特定时刻(如关闭范围时)破坏对象。
创建窗体后,就需要显示它。如果您曾经看过 Windows 窗体的文档,可能已经注意到了 Form 方法 Show,这意味着:
void __stdcall WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
long lpCmdLine,
int nCmdShow)
{
Form* form = new Form();
form-Show(); // This is not what you want to do.
}
虽然上面的代码可以显示窗体,但是必须眼疾手快才可以看到该窗体,这是因为 Show 以无模式的方式显示窗体。这意味着在 Show 将新的窗体显示到屏幕上之后,会立即将控制权返回给 Main 函数,该函数在返回时将退出进程,同时关闭刚刚显示不久的窗体。要以有模式的方式显示窗体,文档建议使用 ShowDialog 函数:
void __stdcall WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
long lpCmdLine,
int nCmdShow)
{
Form* form = new Form();
form-ShowDialog(); // This is not quite what you want to do.
}
虽然这些代码事实上可以显示一个空窗体,并且在用户关闭它之后才会将控制权返回给 Main 函数,但通常您并不会编写这样的代码。相反,您将把一个窗体指定为主窗体,使得应用程序的其他部分可以把它当作主窗体来进行访问。