OpenGL Integrated with MFC (SDI)

Integrating OpenGL with MFC is just accessing those of main pieces within the MFC framework. I won't spend a lot of time explaining MFC, but a base overview would probably be useful. In this section I will cover setting up Visual Studio for OpenGL, handling the CView creation, and dealing with messages.

Setting up Visual Studio for OpenGL

MFC supplies an application framework with four main parts that allows application writers to skip most of the skeleton code for an application and only add the code that is specific to their program. The four parts are the CWinApp (main application class), CMainFrame (outer window frame including menus), CView (main work area), and CDocument (data behind the application). Windows calls this the Document/View architecture; there are quite a few articles in the MSDN about this topic, so I won't add to the confusion here. The basic concept is that the CMainFrame acts as the container for any and all CViews within the application. The CView acts like a window into the data (for instance, you can view the data as a bar chart, a pie chart, or a list of numbers). The CDocument is where all the actual data is stored. There is nothing in MFC that requires you to follow this framework, but Microsoft makes it somewhat painful if you don't. You can actually have a CView that contains all of the data and not use the CDocument at all. For our purposes we will try to use the framework as it was intended.

Since the CView is the window into the data and it contains our canvas, it is the most logical place for us to set up and store all of our necessary rendering pieces. We should set up our drawable, configure it, and create our graphics context all within the confines of the CView class. The CView class is also going to be responsible for handling all of our relevant messages. The CView will only rely upon the CDocument to draw the data when it is ready to update the drawable; otherwise, it should handle almost everything else.

For the sake of this article I am assuming that you know enough about Visual C++ to at least create a Single Document Interface (SDI) MFC application. It is just a matter of clicking the correct buttons in the application wizard. Once you have a base SDI MFC application then we can start modifying it to support OpenGL.

The first step is to include the OpenGL header files so that we have prototypes for the OpenGL functions. I have found that the StdAfx.h file that the appwizard creates for you is the best place to add these includes. We start by opening the StdAfx.h file and including the following two headers near the end of the file: <gl/gl.h> and <gl/glu.h>. Once this is done we need to add the OpenGL and GLU libraries to the link line so that we pick up the OpenGL functions when we compile. On Windows NT the default OpenGL library is called opengl32.lib and the GLU library is called glu32.lib. We need to add these to the link line by doing the following: Click on the Project pull-down and select the Settings button. When the dialog appears go to the Link tab and add opengl32.lib and glu32.lib to the Object/Library Modules field and then click OK. That's it; we are now ready to start adding code.

Handling the CView Creation

The changes we make will follow the same flow as for the standard Windows application. We will start with the window creation. Just like for the standard application, we need to make sure the window is created with its own DC and that it clips its siblings and children. Since this needs to occur before the window is created, it needs to be handled in a convenient function called PreCreateWindow. The appwizard automatically adds this method to the CView class for you, so you should be able to find it in your *View.cpp file. We can make the needed changes by modifying the CREATESTRUCT that is passed in before it gets passed along to the base class PreCreateWindow function. Your function will end up looking something like the following:

  BOOL CSimpleMFCView::PreCreateWindow 
  (CREATESTRUCT& cs) 
  { 
    // TODO: Modify the Window class or styles 
    here by modifying 
    //  the CREATESTRUCT cs 

    cs.style |= ( WS_CLIPCHILDREN | 
    WS_CLIPSIBLINGS | CS_OWNDC ); 

    return CView::PreCreateWindow(cs); 
  }

The first step is to create the message handler for our WM_CREATE message. Use the Class Wizard to add an OnCreate method to the CView class. This method will handle all of the OpenGL initialization, including getting a handle to the DC, creating the graphics context, and setting up the OpenGL state.

Go to the OnCreate function of your view class and enter the Init code. When you are finished the function should look like this:

  int CSimpleMFCView::OnCreate(LPCREATESTRUCT lpCreateStruct) 
  { 
       if (CView::OnCreate(lpCreateStruct) == -1) 
       return -1; 

       // TODO: Add your specialized creation code here 

       PIXELFORMATDESCRIPTOR   pfd; 
       int     pixelFormat; 

       m_hDC = ::GetDC(m_hWnd); 

       pfd.nSize =             sizeof(PIXELFORMATDESCRIPTOR); 
       pfd.nVersion =          1; 
       pfd.dwFlags =           PFD_DRAW_TO_WINDOW | 
                               PFD_SUPPORT_OPENGL | 
                               PFD_DOUBLEBUFFER; 
       pfd.iPixelType =        PFD_TYPE_RGBA; 
       pfd.cColorBits =        24; 
       pfd.cRedBits =          0; 
       pfd.cRedShift =         0; 
       pfd.cGreenBits =        0; 
       pfd.cGreenShift =       0; 
       pfd.cBlueBits =         0; 
       pfd.cBlueShift =        0; 
       pfd.cAlphaBits =        0; 
       pfd.cAlphaShift =       0; 
       pfd.cAccumBits =        0; 
       pfd.cAccumRedBits =     0; 
       pfd.cAccumGreenBits =   0; 
       pfd.cAccumBlueBits =    0; 
       pfd.cAccumAlphaBits =   0; 
       pfd.cDepthBits =        0; 
       pfd.cStencilBits =      0; 
       pfd.cAuxBuffers =       0; 
       pfd.iLayerType =        PFD_MAIN_PLANE; 
       pfd.bReserved =         0; 
       pfd.dwLayerMask =       0; 
       pfd.dwVisibleMask =     0; 
       pfd.dwDamageMask =      0; 

       pixelFormat = ChoosePixelFormat(m_hDC, &pfd); 

       DescribePixelFormat(m_hDC, pixelFormat, 
                           sizeof(PIXELFORMATDESCRIPTOR), 
                           &pfd); 

       if (pfd.dwFlags & PFD_NEED_PALETTE || 
           pfd.iPixelType == PFD_TYPE_COLORINDEX ) 
               BuildPalette( &pfd ); 

       if ( pfd.dwFlags & PFD_DOUBLEBUFFER ) 
               doubleBuffered = TRUE; 
       else 
               doubleBuffered = FALSE; 

       if(SetPixelFormat(m_hDC, pixelFormat, &pfd) == FALSE) 
                exit(1); 

       m_hRC = wglCreateContext(m_hDC); 
       wglMakeCurrent( m_hDC, m_hRC ); 
       SetupScene(); 
       wglMakeCurrent( NULL, NULL ); 
       return 0; 
  } 

We are going to store the graphics context (m_hRC) and the device context (m_hDC) on the view so that we can access them when we need to. Also note that we need to make sure we call the global ::GetDC function because there is also a GetDC method on the CView class that would return a CDC, a DC class, instead of the HDC that we want.

Dealing with Messages

After modifying the OnCreate function we need to deal with the actual drawing of the scene. In the standard Windows application we handled the redraw whenever we saw the WM_PAINT message. When dealing with MFC the application wizard automatically creates an OnDraw method that is called whenever a redraw is needed. It is in the OnDraw method that we will add the code. When we are done the function should look like the following:

  void CSimpleMFCView::OnDraw(CDC* pDC) 
  { 
       CSimpleMFCDoc* pDoc = GetDocument(); 
       ASSERT_VALID(pDoc); 

       // TODO: add draw code for native data here 
       wglMakeCurrent(m_hDC, m_hRC); 

       glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); 

       glMatrixMode( GL_MODELVIEW ); 

       glPushMatrix(); 
       glTranslatef( 0.0, 0.0, -250.0 ); 

       glColor4f( 1.0, 0.0, 0.0, 1.0 ); 
       glBegin( GL_QUADS ); 
       glVertex2f( -20.0f, -20.0f ); 
       glVertex2f( -20.0f,  20.0f ); 
       glVertex2f( 20.0f, 20.0f ); 
       glVertex2f( 20.0f, -20.0f ); 
       glEnd(); 

       glPopMatrix(); 

       if (doubleBuffered ) 
           SwapBuffers(m_hDC); 
       else 
           glFlush(); 

       wglMakeCurrent( NULL, NULL ); 

       ValidateRect(NULL); 
  } 

For simplicity's sake I added the drawing logic to the CView class, but in most programs I add the logic to the CDocument class. I would start by adding a public method called draw to my CSimpleMFCDoc class and then call this method from the CView class. In this case the OnDraw method would look like this:

  void CSimpleMFCView::OnDraw(CDC* pDC) 
  { 
       CSimpleMFCDoc* pDoc = GetDocument(); 
       ASSERT_VALID(pDoc); 

       // TODO: add draw code for native data here 
       wglMakeCurrent(m_hDC, m_hRC); 

       pDoc->Draw(); 

       if (doubleBuffered ) 
           SwapBuffers(m_hDC); 
       else 
           glFlush(); 

       wglMakeCurrent( NULL, NULL ); 

       ValidateRect(NULL); 
       // Do not call CView::OnPaint() for painting messages 
  } 

The draw logic from the CView class could then be moved into the CDocument class. This would allow the document, which contains all of the data, to decide how it should be drawn.

We now need to add message handlers for three other messages just like we did for the WM_CREATE message. We need handlers for the WM_SIZE, WM_ERASEBKGND, and WM_DESTROY messages. Use the Class Wizard to create methods named Onsize, OnEraseBkgnd, and OnDestroy. When we are done the functions should look like the following:

  void CSimpleMFCView::OnSize(UINT nType, int cx, int cy) 
  { 
       CView::OnSize(nType, cx, cy); 

       // TODO: Add your message handler code here 
       wglMakeCurrent( m_hDC, m_hRC ); 
       glViewport( 0, 0, cx, cy); 

       wglMakeCurrent( NULL, NULL ); 
       InvalidateRect(NULL); 
  } 

  void CSimpleMFCView::OnEraseBkgnd(CDC* pDC) 
  { 
       // TODO: Add your message handler code here and/or call default 

       return 1; 
  } 
  void CSimpleMFCView::OnDestroy() 
  { 
       CView::OnDestroy(); 

       // TODO: Add your message handler code here 
       wglMakeCurrent( NULL, NULL ); 
       wglDeleteContext( m_hRC ); 
       ::ReleaseDC( m_hWnd, m_hDC ); 
  } 

With this you should now be able to get a simple MFC program with OpenGL working properly.

This article covered OpenGL rendering, OpenGL in standard Windows, and the integration of OpenGL with the Microsoft Foundation Classes. Look for a discussion of Windows palettes and OpenGL in the next issue of Developer News.