搬运自CSDN博客:向MFC嵌入OpenGL
本人上计算机图形学课时要做的大作业是基于MFC框架用OpenGL实现一些功能。但是我一开始并不知道怎么在MFC中加入OpenGL模块。于是我在网上找了半天终于找到一篇文章,阅读之后受益匪浅。今天我准备把这篇文章翻译出来。
原文地址:Setting Up OpenGL in an MFC Control
翻译如下:
我在网上没有看到许多文章关于集成功能的,哪怕仅仅是在一个MFC控制结构中设置基本的OpenGL窗口的文章也没有。本教程的目的是一步一步地教大家如何在MFC控制结构(例如图片控制)中初始化一个OpenGL渲染上下文(rendering context),还有基于定时器、改变大小事件、基本相机功能等的基本绘制功能。在本教程中我将在Microsoft Visual Studio .NET 2003 的环境下操作(注:译者用的是VS2015),而且我将尽量让各个水平的人都能理解。我将为初学者增加一些特定的步骤,非初学者也可以跳过这些步骤去找自己所需要的部分。无论如何,欢迎大家的评论(无论好坏),我将近我全力去更新此文直到每个人都满意。谢谢大家。那么让我们开始吧!
第一部分:创建最初的OpenGL窗口
第1步:创建工程
首先我们先要创建一个MFC对话框应用程序项目。文件->新建->项目 选择MFC应用程序并将其命名为"oglMFCDialog"。把工程保存在哪里取决与你,你只要能记住你把它存到哪儿就行了。当MFC应用程序向导出现的时候,在应用程序类型中选择基于对话框然后点击完成按钮。其他附加的设置选项在用户界面功能中。比如说我就选择了最小化框。
第2步 创建控制模块
在资源浏览器标签中找到资源视图,展开Dialog文件夹,然后双击自动生成的IDD_OGLMFCDIALOG_DIALOG对话框。你需要添加一个控制组件用于OpenGL内容的渲染。一个简单的图片控制组件就可以,那么就在工具箱中选择Picture Control吧。
注意:如果工具箱没有出现,在菜单栏中找视图->工具箱把它调出来。
你可以直接将组建拽到你的对话窗口,也可以随时调整大小。
你将需要对这个Picture Control设置一些属性值。当这个Picture Control被选中时,它的属性在右下角,设定以下属性:
Visible:False
ID:IDC_OPENGL
你也许觉得很奇怪为什么将Visible属性设为False。当你在任何一个MFC中加载OpenGL渲染内容时,你能用到的只是这个控件的方形位置来辅助绘制OpenGL内容。由于一些奇怪的原因,如果Visible设为True,这个控件就会覆盖掉你的OpenGL内容。
第3步 添加OpenGL类
接下来,为了设置OpenGL,我选择为其添加一个单独的类。把工程里的主要部分分成单独的类,这是一个好习惯。因此我将把OpenGL和MFC分离。
要添加一个类,点击解决方案资源管理器标签,在树中右击oglMFCDialog工程,选择添加->添加类,再从模板中选择C++类然后点击添加。当一般C++类向导出现时,设置以下属性:
类名:COpenGLControl
基类:CWnd
访问(Access):public
勾选虚析构函数
然后点击完成,就创建成功了。
第4步 添加项目库(Adding Project Libraries)
由于你要用到OpenGL的渲染功能,你还需要向工程中链接一些库。在树中再次右击oglMFCDialog项目,选择属性,在属性页中选择链接器->输入,设置以下变量:
附加依赖项:opengl32.lib;glu32.lib
注意:这两个*.lib文件中间不要有任何空格
第5步 设置类的变量
在本工程的范围内,一些变量需要被添加到OpenGLControl.h中,公有的和私有的都需要,还需要添加#include语句,代码如下:
#include <gl/gl.h>
#include <gl/glu.h>
class COpenGLControl : public CWnd
{
public:
/******************/
/* PUBLIC MEMBERS */
/******************/
// Timer
UINT_PTR m_unpTimer;
private:
/*******************/
/* PRIVATE MEMBERS */
/*******************/
// Window information
CWnd *hWnd;
HDC hdc;
HGLRC hrc;
int m_nPixelFormat;
CRect m_rect;
CRect m_oldWindow;
CRect m_originalRect;
...
特别注意:我把函数按照特定顺序排列,这是非常重要的。因为如果有些函数在别的函数之前被实例化,就会有一个编译错误,这个错误是protected成员引起的。所以,我把这些函数按照以下顺序排列:
- 构造和析构函数。
- 手动添加的函数。
- 自动添加的"afx_msg"函数。
- DECLARE_MESSAGE_MAP()调用。
第6步 添加oglCreate函数
一个新的函数将添加到OpenGLControl.h和OpenGLControl.cpp中。该函数将负责搭建一些基本的窗口变量和对MFC来说很重要的函数调用。我把这个函数命名为oglCreate。添加的代码如下:
OpenGLControl.h
void oglCreate(CRect rect, CWnd *parent);
OpenGLControl.cpp
void COpenGLControl::oglCreate(CRect rect, CWnd *parent)
{
CString className = AfxRegisterWndClass(CS_HREDRAW |
CS_VREDRAW | CS_OWNDC, NULL,
(HBRUSH)GetStockObject(BLACK_BRUSH), NULL);
CreateEx(0, className, "OpenGL", WS_CHILD | WS_VISIBLE |
WS_CLIPSIBLINGS | WS_CLIPCHILDREN, rect, parent, 0);
// Set initial variables' values
m_oldWindow = rect;
m_originalRect = rect;
hWnd = parent;
}
第7步 添加OnPaint函数
接下来需要添加一个消息类函数。该函数与其他函数的区别也很好辨认。当程序中有事件发生时,MFC会调用特定的消息(以WM_为前缀),例如OnPaint,OnSize,OnCreate等。要通过Visual Studio添加消息,你可以在属性窗口中找消息按钮(闪电图标右边的那个)。如果你的光标在你选定的文件中,这就是你添加事件的地方。
说完了这些,首先你需要创建OnPaint事件,通过属性->消息窗口下找到WM_PAINT事件。然后再下拉列表中选择<Add>OnPaint。你注意到消息函数同时加到OpenGLControl.h和OpenGLControl.cpp中。但是,与用户添加的函数不同的是,这个函数有"afx_msg"的前缀,还会在cpp文件的顶部加一个调用。我建议大家不要更改Visual Studio自动增加的东西,除非你知道它们是干什么的。
在OnPaint函数内部,只有一小部分自动生成的代码需要更改。你自己渲染(或者说是绘制)OpenGL窗口的方式与MFC不同,不同在你是通过定时器来绘制的。以防万一接下来需要固定一个特定的帧率。由于要通过定时器来渲染窗口,OnPaint函数就不能按照原有的方式调用。因此就需要加上简单的一行来代替原有的调用。以下代码需要加到OpenGLControl.cpp文件中的OnPaint函数。
OpenGLControl.cpp
void COpenGLControl::OnPaint()
{
//CPaintDC dc(this); // device context for painting
ValidateRect(NULL);
}
第8步 添加OnCreate函数
接下来,按照之前的方法在添加一个消息类,这次选择WM_CREATE消息,选择<Add>OnCreate。和之前一样,新的类也会加到.h和.cpp文件中。当程序调用到它时,你只需要添加一行代码调用之后会增加的函数,oglInitialize函数。
注意:当然,你还没有添加这个函数,运行时会有编译错误。
OpenGLControl.cpp
int COpenGLControl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
oglInitialize();
return 0;
}
第9步 添加oglInitialize函数
正如前文所说的那样,接下来我们将手动的添加这个函数,即oglInitialize函数,该函数在你的OpenGL类被创建的时候调用(通过OnCreate消息)。oglInitialize函数将负责设定所有OpenGL渲染所需要的信息。这些信息包括像素格式、渲染上下文以及清除颜色以确保程序正常交换缓存。
注意:下面的.cpp代码片段的最后一行电泳OnDraw*函数,该函数还没有创建,此时运行将有编译错误。
OpenGLControl.h
void oglInitialize(void)
OpenGLControl.cpp
void COpenGLControl::oglInitialize(void)
{
// Initial Setup:
//
static PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
32, // bit depth
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
16, // z-buffer depth
0, 0, 0, 0, 0, 0, 0,
};
// Get device context only once.
hdc = GetDC()->m_hDC;
// Pixel format.
m_nPixelFormat = ChoosePixelFormat(hdc, &pfd);
SetPixelFormat(hdc, m_nPixelFormat, &pfd);
// Create the OpenGL Rendering Context.
hrc = wglCreateContext(hdc);
wglMakeCurrent(hdc, hrc);
// Basic Setup:
//
// Set color to use when clearing the background.
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClearDepth(1.0f);
// Turn on backface culling
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
// Turn on depth testing
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
// Send draw request
OnDraw(NULL);
}
第10步 添加OnDraw函数
接下来,我们将添加之前在oglInitialize函数中提到的OnDraw函数。这个函数将会扮演消息函数,但是需要手动添加。正如你归纳出来的那样,在该函数的声明语句中需要在前面添加"afx_msg"前缀。你将注意到在cpp代码片段中你实际上什么也不用做,只有一个TODO注释。在本教程后面部分会用它实现一些功能,例如摄像机控制等。
注意:如果你现在运行该程序,他并不会报错,但不幸的是在控件中什么也没有,这取决于你在OnPaint函数中做出的更改。
OpenGLControl.h
afx_msg void OnDraw(CDC *pDC);
OpenGLControl.cpp
void COpenGLControl::OnDraw(CDC *pDC)
{
// TODO: Camera controls.
}
第11步 添加OnTimer函数
现在是时候用定时器来添加之前OnPaint函数的功能了。让我们添加一个新的消息类,选择WM_TIMER消息,然后在下拉列表中选择<Add>OnTimer来创建函数。
** OnTimer函数的代码十分的直白。这个类的定时器每次调用的时程序都会通过该函数执行。因此,函数自动传入的值是一个无符号整数,对应着你手动创建的定时器。使用select** 声明(译者认为应该是switch),我为定时器创建了相应的case。基本的"绘制并交换缓存"过程将在这个定时器里。在本教程的前半部分中你不用绘制任何图形,只需要正确的交换缓存即可。
OpenGLControl.cpp
void COpenGLControl::OnTimer(UINT nIDEvent)
{
switch (nIDEvent)
{
case 1:
{
// Clear color and depth buffer bits
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Draw OpenGL scene
// oglDrawScene();
// Swap buffers
SwapBuffers(hdc);
break;
}
default:
break;
}
CWnd::OnTimer(nIDEvent);
}
第12步 添加OnSize函数
你需要添加的最后一个消息函数是OnSize函数。和之前一样,选择WM_SIZE消息并在下拉列表中选择<Add>OnSize。
OnSize函数在窗口大小改变时被调用。为了防止窗口大小改变造成的渲染窗口的失常,透视及视口等需要调整。在本教程中你其实不需要随窗口大小调整,但是在必要时可以通过以下函数来实现:
OpenGLControl.cpp
void COpenGLControl::OnSize(UINT nType, int cx, int cy)
{
CWnd::OnSize(nType, cx, cy);
if (0 >= cx || 0 >= cy || nType == SIZE_MINIMIZED) return;
// Map the OpenGL coordinates.
glViewport(0, 0, cx, cy);
// Projection view
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// Set our current view perspective
gluPerspective(35.0f, (float)cx / (float)cy, 0.01f, 2000.0f);
// Model view
glMatrixMode(GL_MODELVIEW);
}
现在你已经完成了COpenGLControl类(至少是初始化)。接下来就是将他集成到你的主MFC窗口,即CoglMFCDialogDlg类。
第13步 定制主MFC对话框类
你的最后一步就是将你的OpenGL类集成到主MFC对话框中。这个过程非常简单。首先在oglMFCDialogDlg.h文件中将你的OpenGL类包含进去,然后添加一个COpenGLControl类作为本地变量。
oglMFCDialogDlg.h
#include "OpenGLControl.h"
class CoglMFCDialogDlg : public CDialog
{
private:
COpenGLControl m_oglWindow;
.
.
.
接下来,在oglMFCDialogDlg.cpp文件中已经存在的OnInitDialog函数中return语句之前加上以下代码就可以在你创建的picture控件中启动定时器,渲染OpenGL内容。
注意:在出事测试中,我将定时器的间隔设为1毫秒。
oglMFCDialogDlg.cpp::OnInitDialog
.
.
.
CRect rect;
// Get size and position of the picture control
GetDlgItem(IDC_OPENGL)->GetWindowRect(rect);
// Convert screen coordinates to client coordinates
ScreenToClient(rect);
// Create OpenGL Control window
m_oglWindow.oglCreate(rect, this);
// Setup the OpenGL Window's timer to render
m_oglWindow.m_unpTimer = m_oglWindow.SetTimer(1, 1, 0);
.
.
.
第14步 初始化 OpenGL 控件结果
现在你完成了!现在你应该看到黑色的背景。现在你可以在OnTimer中添加绘制函数。
第二部分:扩展OpenGL的功能(附加)
第15步 绘制几何图形
下一步就是在你的OpenGL空间中绘制一些测试图形。不幸的是,你需要设置摄像机系统,否则无法看到这些图形。如果没有摄像机直向特定点的话,你绘制的图形将被放置在摄像机原本的位置上,这样你就什么也看不见了。所以如果你想先看到你画的东西,请先看第16步:设置Maya风格的摄像机,在第16步中你将创建一个Alias|Wavefront Maya风格的摄像机(用鼠标进行旋转/缩放/平移)。但是我选择在下一步中做这些。
无论如何,本教程绘制的图形是十分简单的,就画一个立方体就行了(6个方形面)。如果你再次调用OnTimer函数,将会调用oglDrawScene函数,你将创建这个函数。
OpenGLControl.h
void oglDrawScene(void);
** OpenGLControl.cpp**
void COpenGLControl::oglDrawScene(void)
{
// Wireframe Mode
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_QUADS);
// Top Side
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
// Bottom Side
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
// Front Side
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
// Back Side
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
// Left Side
glVertex3f(-1.0f, -1.0f, -1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, -1.0f);
// Right Side
glVertex3f( 1.0f, 1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, 1.0f);
glVertex3f( 1.0f, -1.0f, -1.0f);
glVertex3f( 1.0f, 1.0f, -1.0f);
glEnd();
}
现在,你最后要做的是替换OnTimer函数的注释部分,调用oglDrawScene函数。
OpenGLControl.cpp::OnTimer
.
.
.
// Clear color and depth buffer bits
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Draw OpenGL scene
oglDrawScene();
// Swap buffers
SwapBuffers(hdc);
.
.
.
第16步 设置Maya风格的摄像机
Maya风格的摄像机指的是在Alias|Wavefront Maya中使用的摄像机系统。鼠标左键旋转,右键缩放,中键平移。要实现这个功能,你需要创建一个OnMouseMove函数调用。
选择WM_MOUSEMOVE消息,在下拉菜单中选择<Add>OnMouseMove,创建函数。在新建的函数里,你需要为每个按键添加相应的操作。如果你回想起之前的内容,你创建一些成员变量用于旋转、缩放和平移。你将为这些变量设置新的值。
OpenGLControl.h
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
OpenGLControl.cpp
void COpenGLControl::OnMouseMove(UINT nFlags, CPoint point)
{
int diffX = (int)(point.x - m_fLastX);
int diffY = (int)(point.y - m_fLastY);
m_fLastX = (float)point.x;
m_fLastY = (float)point.y;
// Left mouse button
if (nFlags & MK_LBUTTON)
{
m_fRotX += (float)0.5f * diffY;
if ((m_fRotX > 360.0f) || (m_fRotX < -360.0f))
{
m_fRotX = 0.0f;
}
m_fRotY += (float)0.5f * diffX;
if ((m_fRotY > 360.0f) || (m_fRotY < -360.0f))
{
m_fRotY = 0.0f;
}
}
// Right mouse button
else if (nFlags & MK_RBUTTON)
{
m_fZoom -= (float)0.1f * diffY;
}
// Middle mouse button
else if (nFlags & MK_MBUTTON)
{
m_fPosX += (float)0.05f * diffX;
m_fPosY -= (float)0.05f * diffY;
}
OnDraw(NULL);
CWnd::OnMouseMove(nFlags, point);
}
此外,我在COpenGLControl类的构造函数中加一些初始化语句。
OpenGLControl.cpp
COpenGLControl::COpenGLControl(void)
{
m_fPosX = 0.0f; // X position of model in camera view
m_fPosY = 0.0f; // Y position of model in camera view
m_fZoom = 10.0f; // Zoom on model in camera view
m_fRotX = 0.0f; // Rotation on model in camera view
m_fRotY = 0.0f; // Rotation on model in camera view
}
现在最后要做的是改变OnDraw函数,将TODO注释改为以下:
OpenGLControl.cpp::OnDraw
.
.
.
glLoadIdentity();
glTranslatef(0.0f, 0.0f, -m_fZoom);
glTranslatef(m_fPosX, m_fPosY, 0.0f);
glRotatef(m_fRotX, 1.0f, 0.0f, 0.0f);
glRotatef(m_fRotY, 0.0f, 1.0f, 0.0f);
.
.
.
第17步 正确地改变窗口的大小
改变MFC窗口的大小是一个十分繁琐的工作(或许和Visual Basic相比),不过也能轻松完成。而且一旦你做完一次,你就能一直重复使用这段代码。以下就是我摸索出来的,现在还在用的方法。这涉及到MFC和OpenGL的两个OnSize函数/功能。
首先,进入oglMFCDialogDlg.cpp文件中按照之前的方法创建OnSize消息函数。这回为你的MFC部分生成OnSize函数,但是你要加一些代码,整个Onsize函数是这样的。
oglMFCDialogDlg.cpp
void CoglMFCDialogDlg::OnSize(UINT nType, int cx, int cy)
{
CDialog::OnSize(nType, cx, cy);
switch (nType)
{
case SIZE_RESTORED:
{
if (m_oglWindow.m_bIsMaximized)
{
m_oglWindow.OnSize(nType, cx, cy);
m_oglWindow.m_bIsMaximized = false;
}
break;
}
case SIZE_MAXIMIZED:
{
m_oglWindow.OnSize(nType, cx, cy);
m_oglWindow.m_bIsMaximized = true;
break;
}
}
}
你注意到了你改变了一些没有创建的变量的值。所以你现在要找到OpenGL窗口代码,在已经存在的OnSize函数后面添加以下代码。
OpenGLControl.cpp
.
.
.
switch (nType)
{
// If window resize token is "maximize"
case SIZE_MAXIMIZED:
{
// Get the current window rect
GetWindowRect(m_rect);
// Move the window accordingly
MoveWindow(6, 6, cx - 14, cy - 14);
// Get the new window rect
GetWindowRect(m_rect);
// Store our old window as the new rect
m_oldWindow = m_rect;
break;
}
// If window resize token is "restore"
case SIZE_RESTORED:
{
// If the window is currently maximized
if (m_bIsMaximized)
{
// Get the current window rect
GetWindowRect(m_rect);
// Move the window accordingly (to our stored old window)
MoveWindow(m_oldWindow.left,
m_oldWindow.top - 18,
m_originalRect.Width() - 4,
m_originalRect.Height() - 4);
// Get the new window rect
GetWindowRect(m_rect);
// Store our old window as the new rect
m_oldWindow = m_rect;
}
break;
}
}
.
.
.
以上就是这些。简单?不简单?请随意发表评论。感谢你的阅读,希望能帮到你。