Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / X11

OpenGL in Python with TKinter

5.00/5 (4 votes)
25 Jan 2016CPOL14 min read 51.7K   1.4K  
Why simple if we can be complex?

Background

Was in 2003 my first contact with 3D programming. It was also when I met portal Nehe Programming tutorials (today considered as legacy in his site).

That time the challenges in the tutorials were:

  • C Language: no problem here, after all programming in C will always be an enjoyable  challenge;
  • Windows Programming, Ingeniously simplified in the tutorials (but who was beginner like I was, it resembles the Greek)
  • OpenGL Programming and requirements for 3D programming (well taught in practice);

After, I have learned that there IS simplified way to create this type of application. 

Using libraries as GLUT, a friendly layer provided for creating user interfaces and data entry, turns possible to create an application with 3D capabilities.

On one side there was flexibility but bringing complexity on the other was easy, but with limitations.

Some years passed and at the end of 2008, I began to learn Python.

And since then, when I come across solutions in other languages, or platforms, I was curious to know how I would do in Python with an extra difficulty:

I have to remake just using the library, only Python standard library.

Returning to 3D programming.

With Python there are a range of options for creating applications with 3D like  PyOpenGL, VPython, pyglet and newfound pyGLFW.

But again, I always want to use only Python and its standard library.

However the ability to build GUI in the standard library is using TkInter and it did not have feature or component that allows to use OpenGL to do an application with 3D content.

And so was this for a few years. 

Fortunately, in recent weeks I discovered I was very wrong at that time.

The Solution

From time to time, I looked for solutions in order to use OpenGL in Tkinter. I never find options that allow easy integration. When I found something close to what I would like, it wasn't official or updated to use.

Then one day, I found the post "OpenGL with C and Tcl / Tk" from Lewis Van Winkle.

He speaks briefly and succinctly which prefers make the graphical interface of your applications using Tcl / TK and let the C and OpenGL do the dirty work of 3D.

And he discovered how to use OpenGL in Tcl / Tk when programming in C (in Windows!) easily.

Basically, He passes the id of Tk’s frame widget, configuring it to use in the context of OpenGL (as usually is already done in Windows programming with any other window).

Best impossible, right? Exactly everything I wanted.

After all, from C with Tcl / Tk to Python and TkInter could not be so hard. 

Python is written in C and its standard library uses Tcl / Tk wrapped by TKinter.

And really it was not. 

However, before doing something, I setup a project in Visual Studio to run the original application of Lewis, so that I could have a better understanding how works your code.

Studying the original code

My contact with Tcl / Tk was always superficial. Only after study and develop a bit in Python, I looked to learn the TkInter and finally I met this language. 

I tested a whole code in Tcl through Python to see, for example, three-dimensional solid entirely calculated in Tcl and drawing in Tk (such examples in the Tcl/Tk's wiki).

Still, I did not know how much of the original code made ​​by Lewis, which uses C as connection, I’ll need to remake (in Python or in Tcl).

By the simple code found in the Tcl file (in terms of have few lines and complexity low), I could see that the secret was actually in the C source code.

So, I started studying C code and ignoring the OpenGL commands, only remains Tcl / Tk function calls.

C++
char * p [ ] = {"OpenGL Test", "gui.tcl"};

Tk_Main (2, p, Init);

The above lines made ​​me deduce, initially, that the Tcl file was read and interpreted in the compilation, not being necessary in the final executable (but I was wrong, this TCL file must be in the same directory where the executable will be generated).

When trying to understand the Init function, I figured commands as Tcl_CreateObjCommand and Tcl_CreateTimeHandler could be called by Python, invoking the Tcl interpreter or maybe making an extension in C.

I started to read the documentation the purpose of each of these commands, their arguments and return values.

And began to suspect, that my first thougths, about doing an extension in C or call a routine in pure Tcl, actually was taking (as usual) the most complicated way to solve the problem.

This impression came to get stronger when I (applied the basic development routine):

  • linked the required dependencies in Visual Studio, 
  • Ranning, 
  • errors appearing, 
  • Try solving, searching about them,
  • finally fixing all
    • (and back to Step 2 until find no more errors ...)

And when I got it, the original code ran smoothly.

I could see that not only learned a little of how the Tcl / Tk is used in C, but also that the implementation of Lewis solution on Python and Tkinter, could be simpler than my original idea.

However, before I have this certainty (if would be an extension in C or a TCL routine called by Python or just using pure Python/Tkinter), I came across another problem: I needed Windows and OpenGL's specific functions to call AND try to configure my TkInter Frame object for used the OpenGL.

So, I found another surprise in the Python standard library.

Ctypes

Naturally, I wanted learn more about Python after my first steps. There was a time I wanted to know if was possible and easy to do the Windows' Programming like I saw in the Nehe tutorials.

At this time, I looked for and quickly found the book: Python Programming on Win32 from O'reilly. I quickly flipped through the pages, moved by curiosity to see only the examples. And at the time I had left out 3D programming.

And I do not know exactly whether it was in this book that I knew the first time about ctypes (I think not, because reviewing now, the extension created by Mark Hammond  is named pywin32 (and is well updated, with recent builds in 2014)).

Anyway, the point is that I search once more for Windows’ programming in Python (using OpenGL, if possible), to implement the Lewis solution and found two great Examples:

So, I confess. I DID NOT intensify the study of the Ctypes, because being part of the standard API (officially from the python 2.5) and after a little documentation reading, was enough to believe that would be the solution to be adopted (maybe the only one if I just want to use the standard library).

I focus on code found above and using my PyCharm IDE, I tried to ran them using Python 3.5.1 64-bit.

Analyzing the codes

Thomas’ code needed only an update of print functions and I commented only two asserts.

The code executed, but some ​​exceptions appear:

C++
OSError: exception: access violation reading 0x00000000781BEE10

Right in function DefWindowProc (hWnd,  message, wParam, lParam) that handle messages received by the Windows.

Another problem: The application does not properly ended even if I clicked the standard icon of termination window.

It was needed to end the running application, click on terminate button on PyCharm.

Searching about, I found nothing could explain what should be or how to solve this exception.

So, I move to analysis of Darius’ code.

He executed well too, but with new exceptions:

C++
ctypes.ArgumentError: argument 4: <class 'OverflowError'>: int too long to convert

and again in DefWindowProcW(hwnd, message, wParam, lParam).

And I see something interesting. 

Some executions of this code, showed only the outline of the window with the centralized text (defined in the code) and a background fully transparent.

After more research, none of them made ​​it clear for me, what could be these exceptions.

Yet, in one of these readings, I realized that some were talking about the difference  between of 32 and 64 bit architecture. 

And I came with an idea that could be the Python version I was using. 

I honestly did not want much to accept that could be the answer, but ...

I switched to Python 3.5.1 32-bit to confirm and success!

Both codes worked without any exceptions or errors.

I opted by Thomas’ code because bore the settings needed for OpenGL usage.

By analyzing the code, I learned (with the documentation) that Ctypes already offers an object called WinDLL. 

I have easily access to main DLL from windows justing make an simple call like:

C++
_user32 = WinDLL('user32')

And I will have on _user32 variable all resources offers by user32.dll file.

As I needed access all OpenGL resources, I just make a call similar:

C++
_opengl32 = WinDLL('opengl32')

And It will be done.

Another item required for setting up a window for running OpenGL is access a DeviceContext.

A Device Context is extracted from an Id (which in Windows is data type called HWND) of a window.

This ID, as shown below by Lewis code, can be obtained from a Tk frame widget:

Original (Lewis):

C++
// Grab the HWND from Tk Frame (defined in tcl source)
Tcl_GetIntFromObj (interp, objv [1] (int *) & hWnd);

// Setup OpenGL
dc = GetDC (hWnd)

Python using ctypes:

C++
hdc = winapi._user32.getDC(<id / window hWnd>)

Being winapi a python module defined by Thomas with the structures required by the Windows programming.

To access the OpenGL commands, It was necessary only include the prefix:

C++
winapi._opengl32

Being _opengl32 a variable defined in winapi module like did above.

Thus, a basic OpenGL command:

C++
glOrtho(...)

becomes

C++
winapi._opengl32.glOrtho(...)

This prefix is certainly not required, but I preferred to keep the structure defined in module created by Thomas just for this initial moment.

I think making a wrapper will simplify (which probably the third-party applications do).

Having now a functional and basic code understanding, I added the Lewis’ code in the Thomas’ code just for one more test.

Made some adjustments and two more difficulties appeared:

  • access the constants of OpenGL;
  • and making conversions for data types expected by an OpenGL application;

To make running, it was necessary to replace the OpenGL constants used in the Lewis code by its numeric value or hexadecimal values ​​(they are all direct consulted and copied from opengl.h)

This hack was done because there is no way to extract from a DLL (as far as I informed me ) values ​​defined to a constant.

The second difficulty: because we are accessing OpenGL features, the data types used are the same in C language, just rewritten under another name (and very simplistic one). Naturally, this data types differ from Python types because they are thinking to be closer to the hardware.

In this case, the (once more) powerful ctypes provides classes that assist in the conversion to these data types expected by OpenGL. 

Using glOrtho function as an example again, we can see her reference (on opengl header).

Its signature is defined as:

C++
void WINGDIAPI APIENTRY glOrtho (GLdouble left, GLdouble right , GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);

This function has six parameters, all GLdouble data type that matches the data type double of C Language.

Thus, to convert an integer or floating point Python type for double I used to c_double class. 

Soon the line used in the final code for this test became:

C++
winapi._opengl32.glOrtho(
        winapi.c_double(prm1),
        winapi.c_double(prm2),
        winapi.c_double(-2),
        winapi.c_double(2),
        winapi.c_double(-2),
        winapi.c_double(2)
)

So, made ​​the necessary adjustments, it all ran perfectly.

Now, with all that I could make the final code as I expected.

Application

Final structure of my final application became:

  • Project Folder (TkInterOgl)
    • auxiliary folder(library maybe) (tko)
      • python module: gdi_hdr.py
      • python module: ogl_hdr.py
      • python module: tk_win.py
    • python module (entry point): main.py

Auxiliary modules were structured in order to map the commands I used, following the same idea defined by Thomas in his winapi module. These wrapping was allowed thankfully by Ctypes.

This wrapping simplifies calls and minimizes manual conversions.

I specify which types of data will be received by parameters, but also defines the data type expected. Like the OpenGL function below.

Her original signature was:

C++
WINGDIAPI void APIENTRY glBegin (GLEnum mode);

And his wrap was:

C++
glBegin = _libGL.glBegin
glBegin.restype = None
glBegin.argtypes = [GLEnum]

The member restype define the return data type (None plays the data type void in C)

The member argtypes expects an empty list (if the function does not require arguments) or a list of data types (according with every parameter expected).

In the example above, the original function takes only one parameter: GLEnum data type. 

Consulting the OpenGL header file, I find that GLEnum type actually is the same as unsigned int in C language.

So GLEnum in our example is previously defined as:

C++
GLEnum = c_uint

Being c_uint the class defined in the ctypes for conversion of unsigned int data type values.

Returning the structuring of the final application, the gdi_hdr.py module map functions and constants necessary by Windows programming (PIXELFORMATDESCRIPTOR, GetDC, ChoosePixelFormat etc ...). It is a summary of winapi module.

The  ogl_hdr.py module it’s like the Python version of OpenGL header file (but wrap only the minimum required for my test application).

These definitions were based both in the OpenGL header opengl.h or consulting the MSDN reference to find the signature of the functions used in Windows programming.

As for the standard coding format: gdi_hdr.py module was defined using PEP8 (I try to follow).

In ogl_hdr.py module, I chose to keep the standard coding format defined by the OpenGL header taking into account those who are already familiar and because it’s also appear to be the format adopted in other frameworks (three.js, JOGL etc.).

And finally, for the tk_win.py module I setup  the definition of a class that inherits the TkInter Frame widget (which maps the original TK Frame widget), include some methods:.

This class has the following methods:

  • _cfg_tkogl: 
    • Retrieves the Device Context from the frame widget and configures to receive OpenGL’s commands;
    • Invokes set_ortho_view method;
    • and configures the application to run after 10ms, the render_loop method
  • on_resize:
    • To be developer-defined operations to be performed when the application window has its size scaled;
  • _render_loop:
    • Loop for animation effect;
    • invokes the method render_scene;
    • and swap buffers;
  • render_scene:
    • To be developer-defined operations. It is where are found the OpenGL commands that will build the 3D effects;
  • set_ortho_view:.
    • To be developer-defined operations for viewing 3D space

This class is designed to be inherited and defined in the developer application.

And finally, the entry point (main.py) is a python module having an example of a class definition.

This example has the original OpenGL code sample made by Lewis, which allowed all this journey.

And the result (with bonus):

Points of Interest

  • First, I consider the goal successfully achieved. I can use OpenGL in Python with Tkinter without third-party resources;
  • I believe I have had a brief (very brief) notion of work by projects like PyOpenGL,  among others. Not by chance, when I was mapping the OpenGL functions, I needed to solve a difficulty. I access the PyOpenGL reference to see if could find a solution in their definitions (and I found). That's when I became aware how interesting is to see, how these libraries offer many resources and how these resources could be made just with Python and how much could be a C extension;
  • Ctypes is a library to be studied with care. It's really the python-bridge to existing resources in the Operating System (a little obvious maybe, but not for me initially);
  • How really could be simple some solutions (and how it can take time to find them, INMHO);

Bonus

  • The application also runs on Linux
    • I used the Xubuntu 15.10 32-bit (installed with minimal iso, in a virtual box);
    • I had to install: 
      • libgl1-mesa-dev
      • libglu1-mesa-dev-devlibx11
      • libx11-dev
      • I used the pyenv to install Python 3.5.1;
    • You must manually set the way to the libGL.so (ogl_hdr.py) and X11lib.so (x11_gdi.py) as these files may be elsewhere on other Linux distributions;
    • The window configuration for OpenGL on Linux appears to be much more simpler.
    • I noticed an animation slightly slower in reference of the Windows version, however this must be due to virtualbox drivers and also for being a virtualized system;
  • Future projects based on this article:
    • Using the TkInter to build applications to understand the OpenGL features;
    • Create my particle engine 😄;
    • Porting simple applications that used OpenGL;
  • And some questions that need to be answered:
    • What could be the most natural way to ask the Python developers an initial mapping of the OpenGL header?
    • Or an extension to improve Tk integration with OpenGL? (I would like to see this features as part of standard library);
    • How far this alternative allows the use of OpenGL? 
      • Allowed the use of shaders?
  • Last but not less important improve a lot more my English (written and talk). 😀
  • Example code available on my github as repository: pytkogl

 

Acknowledgments

Thanks to:

for sharing your knowledge and code, making this possible.

History

2016-01-25 - Add a acknowledgements;

2016-01-24 - add github link to example code repository; fix screenshot image and text format;

2016-01-22 - single quotes correction and _opengl32 change to _libGL (used in the final application)

2016-01-21 - Post on CodeProject Submission Wizard;

2016-01-11 - Translated version from Google Docs tool;

2015-12-26 - Article creation in Brazilian Portuguese in my Google Docs;

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)