用多线程方法实现在MFC/WIN32中调用OpenGL函数并创建OpenGL窗口

≯℡__Kan透↙ 提交于 2019-12-07 14:11:41

OpenGL相关的工具库中的OpenGL程序往往都是在C函数main中初始化和创建的,使用控制台来完成显示和控制颇为不便。如果能够在MFC中OpenGL函数并创建OpenGL窗口,并且可以将控制参数传入给OpenGL则可以得到很好的交互性能。自己查找很多文献资料,貌似都是说要在MFC中显示OpenGL都是通过微软的wgl扩展来完成,但是wgl很早就停止更新了的并且自己写的wgl运行框架尽管有些时候可以使用但在自己的电脑上却总是发现有运行有内存泄漏的问题并且加载opengl程序也非常慢,也许是显卡驱动的问题?但自己装的英伟达的GTX560显卡并且是最新的显卡驱动,也还是存在这个问题,看来应该是自己写的和参考的wgl框架有bug。本来是想用glut的却发现也是很久就停止更新了,只有freeglut不错,今年初还出了更新的。于是决定使用glew+freeglut来实现这个想法。

不过有一个问题就是glut的初始化函数往往都是写的glutInit(&argc,argv);其中argcargv两个参数是从控制台下的C函数的main(int argc,char *argv[])中传过来的,argc记录的是命令行中输入参数的数目,argv是一个拥有argc个元素的字符串数组,每个元素保存一个命令行中输入的参数。但是在MFC中默认是不会生成控制台窗口的,而实际上如果蹦出个控制台来控制opengl也不太好(这是可以实现的,可以参考《GUI程序也能使用控制台窗口》等文章http://www.cppblog.com/wish/articles/23642.html)偶然看到pfan论坛的eastcowboy的一个帖子《OpenGL入门学习——写给想用计算机画图的朋友》中提到可以在WinMain中初始化并且创建OpenGL窗口,通过自定义argcargv参数的值达到了欺骗glutInit完成初始化并且不依赖与命令行/控制台窗口。个人觉得既然可以自定义初始化参数,那么就实际上可以在MFC中的任何地方初始化和创建OpenGL窗口了。但是为了不影响MFC正常工作、完成与用户交互即UI的功能,可以通过多线程来完成,即可以将OpenGL的运行封装在一个工作线程的run函数中(这实际上真是有趣:这个名义上的工作线程却是可以创建窗口显示的)。

这里给出一个MFC对话框实现的例子:

第一步就是创建一个MFC对话框应用程序,我这里取名MFCwithOpenGLWindow。第二步添加一个启动OpenGL按钮IDC_STARTOPENGL及其对应的消息响应函数OnStartOpengl().

第三步就是添加相应的函数。在对话框实现MFCwithOpenGLWindowDlg.cpp文件顶部添加两个全局函数声明(因为是子线程函数,必须是全局的)

int OpenGLThread(LPVOID lpv);//线程run函数
void DisplayGLView(void);//被线程函数调用以在OpenGL窗口中绘图显示的函数

接下来在对话框实现MFCwithOpenGLWindowDlg.cpp文件中给出线程函数的实现(这里借鉴了《OpenGL入门学习——写给想用计算机画图的朋友》帖子中的程序,只不过添加了显示绘图的颜色为红色)

void DisplayGLView()//被线程函数调用以在OpenGL窗口中绘图显示的函数
{
      glClear(GL_COLOR_BUFFER_BIT);//清空颜色缓冲
      glColor3f(1.0,0.0,0.0);//设置绘图为红色
      glRectf(-0.5f,-0.5f,0.5f,0.5f);//绘制一个正方形
      glFlush();//刷新显示缓冲完成显示
}
int OpenGLThread(LPVOID lpv)//线程run函数
{
      //获得线程创建时对话框传入的参数,可以通过这里传递给opengl一些控制参数
      CMFCwithOpenGLWindowDlg* pTaskMain = (CMFCwithOpenGLWindowDlg *) lpv;
      int argc=1;
      char *argv[]={"OpenGL Thread "};
      glutInit(&argc,argv);//初始化glut
      glutInitDisplayMode(GLUT_RGB|GLUT_SINGLE);//设置显示模式
      glutInitWindowPosition(100,100);//设置opengl窗口显示位置
      glutInitWindowSize(400,400);//设置opengl窗口大小
      glutCreateWindow("OpenGL Thread Window");//设置opengl窗口标题
      glutDisplayFunc(&DisplayGLView);//调用显示回调函数绘图
      //调用freeglut中的函数设置opengl窗口被关闭和结束返回时可以继续执行glutMainLoop后面的部分
      glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS);
      glutMainLoop();//进入glut循环
      //AfxMessageBox("it end");
      AfxEndThread(0);//结束线程返回,以免出现意外结束
      return 1;//结束时返回
}



    红色部分非常重要,首先通过调用glutSetOption设置了opengl窗口被关闭时或者其他结束响应事件发生之后可以继续执行glutMainLoop死循环后面的语句,一方面可以正确地调用AfxEndThread函数结束子线程而不致于opengl对话框关闭时之间调用exit(0)MFC主对话框也给关闭了即结束了主线程,还可以完成一些清理工作,比如相应的内存释放等以免造成内存泄漏事件。该函数需要freeglut的支持,在glut中是没有这个函数的,由此可以看到freeglutglut的完善和补充,,使得程序更为可靠和安全了。

最后就是在启动OpenGL按钮对应的消息响应函数OnStartOpengl()中创建opengl线程,将opengl窗口显示出来,这里比较简单只有一句:

      CWinThread *TEST1=AfxBeginThread((AFX_THREADPROC)OpenGLThread,this);//创建opengl线程

如此,编译程序并运行,点击启动OpenGL按钮就可以得到一个绘制了一个红色正方形的opengl窗口,如下图所示

为了使程序编译通过,需要下载和安装glewfreeglut库,并且在MFCwithOpenGLWindowDlg.cpp文件中需要引用对应的头文件,在VC6.0或者VS平台中设置工程的头文件,库函数以及可执行dll文件的目录,这里为了方便使用已将相关的库的文件放置在了工程目录下,仅需要在MFCwithOpenGLWindowDlg.cpp文件中设置如下:

#include "./include/GL/glew.h"
#include "./include/GL/freeglut.h"
#pragma comment(lib,"glew32.lib")
#pragma comment(lib,"freeglut.lib")

    为了验证可以通过MFCOpenGL窗口绘图进行控制,这里进一步实现了一个旋转前面绘制出来的红色正方形。

首先在前面的对话框中再添加一个按钮IDC_ROTATEGLVIEW用于旋转GL绘图。

然后添加该按钮的消息响应函数OnRotateGLview(),实现如下:

   

csfortangle.Lock();       //临界区加锁
      rtangle=rtangle+5.0f;        //增加旋转角度
      if (rtangle>180.0f)       //处理旋转角度范围
      {
             rtangle=0;
      }
      csfortangle.Unlock();    //临界区开锁

其中csfortangle是rtangle角度对应的临界区对象用于实现线程之间对rtangle变量的访问控制使得每时刻仅有一个线程对其操作,在对话框实现文件的顶部定义的:

GLfloat rtangle=0;                        //旋转角度

CCriticalSection csfortangle;  //控制旋转角度线程之间访问同步的临界区对象

同时要使用临界区对象,必须加上对应的头文件:

#include "Afxmt.h"            //引入MFC中的线程/进程同步类,这里使用临界区对象。

为了实现真正的旋转绘制的图形,在显示回调函数DisplayGLView中修改如下,即增加红色部分:

      glClear(GL_COLOR_BUFFER_BIT);//清空颜色缓冲
      glLoadIdentity();                             //复位当前模型视角矩阵
      csfortangle.Lock();                                //临界区加锁
      glRotatef(rtangle,0.0f,0.0f,1.0f);   //绕Z轴旋转绘制的图形
      csfortangle.Unlock();                     //临界区开锁
      glColor3f(1.0,0.0,0.0);//设置绘图为红色
      glRectf(-0.5f,-0.5f,0.5f,0.5f);//绘制一个正方形
      glFlush();//刷新显示缓冲完成显示最后编译运行。



  
  测试时创建 opengl 窗口后首先点击对话框中的“旋转 GL 视图”按钮增加旋转的角度,然后鼠标切换到 opengl 窗口点击一下(为了产生一个点击事件使得 glut 能够调用显示回调函数 DisplayGLView 旋转图形并刷新显示,也可以通过定时等事件来达到相应的目的)就可以看到旋转后的图形。

如果不想手动去切换,可以通过glut中的定时事件来触发显示函数回调。

在前面程序基础上实现如下:

      首先添加一个glut定时timerEvent回调函数,相当于MFC中的ontimer定时器响应函数:

void timerEvent(int value)//GLUT定时回调函数
{
   glutPostRedisplay();//发送重新显示消息,刷新显示
   glutTimerFunc(REFRESH_DELAY, timerEvent, 0);//重置定时器
}



然后修改OpenGLThread函数如下:

int OpenGLThread(LPVOID lpv)//线程run函数

{

      //获得线程创建时对话框传入的参数,可以通过这里传递给opengl一些控制参数

      CMFCwithOpenGLWindowDlg* pTaskMain = (CMFCwithOpenGLWindowDlg *) lpv;

 

      int argc=1;

      char *argv[]={"OpenGL Thread "};

      glutInit(&argc,argv);//初始化glut

      glutInitDisplayMode(GLUT_RGB|GLUT_SINGLE);//设置显示模式

      glutInitWindowPosition(100,100);//设置opengl窗口显示位置

      glutInitWindowSize(400,400);//设置opengl窗口大小

      glutCreateWindow("OpenGL Thread Window");//设置opengl窗口标题

      glutDisplayFunc(&DisplayGLView);//注册显示回调函数绘图

      glutTimerFunc(REFRESH_DELAY, timerEvent, 0);//注册定时回调函数

      //调用freeglut中的函数设置opengl窗口被关闭和结束返回时可以继续执行glutMainLoop后面的部分

      glutSetOption(GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS);

      glutMainLoop();//进入glut循环,处理响应事件

      //AfxMessageBox("it end");

      AfxEndThread(0);//结束线程返回,以免出现意外结束
      return 1;//结束时返回
}



其中REFRESH_DELAY为文件顶部的定时时间ms数值宏定义,这里取200ms,重新编译并运行程序就可以实现在MFC中点击“旋转GL视图”即可连续地旋转绘制的正方形了。

为了验证glut窗口中也能够通过按钮实现正方形旋转的控制,可以进一步添加一个全局按钮控制回调函数:

void keyboard(unsigned char key, int /*x*/, int /*y*/)//GLUT按键回调函数
{
             switch(key)
             {
                    case '+'://+号实现旋转的逆时针角度增加
                           {
                                  csfortangle.Lock();        //临界区加锁
                                  rtangle=rtangle+5.0f;          //增加旋转角度
                                  if (rtangle>180.0f)        //处理旋转角度范围
                                  {
                                         rtangle=0;
                                  }
                                  csfortangle.Unlock();     //临界区开锁
                           }
                           break;
                    case '-'://-号实现旋转的顺时针角度增加
                           {
                                  csfortangle.Lock();        //临界区加锁
                                  rtangle=rtangle-5.0f;    //增加旋转角度
                                  if (rtangle<-180.0f)        //处理旋转角度范围
                                  {
                                         rtangle=0;
                                  }
                                  csfortangle.Unlock();     //临界区开锁
                           }
                           break;
                    default:
                           break;
             }
}



并且在创建线程时将函数注册到glut中:

      glutKeyboardFunc(keyboard);//注册键盘按钮控制回调函数

编译之后运行就可以看到既可以通过glut的窗口中通过键盘按钮控制正方形旋转,也可以通过MFC对话框按钮实现。

 

补充:

如果要通过主线程如UI线程来结束自己创建的opengl线程,则可以在自己定义的显示函数或者定时器处理函数中加上一个条件变量如Boolean型的变量,当该变量条件为真时就调用freeglut的退出循环函数glutLeaveMainLoop(),它将直接跳出glutMainLoop,接着执行AfxEndThread()结束线程返回,从而实现提前退出和结束线程。

其实通过阅读freeglut的源程序可以发现windows平台上它实际上对窗口的绘制对事件的响应都是对windows API的封装,尤其对于窗口和图形的绘制也是对微软wgl的封装,如fgSetWindow函数中封装了wglMakeCurrent函数和ReleaseDC函数,fgWindowProc函数则更是专门封装了对应win32窗口处理的函数,其中封装了wglCreateContextwglGetCurrentContext等等函数。

 

最后给出本文中的示例源代码下载地址:

http://download.csdn.net/detail/menglongbor/4268748

参考文献:

GUI程序也能使用控制台窗口,http://www.cppblog.com/wish/articles/23642.html

OpenGL入门学习——写给想用计算机画图的朋友,http://bbs.pfan.cn/post-184355.html

OpenGL内存泄漏之主循环函数glutMainLoop()

http://blog.csdn.net/ronggang175/article/details/6068854

坑爹的sshglutMainLoop

http://hi.baidu.com/tyxxybd/blog/item/64f6040f698402306159f337.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!