glitter Example: Qt

Summary

This program will show a Qt application to display meshes.

Front matter

Module docstring

The module docstring is used as a description of this example in the generated documentation:

"""Example for Qt (PySide) interaction.

@author: Stephan Wenger
@date: 2012-03-16
"""

Imports

Some math functions are required for mouse interaction:

from math import sqrt, exp

We use PySide for Qt interaction:

from PySide import QtCore, QtGui

Note that we did not import the QtOpenGL package. Instead, we will use a Qt OpenGL widget that doubles as a glitter Context:

from glitter.contexts.qt import QtWidget

If you'd rather use an existing Qt OpenGL context, QtContextWrapper provides an alternative solution.

Since there are no more build-in matrices in the OpenGL core profile, we use glitter's matrix constructors. glitter also provides a method for loading meshes from files that we will use:

from glitter import rotation_matrix, scale_matrix, identity_matrix, load_mesh

Qt GUI

Qt OpenGL Widget

OpenGL rendering in Qt is through an OpenGL widget. Instead of subclassing QGLWidget directly, we inherit from glitter's QtWidget which acts as a glitter Context. This not only provides us with a convenient interface for changing OpenGL state, it also ensures that the correct context is made active whenever OpenGL commands are issued.

class Canvas(QtWidget):

Initialization

The canvas will store a mesh to display, a modelview matrix that can be changed by moving the mouse, and a helper variable for the mouse interaction:

    def __init__(self, parent=None):
        super(Canvas, self).__init__(parent)
        self.mesh = None
        self.modelview_matrix = identity_matrix()
        self.lastPos = QtCore.QPoint()

The size of the canvas is specified by overriding sizeHint():

    def sizeHint(self):
        return QtCore.QSize(512, 512)

When the user requests loading a mesh via the menu system, we create a Pipeline containing the mesh vertices and an appropriate shader, all with a single call to load_mesh() from the glitter.convenience module. Also, we can pass additional parameters to the Pipeline constructor. In this case, we want depth testing to be enabled whenever the pipeline is executed, so we pass depth_text=True:

    def loadMesh(self, filename):
        self.mesh = load_mesh(filename, context=self, depth_test=True)

Note that we pass the canvas as the context parameter to make sure the pipeline is created in the correct context. This is necessary because in a real-world application, we cannot be sure that the canvas context is currently active.

When the mesh is loaded, we ask Qt to redraw the OpenGL screen:

        self.updateGL()

Mouse interaction

The viewpoint can be changed by moving the mouse with a button pressed: left button rotates, right button zooms.

When a mouse button is pressed, we store the position where it was pressed:

    def mousePressEvent(self, event):
        self.lastPos = QtCore.QPoint(event.pos())

When the mouse is moved, we take action according to the type of the pressed button:

    def mouseMoveEvent(self, event):

First, we compute the mouse movement since the last time we processed a mouse event. If there was no movement (which does happen from time to time), we exit early to avoid division by zero later on:

        dx, dy = event.x() - self.lastPos.x(), event.y() - self.lastPos.y()
        if dx == dy == 0:
            return

If the left mouse button is down, we rotate the modelview matrix about an axis within the image plane that is perpendicular to the direction of mouse movement. The amount of rotation is proportional to the distance travelled. Then we cause a screen redraw by calling updateGL().

        elif event.buttons() & QtCore.Qt.LeftButton:
            self.modelview_matrix *= rotation_matrix(-sqrt(dx ** 2 + dy ** 2), (dy, dx, 0.0), degrees=True)
            self.updateGL()

If the right mouse button is down, we scale the modelview matrix dependent on the vertical mouse movement. Again, a screen redraw is triggered by calling updateGL().

        elif event.buttons() & QtCore.Qt.RightButton:
            self.modelview_matrix *= scale_matrix(exp(-0.01 * dy))
            self.updateGL()

Finally, the mouse position is stored for processing of the following mouse event.

        self.lastPos = QtCore.QPoint(event.pos())

OpenGL interaction

Whenever the canvas is resized (also on creation), we set the viewport (which is actually a glitter Context property) to the full window and change the projection matrix such that it encompasses the whole range from -1 to +1:

    def resizeGL(self, w, h):
        self.viewport = (0, 0, w, h)
        self.projection_matrix = scale_matrix(h / float(w) if w > h else 1.0, w / float(h) if h > w else 1.0, 0.4)

The paintGL() method is called by Qt whenever the canvas needs to be redrawn:

    def paintGL(self):

First, we clear the canvas using Context.clear().

        self.clear()

Then, if a mesh pipeline has been loaded, we draw it with the modelview_matrix uniform set. The pipeline binds the vertex array and the shader, sets the depth test we requested earlier, draws the vertex array, and resets all modified state:

        if self.mesh is not None:
            self.mesh.draw_with(modelview_matrix=self.projection_matrix * self.modelview_matrix)

Finally, when the user requests resetting the view via the menu system, we create a clean modelview matrix and update the screen:

    def resetView(self):
        self.modelview_matrix = identity_matrix()
        self.updateGL()

Qt Main Window

The OpenGL widget will be displayed within a QMainWindow that provides menus, keyboard shortcuts, and many other amenities:

class MainWindow(QtGui.QMainWindow):

Initialization

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

First, we create an instance of the Canvas class we defined previously and make it the main widget in the window:

        self.canvas = Canvas()
        self.setCentralWidget(self.canvas)

Then, we create menus and keyboard shortcuts for opening meshes, resetting the view, and exiting the program:

        self.fileMenu = self.menuBar().addMenu("&File")
        self.fileOpenMeshAction = QtGui.QAction(u"&Open Mesh\u2026", self)
        self.fileOpenMeshAction.setShortcut(QtGui.QKeySequence(QtCore.Qt.Key_O))
        self.fileOpenMeshAction.triggered.connect(self.fileOpenMesh)
        self.fileMenu.addAction(self.fileOpenMeshAction)
        self.fileMenu.addSeparator()
        self.fileQuitAction = QtGui.QAction("&Quit", self)
        self.fileQuitAction.setShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Escape))
        self.fileQuitAction.triggered.connect(self.close)
        self.fileMenu.addAction(self.fileQuitAction)

        self.viewMenu = self.menuBar().addMenu("&View")
        self.viewResetAction = QtGui.QAction("&Reset", self)
        self.viewResetAction.setShortcut(QtGui.QKeySequence(QtCore.Qt.Key_R))
        self.viewResetAction.triggered.connect(self.canvas.resetView)
        self.viewMenu.addAction(self.viewResetAction)

File loading

When the file open action is triggered via the menu or a keyboard shortcut, we display a dialog for selecting a file. If a file is selected, we tell the canvas to load a mesh from it.

    def fileOpenMesh(self, filename=None):
        if filename is None:
            dialog = QtGui.QFileDialog(self, "Open Mesh")
            dialog.setNameFilters(["%s files (*.%s)" % (x.upper(), x) for x in load_mesh.supported_formats])
            dialog.setViewMode(QtGui.QFileDialog.Detail)
            dialog.setAcceptMode(QtGui.QFileDialog.AcceptOpen)
            dialog.setFileMode(QtGui.QFileDialog.ExistingFile)
            if dialog.exec_():
                filename = dialog.selectedFiles()[0]
        if filename is not None:
            self.canvas.loadMesh(filename)

Main section

Finally, if this program is being run from the command line, we create a QApplication, show a MainWindow instance and run the Qt main loop.

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())

When the main window is closed, the application will exit cleanly.