Introduction

Most of the basic OpenGL setup code that I have come across is demonstrated either using a windowing-system-neutral approach using glut or specific to some GUI toolkit like MFC, GTK+ or Qt. For instance, the glut examples show how to create a ready-to-use window with glutCreateWindow or MFC examples show how to setup an OpenGL context in a CView derived class. GTK+ uses some extensions like gtkglarea or gtkglext and Qt has its own QGLWidget which hides all the OpenGL setup implementations from the user. Good enough if you want to stick to one toolkit or operating system with your code. But imagine a situation where you want to change the toolkit or the OS, say from MFC to Motif! The OpenGL view class that you have written has to go through a lot of change! So why don't we have a platform and toolkit neutral class which can simply render into any window created on any platform?

Background

In this article, I would like to demonstrate how to create a platform and toolkit independent OpenGL view class. For that, we need to first understand the primary requirement for setting up an OpenGL rendering on any window.

OpenGL rendering on any toolkit's window boils down to the underlying windowing system supported on a particular OS. That is, the Device Context on Microsoft Windows and the Display of the X Window System on UNIX/Linux. Although I haven't tested the code on MacOSX, since MacOSX also uses the X Window System as its windowing system, this article is also applicable for MacOSX.

The reader must understand that this article is not about teaching OpenGL or any specific toolkit. This article is intended for those who already have a good understanding of OpenGL and at least one GUI toolkit. Details of creating DLLs or UNIX/Linux shared objects are also out of scope of this article. I am trying to show how to write OpenGL code that is portable to not only different OSs but also to other GUI toolkits. Though language portability can be achieved by writing suitable wrappers on the code shown here, we are focusing on writing the code in C/C++.

Using the Code

First, we need to prepare the window so that it should accept its pixel rendering instructions from the OpenGL API. Then, we must handle the OpenGL drawing commands. Next, we must also handle the effects of resizing the window on OpenGL rendering. So essentially, the three prime methods of our class will be for setting up the window, rendering the OpenGL stuff and resizing the window.

For getting hold of the windowing system, the key things are HWND (Handle to the Window) and HDC (Handle to the Device Context) on Microsoft Windows and Display (X Display is somewhat equivalent to MSWin Device Context though the theory of X Windows is a bit different from MSWin and not in the scope of this article) and Window (equivalent to HWND) on X Windows. If we can access these things from any toolkit, we can pretty much enable our OpenGL view class to render onto any toolkit's window. And trust me, any good toolkit should have functions or methods to provide you the window handle and the Display. The window handle is nothing but HWND or Window.

So let us look at the code now. We would be writing conditional compilation preprocessor statements to make our class portable on both Windows and UNIX/Linux.

Since our class would be in a shared library, we would be creating a DLL (GLView.dll) on Windows and a shared object file (libGLView.so) on Linux. We need to export the class symbols on Windows and hence would be using the __declspec extensions. Linux does not need that and hence we mask it using preprocessor directives.

Then we include the necessary headers, again using preprocessor directives to make platform specific includes.

#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

And now our class declaration...Note, the methods and data members are also guarded by preprocessor directives.

Let us have a look at the private data members. The WIN32 members are of type HWND, HDC and HGLRC. HGLRC is the handle to the GL Rendering Context provided by Windows to make the window OpenGL-aware. HDC can be obtained using Windows API function GetDC() and HGLRC can created by Windows specific GL API function wglCreateContext() once HWND is obtained.

The X Windows members are of type Display, Window and GLXContext. GLXContext is similar to HGLRC which again can be created using X Window specific GL API function glXCreateContext() once Display and Window is obtained.Remember, creation of the OpenGL Rendering Context is the key entry point to begin rendering to any window.

Now, let us look at the important methods of the class. The constructor essentially does only the initialization of the private variables.This first important method of the class is SetWindow(). This method has a different signature on Linux than on Windows. The difference is that on Windows, it takes the HWND as the argument whereas Display and Window as arguments on Linux. Note, though I am referring as Linux, it's applicable to any OS that has X Windows as its windowing system. Let us look at the implementation of the SetWindow() method for both systems.

The method simply sets the arguments to the respective private data members.

The next method is SetupGLContext. This method is actually responsible to setup the OpenGL context by calling the appropriate platform based API functions discussed earlier. This method has code discretized by preprocessor directives.

Note the additional method call (SetPixelFormat()), within SetupGLContext(), for Windows platform. This method is purposefully based on the boolean value coming in as the argument to SetupGLContext(). This is done so that GL-ready widgets from some toolkits can be accomodated, that is, resetting of PixelFormat is not required for already set widgets. The developer using the GLView class should discretely pass the boolean value to SetupGLContext based on what kind of widget he is using to fit the GLView class onto.Once the GLContext is created, it is made current and the window ready for receiving OpenGL commands by calling wglMakeCurrent() and glXMakeCurrent() on Windows and X respectively.

The Resize() method is responsible to remap the view-port and the viewing volume to the window after it is resized. It does the common OpenGL view-port and matrix transformations stuff on both platforms except for discretely calling the xxxMakeCurrent() methods.

The RenderScene() method is responsible to show the OpenGL primitives drawn by the programmer onto the OpenGL-ready window. This method too does mostly the common OpenGL stuff except for the xxxMakeCurrent() methods and swapping the display buffers.

The other methods are up to the developer to enhance the GLView class and add functionality like view zooming, panning, etc. To keep the class brief for this tutorial, I have not added many such functionalities of a typical OpenGL View class.

Build a dynamic or a static library and use it as common API for any toolkit or any platform.

Now let us take a look at using our base code library in different toolkits across Windows and Linux.

WINDOWS

To begin with, let's see how to use it in the most commonly used toolkit on Windows -- its own MFC.In an application with the Document/View architecture, the CView derived class is the one that hosts the visualization. We use that class to embed our GLView object. Then we obtain the HWND from the CView derived class and setup our GLView class.Note the call to the GetSafeWnd() method to get the HWND in the OnInitialUpdate() method.The client area rectangle is obtained using the GetClientRect() method and then its dimensions are used to call the Resize() method to initially resize the GLView to fit to the client area of the window.

The OnDraw() method is tapped to call the RenderScene() method of our GLView and OnSize() method to handle the resizing of the GLView.

Now let us have a look at how we can use the GLView in an MFC dialog based application. Since we just need the HWND, we can use our class to render into almost any window or control. But mind you, you can't just render by getting the HWND alone if the paint event of the control is already handled by the control class. In that case, you just need to subclass the control and handle its paint event. The most suitable control in an MFC dialog is the picture control. Just place a Picture Control on the MFC dialog that you desire to render the OpenGL view. Create a variable for the control and you are all ready to use it in the dialog code to get the HWND and render the OpenGL view onto it. The suitable methods in the dialog class are OnInitDialog and OnPaint for creating the view, setting the window and rendering.

Resizing of most dialogs does not happen from the application logic point of view unless your application has specific needs. In that case, for MFC dialogs you need to write additional code so that the control resizes with the dialog and then call the GLView::Resize() method. Subclassing of controls may be required in this case.

UNIX/Linux

Now let us have a look at using our GLView class on UNIX/Linux. The most commonly used GUI toolkit on UNIX is Motif though Linux applications are mostly written in GTK and Qt which are cross platform. However, when different flavours of UNIX comes to one's mind and writing a native application on X, Motif is still the commercially preferred toolkit. It is also one of the oldest and most mature ones. So let us have a look at the code in Motif.

The most suitable widget on Motif is the DrawingArea widget of the widget class xmDrawingAreaWidgetClass. Since most Motif applications are written in plain C style, we have a static global variable of our GLView class. In the main function, we create a DrawingArea widget and then add the callbacks for the Expose event (equivalent to paint event on Windows) and the Resize event.

Cross Platform

GTK+ and Qt are some of the leading GUI toolkits available for writing cross platform GUI applications with the "code once build anywhere" way. One important thing that we must remember while using such toolkits is the fact that we are using them in the first place to have the code built on other platforms without having the need to modify the code. We must also understand that these toolkits have some platform specific functions and classes for us to handle platform specific code just like in our case where we need the HWND on Windows and Window on X. So let us look into how to use these toolkits along with their platform specific functions so that we can write the application on any one platform and make it suitable to be built directly on multiple platforms.

GTK+

We will be using the GtkDrawingArea widget to render our GLView and we wiil be handling the realize, configure and expose events to create our GLView, setup the window and resize it respectively. We will also be using the platform specific header inclusions. Note the inclusion of gdkwin32.h on Windows and gdkx.h on X. The code is pretty much self explanatory and you can easily see how the HWND is obtained for Windows and the Display and Window on X. One notable thing is the macro GTK_WIDGET_UNSET_FLAGS (widget, GTK_DOUBLE_BUFFERED) that we use to disable the built-in double buffering supported on the GtkDrawingArea widget that causes flickering of our GLView.

Qt

Qt is an object oriented C++ cross platform GUI toolkit that is becoming increasingly popular equally for desktop and mobile applications. Qt provides a rich set of widget classes and also a fully loaded OpenGL widget. However, if we have a powerful GLView class fully loaded with all functionality, we can easily port our code from other toolkits to Qt without having to rewrite our OpenGL code using the QGLWidget. To use our GLView in a Qt widget, we write a class derived from QWidget and override its paintEvent() and resizeEvent() methods. Since Qt provides a powerful Paint Engine that obscures our GLView, we need to disable it and also ask the QWidget to directly render to the screen. We disable the Paint Engine by overriding the paintEngine() method to return 0 from it and rendering to the screen directly is done by setting the widget attribute Qt::WA_PaintOnScreen. The constructor is the place where we create our GLView instance and set the window. the method winId() gets the HWND and Window on Windows and X respectively. On X, to get the Display, the QWidget class provides a method x11Info() that gets the QX11Info class object which in turn provides a static method display().

We can very well use our GLView to render onto the QGLwidget too! Just override the initializeGL(), paintGL() and resizeGL() methods. No need to disable the double buffering or ask the widget to directly render to the screen. But then, the boolean argument to SetupGLContext() should be set to false so that it does not reset the pixel format already setup by the QGLWidget.

We have seen how to create an OpenGL view class that can be seamlessly used across different toolkits and platforms.

I hope you enjoyed this article and it makes a lot of sense to make the OpenGL component a flexible one in the software development world where OpenGL is becoming increasingly popular and strong.

The source code for the base code and projects on Windows and Linux has been provided.Windows project is a VS2008 solution that encompasses the GLView DLL project, MFC MDI and Dialog examples, GTK and Qt examples. You need to have the windows version of GTK+ (I used version 2.12.9) and Qt (I used 4.6.3)

The Linux projects require Lesstif (Open Source Motif clone). I believe OpenMotif should also work. And of course, GTK+ and Qt if you want to build those examples too. The build is done using Autotools and is required to be installed.

The project folder contains a readme.txt file which explains how to setup, build and run the examples. It also has two convenience scripts to build and run the library and samples.

Points of Interest

I have successully used this paradigm also to create a COM component for the GLView class which not only works for different toolkits but also with different scripting languages like Tcl/Tk (using Tcom), Python (using comtypes) and VBScript. It also works for Windows Forms in C# and VB.NET using .NET/COM interoperability. But since it is a COM component, its use is restricted to the Windows platform only.

There are better ways to make a fully blown object oriented GLView class using the right Design Patterns, etc. But here, I am focusing on how to write the class in such a way that once you write the code, you need not make any changes to the files and can just compile it on any platform and make it available to any toolkit.

The beauty of this paradigm is that, you have already seen, setting up and rendering a basic OpenGL view requires only 4-5 lines of extra code on any toolkit.

You are right but I haven't really looked into it from that aspect and, as what I know, this is anyway MFC specific issue that is known to come up with some ICDs which needs to be handled in the portion of the code written to configure the MFC window before handing over its HWND to the GLView class. Though I haven't handled that in the MFC code shown in this article, it has not caused any failures on my hardware. Thanks for pointing that out!

The code looks nice. There are a view things. While you do make the code nice. Your GLView class uses Hungarian notation which is not found on any other platform. Also, you have multi OS specific code. I'd advise you to split up the implementation and make a wrapper around that. For instance.

GLView.
GLViewWindows.cpp
GLViewLinux.cpp
GLViewMac.cpp

Now you don't pollute the headers and source code with platform specific #ifdef's all over the place. I'd also advise looking in to CMake for building. You could allow the user to generate project files for IDE's, etc. And you'd only need one specific thing and it's platform independent. Autohell is just a pita.

Thanks for the comments. You are right, when a working and publishable library has to be made, the platform specific files need to be separated. But here, for getting the real understanding of how things work, and to make the explanation and comparison of both code on the same place, its better to have the entire concept in one file.

For CMake, that's one thing I am just getting hold of.. don't know much about using it. But if you look at the Linux code published here, you can see some files that make it evident that I have been using the Eclipse IDE after the initial project was created. Autotools definitely have the limitation to be used on Linux/UNIX only. I am planning to get CMake working for this.

Yes, having the entire concept in one file is the best way to go but the question is which file should that be ? Personally, I think the best way to do it is to have an interface class that takes care of all that for you. Ideally it should be the single place where all of the ifdefs are so that your application code has as few of them as possible.

Sure, I agree with you but here the whole focus is not on building a library but on understanding how to write code so that the same source files can be used for building on multiple platforms without further editing or selecting one set of files for building on one platform and a different set of files for the other. The whole point of write once build anywhere will be lost. And even if you split all the OS specific code into different files for maintainability, there would ultimately be one source file where they must be conditionally referenced using preprocessor directives and you cannot totally eliminate that!

If you look at the large commercial or open source libraries that work on multiple platforms, they are not having any less preprocessor directives. Preprocessor directives are used liberally to make the same code portable.

But I am definitely in favour of having a common build tool like CMake for multiple platforms as suggested in the comment above by xComaWhitex.

I understand, as this is just showing how it's done. I'm just hintered by the way I was taught to doing C++. I use CMake for all my KDE4 programming projects. It's very nice, it might take some time to learn it but once you get the basics, it is quite easy to go from there. CMake is just like Autohell, but more simplistic (except for finding programmes, which is a little difficult to do for a while).

I'd love to see some more articles from you. Happy that you didn't leave out Linux .