OpenGL is a hardware-independent software interface for drawing 3-D graphics, complete with shading, lighting, hidden surface removal, texturing, and more. OpenGL is practically the industry standard for high-quality, fast 3-D graphic applications. It was originally developed by Silicon Graphics Inc. and is available on a variety of platforms, including Windows NT (since version 3.5), Windows 95, and Windows 98.
The two standard references to OpenGL are  and , but they don't deal with Windows. To use OpenGL in Windows, you must first take care of a few details specific to the Windows implementation of OpenGL, then proceed by writing standard OpenGL code. This is an ideal place to drop in some reusable code, as these details are the same for every OpenGL Windows application. Unfortunately, MFC (the Microsoft Foundation Class Library) does not provide any classes that deal with the connection between Win32 and OpenGL. You will find a reusable CView derived class in , together with a good explanation of the details specific to OpenGL programming in Windows. Such a class has two shortcomings with respect to reusability:
- It can be used only to render 3-D images inside a CView in MFC applications using the Document-View Architecture.
- You can create only one instance of such a class in your application.
This effectively reduces the class's usability to SDI (Single Document Interface) applications using MFC's Document-View Architecture and drawing a 3-D image in only one CView.
This article presents a small set of C++ classes that handle the details of preparing a generic window for drawing with OpenGL. You can then proceed with standard OpenGL programming in the appropriate redefined virtual functions. You can inherit from these classes and use them to draw one or more 3-D graphs in one or more windows in your MFC application, even without using the Document-View Architecture. You can draw 3-D graphs inside controls in dialog boxes, Views, popup windows, etc.
This article will not go into any detail regarding OpenGL programming, as it is a vast topic and is very well-covered in  and . I present only some classes that deal with making OpenGL work in an MFC window.
How OpenGL Works in Win32
Before I describe the wrapper classes, I will explain how to make OpenGL work in Windows, though you will not need to know much about that if you use my classes.
Just as drawing 2-D graphs with the Windows GDI (Graphics Device Interface) in a window requires using a Device Context (DC), drawing 3-D graphs with OpenGL in a window requires using a Rendering Context (RC). You create an RC with a call to wglCreateContext and delete it with a call to wglDeleteContext. Unlike GDI functions, which accept the DC handle as a parameter, OpenGL API functions do not accept handles to RCs as parameters. To make the association between the RC and the OpenGL API functions that will draw with it, you must make the RC current with a call to wglMakeCurrent. In wglMakeCurrent, specify the handle of the RC and the handle of a DC you associate with it. wglMakeCurrent effectively associates an RC and a DC, allowing you to use OpenGL to draw in a window.
Only one RC can be made current in any thread, and until an RC is made current, OpenGL functions will have no effect in that thread. If the RC is current in your thread, you can retrieve its handle with a call to wglGetCurrentContext. While the RC is current, you must not release the DC associated with it. Call wglGetCurrentDC to retrieve the handle of the DC associated with the current RC.
The functions just described are some of the main "wiggle" functions (so called because they are prefixed with "wgl") that deal with RCs.
Before creating the RC, you must set the pixel format in the DC that will be associated with the RC. The pixel format defines the properties of each window that uses OpenGL. Here you specify whether you need to use double buffering or GDI calls together with OpenGL calls, how many color bits per pixel you want to use, how many bits per pixel are in the depth buffer, etc. I will not go into detail about how to set up the pixel format; you can find the necessary information in , , and , or in the online help searching for function SetPixelFormat or structure PIXELFORMATDESCRIPTOR. My classes automatically set up the pixel format in a way that will work for most applications that use double buffering (necessary for smooth animations) in RGBA mode (necessary if you don't want to go crazy with palettes and the like). You can customize the pixel format however you prefer in your derived classes.
Before setting the pixel format of your window's DC, you must make sure your window is using the styles WS_CLIPCHILDREN and WS_CLIPSIBLINGS, and that your window's window class has been registered without style CS_PARENTDC.
There are two main strategies for creating an RC and making it current during the life of your window, as shown in Table 1. In the first strategy, you can create the RC and make it current when your window is created, use normal OpenGL calls while processing window messages, then make the RC not current and delete it when the window is destroyed. In the second strategy, you can create the RC when your window is created and then make it current only when you need to call OpenGL functions (typically while handling the WM_PAINT and WM_SIZE messages), then delete it when your window gets destroyed. You should use the first strategy to improve performance in applications that draw on only one window, while you must use the second strategy if you have more than one window that uses OpenGL in your thread.
My COpenGLCtrl class implements the first strategy, while class COpenGLCtrls implements the second strategy. You can derive your class from COpenGLCtrl or COpenGLCtrls according to which strategy you want to use. The actual interface of these two classes and the way you use them are identical. The main rule for using these classes is this: if you create an instance of a class derived directly from COpenGLCtrl, you cannot create any other instance of a class derived from COpenGLCtrl or COpenGLCtrls in the same thread. If you derive your classes from COpenGLCtrls, you can create any number of them. If you don't obey this rule, my classes will start calling ASSERT wildly.
Classes COpenGLCtrl and COpenGLCtrls
My COpenGLCtrl class is derived from MFC's CWnd class, the base class for all windows in MFC. Class COpenGLCtrls is derived from COpenGLCtrl. The class definitions are shown in Figure 1 and the implementations are shown in Figure 2. These classes must do the following:
1) Initialize the window and prepare it for drawing with OpenGL.
2) Handle WM_PAINT, WM_SIZE, and WM_ERASEBACKGROUND messages.
3) Clean up (handle WM_DESTROY).
To handle these tasks, the classes COpenGLCtrl and COpenGLCtrls define a protocol of virtual functions that are called at appropriate times and that you can redefine to render your specific scenes.
The obvious time to initialize the window is while handling the WM_CREATE message. Unfortunately, this will not work if your OpenGL window is a child control defined in a dialog template or if you want to subclass an existing window. The dialog box will create the child window, and when you subsequently attach your CWnd object to the child window (by calling either DDX_Control or SubclassDlgItem), you will have missed the WM_CREATE message.
A good place to do the initialization is in function PreSubclassWindow, overridden from the function in base class CWnd. PreSubclassWindow is called both when you create the window and when it is attached to an existing window. My redefined PreSubclassWindow (Figure 2) first adds the necessary WS_CLIPCHILDREN and WS_CLIPSIBLINGS window styles and checks that class style CS_PARENTDC is not in use, then calls virtual function InitializeOpenGLWindow to do the RC initialization. InitializeOpenGLWindow gets a DC and sets its pixel format; it then creates the RC, makes it current, and calls virtual function OnCreateRC. This last function gives you a place to put code for one-time RC initialization.
Handling WM_PAINT and WM_SIZE Messages
When a WM_SIZE message is received, and when the COpenGLCtrl is attached to an existing control, my classes call ResizeOpenGLWindow, passing it the new size of the window. ResizeOpenGLWindow is an ideal place to set the Viewing, Projection, and Viewport transformations. ResizeOpenGLWindow calls the virtual functions OnViewing, OnProjection, and OnViewport, which you can redefine to set up your transformations. You would normally redefine these and not ResizeOpenGLWindow, but you can redefine ResizeOpenGLWindow to get full control.
The most important message to handle is WM_PAINT. My framework delegates the job to virtual function RedrawOpenGLWindow, which clears the buffers, handles the modelview matrix, and calls virtual function OnRender, which you will redefine to render your scene. Again, you would normally redefine OnRender and not RedrawOpenGLWindow, but you can redefine RedrawOpenGLWindow to get full control.
Handling WM_DESTROY ends the job, so I then call UninitializeOpenGLWindow to do the necessary cleanup. UninitializeOpenGLWindow will call virtual function OnDestroyRC, where you can do your own cleanup.
Deriving from COpenGLCtrl or COpenGLCtrls
Using my classes in your applications is very easy. First, derive a class, say, CMyGraph, from COpenGLCtrl or COpenGLCtrls. The choice of base class depends on the choice of strategy (see Table 1). Remember that if you use class COpenGLCtrl directly you cannot create other classes derived from COpenGLCtrl or COpenGLCtrls.
The next step is to redefine some virtual functions to render your scene:
Redefine this member function to do one-time RC initialization. Here you can set up lights, material properties, and other options. OnCreateRC is called only once, at initialization time.
Redefine this function to set up the viewing transformation, usually by calling glTranslate and glRotate, or gluLookAt.
This function takes as a parameter the aspect ratio of the window. You can use it to set up your projection transformation using glOrtho, glFrustum, or gluPerspective.
This function takes the window size as a parameter. You can use it to set the viewport transformation by calling glViewport. If your scene should fill the entire window, the default implementation works fine.
This is the most important function you must redefine; it must draw the complete scene. You will also set the modeling transformations here.
Use this function to do your own cleanup of OpenGL resources.
After redefining these functions, you can do whatever you need to implement application-specific functionality, such as handling the mouse, keyboard, timers, etc. Note one very important point: if you derived your class from COpenGLCtrls, you cannot call other OpenGL functions from outside the above six functions. For instance, you cannot call other OpenGL functions from inside the mouse handlers. This is because when using COpenGLCtrls, the RC is made current only during initialization, cleanup, and handling of the WM_SIZE and WM_PAINT messages. If you need to call OpenGL functions in other places, you must bracket them within calls to BeginOpenGLDrawing and EndOpenGLDrawing. Class COpenGLCtrl does not have this problem, as the RC remains current during the entire life of the window.
As for setting your pixel format, you have three options. You can accept the pixel format I select, in which case you need do nothing. You can modify some items in the PIXELFORMATDESCRIPTOR structure by redefining my CustomizePixelFormat function, or you can redefine my SetupPixelFormat function to take full control.
Using Your Derived Classes
Once you have derived your class CMyGraph from COpenGLCtrl or COpenGLCtrls, there are four basic ways to use it in your applications:
1) to create a child or popup window that will display your image
2) to subclass an existing window
3) as a child window control in a dialog box or a CFormView
4) to show the image in a CView
Options 1 and 2 are very simple; just call CWnd::Create or CWnd::SubclassWindow, respectively. Note that if you use SublassWindow, the window you are subclassing must not be attached to another CWnd object, and must not have class style CS_PARENTDC. Sometimes you must also redefine PostNcDestroy to delete the CWnd object.
Options 3 and 4 take a little more effort. To use an OpenGL control in a dialog box, you can add a custom control to the dialog resource, and specify "MyOpenGL" as the window class (see Figure 3). Class "MyOpenGL" must have been registered, so call-function COpenGLCtrl::RegisterOpenGLWindowClass in your application object's InitInstance. Then add a CMyGraph data member to your CDialog or CFormView derived class. Add a call to DDX_Control in the DoDataExchange member function of your dialog to link the CMyGraph object to the custom control. Sample Application App3D shows this approach by putting a 3-D control in the About box (Figure 3 and Figure 4).
To draw the graph in a CView, you can use my reusable class COpenGLView (Figure 5) as a base class to, say, CMyView. Then add a CMyGraph data member to CMyView, and pass it to the constructor of the base class, COpenGLView. Done. Now your CMyView is ready for use. Sample application App3D (Figure 4 and Figure 5) shows this approach also by drawing 3-D graphs inside Views. Note, though, that the graph will not be drawn in the actual View, but in a child control that will cover it. This has an implication; since you actually implemented drawing in the child control (CMyGraph), not in the view (CMyView) that is hooked into the MFC framework, the command messages will be impossible to handle in the class that draws the graph. Never fear. COpenGLView hooks your CMyGraph control into MFC's message dispatching mechanism by giving it first crack at processing the messages in the redefined OnCmdMsg function, so you can use ON_COMMAND and ON_UPDATE_COMMAND_UI in your CMyGraph class.
Building OpenGL Applications
In my implementation, there are many calls to my GetOpenGLError function within ASSERT statements. OpenGL is a state machine, and if you do something wrong with it, it will set an error state. It is useful to test this state often in debugging builds, as otherwise unnoticed errors can create difficult-to-track problems in your applications. So I verify that you did not make an error after calling every virtual function that you can redefine, and in many other places also. I suggest you also test this state often in your rendering code, since finding out you made an error after a 1,000-line OnRender function terminates is not that useful.
Another thing you must be careful of is using the OpenGL Programming Guide Auxiliary Library (provided in glaux.h and glaux.lib). Some functions (e.g., auxInitDisplayMode and auxInitPosition) won't be needed if you use my classes. Others (e.g., auxWireSphere and auxSolidCube) might not work as expected under Windows. This is because many functions of the Auxiliary Library work only on the first RC created by the application. I added a call to auxWireSphere in the OnRender function of my sample applications (Figure 4 and Figure 7), so that you can see what the problem is when you run sample App3D. My suggestion is this: don't use the Auxiliary Library unless you create one and only one 3-D graph and it stays alive as long as your application does. Otherwise, use the OpenGL Utility Library directly (see gluSphere, gluNewQuadric, etc.).
My classes automatically include the headers and library files necessary to compile and link with OpenGL, but you must have them installed with your compiler. To run your app you will need opengl32.dll and glu32.dll. These DLLs come with Windows NT, Windows 98, and recent versions of Windows 95. If you are missing some of these files for Windows 95, you can download them from Microsoft (ftp://ftp.microsoft.com/Softlib/MSLFILES/Opengl95.exe).
To show my classes at work, I have implemented three sample applications: Simple3D, Sample3D, and App3D.
Simple3D is (guess what) very simple. The complete application is shown in Figure 8, and the outcome appears in Figure 6. It is a complete MFC application that draws a 3-D image in just 20 lines of code. All the application does is inherit class CMainWindow from COpenGLCtrl, and create a window with it. By default, my COpenGLCtrl class draws a simple 3-D color pyramid, so that you can just drop it in your application and see something.
Sample3D is slightly more complex. In Figure 9, I have defined and implemented class CMyGraph, derived from COpenGLCtrls. I redefined OnRender and friends to draw a color pyramid plus a sphere drawn with the Auxiliary Library (which will work fine in this app, since I only have one window). Then, just to be fancy, I handle a mouse click and drag to rotate the model. Figure 7 shows the outcome. Note that I could have just as well inherited from COpenGLCtrl instead of COpenGLCtrls, but I wanted to show you how to use the exact same class in an MDI (Multiple Document Interface) application. Figure 10 shows the main application object that creates a window of class CMyGraph.
The last sample application, App3D (not shown), is a MFC Document-View MDI application, using the same CMyGraph class of the previous sample (Figure 9) to draw 3-D graphs in Views and Dialog controls. This application is a standard MDI application created with AppWizard; I added only one control to the About box (Figure 3), and then modified four lines of code and added another eight (I counted them). All I had to do was use COpenGLView as a base class for CMyView, and put a CMyGraph object from Sample3D in it. Then I added another object of class CMyGraph to CAboutDlg, and linked it to a custom control in the About box dialog resource. The application code is available in the CUJ online sources (see p. 3 for downloading instructions). To see the sample at work, open several MDI child windows and the About box. You will see that the sphere appears only in the first view created (Figure 4) to show you the problem I described previously regarding the auxiliary library.
By the way, the samples won't look good if you have a 256-color display. If you do, you'll have to work harder to see good 3-D color images (see ).
I did not cover all aspects of using OpenGL in Windows, such as fonts, bitmaps, and palettes. To use fonts in OpenGL windows, you must select a TrueType font in the DC, call wglUseFontBitmaps or wglUseFontOutlines to create the display lists with the font characters (slow operation), then call glCallLists to draw the characters. To use bitmaps as textures, you can use the auxDIBImageLoad function. These topics are very well covered in  and in the online doc. To use palettes, see . Printing is another big part; you can see the GLBMP sample application in the MSDN CD-ROM. The classes presented here encapsulate the core functionality of making OpenGL work in Windows, and are reusable and extensible. The learning curve for mastering OpenGL remains rather steep, but these classes take away most Windows details and make a good starting point to build great 3-D images using the leading 3-D graphics interface.
Thanks to Valeriano Corallo for always pushing for an extra dimension.
Giovanni Bavestrelli lives in Milan and is a software engineer for Techint S.p.A., Castellanza, Italy. He has a degree in Electronic Engineering from the Politecnico di Milano, and writes automation software for Pomini Roll Grinding machines. He has been working in C++ under Windows since 1992, specializing in the design and development of reusable object-oriented libraries. He can be reached at firstname.lastname@example.org.