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。

int WINAPI WinMain(...)
{
    // 实例化 Model and View,这样Controller能引用它们
    ModelGL model;
    Win::ViewGL view;   //在"Win"命名空间中因为它于窗口相关

    //使用指向ModelGL和ViewGL的指针创建ControllerGL对象
    Win::ControllerGL glCtrl(&model, &view);

    // 使用Controller对象创建窗口
    Win::Window glWin(hInst, L"glWinSimple", 0, &glCtrl);
    glWin.setWindowStyle(WS_OVERLAPPEDWINDOW | WS_VISIBLE | 
                         WS_CLIPSIBLINGS | WS_CLIPCHILDREN);
    glWin.setClassStyle(CS_OWNDC);
    glWin.setWidth(400);
    glWin.setHeight(300);

    glWin.create();
    glWin.show();

    // 进入消息循环 //
    int exitCode;
    exitCode = mainMessageLoop();

    return exitCode;
}



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

// 查找最好的像素格式
int findPixelFormat(HDC hdc, int colorBits, int depthBits, int stencilBits)
{
    int currMode;                   // 格式模式的ID
    int bestMode;                   // 返回值,最好的像素格式
    int currScore;                  // 现在像素格式的分数
    int bestScore;                  // 做好的像素格式的分数
    PIXELFORMATDESCRIPTOR pfd;

    // 查找所有可能的像素格式
    bestMode = 0;
    bestScore = 0;
    for(currMode = 1; 
        ::DescribePixelFormat(hdc, currMode, sizeof(pfd), &pfd) > 0;
        ++currMode)
    {
        // 如果不支持opengl则忽略
        if(!(pfd.dwFlags & PFD_SUPPORT_OPENGL))
            continue;

        // 如果不能被渲染到窗口则忽略
        if(!(pfd.dwFlags & PFD_DRAW_TO_WINDOW))
            continue;

        // 如果不支持rgb模式则忽略
        if((pfd.iPixelType != PFD_TYPE_RGBA) || 
           (pfd.dwFlags & PFD_NEED_PALETTE))
            continue;

        // 如果不是双缓冲区则忽略
        if(!(pfd.dwFlags & PFD_DOUBLEBUFFER))
            continue;

        // 尝试寻找最好的像素格式
        currScore = 0;

        // 颜色位
        if(pfd.cColorBits >= colorBits) ++currScore;
        if(pfd.cColorBits == colorBits) ++currScore;

        // 深度位
        if(pfd.cDepthBits >= depthBits) ++currScore;
        if(pfd.cDepthBits == depthBits) ++currScore;

        // 模板位
        if(pfd.cStencilBits >= stencilBits) ++currScore;
        if(pfd.cStencilBits == stencilBits) ++currScore;

        // alpha位
        if(pfd.cAlphaBits > 0) ++currScore;

        // 判断是否是到目前为止最好的模式
        if(currScore > bestScore)
        {
            bestScore = currScore;
            bestMode = currMode;
        }
    }

    return bestMode;
}




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

int mainMessageLoop()
{
    MSG msg;

    while(1)
    {
        if(::PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
        {
            if(msg.message == WM_QUIT)
            {
                break;
            }
            else
            {
                ::TranslateMessage(&msg);
                ::DispatchMessage(&msg);
            }
        }
        else
        {
            // 它不应该在这儿,因为这个消息循环只是被用来处理窗口消息
            render();
        }
    }
    return (int)msg.wParam;
}

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

int mainMessageLoop()
{
    MSG msg;

    // 循环一直到接收WM_QUIT(0)消息
    while(::GetMessage(&msg, 0, 0, 0) > 0)
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

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

// handle WM_CREATE
int ControllerGL::create()
{
    ....
    // 为渲染OpenGL创建一个新的线程
    threadHandle = (HANDLE)_beginthreadex(
                             0, 0, 
                             (unsigned (__stdcall *)(void *))threadFunction,
                             this, 0, &threadId);
    return 0;
}

// 启动新线程
void ControllerGL::threadFunction(void* param)
{
    ((ControllerGL*)param)->runThread();
}

// 渲染循环
void ControllerGL::runThread()
{
    // 在这个线程中设置当前的RC
    ::wglMakeCurrent(viewGL->getDC(), viewGL->getRC());

    // 初始化 OpenGL 状态
    modelGL->init();

    // 渲染循环
    while(loopFlag)
    {
        ::Sleep(10);        // 让步给其它进程和线程

        modelGL->draw();
        viewGL->swapBuffers();
    }

    // 终止渲染线程
    ::wglMakeCurrent(0, 0);     // 重置RC
    ::CloseHandle(threadHandle);
}




例子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系统向你的应用程序发送消息,消息会被传送到你所指定的窗口过程中。

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

LRESULT CALLBACK wndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch(msg)
    {
    case WM_COMMAND:
        ...
        break;

    case WM_CREATE:
        ...
        break;

    // 更多的消息
    ...

    default:
        return ::DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return 0;
}

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

实际的消息路由如下:

LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    LRESULT returnValue = 0;    // 返回值

    // find controller associated with window handle
    static Win::Controller *ctrl;
    ctrl = (Controller*)::GetWindowLongPtr(hwnd, GWL_USERDATA);

    if(msg == WM_NCCREATE)      // Non-Client Create
    {
        // WM_NCCREATE 消息在非客户端的部分创建之前被触发(边框,
        // 标题栏, 菜单,等) 。 这个消息的lParam参数中有一个指向
        // CREATESTRUCT结构体的指针。CREATESTRUCT结构体中的lpCreateParams
        // 实际包含CreateWindowEX()中的lpPraram参数。
        // 首先,检索在Win::Window中设置的指向Controller的指针
        ctrl = (Controller*)(((CREATESTRUCT*)lParam)->lpCreateParams);
        ctrl->setHandle(hwnd);

        // 第二步,保存指向Controller的指针到GWL_USERDATA中,
        // 这样,其他消息可以被这个相关联的Controller路由
        ::SetWindowLongPtr(hwnd, GWL_USERDATA, (LONG_PTR)ctrl);

        return ::DefWindowProc(hwnd, msg, wParam, lParam);
    }

    // 检查空指针,因为GWL_USERDATA初始化为0, 
    // 然后我们在 WM_NCCREATE消息被触发时储存了一个有效的指针
    if(!ctrl)
        return ::DefWindowProc(hwnd, msg, wParam, lParam);

    // 消息路由到指定的Controller中
    switch(msg)
    {
    case WM_CREATE:
        returnValue = ctrl->create();
        break;

    // 更多的消息
    ...

    default:
        returnValue = ::DefWindowProc(hwnd, msg, wParam, lParam);
    }

    return returnValue;
}

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

// 创建主窗口
Win::ControllerMain mainCtrl;
Win::Window mainWin(hInst, "glWinApp", 0, &mainCtrl);
mainWin.create();

// 创建OpenGL渲染窗口
Win::ControllerGL glCtrl(&modelGL, &viewGL);
Win::Window glWin(hInst, L"WindowGL", mainWin.getHandle(), &glCtrl);
glWin.create();

// 创建包含控件的窗口
Win::ControllerFormGL formCtrl(&modelGL, &viewFormGL);
Win::DialogWindow glDialog(hInst, IDD_FORMVIEW, mainWin.getHandle(), &formCtrl);
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管理所有的控件界面。






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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值