编写独立于平台和 GUI 工具包的 OpenGL 类
演示如何编写与操作系统和 GUI 工具包无关的 OpenGL 视图类的文章
引言
我遇到的许多基本 OpenGL 设置代码,要么使用不依赖窗口系统的 glut 方法进行演示,要么针对 MFC、GTK+ 或 Qt 等特定 GUI 工具包。例如,glut 示例展示了如何使用 glutCreateWindow
创建即用型窗口,而 MFC 示例则展示了如何在 CView
派生类中设置 OpenGL
上下文。GTK+ 使用 gtkglarea
或 gtkglext
等扩展,Qt 拥有自己的 QGLWidget
,它隐藏了所有 OpenGL 设置实现。如果您想在代码中坚持使用一种工具包或操作系统,这样已经足够了。但想象一下,您想更改工具包或操作系统,例如从 MFC 更改为 Motif!您编写的 OpenGL
视图类需要进行大量更改!那么,为什么我们不能有一个平台和工具包无关的类,它可以简单地渲染到任何平台上的任何窗口呢?
背景
在本文中,我想演示如何创建一个平台和工具包无关的 OpenGL
视图类。为此,我们需要首先理解在任何窗口上设置 OpenGL
渲染的首要要求。
在任何工具包的窗口上进行 OpenGL
渲染,最终都归结为特定操作系统支持的底层窗口系统。也就是说,在 Microsoft Windows 上是设备上下文 (Device Context),在 UNIX/Linux 上是 X Window System 的显示 (Display)。虽然我没有在 MacOSX 上测试过代码,但由于 MacOSX 也使用 X Window System 作为其窗口系统,因此本文也适用于 MacOSX。
读者必须明白,本文不是关于教授 OpenGL 或任何特定工具包的。本文适用于那些已经对 OpenGL 和至少一种 GUI 工具包有深刻理解的人。创建 DLL 或 UNIX/Linux 共享对象的详细信息也超出了本文的范围。我试图展示如何编写不仅可以移植到不同操作系统,还可以移植到其他 GUI 工具包的 OpenGL 代码。尽管通过编写此处的代码的合适包装器可以实现语言可移植性,但我们专注于使用 C/C++ 编写代码。
Using the Code
首先,我们需要准备窗口,使其能够接受来自 OpenGL API 的像素渲染指令。然后,我们必须处理 OpenGL 绘图命令。接下来,我们还必须处理窗口大小调整对 OpenGL 渲染的影响。因此,本质上,我们类的三个主要方法将用于设置窗口、渲染 OpenGL 内容以及调整窗口大小。
要获取窗口系统,关键在于 Microsoft Windows 上的 HWND
(窗口句柄)和 HDC
(设备上下文句柄),以及 UNIX/Linux 上的 Display
(X 显示在某种程度上等同于 MSWin 设备上下文,尽管 X Windows 的理论与 MSWin 略有不同,并且不属于本文范围)和 Window
(等同于 HWND
)。如果我们能够从任何工具包访问这些内容,我们就可以让我们的 OpenGL
视图类渲染到任何工具包的窗口上。相信我,任何好的工具包都应该有函数或方法为您提供窗口句柄和显示。
那么,让我们来看看代码。我们将使用条件编译预处理器语句来使我们的类在 Windows 和 UNIX/Linux 上都可移植。
由于我们的类将是一个共享库,我们将在 Windows 上创建一个 DLL(GLView.dll),在 Linux 上创建一个共享对象文件(libGLView.so)。我们需要在 Windows 上导出类符号,因此会使用 __declspec
扩展。Linux 不需要这个,因此我们使用预处理器指令来屏蔽它。
#ifdef GLVIEW_EXPORTS
#ifdef WIN32
#define GLVIEW_API __declspec(dllexport)
#else
#define GLVIEW_API
#endif
#else
#ifdef WIN32
#define GLVIEW_API __declspec(dllimport)
#else
#define GLVIEW_API
#endif
#endif
然后我们包含必要的头文件,同样使用预处理器指令来实现特定于平台的包含。
#ifdef WIN32
#include <windows.h> // required by OpenGL and must be included before gl.h
#include <TCHAR.H> // for unicode support
#endif
#include <GL/gl.h>
#include <GL/glu.h>
#ifndef WIN32 // X Windows only
#include <GL/glx.h>
#endif
现在是我们的类声明...
请注意,方法和数据成员也由预处理器指令保护。
class GLVIEW_API CGLView
{
public:
CGLView(void);
#ifdef WIN32
void SetWindow(HWND ihWnd);
#else
void SetWindow(Display* pDisp, const Window& wnd);
#endif
bool SetupGLContext(bool iSetPixelFormat);
void Resize(unsigned short iWidth, unsigned short iHeight);
void RenderScene(void);
void Refresh( void);
private:
#ifdef WIN32
int SetPixelFormat(HDC hdc);
#endif
void Setup3DEnvironment();
void DrawGradientBackground();
void InitDisplayLists();
void DoAntialiasing();
private:
#ifdef WIN32
HWND m_hWnd;
HDC m_hDC;
HGLRC m_hGLRC;
#else
Display* m_pXDisplay;
int m_iXScreen;
Window m_iXWindow;
GLXContext m_hGLContext;
XVisualInfo *m_hVisual;
Colormap m_ColMap;
#endif
// other private members hereafter...
};
让我们看看 private
数据成员。WIN32 成员是 HWND
、HDC
和 HGLRC
类型。HGLRC
是 Windows 提供的 GL 渲染上下文句柄,用于使窗口支持 OpenGL。HDC
可以通过 Windows API 函数 GetDC()
获得,一旦获得 HWND
,就可以通过 Windows 特定的 GL API 函数 wglCreateContext()
创建 HGLRC
。
X Windows 成员是 Display
、Window
和 GLXContext
类型。GLXContext
与 HGLRC
类似,一旦获得 Display
和 Window
,就可以通过 X Window 特定的 GL API 函数 glXCreateContext()
创建。
请记住,创建 OpenGL 渲染上下文是开始在任何窗口上渲染的关键入口点。
现在,让我们看看类中的重要方法。构造函数基本上只进行 private
变量的初始化。
类中第一个重要的方法是 SetWindow()
。此方法在 Linux 上的签名与 Windows 上不同。区别在于,在 Windows 上,它将 HWND
作为参数,而在 Linux 上,它将 Display
和 Window
作为参数。请注意,尽管我提到了 Linux,但它也适用于任何以 X Windows 作为其窗口系统的 OS。让我们看看两个系统上 SetWindow()
方法的实现。
#ifdef WIN32
void CGLView::SetWindow(HWND ihWnd)
{
m_hWnd = ihWnd;
}
#else
void CGLView::SetWindow(Display* pDisp, const Window& wnd)
{
m_pXDisplay = pDisp;
m_iXWindow = wnd;
}
#endif
该方法只是将参数设置给相应的 private
数据成员。
下一个方法是 SetupGLContext
。此方法实际上负责通过调用前面讨论的适当的基于平台的 API 函数来设置 OpenGL 上下文。此方法使用预处理器指令分隔代码。
bool CGLView::SetupGLContext(bool iSetPixelFormat)
{
#ifdef WIN32
if(m_hWnd == NULL)
{
return false;
}
m_hDC = ::GetDC(m_hWnd);
if(iSetPixelFormat)
{
SetPixelFormat(m_hDC);
}
if (m_hGLRC = ::wglCreateContext(m_hDC))
{
// try to make it the thread's current rendering context
if(false == wglMakeCurrent(m_hDC, m_hGLRC))
{
MessageBox(m_hWnd, _T("Failed wglMakeCurrent"),
_T("Error!"), MB_ICONERROR);
return false;
}
else
{
glClearColor(0.0, 0.0, 0.0, 1.0);
InitDisplayLists();
}
}
return true;
#else
int attrListSgl[] =
{ GLX_RGBA, GLX_RED_SIZE, 4, GLX_GREEN_SIZE, 4, GLX_BLUE_SIZE, 4,
GLX_DEPTH_SIZE, 16, None };
int attrListDbl[] =
{ GLX_RGBA, GLX_DOUBLEBUFFER, GLX_RED_SIZE, 4, GLX_GREEN_SIZE, 4,
GLX_BLUE_SIZE, 4, GLX_DEPTH_SIZE, 16, None };
m_iXScreen = DefaultScreen(m_pXDisplay);
m_hVisual = glXChooseVisual(m_pXDisplay, m_iXScreen, attrListDbl);
if (NULL == m_hVisual)
{
m_hVisual = glXChooseVisual(m_pXDisplay, m_iXScreen, attrListSgl);
cout << "Singlebuffered : true" << endl;
if(NULL == m_hVisual)
{
cerr << "Could not get suitable XVisualInfo\n" << endl;
return false;
}
}
else
{
cout << "Doublebuffered : true\n" << endl;
}
// Create the rendering context
m_hGLContext = glXCreateContext(m_pXDisplay, m_hVisual, 0, GL_TRUE);
// Create a colormap
m_ColMap = XCreateColormap
(m_pXDisplay, RootWindow(m_pXDisplay, m_hVisual->screen),
m_hVisual->visual, AllocNone);
// Make the rendering context current, perform initialization, then
// deselect it
if(False == glXMakeCurrent(m_pXDisplay, m_iXWindow, m_hGLContext))
{
return false;
}
else
{
glClearColor(0.0, 0.0, 0.0, 1.0);
InitDisplayLists();
}
glXMakeCurrent(m_pXDisplay, None, NULL);
return true;
#endif
}
请注意,对于 Windows 平台,在 SetupGLContext()
中有一个额外的函数调用(SetPixelFormat()
)。该方法基于传递给 SetupGLContext()
的布尔值参数。这样做是为了兼容某些工具包中已准备好 GL 的小部件,即对于已设置的小部件,无需重置 PixelFormat
。使用 GLView
类的开发人员应根据他使用的小部件类型,谨慎地将布尔值传递给 SetupGLContext
,以便将 GLView
类集成到其中。
一旦创建了 GLContext
,它就会被设为当前上下文,并通过在 Windows 和 X 上分别调用 wglMakeCurrent()
和 glXMakeCurrent()
使窗口准备好接收 OpenGL 命令。
Resize()
方法负责在窗口调整大小时重新映射视口和观察体积到窗口。它在两个平台上执行通用的 OpenGL 视口和矩阵转换操作,除了分别调用 xxxMakeCurrent()
方法。
void CGLView::Resize(unsigned short iWidth, unsigned short iHeight)
{
GLdouble modelMatrix[16];
GLdouble projMatrix[16];
GLint viewport[4];
winH = (GLdouble)iHeight;
winW = (GLdouble)iWidth;
// setup viewport, projection etc.:
/* Prevent a divide by zero*/
if(iHeight == 0)
iHeight = 1;
#ifdef WIN32
wglMakeCurrent(m_hDC, m_hGLRC);
#else
glXMakeCurrent(m_pXDisplay, m_iXWindow, m_hGLContext);
#endif
glViewport (0, 0, iWidth, iHeight);
glMatrixMode (GL_PROJECTION);
glLoadIdentity ();
if (iWidth <= iHeight)
glOrtho (-nRange, nRange, -nRange*iHeight/iWidth, nRange*iHeight/iWidth,
-nRange*10000, nRange*10000);
else
glOrtho (-nRange*iWidth/iHeight, nRange*iWidth/iHeight, -nRange, nRange,
-nRange*10000, nRange*10000);
glMatrixMode (GL_MODELVIEW);
glLoadIdentity ();
...
#ifdef WIN32
wglMakeCurrent(NULL,NULL);
#else
glXMakeCurrent(m_pXDisplay, None, NULL);
#endif
}
RenderScene()
方法负责在已准备好 OpenGL 的窗口上显示程序员绘制的 OpenGL 原始对象。此方法也主要执行通用的 OpenGL 操作,除了 xxxMakeCurrent()
方法和交换显示缓冲区。
void CGLView::RenderScene(void)
{
#ifdef WIN32
wglMakeCurrent(m_hDC, m_hGLRC);
#else
glXMakeCurrent(m_pXDisplay, m_iXWindow, m_hGLContext);
#endif
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
DrawGradientBackground();
DoAntialiasing();
Setup3DEnvironment();
glPushMatrix();
{
//Draw stuff here
glRotatef(-45.0f, 1.0f, 0.0f, 0.0f);
glRotatef(-45.0f, 0.0f, 0.0f, 1.0f);
glColor4ub(125, 255, 255, 255);
drawTorus(30, 20, 50, 25, false);
}
glPopMatrix();
#ifdef WIN32
SwapBuffers(m_hDC);
wglMakeCurrent(NULL, NULL);
#else
glXSwapBuffers(m_pXDisplay, m_iXWindow);
glXMakeCurrent(m_pXDisplay, None, NULL);
#endif
}
Refresh()
方法负责重绘窗口,因此包含特定于平台的代码。
void CGLView::Refresh( void)
{
#ifdef WIN32
::InvalidateRect(m_hWnd, NULL, FALSE);
#else
// setup event data
XWindowAttributes winattr;
Status st = XGetWindowAttributes(m_pXDisplay, m_iXWindow, &winattr);
XExposeEvent ev =
{ Expose, 0, 1, m_pXDisplay, m_iXWindow, 0, 0,
winattr.width, winattr.height, 0 };
// send event to display connection
XSendEvent(m_pXDisplay, m_iXWindow, False, ExposureMask, (XEvent *) &ev);
XFlush(m_pXDisplay);
#endif
}
其他方法由开发人员自行增强 GLView
类并添加视图缩放、平移等功能。为了使本教程中的类保持简洁,我没有添加典型 OpenGL
视图类的许多此类功能。
构建一个动态或静态库,并将其用作任何工具包或任何平台的通用 API。
现在让我们看看如何在 Windows 和 Linux 上的不同工具包中使用我们的基础代码库。
WINDOWS
首先,让我们看看如何在 Windows 上最常用的工具包 - 其自己的 MFC 中使用它。
在具有文档/视图架构的应用程序中,CView
派生类是托管可视化的类。我们使用该类来嵌入我们的 GLView
对象。然后,我们从 CView
派生类获取 HWND
并设置我们的 GLView
类。
请注意,在 OnInitialUpdate()
方法中调用 GetSafeWnd()
方法来获取 HWND
。
客户端区域矩形使用 GetClientRect()
方法获得,然后使用其尺寸调用 Resize()
方法,将 GLView
初始调整到窗口客户端区域。
OnDraw()
方法被用于调用我们 GLView
的 RenderScene()
方法,而 OnSize()
方法用于处理 GLView
的调整大小。
CGLViewMFCMDIAppView::CGLViewMFCMDIAppView()
{
// TODO: add construction code here
m_pGLView = new CGLView();
}
void CGLViewMFCMDIAppView::OnInitialUpdate()
{
CView::OnInitialUpdate();
if(m_pGLView)
{
m_pGLView->SetWindow(GetSafeHwnd());
m_pGLView->SetupGLContext(true);
CRect rect;
GetClientRect(&rect);
m_pGLView->Resize(rect.Width(), rect.Height());
}
}
void CGLViewMFCMDIAppView::OnDraw(CDC* /*pDC*/)
{
CGLViewMFCMDIAppDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
if(m_pGLView)
{
m_pGLView->RenderScene();
}
}
void CGLViewMFCMDIAppView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
if(m_pGLView)
{
m_pGLView->Resize(cx, cy);
}
}
现在让我们看看如何在 MFC 对话框应用程序中使用 GLView
。由于我们只需要 HWND
,我们可以使用我们的类来渲染到几乎任何窗口或控件。但请注意,如果控件的绘制事件已经被控件类处理,您不能仅仅通过获取 HWND
来渲染。在这种情况下,您只需要子类化控件并处理其绘制事件。MFC 对话框中最合适的控件是图片控件。只需在您希望渲染 OpenGL 视图的 MFC 对话框上放置一个 Picture Control。为该控件创建一个变量,您就可以在对话框代码中使用它来获取 HWND
并将 OpenGL 视图渲染到其中。对话框类中适合的方法是 OnInitDialog
和 OnPaint
,用于创建视图、设置窗口和渲染。
BOOL CGLViewMFCDlgAppDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
if(m_pGLView)
{
m_pGLView->SetWindow(m_cGLFrame.GetSafeHwnd());
m_pGLView->SetupGLContext(true);
CRect rect;
m_cGLFrame.GetClientRect(&rect);
m_pGLView->Resize(rect.Width(), rect.Height());
}
return TRUE; // return TRUE unless you set the focus to a control
}
void CGLViewMFCDlgAppDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND,
reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialog::OnPaint();
if(m_pGLView)
{
m_pGLView->RenderScene();
}
}
}
从应用程序逻辑的角度来看,大多数对话框的调整大小不会发生,除非您的应用程序有特殊需求。在这种情况下,对于 MFC 对话框,您需要编写额外的代码,以便控件与对话框一起调整大小,然后调用 GLView::Resize()
方法。在这种情况下可能需要子类化控件。
UNIX/Linux
现在让我们看看如何在 UNIX/Linux 上使用我们的 GLView
类。UNIX 上最常用的 GUI 工具包是 Motif,尽管 Linux 应用程序主要用 GTK 和 Qt 编写,它们是跨平台的。然而,当人们想到不同版本的 UNIX 并编写 X 上的原生应用程序时,Motif 仍然是商业上首选的工具包。它也是最古老、最成熟的工具包之一。所以让我们来看看 Motif 中的代码。
Motif 上最合适的控件是 xmDrawingAreaWidgetClass
小部件类的 DrawingArea
控件。由于大多数 Motif 应用程序都是用纯 C 风格编写的,我们有一个 static
全局变量来存储我们的 GLView
类。在主函数中,我们创建一个 DrawingArea
控件,然后添加 Expose
事件(相当于 Windows 上的绘制事件)和 Resize
事件的回调。
drawing_area=XtCreateManagedWidget("drawing_area", xmDrawingAreaWidgetClass, form,al,ac);
XtAddCallback (drawing_area, XmNexposeCallback,
(XtCallbackProc)exposeCB, NULL);
XtAddCallback (drawing_area, XmNresizeCallback,
(XtCallbackProc)resizeCB, NULL);
然后,我们使用我们 GLView
的方法来设置窗口,方法是从 DrawingArea
控件中检索 Display
和 Window
。
pGLView = new CGLView();
if(pGLView == NULL)
{
cout << "Failed to create CGLView!" << endl;
exit(0);
}
else
{
Arg args[5];
int n = 0;
Dimension width, height;
XtSetArg (args[n], XmNwidth, &width); n++;
XtSetArg (args[n], XmNheight, &height); n++;
XtGetValues (drawing_area, args, n);
pGLView->SetWindow(XtDisplay(drawing_area), XtWindow(drawing_area));
pGLView->SetupGLContext(true);
pGLView->Resize(width, height);
}
然后我们只需实现 Expose
和 Resize
事件的回调方法。
void exposeCB(Widget w,
int client_data,
XmDrawingAreaCallbackStruct *cd)
{
if(pGLView != NULL)
{
pGLView->RenderScene();
}
}
void resizeCB(Widget w,
int client_data,
XmDrawingAreaCallbackStruct *cd)
{
Arg args[5];
int n = 0;
Dimension width, height;
XtSetArg (args[n], XmNwidth, &width); n++;
XtSetArg (args[n], XmNheight, &height); n++;
XtGetValues (w, args, n);
if(pGLView != NULL)
{
pGLView->Resize(width, height);
}
}
就是这样!我们的 GLView
正在原生 Motif 窗口上渲染!
跨平台
GTK+ 和 Qt 是用于编写跨平台 GUI 应用程序的一些领先的 GUI 工具包,它们遵循“一次编写,随处构建”的模式。在使用这些工具包时,我们必须记住的一件重要事情是,我们使用它们是为了能够在其他平台上构建代码,而无需修改代码。我们还必须理解,这些工具包有一些特定于平台的函数和类供我们处理特定于平台的代码,就像在我们的例子中,我们需要 Windows 上的 HWND
和 X 上的 Window
一样。所以让我们看看如何在这些工具包中使用它们以及它们的特定于平台的函数,以便我们可以在一个平台上编写应用程序,并使其能够直接在多个平台上构建。
GTK+
我们将使用 GtkDrawingArea
控件来渲染我们的 GLView
,并处理 realize、configure 和 expose 事件来创建我们的 GLView
、设置窗口并相应地调整它的大小。我们还将使用特定于平台的头文件包含。请注意,在 Windows 上包含 gdkwin32.h
,在 X 上包含 gdkx.h
。代码基本上是自解释的,您可以轻松地看到如何在 Windows 上获得 HWND
,以及在 X 上获得 Display
和 Window
。一个值得注意的事情是我们使用的宏 GTK_WIDGET_UNSET_FLAGS (widget, GTK_DOUBLE_BUFFERED)
,它用于禁用 GtkDrawingArea
控件内置的双缓冲功能,该功能会导致我们的 GLView
闪烁。
#ifdef WIN32
#include <gdk/gdkwin32.h>
#else
#include <gdk/gdkx.h>
#endif
void
UpdateDrawingArea (GtkWidget * widget)
{
GtkWidget *drw = lookup_widget (GTK_WIDGET (widget), "drawingarea1");
gdk_window_invalidate_rect (GTK_WIDGET (drw)->window,
>K_WIDGET (drw)->allocation, FALSE);
}
void
on_drawingarea1_realize (GtkWidget * widget, gpointer user_data)
{
// Unset Double Buffering on DrawingArea to
// eliminate flickering of glview
GTK_WIDGET_UNSET_FLAGS (widget, GTK_DOUBLE_BUFFERED);
pGLView = new CGLView ();
if (pGLView)
{
GdkWindow *gdkwin = GTK_WIDGET (widget)->window;
#ifdef WIN32
glong hWnd;
hWnd = (glong) gdk_win32_drawable_get_handle (gdkwin); // get HWND
pGLView->SetWindow ((HWND) hWnd);
#else
Display* pDisplay = gdk_x11_drawable_get_xdisplay(gdkwin);
Window window = (Window) gdk_x11_drawable_get_xid(gdkwin);
pGLView->SetWindow (pDisplay, window);
#endif
pGLView->SetupGLContext (TRUE);
gint w, h;
gtk_widget_get_size_request (widget, &w, &h);
pGLView->Resize (w, h);
}
}
gboolean
on_drawingarea1_configure_event (GtkWidget * widget,
GdkEventConfigure * event,
gpointer user_data)
{
if (pGLView)
{
pGLView->Resize (event->width, event->height);
}
return FALSE;
}
gboolean
on_drawingarea1_expose_event (GtkWidget * widget,
GdkEventExpose * event, gpointer user_data)
{
if (pGLView)
{
pGLView->RenderScene ();
UpdateDrawingArea (GTK_WIDGET (widget));
}
return FALSE;
}
Qt
Qt 是一个面向对象的 C++ 跨平台 GUI 工具包,在桌面和移动应用程序中都越来越受欢迎。Qt 提供了一套丰富的控件类以及一个功能齐全的 OpenGL 控件。然而,如果我们有一个功能强大的 GLView
类,我们可以轻松地将代码从其他工具包移植到 Qt,而无需使用 QGLWidget
重写我们的 OpenGL 代码。要在 Qt 控件中使用我们的 GLView
,我们编写一个派生自 QWidget
的类,并重写其 paintEvent()
和 resizeEvent()
方法。由于 Qt 提供了一个强大的 Paint Engine 来隐藏我们的 GLView
,我们需要禁用它,并要求 QWidget
直接渲染到屏幕。通过重写 paintEngine()
方法使其返回 0 来禁用 Paint Engine,并通过设置控件属性 Qt::WA_PaintOnScreen
来直接渲染到屏幕。构造函数是我们创建 GLView
实例并设置窗口的地方。winId()
方法在 Windows 和 X 上分别获取 HWND
和 Window
。在 X 上,要获取 Display
,QWidget
类提供了一个方法 x11Info()
,该方法获取 QX11Info
类对象,该对象又提供了一个静态方法 display()
。
#ifndef WIN32
#include <QX11Info>
#endif
GLFrame::GLFrame(QWidget *parent)
: QWidget(parent)
{
m_pGLView = new CGLView();
if(m_pGLView)
{
setAttribute(Qt::WA_PaintOnScreen);
#ifdef WIN32
m_pGLView->SetWindow((HWND)winId());
#else
Display* pDisp = x11Info().display();
Window wnd = winId();
m_pGLView->SetWindow(pDisp, wnd);
#endif
m_pGLView->SetupGLContext(true);
m_pGLView->Resize(width(), height());
}
}
QPaintEngine * GLFrame::paintEngine () const
{
return 0;
}
void GLFrame::paintEvent(QPaintEvent* /*event*/)
{
if(m_pGLView)
{
m_pGLView->RenderScene();
}
}
void GLFrame::resizeEvent(QResizeEvent* event)
{
if(m_pGLView)
{
QSize size = event->size();
m_pGLView->Resize(size.width(), size.height());
}
}
我们也可以非常方便地将我们的 GLView
渲染到 QGLwidget
上!只需重写 initializeGL()
、paintGL()
和 resizeGL()
方法即可。无需禁用双缓冲或要求控件直接渲染到屏幕。但是,在这种情况下,应将 SetupGLContext()
的布尔参数设置为 false
,以避免重置 QGLWidget
已设置的像素格式。
我们已经看到了如何创建一个 OpenGL
视图类,该类可以无缝地用于不同的工具包和平台。
我希望您喜欢这篇文章,它对于在 OpenGL 越来越受欢迎且强大的软件开发世界中使 OpenGL 组件变得灵活非常有意义。
基础代码以及 Windows 和 Linux 上的项目源代码已提供。
Windows 项目是一个 VS2008 解决方案,它包含 GLView DLL 项目、MFC MDI 和对话框示例、GTK 和 Qt 示例。您需要拥有 Windows 版本的 GTK+(我使用了 2.12.9 版)和 Qt(我使用了 4.6.3 版)。
Linux 项目需要 Lesstif(开源 Motif 克隆)。我相信 OpenMotif 也应该可以工作。当然,如果您也想构建那些示例,还需要 GTK+ 和 Qt。构建使用 Autotools 完成,并且需要安装。
项目文件夹包含一个 readme.txt 文件,其中解释了如何设置、构建和运行示例。它还提供了两个方便的脚本来构建和运行库和示例。
关注点
我也成功地使用这种范式为 GLView
类创建了一个 COM 组件,该组件不仅适用于不同的工具包,还适用于不同的脚本语言,如 Tcl/Tk(使用 Tcom)、Python(使用 comtypes)和 VBScript。它还可以通过 .NET/COM 互操作性用于 C# 和 VB.NET 中的 Windows Forms。但是,由于它是一个 COM 组件,因此其使用仅限于 Windows 平台。
有更好的方法来创建一个功能齐全的面向对象的 GLView
类,例如使用合适的设计模式等。但在这里,我专注于如何编写类,以便一旦编写了代码,您就不需要对文件进行任何更改,只需在任何平台上编译它并使其可供任何工具包使用。
这种范式的优点是,您已经看到,设置和渲染一个基本的 OpenGL 视图在任何工具包上只需要 4-5 行额外的代码。
历史
- 版本 1.0