How to use GR3 with wxWidgets

This tutorial will show you how to create a widget fit for drawing on it with GR3 and it will give you a basic example of an interactive system. As GR3 only requires an existing (and “current”) OpenGL context, adding GR3 graphics to wx is rather easy once you get OpenGL running. The code was tested with wxPython version 4.0.4 osx-cocoa (phoenix) wxWidgets 3.0.5.

As a start, we use a wx.Frame sub-class with a panel for content and a basic app:

import wx

class MainFrame(wx.Frame):
    def __init__(self, parent=None, *args, **kwargs):
        super(MainFrame,self).__init__(parent, *args, **kwargs)

        self.panel = wx.Panel(self)

        self.panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.panel.SetSizerAndFit(self.panel_sizer)

        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer.Add(self.panel,1,wx.EXPAND)
        self.SetSizerAndFit(self.sizer)


if __name__ == "__main__":
    app = wx.App()
    frame = MainFrame(title="Example 1")
    frame.Show()
    app.MainLoop()

To use OpenGL in wxWidgets, we can use the class GLCanvas from the wx.glcanvas package along with the GLContext class used for creating an OpenGL context for drawing the in GLCanvas. The following example adds such a canvas widget and binds its paint event. In the paint method, the OpenGL version will be queried (using PyOpenGL) to see whether the context creation was a success. The minimum size of the canvas is also set to make sure that it will be visible instead of being reduced to 0x0 pixels by Fit(). Note the call to SetCurrent(self.gl_context), used to make our context the current OpenGL context.

import wx
import wx.glcanvas
from OpenGL.GL import glGetString, GL_VERSION

class MainFrame(wx.Frame):
    def __init__(self, parent=None, *args, **kwargs):
        super(MainFrame,self).__init__(parent, *args, **kwargs)

        self.panel = wx.Panel(self)

        gl_canvas_attribs = [wx.glcanvas.WX_GL_RGBA,
                             wx.glcanvas.WX_GL_DOUBLEBUFFER,
                             wx.glcanvas.WX_GL_DEPTH_SIZE, 16]
        self.gl_canvas = wx.glcanvas.GLCanvas(self.panel, attribList=gl_canvas_attribs)
        self.gl_context = wx.glcanvas.GLContext(self.gl_canvas)
        self.gl_canvas.SetMinSize((150,150))
        self.gl_canvas.Bind(wx.EVT_PAINT, self.on_paint_gl_canvas)

        self.panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.panel_sizer.Add(self.gl_canvas, 1, wx.EXPAND)
        self.panel.SetSizerAndFit(self.panel_sizer)

        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer.Add(self.panel,1,wx.EXPAND)
        self.SetSizerAndFit(self.sizer)

    def on_paint_gl_canvas(self,evt):
        self.gl_canvas.SetCurrent(self.gl_context)
        print(glGetString(GL_VERSION))


if __name__ == "__main__":
    app = wx.App()
    frame = MainFrame(title="Example 1")
    frame.Show()
    app.MainLoop()

Since the widget has an OpenGL context now, it is time to add GR3 to it. That’s what’s done in the next example. There are several additions:

  1. gr3_initialized and init_gr3() – these are used to make sure GR3 is initialized exactly once for this widget.
  2. gr3.drawimage() with the canvas’ size and the drawable type OpenGL
  3. SwapBuffers() to bring the drawn image to the screen (swapping foreground and background buffer)
  4. update_scene() – this function is going to contain the scene description and causes the widget to be redrawn after any changes to the scene
  5. the EVT_CLOSE handler to make sure GR3 is terminated
import wx
import wx.glcanvas
import gr3

class MainFrame(wx.Frame):
    def __init__(self, parent=None, *args, **kwargs):
        super(MainFrame,self).__init__(parent, *args, **kwargs)

        self.Bind(wx.EVT_CLOSE,self.on_close)
        self.panel = wx.Panel(self)

        gl_canvas_attribs = [wx.glcanvas.WX_GL_RGBA,
                             wx.glcanvas.WX_GL_DOUBLEBUFFER,
                             wx.glcanvas.WX_GL_DEPTH_SIZE, 16]
        self.gl_canvas = wx.glcanvas.GLCanvas(self.panel, attribList=gl_canvas_attribs)
        self.gl_context = wx.glcanvas.GLContext(self.gl_canvas)
        self.gl_canvas.SetMinSize((150,150))
        self.gl_canvas.Bind(wx.EVT_PAINT, self.on_paint_gl_canvas)
        self.gr3_initialized = False

        self.panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.panel_sizer.Add(self.gl_canvas, 1, wx.EXPAND)
        self.panel.SetSizerAndFit(self.panel_sizer)

        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer.Add(self.panel,1,wx.EXPAND)
        self.SetSizerAndFit(self.sizer)

    def on_paint_gl_canvas(self,evt):
        self.gl_canvas.SetCurrent(self.gl_context)
        size = self.gl_canvas.GetSize()
        if not self.gr3_initialized:
            self.init_gr3()
        gr3.drawimage(0, size.width, 0, size.height, int(size.width), int(size.height),
                      gr3.GR3_Drawable.GR3_DRAWABLE_OPENGL)
        self.gl_canvas.SwapBuffers()

    def init_gr3(self):
        if self.gr3_initialized:
            return
        self.gr3_initialized = True

        gr3.init()
        gr3.setcameraprojectionparameters(45, 1, 200)
        gr3.cameralookat(0, 0, -3, 0, 0, 0, 0, 1, 0)

        self.update_scene()

    def update_scene(self):
        gr3.clear()
        self.Refresh()

    def on_close(self, event):
        if self.gr3_initialized:
            gr3.terminate()
        event.Skip()

if __name__ == "__main__":
    app = wx.App()
    frame = MainFrame(title="Example 1")
    frame.Show()
    app.MainLoop()

The last example only shows a black canvas, but everything needed for using GR3 is ready now. Therefore the next example is an example application for selecting colors from the HSV system and getting their HTML hex code:

../_images/wx_screenshot.png

Whenever the user changes the color, the scene is updated and Refresh() causes a paint event.

import wx
import wx.glcanvas
import gr3
import colorsys

class MainFrame(wx.Frame):
    def __init__(self, parent=None, *args, **kwargs):
        super(MainFrame,self).__init__(parent, *args, **kwargs)

        self.Bind(wx.EVT_CLOSE,self.on_close)
        self.panel = wx.Panel(self)

        gl_canvas_attribs = [wx.glcanvas.WX_GL_RGBA,
                             wx.glcanvas.WX_GL_DOUBLEBUFFER,
                             wx.glcanvas.WX_GL_DEPTH_SIZE, 16]
        self.gl_canvas = wx.glcanvas.GLCanvas(self.panel, attribList=gl_canvas_attribs)
        self.gl_context = wx.glcanvas.GLContext(self.gl_canvas)
        self.gl_canvas.SetMinSize((150,150))
        self.gl_canvas.Bind(wx.EVT_PAINT, self.on_paint_gl_canvas)
        self.gr3_initialized = False

        self.color_panel = wx.Panel(self.panel)
        self.hue_slider = wx.Slider(self.color_panel, value=360, minValue=0, maxValue=360)
        self.hue_slider.Bind(wx.EVT_SCROLL, self.on_color_changed)
        self.saturation_slider = wx.Slider(self.color_panel, value=100, minValue=0, maxValue=100)
        self.saturation_slider.Bind(wx.EVT_SCROLL, self.on_color_changed)
        self.value_slider = wx.Slider(self.color_panel, value=100, minValue=0, maxValue=100)
        self.value_slider.Bind(wx.EVT_SCROLL, self.on_color_changed)
        self.html_notation_box = wx.TextCtrl(self.color_panel)
        self.html_notation_box.Disable()

        self.color_panel_sizer = wx.BoxSizer(wx.VERTICAL)
        self.color_panel_sizer.Add(wx.StaticText(self.color_panel, label="Hue:"))
        self.color_panel_sizer.Add(self.hue_slider)
        self.color_panel_sizer.Add(wx.StaticText(self.color_panel, label="Saturation:"))
        self.color_panel_sizer.Add(self.saturation_slider)
        self.color_panel_sizer.Add(wx.StaticText(self.color_panel, label="Value:"))
        self.color_panel_sizer.Add(self.value_slider)
        self.color_panel_sizer.Add(wx.StaticText(self.color_panel, label="HTML hex code:"))
        self.color_panel_sizer.Add(self.html_notation_box,0,wx.EXPAND | wx.ALL, 4)
        self.color_panel.SetSizerAndFit(self.color_panel_sizer)

        self.panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.panel_sizer.Add(self.gl_canvas, 1, wx.EXPAND)
        self.panel_sizer.Add(self.color_panel)
        self.panel.SetSizerAndFit(self.panel_sizer)

        self.sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.sizer.Add(self.panel,1,wx.EXPAND)
        self.SetSizerAndFit(self.sizer)

    def on_paint_gl_canvas(self,evt):
        self.gl_canvas.SetCurrent(self.gl_context)
        size = self.gl_canvas.GetSize()
        if not self.gr3_initialized:
            self.init_gr3()
        gr3.drawimage(0, size.width, 0, size.height, int(size.width), int(size.height),
                      gr3.GR3_Drawable.GR3_DRAWABLE_OPENGL)
        self.gl_canvas.SwapBuffers()

    def init_gr3(self):
        if self.gr3_initialized:
            return
        self.gr3_initialized = True

        gr3.init()
        gr3.setcameraprojectionparameters(45, 1, 200)
        gr3.cameralookat(0, 0, -3, 0, 0, 0, 0, 1, 0)

        self.on_color_changed(None)
        self.update_scene()

    def update_scene(self):
        gr3.clear()
        gr3.drawspheremesh(1, (0, 0, 0), (self.red, self.green, self.blue), 1)
        self.Refresh()

    def on_color_changed(self, event):
        hue = self.hue_slider.GetValue()
        saturation = self.saturation_slider.GetValue()
        value = self.value_slider.GetValue()
        self.red, self.green, self.blue = colorsys.hsv_to_rgb(hue/360.0,saturation/100.0, value/100.0)
        html_notation = '#' + hex(256+int(255*self.red))[3:] + hex(256+int(255*self.green))[3:] + hex(256+int(255*self.blue))[3:]
        self.html_notation_box.SetValue(html_notation)
        self.update_scene()

    def on_close(self, event):
        if self.gr3_initialized:
            gr3.terminate()
        event.Skip()

if __name__ == "__main__":
    app = wx.App()
    frame = MainFrame(title="Example 1")
    frame.Show()
    app.MainLoop()