Custom data visualization with PyOpenGL and PyQt — Part 1
A not so long ago, while I was tinkering with a drone project, I needed a way to display telemetry data collected in real time during flight. Although, there are a lot of readily available solutions for this task, I wanted to create a custom data visualization tool of my own, because… why not? To tackle this task, I decided to implement a data visualization tool using Qt and OpenGL. My original tool was based on C++, but for the sake of simplicity I’ve decided to provide a tutorial based on PyQt and PyOpenGL.
This blog post will be the first part of a series posts which will cover a specific use case of PyOpenGL and PyQt for displaying custom graphics, focusing specifically on creating different types of data display widgets. In the first part I’ll cover the basics of creating a new window in PyQt and a basic OpenGL widget within it. In the next sections we’ll generate more complex 2D and 3D graphics and lastly connect the OpenGL widgets to our data
PyQt and PyOpenGL
The tools we’ll be using to draw different types of widgets are Python, PyQt and PyOpenGL:
- PyQt is a python API for the Qt library, which is a framework which allows to build customized GUI applications. In fact it provides much more functionality than that, but for our use case we’ll be using PyQt as a host for our OpenGL widgets. Although, Qt comes with an awesome GUI designer, Qt Designer, we’ll be coding all of the Qt related code manually, because a) I dislike automatically generated code b) Qt is simple and clear enough to code everything without Qt designer.
- OpenGL is a cross-platform/cross-language API for rendering custom 2D and 3D graphics. This API is used to communicate with GPUs. PyOpenGL is a python version of OpenGL.
Installing PyQt and PyOpenGL
Before writing any python code, we’ll have to install the following components via pip:
pip install PyQt5
I used an old(er) Qt version when writing this tutorial, but you can also run the same with Qt6.
pip install PyOpenGL PyOpenGL_accelerate
…and we’ll also have to install NumPy, which we’ll use to transfer arrays of vertices to something called a VBO. Don’t worry, it’ll be explained later on.
Pip install numpy
Creating a window in Qt
First thing first, in our main python script we’ll need to import all the required Qt dependencies:
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
Now we’ll create a custom Qt widget by inheriting from QMainWindow:
# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Custom GL app")
self.resize(600, 600)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
At this point, if we’ll run our code an empty window will pop up of size 600x600:
Creating a custom OpenGL Widget
Next thing we’ll create a new OpenGL widget and add it to our MainWindow class. For now, we’ll implement a very basic PyOpenGL widget which only clears the openGL colorbuffer, which means it essentially clears the display for the OpenGL widget and sets it to a custom color.
First we’ll import two dependencies:
from OpenGL.GL import *
from PyQt5.QtOpenGL import *
OpenGL, so we’ll be able to control OpenGL specific commands and control the OpenGL state machine, and QtOpenGL to inherit from the QGLWidget and implement our own custom OpenGL widget which we’ll add to our MainWindow.
Next we’ll implement our own custom Qt OpenGl widget.
# implementing a custom openGL widget
class glWidget(QGLWidget):
def __init__(self, parent):
self.parent = parent
QGLWidget.__init__(self, parent)
def initializeGL(self):
…
def resizeGL(self, w: int, h: int) -> None:
…
def paintGL(self):
…
For our basic application we’ll need to override four methods:
- The class constructor
- initlalizeGL — Where we’ll initialize different OpenGL buffers and variables, which will be later on used during rendering.
- resizeGL — Which defines how to handle screen resize events.
- paintGL — Which is called when the OpenGL class is required to render a new scene. This method is called by resize events and the update/updateGL methods, which we will use to update our OpenGL scene.
For now we have nothing to initialize or prepare before clearing the screen, so we’ll leave initializeGL empty:
def initializeGL(self):
pass
For handling resize events we’ll add the command glViewport(0,0,w, h). glViewport tells OpenGL what the size of our rendering screen is. With the first two parameters defining the left bottom corner coordinates of the window, and w and h defining the size of the window.
def resizeGL(self, w: int, h: int) -> None:
# override resizeGL method to handle screen resizing
glViewport(0, 0, w, h)
The function resizeGL when called by a resize event receives the updated size of the window.
Finally we’ll implement paintGL to draw on screen:
def paintGL(self) -> None:
# override paintGL method to customize how to draw on screen
glClearColor(0.0, 0.0, 0.5, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
First we set the color of the screen using glClearColor — the four parameters define the RGBA values of the screen, and then we call glClear to clear the screen with the defined color. In our case our clearColor is blue.
Adding the widget to the MainWindow
Finally we’ll add a new instance of our custom OpenGL widget to the constructor of MainWindow.
self.gl = glWidget(self)
And we’ll also define the OpenGL version we want to use in our project:
self.gl.format().setVersion(4, 2)
self.gl.format().setProfile(QGLFormat.CoreProfile)
Now If we’ll run the code we’ll see the following exciting blue window:
You can find the full version of the code in the following git repository: link
Summary
In part 1 we learned how to create a new Qt window and add a basic OpenGL widget as its central widget. In the next blog post, we’ll define basic vertex and fragment shaders and draw our first shape on screen.