Setting Up OpenGL in an MFC Control

I haven't seen many articles on the Web integrate functionality other than just setting a basic OpenGL window inside of an existing MFC control. My goal in this tutorial is to set up, step-by-step, how to initialize an OpenGL rendering context inside of an MFC control such as a picture control, as well as basic functionality to draw based on a timer, resizing issues, basic camera functionality, and more. I will go step-by-step in the Microsoft Visual Studio .NET 2003 environment and it will be designed for all levels of understanding. I will add steps for the beginners for certain areas and perhaps steps that the more adept person can skip over and delve into what they're truly looking for. Anyway, I appreciate any kind of comments on this (good or bad) and will try my best to update it to everyone's liking. Thanks and enjoy!

Note: This article is a reprint from a tutorial on my Web site, located here. Please look at it for a more picture-oriented step-by-step tutorial.

Part I: Creating the Initial OpenGL Window

Step 1: Creating the Project

You will begin by starting a new MFC Dialog application project. File->New->Project... Select MFC Application from the Templates window and name it "oglMFCDialog". Where you place it on your hard drive is up to you. Just remember where you saved it.

When the MFC Application Wizard pops up, under Application Type select Dialog based and click the Finish button. Any additional preferences and features can be changed under User Interface Features. For my example, I just selected the bare minimum.

Step 2: Creating the Control

Navigate to the Resource View tab from the Solution Explorer tab, expand the Dialog folder, and double-click on the automatically generated IDD_OGLMFCDIALOG_DIALOG dialog box. You will need to add a control in which the OpenGL rendering context will be placed. A simple picture control will do fine, so select Picture Control from the Toolbox.

Note: If your Toolbox does not show up, simply go to View->Toolbox to bring it up.

Drag and drop or select-drag the control onto your dialog window. Resize accordingly.

You will need to set some properties of the picture control. With the picture control selected, the properties window usually will be located on your bottom right of the environment. Set the following variables:

  • Visible: False
  • ID: IDC_OPENGL

You may be wondering why you set the Visible attribute to False. When you load the OpenGL rendering context inside of any MFC control, you will just use the control's window rect coordinates to draw the OpenGL stuff. For some odd reason, if the visibility of the picture control is set to True, the control will cover up all the OpenGL you draw inside of the rect.

Step 3: Adding the OpenGL Class

Next, to set up the OpenGL, I opted to keep it nice and neat and add a separate class for it. It's good practice to keep major components of a project in separate classes, so I will keep the OpenGL and the MFC separate.

To add a class, under the Solution Explorer tab, right-mouse click on the oglMFCDialog project in the tree and go to Add->Add Class... Select Generic C++ Class from the template and click Open. When the C++ Class Wizard pops up, set the following variables:

  • Class name: COpenGLControl
  • Base class: CWnd
  • Access: public
  • Check Virtual destructor

Click the Finish button and you will have your new class added to the project.

Step 4: Adding Project Libraries

Because you will be accessing OpenGL's rendering functions, you also will need to add some libraries to link into the project. To do this, right-mouse click on the oglMFCDialog project in the tree once again and select Properties.

When the oglMFCDialog Property Pages box pops up, select Linker->Input and set the following variables:

Additional Dependencies: opengl32.lib glu32.lib

Note: Do not put any spaces between the two *.lib files; simply type them just as I have listed above.

Step 5: Setting Up Class Variables

For the duration of the project, some local variables will need to be added to the OpenGLControl.h, both public and private, as well as two #include calls for the OpenGL functions to work. They are as follows:

#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;
   .
   .
   .

Important Note: I keep the functions of this project in a specific order. This is rather important because, if some are instantiated before others, you will get a compiler error due to protected member calls. So, I have set them up in the following order:

  1. Constructor and destructor
  2. Manually added functions
  3. Automatically added "afx_msg" functions
  4. The DECLARE_MESSAGE_MAP() call

Step 6: Adding the oglCreate Function

A new function will need to be added to both the OpenGLControl.h and OpenGLControl.cpp files. The function will be responsible for setting up some basic window variables and function calls important to the MFC; I named this function oglCreate. The portion of code below to be added to the header file can be placed under a new public routine section.

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;
}

Step 7: Adding the OnPaint Function

A message class function will need to be added next. How this differs from the previous function is rather simple. MFC calls certain messages (with the WM_ prefix) when specific events happen within the program. Some of the main ones are OnPaint, OnSize, OnCreate, and so forth. To add one of these through Visual Studio, there is a Messages button located in the Properties dockable window with an icon next to the lightning bolt icon. If your cursor is inside the proper *.cpp or *.h file (in your case, for example, OpenGLControl.cpp), this is where you need to add these kinds of messages.

Having said that, first you will need to create an OnPaint message by locating the WM_PAINT message under the Properties->Messages window. Then, select <Add> OnPaint from the dropdown list. You will notice that the message function has been added to both the OpenGLControl.h and OpenGLControl.cpp. But, instead of like a regular user-added function, it will have the prefix "afx_msg" and will also have a call placed in the message map at the top of the *.cpp file. I recommend not changing anything added automatically by Visual Studio unless you know what you're changing.

Inside the OnPaint function, only a little bit of the automatically generated code will need to be changed. You will be rendering (or "painting") the OpenGL window unlike the MFC windows' rendering, in that you will be doing it through a timer. This is in case the need to clamp to a certain frame rate arises later on in a project you do. To render the window control through a timer, the OnPaint cannot be called like it originally is; therefore, adding a simple line of code and commenting out (or deleting) the old calls is needed. Following is the code that will need to be added into this OnPaint function in the OpenGLControl.cpp file:

OpenGLControl.cpp:
void COpenGLControl::OnPaint()
{
   //CPaintDC dc(this);    // device context for painting
   ValidateRect(NULL);
}

Step 8: Adding the OnCreate Function

Next, add another message class like you did in the previous step, but this time select the WM_CREATE message and from the dropdown, select <Add> OnCreate. Again, the new class will be added to both the *.h and *.cpp files.

As far as the code calls for this, you will only need to add one line of code and that's a call to your next manually added function, the oglInitialize function.

Note: Of course, this new function hasn't been added yet, so if you try to run it you will get a compiler error.

OpenGLControl.cpp:

int COpenGLControl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   if (CWnd::OnCreate(lpCreateStruct) == -1)
      return -1;

   oglInitialize();

   return 0;
}

Step 9: Adding the oglInitialize Function

As mentioned in the previous step, the next manually added function, the oglInitialize function, is called when your OpenGL class is created (by means of the OnCreate message). The oglInitialize function will be responsible for setting up all of the information OpenGL needs to render. These include the pixel format, rendering context, and also a clear color to make sure that it's swapping the buffers correctly.

Note: The last line of code in the *.cpp portion below calls an OnDraw function, which has not yet been created. Therefore, another compiler error will occur if you try running this.

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);
}

Step 10: Adding the OnDraw Function

Next, the OnDraw function mentioned in the previous step's oglInitialize function needs to be added. This will have to act as a message function, but will need to be added manually. And as you might gather already, the "afx_msg" prefix will need to be added to the function's declaration. You will notice in the *.cpp portion that you don't actually call any procedures yet, just a commented TODO. This will be mentioned later on in the tutorial, for camera controls and such.

Note: If you try running this now, it will compile correctly, but unfortunately still nothing is being drawn into the control. This is due to the change you made in OnPaint.

OpenGLControl.h:

afx_msg void OnDraw(CDC *pDC);

OpenGLControl.cpp:

void COpenGLControl::OnDraw(CDC *pDC)
{
   // TODO: Camera controls.
}

Setting Up OpenGL in an MFC Control

Step 11: Adding the OnTimer Function

It's now time to add what will succeed the previous functionality of the OnPaint through a timer. Add a new message class by selecting the WM_TIMER message and select <Add> OnTimer from the dropdown list to create the function.

The code of the OnTimer function is rather straightforward. Every time a timer is called in this class, it will run through this function. Therefore, the value passed through the function automatically houses an unsigned integer corresponding to the timer you create manually. Using a select statement, I created the case for the timer you will render with. The basic "draw then swap buffers" routine is now placed inside this timer. In the first steps of this tutorial, though, you won't actually draw images to the screen, but only correctly swap the buffers to the clear color.

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);
}

Step 12: Adding the OnSize Function

The last message function you need to add will be the OnSize function. Like you've done before, select the WM_SIZE message and select <Add> OnSize from the dropdown to create the function.

The OnSize function is called when the window is resized. To prevent the OpenGL rendering from getting all wacky from resizing the window, a few things such as the perspective and viewport need to be tweaked. In the tutorial, you won't actually resize the control according to the window, but that could be achieved in this function if the need arises.

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);
}

You are now done with the COpenGLControl class (the initial setup, at least). So, the next step is to integrate the class into your main MFC window, the CoglMFCDialogDlg class.

Step 13: Customizing the Main MFC Dialog Class

Your last step is to integrate your OpenGL class into the main MFC dialog. This is done rather painlessly. First, inside the oglMFCDialogDlg.h file you will need to #include the created OpenGL class header file and also create an instance of the COpenGLControl class as a local variable to this class.

oglMFCDialogDlg.h:

#include "OpenGLControl.h"

class CoglMFCDialogDlg : public CDialog
{
   private:
      COpenGLControl m_oglWindow;
   .
   .
   .

Next, inside of the oglMFCDialogDlg.cpp file, add to the already existing OnInitDialog function before the return statement the following code to create your OpenGL class inside the picture control you created and start up the timer to render the scene.

Note: For initial test purposes, I set the timer to 1 millisecond to call your timer, or a non-clamp.

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);
.
.
.

Step 14: Initial OpenGL Control Results

And there you have it. Now you should have the black clear color constantly being swapped through the buffers! Now, from here you can add a drawing function to do all your rendering work and place that in the OnTimer function where the commented line suggests.

[oglTut010.jpg]

Part II: Extending the OpenGL Functionality (Extras)

Step 15: Drawing Geometrical Shapes

The next step would be to start drawing some test shapes in your OpenGL control. Unfortunately, you won't be able to actually completely see them yet until you set up some kind of camera system. Without a camera pointing you to a certain point to look at the shapes you draw, the shapes created will just be placed on the origin of the camera and thus won't be viewable. So, if you want to see what you draw first instead, skip down to Step 16, Setting Up a Maya-Style Camera, in which you will create an Alias|Wavefront Maya-style camera (rotate/zoom/translate with mouse buttons). But, I chose to start drawing and setting the camera up afterwards instead for this tutorial.

Anyway, drawing the shapes on this tutorial will be very simple; just create a standard cube by drawing the six sides with quads (4-vertex shapes). If you recall in the OnTimer function, there's a commented-out call to a non-existant oglDrawScene function; you now will create this function.

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();
}

Now, one last thing you'll need to do is uncomment the commented-out call to the oglDrawScene function in the OnTimer function.

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);
.
.
.

Step 16: Setting Up a Maya-Style Camera

The Maya-style camera refers to the camera system used in Alias|Wavefront Maya in which the mouse button->drag performs a certain operation: Left: Rotate, Right: Zoom, and Middle: Translate. To do this, you'll need to make an OnMouseMove function call:

Select the WM_MOUSEMOVE message and select from the dropdown, <Add> OnMouseMove, to create the function. Inside this newly created function, you'll need to add the code to perform each of the buttons' given operations. If you recall from early on in the tutorial, you created some member variables for rotate, zoom, and position. You'll use these variables and set their new values according to the position variables passed into the function by Windows.

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);
}

Furthermore, I have put a few initial variable settings into the constructor of the COpenGLControl class that help the camera work properly.

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
}

Now, the last step to do is, if you recall from when you first created the OnDraw function, to change the commented TODO of the camera controls to the following:

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);
.
.
.

[oglTut014.jpg]

Step 17: Resizing the Window Correctly:

Resizing windows in MFC are a rather tedious job, compared to perhaps Visual Basic, but it can be done quite easily; and once you've done it one time, you reuse the code pretty much forever. So, here's the way I had figured it out and still use to this day. It involves both OnSize functions for both MFC and OpenGL.

First, go into your oglMFCDialogDlg message function and create an OnSize function with the same technique you've used throughout this tutorial. This will generate your OnSize for the MFC portion, but you'll need to add some code; the entire OnSize class will now look like this:

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;
      }
   }
}

You may have noticed this changes the state of a variable you have not yet created, so you now will do the OpenGL window portion, inside your already created OnSize message function there. So, place the following code right after the code that's already inside the OnSize function:

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;
   }
}
.
.
.

And that's about it. Quite simple, no? Feel free to post any comments whatsoever on this article. Thanks for viewing; hope it helps!



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

  • Businesses are moving more and more of their customer transactions to the web. Security is understandably a top concern as online transactions increase, so it is important to make sure your electronic signature provider meets the highest security standards. That means more than simply passing a security audit or obtaining a certification. This white paper provides recommendations for taking a broader view of e-signature security, and answers key questions that help identify the security requirements against …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds