OpenGL系列教程之十二:OpenGL Windows图形界面应用程序

这篇文章是关于使用MVC(Model-View-Controller,模型-视图-控制)框架在windows平台下创建OpenGL图形界面应用程序。MVC框架在GUI(Graphic User Interface,图形用户界面)应用程序中被普遍使用,并且在很多GUI库中被使用,例如.NET,MFC,Qt,Java等。MVC框架的好处是将与系统无关的OpenGL命令和Windows系统相分离,和多个窗口之间的通用的消息路由。


下载:glWinSimple.zipglWin.zip






MVC 模式是将应用程序划分成3个独立的组件:模型层,视图层,控制层来尽可能地降低他们之间的相互依赖。


MVC设计模式

模型层是应用程序的核心部分,它包含了所有应用程序中的数据和定义应用程序行为的实现。更重要的是,模型层没有任何指向视图层和控制层的引用,这意味着模型层是完全独立的。它完全不知道视图层和控制层的行为。模型层简单地加工视图层和控制层的请求。

视图层负责渲染可视化的内容到屏幕上。视图层不含有指向控制层的引用(与控制层独立)。它只在控制层发送更新请求时执行渲染操作。然而,视图层需要有指向模型层的引用,因为它需要知道从哪里得到数据,这样才能将数据渲染到屏幕上。

控制层是用户和应用程序之间的桥梁,它接受和处理所有的用户事件,如何鼠标和键盘输入。这个模块需要知道访问那个模型层组件和视图层组件。为了处理用户事件,控制层组件请求模型层处理新的数据,同时,告诉视图层更新显示的数据。

这儿有一个非常简单的例子。假设你做了一个将加拿大元转换成美元的工具。当用户点击“Convert”按钮时,你的应用程序应该怎么做?


一个GUI例子:货币兑换

  1. 控制层组件首先接受按钮点击事件
  2. 控制层组件将输入的数据发送到模型层并请求转换
  3. 模型层将输入数据转换成美元并保存结果
  4. 控制层逐渐请求视图层组件显示结果
  5. 视图层组件从模型层接收数据
  6. 视图层组件在屏幕上显示结果
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。

[cpp]  view plain  copy
  1. int WINAPI WinMain(...)  
  2. {  
  3.     // 实例化 Model and View,这样Controller能引用它们  
  4.     ModelGL model;  
  5.     Win::ViewGL view;   //在"Win"命名空间中因为它于窗口相关  
  6.   
  7.     //使用指向ModelGL和ViewGL的指针创建ControllerGL对象  
  8.     Win::ControllerGL glCtrl(&model, &view);  
  9.   
  10.     // 使用Controller对象创建窗口  
  11.     Win::Window glWin(hInst, L"glWinSimple", 0, &glCtrl);  
  12.     glWin.setWindowStyle(WS_OVERLAPPEDWINDOW | WS_VISIBLE |   
  13.                          WS_CLIPSIBLINGS | WS_CLIPCHILDREN);  
  14.     glWin.setClassStyle(CS_OWNDC);  
  15.     glWin.setWidth(400);  
  16.     glWin.setHeight(300);  
  17.   
  18.     glWin.create();  
  19.     glWin.show();  
  20.   
  21.     // 进入消息循环 //  
  22.     int exitCode;  
  23.     exitCode = mainMessageLoop();  
  24.   
  25.     return exitCode;  
  26. }  



创建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类中。

[cpp]  view plain  copy
  1. // 查找最好的像素格式  
  2. int findPixelFormat(HDC hdc, int colorBits, int depthBits, int stencilBits)  
  3. {  
  4.     int currMode;                   // 格式模式的ID  
  5.     int bestMode;                   // 返回值,最好的像素格式  
  6.     int currScore;                  // 现在像素格式的分数  
  7.     int bestScore;                  // 做好的像素格式的分数  
  8.     PIXELFORMATDESCRIPTOR pfd;  
  9.   
  10.     // 查找所有可能的像素格式  
  11.     bestMode = 0;  
  12.     bestScore = 0;  
  13.     for(currMode = 1;   
  14.         ::DescribePixelFormat(hdc, currMode, sizeof(pfd), &pfd) > 0;  
  15.         ++currMode)  
  16.     {  
  17.         // 如果不支持opengl则忽略  
  18.         if(!(pfd.dwFlags & PFD_SUPPORT_OPENGL))  
  19.             continue;  
  20.   
  21.         // 如果不能被渲染到窗口则忽略  
  22.         if(!(pfd.dwFlags & PFD_DRAW_TO_WINDOW))  
  23.             continue;  
  24.   
  25.         // 如果不支持rgb模式则忽略  
  26.         if((pfd.iPixelType != PFD_TYPE_RGBA) ||   
  27.            (pfd.dwFlags & PFD_NEED_PALETTE))  
  28.             continue;  
  29.   
  30.         // 如果不是双缓冲区则忽略  
  31.         if(!(pfd.dwFlags & PFD_DOUBLEBUFFER))  
  32.             continue;  
  33.   
  34.         // 尝试寻找最好的像素格式  
  35.         currScore = 0;  
  36.   
  37.         // 颜色位  
  38.         if(pfd.cColorBits >= colorBits) ++currScore;  
  39.         if(pfd.cColorBits == colorBits) ++currScore;  
  40.   
  41.         // 深度位  
  42.         if(pfd.cDepthBits >= depthBits) ++currScore;  
  43.         if(pfd.cDepthBits == depthBits) ++currScore;  
  44.   
  45.         // 模板位  
  46.         if(pfd.cStencilBits >= stencilBits) ++currScore;  
  47.         if(pfd.cStencilBits == stencilBits) ++currScore;  
  48.   
  49.         // alpha位  
  50.         if(pfd.cAlphaBits > 0) ++currScore;  
  51.   
  52.         // 判断是否是到目前为止最好的模式  
  53.         if(currScore > bestScore)  
  54.         {  
  55.             bestScore = currScore;  
  56.             bestMode = currMode;  
  57.         }  
  58.     }  
  59.   
  60.     return bestMode;  
  61. }  




MS是一个事件驱动的系统。一个事件驱动的windows应用程序如果没有事件触发的话将不会做任何事情。然而,OpenGL渲染窗口即使没有事件触发也需要经常的更新。一个解决OpenGL窗口经常更新的方法是在消息循环中使用PeekMessage()。

[cpp]  view plain  copy
  1. int mainMessageLoop()  
  2. {  
  3.     MSG msg;  
  4.   
  5.     while(1)  
  6.     {  
  7.         if(::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))  
  8.         {  
  9.             if(msg.message == WM_QUIT)  
  10.             {  
  11.                 break;  
  12.             }  
  13.             else  
  14.             {  
  15.                 ::TranslateMessage(&msg);  
  16.                 ::DispatchMessage(&msg);  
  17.             }  
  18.         }  
  19.         else  
  20.         {  
  21.             // 它不应该在这儿,因为这个消息循环只是被用来处理窗口消息  
  22.             render();  
  23.         }  
  24.     }  
  25.     return (int)msg.wParam;  
  26. }  

然而,为渲染OpenGL使用一个单独的线程是一个更好的方法。多线程的好处是你可以离开消息循环去做其他的事情(只处理windows事件),单独的线程将会独立地处理渲染OpenGL场景。我们也不会再消息循环中暴漏任何OpenGL渲染的方法。这样,消息循环可以和以前一样简单。

[cpp]  view plain  copy
  1. int mainMessageLoop()  
  2. {  
  3.     MSG msg;  
  4.   
  5.     // 循环一直到接收WM_QUIT(0)消息  
  6.     while(::GetMessage(&msg, 0, 0, 0) > 0)  
  7.     {  
  8.         ::TranslateMessage(&msg);  
  9.         ::DispatchMessage(&msg);  
  10.     }  
  11.   
  12.     return (int)msg.wParam;  
  13. }  

当一个窗口创建时(WM_CREATE事件触发),ControllerGL对象将会初始化ModelGL和ViewGL对象,然后,为OpenGL渲染开启一个新的线程。_beginthreadex()被用来创建一个新的线程。请查看ControllerGL::create()和ControllerGL::runThread()获取更多的细节。

[cpp]  view plain  copy
  1. // handle WM_CREATE  
  2. int ControllerGL::create()  
  3. {  
  4.     ....  
  5.     // 为渲染OpenGL创建一个新的线程  
  6.     threadHandle = (HANDLE)_beginthreadex(  
  7.                              0, 0,   
  8.                              (unsigned (__stdcall *)(void *))threadFunction,  
  9.                              this, 0, &threadId);  
  10.     return 0;  
  11. }  
  12.   
  13. // 启动新线程  
  14. void ControllerGL::threadFunction(void* param)  
  15. {  
  16.     ((ControllerGL*)param)->runThread();  
  17. }  
  18.   
  19. // 渲染循环  
  20. void ControllerGL::runThread()  
  21. {  
  22.     // 在这个线程中设置当前的RC  
  23.     ::wglMakeCurrent(viewGL->getDC(), viewGL->getRC());  
  24.   
  25.     // 初始化 OpenGL 状态  
  26.     modelGL->init();  
  27.   
  28.     // 渲染循环  
  29.     while(loopFlag)  
  30.     {  
  31.         ::Sleep(10);        // 让步给其它进程和线程  
  32.   
  33.         modelGL->draw();  
  34.         viewGL->swapBuffers();  
  35.     }  
  36.   
  37.     // 终止渲染线程  
  38.     ::wglMakeCurrent(0, 0);     // 重置RC  
  39.     ::CloseHandle(threadHandle);  
  40. }  




例子2:glWin


这个例子有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“按钮时会发生下面的事情:
  1. ControllerFormGL首先接收到BN_CLICKED事件
  2. ControllerFormGL在ModelGL中设置动画标志为真,这样地球会旋转
  3. ControllerFormGL通知ViewFormGL改变按钮的文字为”Stop“





一个windows应用程序在你创建窗口(更加正确的说法是使用RegisterClass()或RegisterClassEX()注册窗口)的时候需要提供一个回调函数(窗口过程)。当某件事情在你的程序中发生,例如,用户点击了按钮,windows系统向你的应用程序发送消息,消息会被传送到你所指定的窗口过程中。

一般情况下,窗口过程如下:

[cpp]  view plain  copy
  1. LRESULT CALLBACK wndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)  
  2. {  
  3.     switch(msg)  
  4.     {  
  5.     case WM_COMMAND:  
  6.         ...  
  7.         break;  
  8.   
  9.     case WM_CREATE:  
  10.         ...  
  11.         break;  
  12.   
  13.     // 更多的消息  
  14.     ...  
  15.   
  16.     default:  
  17.         return ::DefWindowProc(hWnd, msg, wParam, lParam);  
  18.     }  
  19.   
  20.     return 0;  
  21. }  

MVC框架的另外一个好处是动态消息路由,就是将消息分发给与窗口句柄相关的Controller。因此我们可以创建一个窗口过程并在你创建的所有窗口中使用它。基本的思想是当你创建一个窗口时将指向Controller对象的指针放在CreateWindow()或CreateWindowEx()的lpParam参数中。当WM_NCCREATE消息触发时,我们从lpCreateParams参数中提取出这个指针并且将它存储为窗口GWL_USERDATA属性值。然后,另一个消息触发时,我们简单地查找窗口的GWL_USERDATA值来查看那个Controller与窗口相关联。由于这个窗口过程被所有你创建的窗口使用,你不需要再重新编写其他的窗口过程。

实际的消息路由如下:

[cpp]  view plain  copy
  1. LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)  
  2. {  
  3.     LRESULT returnValue = 0;    // 返回值  
  4.   
  5.     // find controller associated with window handle  
  6.     static Win::Controller *ctrl;  
  7.     ctrl = (Controller*)::GetWindowLongPtr(hwnd, GWL_USERDATA);  
  8.   
  9.     if(msg == WM_NCCREATE)      // Non-Client Create  
  10.     {  
  11.         // WM_NCCREATE 消息在非客户端的部分创建之前被触发(边框,  
  12.         // 标题栏, 菜单,等) 。 这个消息的lParam参数中有一个指向  
  13.         // CREATESTRUCT结构体的指针。CREATESTRUCT结构体中的lpCreateParams  
  14.         // 实际包含CreateWindowEX()中的lpPraram参数。  
  15.         // 首先,检索在Win::Window中设置的指向Controller的指针  
  16.         ctrl = (Controller*)(((CREATESTRUCT*)lParam)->lpCreateParams);  
  17.         ctrl->setHandle(hwnd);  
  18.   
  19.         // 第二步,保存指向Controller的指针到GWL_USERDATA中,  
  20.         // 这样,其他消息可以被这个相关联的Controller路由  
  21.         ::SetWindowLongPtr(hwnd, GWL_USERDATA, (LONG_PTR)ctrl);  
  22.   
  23.         return ::DefWindowProc(hwnd, msg, wParam, lParam);  
  24.     }  
  25.   
  26.     // 检查空指针,因为GWL_USERDATA初始化为0,   
  27.     // 然后我们在 WM_NCCREATE消息被触发时储存了一个有效的指针  
  28.     if(!ctrl)  
  29.         return ::DefWindowProc(hwnd, msg, wParam, lParam);  
  30.   
  31.     // 消息路由到指定的Controller中  
  32.     switch(msg)  
  33.     {  
  34.     case WM_CREATE:  
  35.         returnValue = ctrl->create();  
  36.         break;  
  37.   
  38.     // 更多的消息  
  39.     ...  
  40.   
  41.     default:  
  42.         returnValue = ::DefWindowProc(hwnd, msg, wParam, lParam);  
  43.     }  
  44.   
  45.     return returnValue;  
  46. }  

在main函数中向下面的代码一样创建3个窗口。注意当我们在初始化窗口时提供了指向Controller对象的指针。这个指针的值必须保存在在每个窗口的GWL_USERDATA中,然后消息路由将会动态地将窗口事件分发到每个Controller中。

[cpp]  view plain  copy
  1. // 创建主窗口  
  2. Win::ControllerMain mainCtrl;  
  3. Win::Window mainWin(hInst, "glWinApp", 0, &mainCtrl);  
  4. mainWin.create();  
  5.   
  6. // 创建OpenGL渲染窗口  
  7. Win::ControllerGL glCtrl(&modelGL, &viewGL);  
  8. Win::Window glWin(hInst, L"WindowGL", mainWin.getHandle(), &glCtrl);  
  9. glWin.create();  
  10.   
  11. // 创建包含控件的窗口  
  12. Win::ControllerFormGL formCtrl(&modelGL, &viewFormGL);  
  13. Win::DialogWindow glDialog(hInst, IDD_FORMVIEW, mainWin.getHandle(), &formCtrl);  
  14. glDialog.create();  

创建包含控件的窗口时有一点点的不同。我们在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管理所有的控件界面。

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
NEHE的OPENGL教程 第42课 多视窗口… NEHE的OPENGL教程 第42课 多视窗口… NeHe的OPENGL中文教程:第41课 体… NeHe的OPENGL中文教程:第40课 绳… NeHe的OPENGL中文教程:第39课 物… NeHe的OPENGL中文教程:第39课 物… NeHe的OPENGL中文教程:第38课 从… NeHe的OPENGL中文教程:第37课 卡… 愚人节十大IT假新闻:Opera浏览器… NeHe的OPENGL中文教程:第36课 放… NeHe的OPENGL中文教程:第35课 AVI… NeHe的OPENGL中文教程:第35课 AVI… NeHe的OPENGL中文教程:第34课 从… NeHe的OPENGL中文教程:第33课 加… NeHe的OPENGL中文教程:第32课 Alp… NeHe的OPENGL中文教程:第32课 Alp… NeHe的OPENGL中文教程:第32课 Alp… NeHe的OPENGL中文教程:第31课 模… NEHE的OPENGL中文教程:第30课 碰… NEHE的OPENGL中文教程:第30课 碰… NeHe的OPENGL中文教程:第29课 Bli… NeHe的OPENGL中文教程:第28课 贝… NeHe的OPENGL中文教程:第27课 影… NeHe的OPENGL中文教程:第26课剪裁… NeHe的OPENGL中文教程:第25课 变… NeHe的OPENGL中文教程:第24课 TAG… NeHe的OPENGL中文教程:第23课 球… NeHe的OPENGL中文教程:第22课 凸… NeHe的OPENGL中文教程:第22课 凸… NeHe的OPENGL中文教程:第21课 反… NeHe的OPENGL中文教程:第21课 反… NeHe的OPENGL中文教程:第20课 蒙… NeHe的OPENGL中文教程:第19课 粒… NeHe的OPENGL中文教程:第18课 二… NeHe的OPENGL中文教程:第17课 2D… NeHe的OPENGL中文教程:第16课 雾 NeHe的OPENGL中文教程:第15课 图… NeHe的OPENGL中文教程:第14课 图… NeHe的OPENGL中文教程:第13课 位… NeHe的OPENGL中文教程:第12课 显… NeHe的OPENGL中文教程:第11课 飘… NeHe的OPENGL中文教程:第十课 漫… NeHe的OPENGL中文教程:第九课 漂… NeHe的OPENGL中文教程:第八课 Alp… NeHe的OPENGL中文教程:第七课 纹… NeHe的OPENGL中文教程:第七课 纹… NeHe的OPENGL中文教程:第六课 纹… NeHe的OPENGL中文教程:第五课 向3… NeHe的OPENGL中文教程:第四课 旋… NeHe的OPENGL中文教程:第三课 着… NeHe的OPENGL中文教程:第二课 多… NeHe的OPENGL中文教程:第一课 新… NeHe的OPENGL中文教程:第一课 新… DirectX与OpenGL方面的经典电子书… VC++ 6.0下OpengGL配置以及glut配… 怎样开始学习OpenGL
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值