这篇文章是关于使用MVC(Model-View-Controller,模型-视图-控制)框架在windows平台下创建OpenGL图形界面应用程序。MVC框架在GUI(Graphic User Interface,图形用户界面)应用程序中被普遍使用,并且在很多GUI库中被使用,例如.NET,MFC,Qt,Java等。MVC框架的好处是将与系统无关的OpenGL命令和Windows系统相分离,和多个窗口之间的通用的消息路由。
MVC 模式是将应用程序划分成3个独立的组件:模型层,视图层,控制层来尽可能地降低他们之间的相互依赖。
MVC设计模式
模型层是应用程序的核心部分,它包含了所有应用程序中的数据和定义应用程序行为的实现。更重要的是,模型层没有任何指向视图层和控制层的引用,这意味着模型层是完全独立的。它完全不知道视图层和控制层的行为。模型层简单地加工视图层和控制层的请求。
视图层负责渲染可视化的内容到屏幕上。视图层不含有指向控制层的引用(与控制层独立)。它只在控制层发送更新请求时执行渲染操作。然而,视图层需要有指向模型层的引用,因为它需要知道从哪里得到数据,这样才能将数据渲染到屏幕上。
控制层是用户和应用程序之间的桥梁,它接受和处理所有的用户事件,如何鼠标和键盘输入。这个模块需要知道访问那个模型层组件和视图层组件。为了处理用户事件,控制层组件请求模型层处理新的数据,同时,告诉视图层更新显示的数据。
这儿有一个非常简单的例子。假设你做了一个将加拿大元转换成美元的工具。当用户点击“Convert”按钮时,你的应用程序应该怎么做?
一个GUI例子:货币兑换
- 控制层组件首先接受按钮点击事件
- 控制层组件将输入的数据发送到模型层并请求转换
- 模型层将输入数据转换成美元并保存结果
- 控制层逐渐请求视图层组件显示结果
- 视图层组件从模型层接收数据
- 视图层组件在屏幕上显示结果
MVC设计模式的主要的优点是清晰和模块化。由于MVC模式清晰地将一个应用程序划分成了3个逻辑上的组件,它变得更干净和更容易理解每个组件的任务,并且每个模块能单独地被多个开发着维护。由于高效的模块化,组件更加易于扩展和集成。例如:你可以自定义一个视图显示界面而不需要修改模型层和控制层,或者同时添加多个视图。
这是一个简单的OpenGL Windows应用程序。它除了鼠标交互之外不含有其他GUI控制事件。然而,这个例子很容易理解如何利用MVC模型实现一个OpenGL应用程序。我们将在下面讨论更复杂的情况。
这个应用程序有3个独立的C++类,ModelGL,ViewGL和ControllerGL。对OpenGL应用程序来说,所有与系统无关的OpenGL命令被放在ModelGL组件中,这样模型层组件可以被多个平台平台重复使用而不需要做任何更改。因此,模型层和视图层和控制层完全独立。
视图层组件用来将需要显示的数据渲染到屏幕中。因此,所有显示设备属性(渲染上下文,颜色位,等)在这个组件内。与系统相关的OpenGL命令也放在这个组件中,例如wglCreateContext()和wglMakeCurrent()。视图层组件不含有指向控制层的引用(与控制层独立),但是可能需要引用模型层组件,例如:得到模型层的数据来更新视图层的内容。
控制层组件首先接收用户事件,然后更新模型层的状态,通知视图层逐渐渲染场景。它有基本的输入处理函数来处理键盘和鼠标事件。请仔细查看ControllerGL类中的代码:keyDown(),IButtonDown(),rButtonDown(),mouseMove()等。ControllerGL类从Controller类派生过来。你可以简单地添加事件处理到ControllerGL类中如果你需要重写默认的行为。
这3个对象都是在main函数中创建的,然后一个指向ControllerGL对象的窗口被创建。我使用了一个帮助类WIndow.cpp类来创建一个窗口。注意main函数依然非常简单,所有详细的实现细节都被移到这3个单独的组件中了:ModelGL,ViewGL和ControllerGL。
创建OpenGL窗口和创建其他普通的窗口除了OpenGL渲染上下文(rendering context,RC)之外其他都是一样的。OpenGL渲染上下文是连接OpenGL和Windows系统的端口。所有OpenGL命令可以通过这个渲染上下文传送。渲染上下文必须和一个和它拥有同样像素格式的设备上下文(device context,DC)相关联。因此,OpenGL可以在设备表面绘制图形。
在你的WM_CREATE消息处理函数中,你可以创建一个RC:
- 使用GetDC()函数得到OpenGL窗口的DC和窗口句柄
- 使用SetPixelFormat()函数和DC设置希望的像素格式
- 使用wglCreateContext()函数和DC创建RC
- 使用ReleaseDC()释放DC
在你的WM_CLOSE消息处理函数中,你可以删除RC:
- 使用wglMakeCurrent()函数和NULL参数释放RC
- 使用wglDeleteContext()函数删除RC
在你的渲染循环中,在调用任何的OpenGL命令之前使用wglMakeCurrent()函数设置渲染上下文为当前的RC。我为渲染循环使用了一个单独的线程。请看下面的部分:“渲染OpenGL的单独线程”。
查找希望的像素格式可以通过使用DescribelPixFormat()函数来查找所有的像素格式。一个标准的查找最好的像素格式的机制的方法findPixelFormat()定义在ViewGL类中。
MS是一个事件驱动的系统。一个事件驱动的windows应用程序如果没有事件触发的话将不会做任何事情。然而,OpenGL渲染窗口即使没有事件触发也需要经常的更新。一个解决OpenGL窗口经常更新的方法是在消息循环中使用PeekMessage()。
然而,为渲染OpenGL使用一个单独的线程是一个更好的方法。多线程的好处是你可以离开消息循环去做其他的事情(只处理windows事件),单独的线程将会独立地处理渲染OpenGL场景。我们也不会再消息循环中暴漏任何OpenGL渲染的方法。这样,消息循环可以和以前一样简单。
当一个窗口创建时(WM_CREATE事件触发),ControllerGL对象将会初始化ModelGL和ViewGL对象,然后,为OpenGL渲染开启一个新的线程。_beginthreadex()被用来创建一个新的线程。请查看ControllerGL::create()和ControllerGL::runThread()获取更多的细节。
这个例子有3个窗口,OpenGL渲染子窗口,包含所有控件的子窗口,以及容纳这两个窗口的主窗口。(在主窗口中有菜单栏和状态栏,但是在这篇文章中我们不讨论它们)
下载源文件和可执行文件: glWin.zip
由于有3个窗口,需要3个Controller对象,每个窗口一个Controller对象:ControllerMain,ControllerGL,ControllerFormGL。两个View控件,ViewGL和ViewFormGL,一个用来渲染OpenGL,一个用来显示控件。因为主窗口只是一个简单的容器,它没有View控件。注意只需要一个Model对象,它被3个Controller和2个View引用。
glWin的MVC图
ViewFormGL是一个包含控件(按钮,文本框等)的对话框窗口。因此,所有的控件被定义在ViewFormGL类中。我为经常使用的控件实现了对应的类,它们在"Controlls.h"中声明。现在使用的控件有Button,RadioButton,CheckBox,TextBox,EditBox,ListBox,TrackBar,Combox和TreeView。
当用户点击”Animate“按钮时会发生下面的事情:
- ControllerFormGL首先接收到BN_CLICKED事件
- ControllerFormGL在ModelGL中设置动画标志为真,这样地球会旋转
- ControllerFormGL通知ViewFormGL改变按钮的文字为”Stop“
一个windows应用程序在你创建窗口(更加正确的说法是使用RegisterClass()或RegisterClassEX()注册窗口)的时候需要提供一个回调函数(窗口过程)。当某件事情在你的程序中发生,例如,用户点击了按钮,windows系统向你的应用程序发送消息,消息会被传送到你所指定的窗口过程中。
一般情况下,窗口过程如下:
MVC框架的另外一个好处是动态消息路由,就是将消息分发给与窗口句柄相关的Controller。因此我们可以创建一个窗口过程并在你创建的所有窗口中使用它。基本的思想是当你创建一个窗口时将指向Controller对象的指针放在CreateWindow()或CreateWindowEx()的lpParam参数中。当WM_NCCREATE消息触发时,我们从lpCreateParams参数中提取出这个指针并且将它存储为窗口GWL_USERDATA属性值。然后,另一个消息触发时,我们简单地查找窗口的GWL_USERDATA值来查看那个Controller与窗口相关联。由于这个窗口过程被所有你创建的窗口使用,你不需要再重新编写其他的窗口过程。
实际的消息路由如下:
在main函数中向下面的代码一样创建3个窗口。注意当我们在初始化窗口时提供了指向Controller对象的指针。这个指针的值必须保存在在每个窗口的GWL_USERDATA中,然后消息路由将会动态地将窗口事件分发到每个Controller中。
创建包含控件的窗口时有一点点的不同。我们在WM_INITDIALOG消息触发时就将Controller的指针保存在GWL_USERDATA中了(而不是在WM_NCCREATE消息触发时)。查看procedure.cpp获取更多的细节。
这个MVC框架提供了一个基本的Controller类,它是默认的时间处理器。事实上,它不做任何事情并返回0.当一个消息触发时如果想做一个有意义的事情,你需要创建一个继承自Controller类的类,并重写它的虚方法。虚方法的名字和去除前缀的消息的名字一样。例如,IButtonDown()函数是处理WM_LBUTTONDOWN消息的。
对于上面的glWin例子,ControllerMain,ControllerGL,ControllerFormGL都派生自Controller类。
ControllerMain负责当用户点击关闭按钮或”Exit“按钮时处理WM_CLOSE消息来终止应用程序。
ControllerGL简单处理相机操作器的鼠标交互(缩放和旋转相机),和当WM_CREATE消息到来时为OpenGL渲染创建一个单独的线程。
ControllerFormGL管理所有的控件界面。