Review of OpenGL ES 3.0 programming guide


opengles3.jpeg

Figure 1: Open GL ES 3.0

I found this book to be a very good introduction to opengl es. It has a nice progression through the chapters building up to more advanced topics near the end, for example terrain generation and advanced shader effects.

This book seems to be aimed at users new to opengl es or users transitionaing from other versions of opengl to es 3.0. If you're familiar with other versions of opengl you will likely use this book as more of a reference to look up the differences in the function calls.

I was very impressed by the fact that the example code can be compiled on multiple platforms, including dekstop and mobile. The example code uses various helper functions which get created and explained as you progress through the book. All code will run on linux, windows, mac, andriod and ios devices with build instruction for each platform at the end of the book.

My main crtisism of the book would be that it explains some of the maths, but not in enough detail to be useful for someone who wants to understaanding the maths in depth. Therefore if you want to learn exactly how some of the math works another book will likely help however I don't think in depth understanding is essential early on.

Most of my knowledge regarding opengl has been gained through lots of googleing, this book certainly helped me fill in blanks and explained things in much more depth than trawling through the internet.

Drawing a cube while rendering widgets


cube-with-widgets.png

Figure 1: Cube with widgets

Example shaded 3D cube

Now we will expand on the previous example and add a cube class which will give us a cube to work with.

In this example we use a .kv file and create some widgets we also load a custom widget to load our scene into.

This should give you a good idea of rendering your own scene and usig kivy widgets to control the scene if you need to later on.

Pack a load of widgets around our model, we will not do anything with these other than draw them to the screen.

import kivy
kivy.require('1.0.7')

from kivy.app import App
from opengl_widget import OpenglWidget


class DemoApp(App):
    pass

if __name__ == '__main__':
    DemoApp().run()

below is the interface fille that is loaded to pack a load of widgets around our model, we will not do anything with these other than display them.

#:kivy 1.0
FloatLayout:
    GridLayout:
	cols: 1
	row_force_default: False
	padding: 5
	BoxLayout:
	    height: 80
	    size_hint_y: None
	    Button:
		text: 'Button 1'
	    Button:
		text: 'Button 2'
	BoxLayout:
	    Accordion:
		orientation: 'vertical'
		AccordionItem:
		    title: 'Panel 1'
		    Button:
			text: 'Button 1'
		    Button:
			text: 'Button 2'
		    Button:
			text: 'Button 3'

		AccordionItem:
		    title: 'Panel 2'
		    Button:
			text: 'Button 4'
		    Button:
			text: 'Button 5'
		    Button:
			text: 'Button 6'

		AccordionItem:
		    title: 'Panel 3'
		    Button:
			text: 'Button 7'
		    Button:
			text: 'Button 8'
		    Button:
			text: 'Button 9'
	    OpenglWidget:
		width: 200
		height: 200
	    TreeView:
		label: 'Toolsets'
	BoxLayout:
	    height: 40
	    size_hint_y: None
	    Button:
		text: 'Button 1'
	    Button:
		text: 'button 2'

Very simple solid colour shader for our cube.

---VERTEX SHADER-------------------------------------------------------
#ifdef GL_ES
    precision highp float;
#endif

attribute vec3  v_pos;
attribute vec4  v_color;

uniform mat4 modelview_mat;
uniform mat4 projection_mat;

varying vec4 frag_color;

void main (void) {
    vec4 pos = modelview_mat * vec4(v_pos,1.0);
    gl_Position = projection_mat * pos;
    frag_color = v_color;
}


---FRAGMENT SHADER-----------------------------------------------------
#ifdef GL_ES
    precision highp float;
#endif

varying vec4 frag_color;
varying vec2 uv_vec;

uniform sampler2D tex;

void main (void){
    gl_FragColor = frag_color;
}

This is the meat of the code it creates a custom widget, it gets loaded from the interface file above and then handlers rendering the scene .

import os
import sys
from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.core.image import Image
from kivy.uix.widget import Widget
from kivy.resources import resource_find
from kivy.graphics.transformation import Matrix
from kivy.graphics.opengl import *
from kivy.graphics import *

from kivy.uix.widget import Widget
from kivy.graphics import Color, Ellipse

from numpy import array

class point:
    __slots__ = ['x', 'y', 'z', 'xyz', 'vertex']

    def __init__(self, p, c=(1, 0, 0)):
	""" Position in 3d space as a tuple or list, and colour in tuple or list format"""
	self.x, self.y, self.z = p
	self.vertex = array([self.x, self.y, self.z, c[0], c[1], c[2]], 'f')

class cube:
    def __init__(self, p1, color, size=0.5):
	self.color = array([1, 0, 0], 'f')
	self.points = (
	    point((p1[0] - size, p1[1] + size, p1[2] - size), (color)),
	    point((p1[0] - size, p1[1] + size, p1[2] + size), (color)), 
	    point((p1[0] + size, p1[1] + size, p1[2] + size), (color)),
	    point((p1[0] + size, p1[1] + size, p1[2] - size), (color)),

	    point((p1[0] - size, p1[1] - size, p1[2] - size), (color)),
	    point((p1[0] - size, p1[1] - size, p1[2] + size), (color)), 
	    point((p1[0] + size, p1[1] - size, p1[2] + size), (color)),
	    point((p1[0] + size, p1[1] - size, p1[2] - size), (color)),

	    )

    def get_data(self):
	return (
	    self.points[0].vertex, self.points[2].vertex, self.points[1].vertex, 
	    self.points[0].vertex, self.points[3].vertex, self.points[2].vertex, 

	    self.points[0].vertex, self.points[1].vertex, self.points[5].vertex, 
	    self.points[0].vertex, self.points[5].vertex, self.points[4].vertex,

	    self.points[0].vertex, self.points[7].vertex, self.points[3].vertex, 
	    self.points[0].vertex, self.points[4].vertex, self.points[7].vertex,

	    self.points[6].vertex, self.points[2].vertex, self.points[3].vertex, 
	    self.points[6].vertex, self.points[3].vertex, self.points[7].vertex, 

	    self.points[6].vertex, self.points[1].vertex, self.points[2].vertex,
	    self.points[6].vertex, self.points[5].vertex, self.points[1].vertex,

	    self.points[6].vertex, self.points[4].vertex, self.points[5].vertex,
	    self.points[6].vertex, self.points[7].vertex, self.points[4].vertex,
	)



class OpenglWidget(Widget):
    def __init__(self, **kwargs):
	self.canvas = RenderContext(compute_normal_mat=True)
	self.canvas.shader.source = resource_find('kivy.glsl')

	self.c = cube((0, 0, 0), (1, 0, 0), 2.0)
	self.vertices = []
	for item in self.c.get_data():
	    for a in item:
		self.vertices.append(a)
	    self.vertices.append(1) # add alpha

	self.indices = range(0, len(self.vertices))

	with self.canvas:
	    self.cb = Callback(self.setup_gl_context)
	    PushMatrix()
	    self.setup_scene()
	    PopMatrix()
	    self.cb = Callback(self.reset_gl_context)
	Clock.schedule_interval(self.update_glsl, 1 / 60.)

    def setup_gl_context(self, *args):
	glEnable(GL_DEPTH_TEST)

    def reset_gl_context(self, *args):
	glDisable(GL_DEPTH_TEST)

    def update_glsl(self, *largs):
	aspect = float(self.height) / float(self.width)
	projection_mat = Matrix()
	projection_mat.perspective(45.0, aspect, 1.0, 80.0)
	model = Matrix().look_at(
	    0.0, 0.0, 25.0,
	    0.0, 0.0, 0.0,
	    0.0, 1.0, 0.0)

	self.canvas['projection_mat'] = projection_mat
	self.canvas['modelview_mat'] = model
	self.rot.angle += 1

    def setup_scene(self):
	Color(0, 0, 0, 1)
	PushMatrix()
	self.rot = Rotate(1, 0, 1, 0)

	vertex_format = [
	    ('v_pos', 3, 'float'),
	    ('v_color', 4, 'float'),
	]

	UpdateNormalMatrix()
	self.mesh = Mesh(
	    vertices=self.vertices,
	    indices=self.indices,
	    fmt=vertex_format,
	    mode='triangles',
	)
	PopMatrix()

Draw a textured square with kivy


quad-texture.png

Figure 1: Kivy textured quad

Textured quad source code

This example expands on the first and simply loads a texture and applys it to two triangles which make up a square.

from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.core.image import Image
from kivy.uix.widget import Widget
from kivy.resources import resource_find
from kivy.graphics.transformation import Matrix
from kivy.graphics.opengl import *
from kivy.graphics import *


class Renderer(Widget):
    def __init__(self, **kwargs):
	self.canvas = RenderContext(compute_normal_mat=True)
	self._tpath = resource_find('testing.png')
	self.canvas.shader.source = resource_find('shaders-opengl-texture.glsl')

	super(Renderer, self).__init__(**kwargs)
	with self.canvas:
	    self.cb = Callback(self.setup_gl_context)
	    PushMatrix()
	    self.setup_scene()
	    PopMatrix()
	    self.cb = Callback(self.reset_gl_context)
	Clock.schedule_interval(self.update_glsl, 1 / 60.)

    def setup_gl_context(self, *args):
	glEnable(GL_DEPTH_TEST)

    def reset_gl_context(self, *args):
	glDisable(GL_DEPTH_TEST)

    def update_glsl(self, *largs):
	proj = Matrix().view_clip(0, self.width, 0, self.height, 1, 100, 0)
	self.canvas['projection_mat'] = proj

    def setup_scene(self):
	Color(0, 0, 0, 1)
	PushMatrix()

	indices = [0, 1, 2, 3, 0, 2]
	vertex_format = [
	    ('v_pos', 3, 'float'),
	    ('v_uv', 2, 'float'),
	]

	vertices = [
	  10.0 , 10.0 , 1.0, 0.0, 0.0,
	  10.0 , 200.0, 1.0, 0.0, 1.0,
	  200.0, 200.0, 1.0, 1.0, 1.0,
	  200.0, 10.0 , 1.0, 1.0, 0.0,
	]

	UpdateNormalMatrix()
	self.mesh = Mesh(
	    vertices=vertices,
	    indices=indices,
	    fmt=vertex_format,
	    mode='triangles',
	)
	self.mesh.texture = Image(self._tpath).texture
	PopMatrix()


class RendererApp(App):
    def build(self):
	return Renderer()

if __name__ == "__main__":
    RendererApp().run()
---VERTEX SHADER-------------------------------------------------------
#ifdef GL_ES
    precision highp float;
#endif

attribute vec3  v_pos;
attribute vec2  v_uv;

uniform mat4 modelview_mat;
uniform mat4 projection_mat;

varying vec2 uv_vec;

void main (void) {
    vec4 pos = modelview_mat * vec4(v_pos,1.0);
    gl_Position = projection_mat * pos;
    uv_vec = v_uv;
}


---FRAGMENT SHADER-----------------------------------------------------
#ifdef GL_ES
    precision highp float;
#endif

varying vec4 frag_color;
varying vec2 uv_vec;

uniform sampler2D tex;

void main (void){
    vec4 color = texture2D(tex, uv_vec);
    gl_FragColor = color;
}

Draw a simple triangle with kivy


triangle.png

Figure 1: Kivy shaded triangle

Shaded triangle source code

This example will create a simple triangle and render it on screen, opengl in kivy is wrapped so you use generally use helper methods for rendering.

the main one is meshes for loading in your points and indices and textures to draw your model and rendercontext which activates your shaders.

from kivy.app import App
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.core.image import Image
from kivy.uix.widget import Widget
from kivy.resources import resource_find
from kivy.graphics.transformation import Matrix
from kivy.graphics.opengl import *
from kivy.graphics import *


class Renderer(Widget):
    def __init__(self, **kwargs):
	self.canvas = RenderContext(compute_normal_mat=True)
	self.canvas.shader.source = resource_find('shaders-opengl-triangle.glsl')

	super(Renderer, self).__init__(**kwargs)
	with self.canvas:
	    self.cb = Callback(self.setup_gl_context)
	    PushMatrix()
	    self.setup_scene()
	    PopMatrix()
	    self.cb = Callback(self.reset_gl_context)
	Clock.schedule_interval(self.update_glsl, 1 / 60.)

    def setup_gl_context(self, *args):
	glEnable(GL_DEPTH_TEST)

    def reset_gl_context(self, *args):
	glDisable(GL_DEPTH_TEST)

    def update_glsl(self, *largs):
	proj = Matrix().view_clip(0, self.width, 0, self.height, 1, 100, 0)
	self.canvas['projection_mat'] = proj

    def setup_scene(self):
	Color(0, 0, 0, 1)
	PushMatrix()

	indices = [0, 1, 2, 3, 0, 2]
	vertex_format = [
	    ('v_pos', 3, 'float'),
	    ('v_color', 4, 'float'),
	]
	vertices = [
	  10.0 , 10.0 , 1.0, 1.0, 1.0, 0.0, 0.0,
	  10.0 , 200.0, 1.0, 1.0, 0.0, 1.0, 0.0,
	  200.0, 200.0, 1.0, 1.0, 0.0, 0.0, 1.0,
	]

	UpdateNormalMatrix()
	self.mesh = Mesh(
	    vertices=vertices,
	    indices=indices,
	    fmt=vertex_format,
	    mode='triangles',
	)
	PopMatrix()


class RendererApp(App):
    def build(self):
	return Renderer()

if __name__ == "__main__":
    RendererApp().run()
---VERTEX SHADER-------------------------------------------------------
#ifdef GL_ES
    precision highp float;
#endif

attribute vec3  v_pos;
attribute vec4  v_color;

uniform mat4 modelview_mat;
uniform mat4 projection_mat;

varying vec4 frag_color;

void main (void) {
    vec4 pos = modelview_mat * vec4(v_pos,1.0);
    gl_Position = projection_mat * pos;
    frag_color = v_color;
}


---FRAGMENT SHADER-----------------------------------------------------
#ifdef GL_ES
    precision highp float;
#endif

varying vec4 frag_color;
varying vec2 uv_vec;

uniform sampler2D tex;

void main (void){
    gl_FragColor = frag_color;
}

GTK-3 Simple opengl app with touch events


tut14-touchscreen-events.png

Figure 1: Opengl touch events

This is a much more complicated example which mashes some of the previous examples together, I have written it as an example but also to test out shaders and different pipelines to get better opengl performance.

Hopefully this example will help you get started writing awesome touch based apps with gtk, if anything is unclear let me know in the comments below.

OpenGL in a drawing area

Touch screen events

#!/usr/bin/env python
import sys
import time
import random
import pprint

import Xlib
from Xlib.display import Display
from gi.repository import Gtk, Gdk, GdkX11, GLib, GObject
from ctypes import *
from numpy import array

from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.GLU import gluPerspective, gluLookAt
from OpenGL.arrays import vbo
from OpenGL import GLX
try:
    from OpenGL.GLX import struct__XDisplay
except ImportError as err:
    from OpenGL.raw._GLX import struct__XDisplay
from OpenGL.GL import GL_VERTEX_SHADER, GL_FRAGMENT_SHADER
from OpenGL.GL import shaders, glGetUniformLocation

from helper import shader
from helper import cube as createcube

class gtkgl:
    """ these method do not seem to exist in python x11 library lets exploit the c methods 
    useful link http://www.opengl.org/wiki/Programming_OpenGL_in_Linux:_GLX_and_Xlib"""
    xlib = cdll.LoadLibrary('libX11.so')
    xlib.XOpenDisplay.argtypes = [c_char_p]
    xlib.XOpenDisplay.restype = POINTER(struct__XDisplay)
    xdisplay = xlib.XOpenDisplay(None)
    display = Xlib.display.Display()
    attrs = []

    xwindow_id = None
    width = height = 500

    def __init__(self):
	""" lets setup are opengl settings and create the context for our window """
	self.add_attribute(GLX.GLX_RGBA, True)
	self.add_attribute(GLX.GLX_RED_SIZE, 8)
	self.add_attribute(GLX.GLX_GREEN_SIZE, 8)
	self.add_attribute(GLX.GLX_BLUE_SIZE, 8)
	self.add_attribute(GLX.GLX_DOUBLEBUFFER, 1)
	self.add_attribute(GLX.GLX_DEPTH_SIZE, 24)

	xvinfo = GLX.glXChooseVisual(self.xdisplay, self.display.get_default_screen(), self.get_attributes())
	print("run glxinfo to match this visual id %s " % hex(xvinfo.contents.visualid))
	self.context = GLX.glXCreateContext(self.xdisplay, xvinfo, None, True)

    def add_attribute(self, setting, value):
	"""just to nicely add opengl parameters"""
	self.attrs.append(setting)
	self.attrs.append(value)

    def get_attributes(self):
	""" return our parameters in the expected structure"""
	attrs = self.attrs + [0, 0]
	return (c_int * len(attrs))(*attrs)

    def configure(self, wid):
	"""  """
	self.xwindow_id = GdkX11.X11Window.get_xid(wid)
	if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
	    print ('failed configuring context')
	glViewport(0, 0, self.width, self.height)

    def draw_start(self):
	"""make cairo context current for drawing"""
	if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
	    print ("failed to get the context for drawing")

    def draw_finish(self):
	"""swap buffer when we have finished drawing"""
	GLX.glXSwapBuffers(self.xdisplay, self.xwindow_id)

class scene:
    width, height = 600, 600

    rotationx = 0.0
    rotationy = 0.0

    rotation_incx = 0.5
    rotation_incy = 0.5
    radius = 0

    touch_count = 0
    touch_previous = 0, 0
    touch_start_one = 0, 0
    touch_start_two = 0, 0
    touch_end_one = 0, 0
    touch_end_two = 0, 0
    touch_time = 0

    camera_distance = 25

    cube_length = 1.0
    cube_size = cube_length / 2

    def __init__(self):
	"""setup everything in the correct order"""
	self.glwrap = gtkgl()
	self.setup_opengl()
	self.generate()
	self.gui()

	self.mode = 'vbo all cubes'

    def gui(self):
	"""load in the gui and connect the events and set our properties"""
	self.start_time = time.time()
	self.frame = 1
	xml = Gtk.Builder()
	xml.add_from_file('gui.glade')

	self.window = xml.get_object('window1')

	self.mode_widget = xml.get_object('cmbmode')
	self.mode_widget.connect('changed', self.change_mode)

	self.rotate_widget = xml.get_object('spinrotate')
	self.rotate_widget.connect('value-changed', self.change_rotate_speed)

	self.radius_widget = xml.get_object('spinradius')
	self.radius_widget.connect('value-changed', self.change_radius)

	self.color_widget = xml.get_object('btncolor')
	self.color_widget.connect('clicked', self.change_color)

	self.canvas_widget = xml.get_object('canvas')
	self.canvas_widget.connect('configure_event', self.on_configure_event)
	self.canvas_widget.connect('draw', self.on_draw)
	self.canvas_widget.set_double_buffered(False)
	self.canvas_widget.set_size_request(self.glwrap.width, self.glwrap.height)
	self.canvas_widget.add_events(Gdk.EventMask.TOUCH_MASK)
	self.canvas_widget.connect('touch-event', self.touched)

	self.window.show_all()
	GObject.idle_add(self.loop_draw)

    def touched(self, widget, ev):
	"""basic touch support, count the touches so we no how many fingers
	basic pinc zoom along the x, single finger slide to rotate"""
	if ev.get_source_device().get_source() == Gdk.InputSource.TOUCHSCREEN:
	    if ev.touch.type == Gdk.EventType.TOUCH_BEGIN:
		self.touch_start = ev.touch.x, ev.touch.y
		self.touch_count += 1
		if self.touch_count == 2:
		    self.touch_start_two = ev.touch.x, ev.touch.y
		    self.touch_previous =  ev.touch.x, ev.touch.y

	if ev.touch.type == Gdk.EventType.TOUCH_UPDATE:
	    if ev.touch.time - self.touch_time < 100:
		return True

	    if self.touch_count == 2:
		#basic pinch zoom along the x axis
		d1 = self.touch_previous[0] - ev.touch.x
		if d1 > 1:
		    self.camera_distance += self.camera_distance * 0.05
		    self.touch_previous =  ev.touch.x, ev.touch.y
		if d1 < 1:
		    self.camera_distance -= self.camera_distance * 0.05
		    self.touch_previous =  ev.touch.x, ev.touch.y
		self.update_camera()
	    self.touch_time = ev.touch.time

	if ev.touch.type == Gdk.EventType.TOUCH_END:
	    self.touch_end = ev.touch.x, ev.touch.y
	    #set rotation when touch ends
	    if self.touch_count == 1:
		self.rotation_incx = (self.touch_start[0] - self.touch_end[0]) * 0.01
		self.rotation_incy = (self.touch_start[1] - self.touch_end[1]) * 0.01
	    self.touch_count = 0

    def in_circle(self, center_x, center_y, center_z, radius, x, y, z):
	""" test if our cordinate lies inside our sphere"""
	square_dist = (center_x - x) ** 2 + (center_y - y) ** 2 + (center_z - z) ** 2
	return square_dist <= radius ** 2

    def change_color(self, widget):
	#regenerate the scene
	self.generate()

    def change_mode(self, widget):
	#change whats drawn and how
	self.mode = widget.get_active_text().lower()
	print(widget.get_active_text())

    def change_rotate_speed(self, widget):
	#handle spinner rotation speed event
	self.rotation_incx = widget.get_value()
	self.rotation_incy = widget.get_value()

    def change_radius(self, widget):
	#increase size of circle and number of polygons
	self.radius = int(widget.get_value())
	self.generate()

    def loop_draw(self):
	#send redraw event to drawing area widget
	self.canvas_widget.queue_draw()
	return True

    def on_configure_event(self, widget, event):
	"""if we recieve a configure event for example a resize, then grab the context settings and resize our scene """
	self.glwrap.width = widget.get_allocation().width
	self.glwrap.height = widget.get_allocation().height
	self.width, self.height = self.glwrap.width, self.glwrap.height

	#update our states because we have reconfigured the display
	self.glwrap.configure(widget.get_window())
	self.glwrap.draw_start()
	self.update_camera()
	self.setup_shaders()

	glEnable(GL_DEPTH_TEST)
	glDepthMask(GL_TRUE)
	glDepthFunc(GL_LEQUAL)
	glDepthRange(0.0, 1.0)
	glEnable(GL_CULL_FACE)
	glCullFace(GL_BACK)
	glFrontFace(GL_CW)

	self.glwrap.draw_finish()
	return True

    def on_draw(self, widget, context):
	"""if we recieve a draw event redraw our opengl scene"""
	self.elapsed_time = time.time() - self.start_time
	self.frame += 1

	if self.elapsed_time > 1:
	    print('fps %d' % self.frame)
	    self.start_time = time.time()
	    self.frame = 1
	self.glwrap.draw_start()
	self.draw()
	self.glwrap.draw_finish()

    def generate(self):
	self.cubes = []
	#position cubes inside a sphere radius
	for shift_x in range(-self.radius, self.radius + 1):
	    for shift_y in range(-self.radius, self.radius + 1):
		for shift_z in range(-self.radius, self.radius + 1):
		    x = shift_x * self.cube_length
		    y = shift_y * self.cube_length
		    z = shift_z * self.cube_length
		    if not self.in_circle(0, 0, 0, self.radius, x, y, z):
			continue
		    #random colours / textures if we want
		    color = random.choice([0.85, 0.15]), random.choice([0.85, 0.15]), random.choice([0.85, 0.15])
		    self.cubes.append(createcube((x, y, z), color, self.cube_size))

	self.test_cube = createcube((x, y, z), (random.choice([0.85, 0.15]), random.choice([0.85, 0.15]), random.choice([0.85, 0.15])), 6)
	faces = []
	for cube in self.cubes:
	    faces += cube.get_data()

	print('Generated %s Cubes' % str(len(self.cubes)))
	print('Generated %s Tringles' % str(len(faces) / 3))
	self.vbuffer = vbo.VBO(array(faces, 'f'))

    def setup_shaders(self):
	self.shader_program = shader()
	self.shader_program.compile()

    def setup_opengl(self):
	glShadeModel(GL_SMOOTH)
	glClearColor(0.0, 0.0, 0.0, 0.0)
	glClearDepth(1.0)
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
	glPolygonMode(GL_FRONT, GL_FILL)

    def update_camera(self):
	glViewport(0, 0, self.width, self.height)
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity()
	gluPerspective(45, 1.0 * self.width / self.height, 1.0, 80.0)
	gluLookAt(self.camera_distance, self.camera_distance, self.camera_distance,     # location
		  0.0, 0.0, 0.0,        # lookat
		  0.0, 1.0, 0.0)        # up direction
	glMatrixMode(GL_MODELVIEW)
	glLoadIdentity()

    def draw_test(self):
	#lets do a simple rotation so we can see the objects are 3d
	glRotatef(self.rotationx, 1.0, 0.0, 0.0)
	self.rotationx += self.rotation_incx
	glRotatef(self.rotationy, 0.0, 1.0, 0.0)
	self.rotationy += self.rotation_incy

	#use our shader program and enable vertex loading 
	glUseProgram(self.shader_program.program)
	glEnableClientState(GL_VERTEX_ARRAY)
	glEnableClientState(GL_COLOR_ARRAY)

	#render the triangles into a  virtual buffer object
	self.test_cube.bind()
	glVertexPointer(3, GL_FLOAT, 24, self.test_cube.vbuffer)
	glColorPointer(3, GL_FLOAT, 24, self.test_cube.vbuffer + 12)
	glDrawArrays(GL_TRIANGLES, 0, self.test_cube.vbuffer_size)
	self.test_cube.unbind()

	#restore opengl to our previous state
	glDisableClientState(GL_COLOR_ARRAY)
	glDisableClientState(GL_VERTEX_ARRAY)
	shaders.glUseProgram(0)

    def draw_vbo_per_cube(self):
	#lets do a simple rotation so we can see the objects are 3d
	glRotatef(self.rotationx, 1.0, 0.0, 0.0)
	self.rotationx += self.rotation_incx
	glRotatef(self.rotationy, 0.0, 1.0, 0.0)
	self.rotationy += self.rotation_incy

	#  use our shader program and enable vertex loading 
	glUseProgram(self.shader_program.program)
	glEnableClientState(GL_VERTEX_ARRAY)
	glEnableClientState(GL_COLOR_ARRAY)

	#  render the triangles into a  virtual buffer object
	for shape in self.cubes:
	    shape.bind()
	    glVertexPointer(3, GL_FLOAT, 24, shape.vbuffer)
	    glColorPointer(3, GL_FLOAT, 24, shape.vbuffer + 12)
	    glDrawArrays(GL_TRIANGLES, 0, shape.vbuffer_size)
	    shape.unbind()

	#restore opengl to our previous state
	glDisableClientState(GL_COLOR_ARRAY)
	glDisableClientState(GL_VERTEX_ARRAY)
	shaders.glUseProgram(0)

    def draw_vbo_all_cubes(self):
	#lets do a simple rotation so we can see the objects are 3d
	glRotatef(self.rotationx, 1.0, 0.0, 0.0)
	self.rotationx += self.rotation_incx
	glRotatef(self.rotationy, 0.0, 1.0, 0.0)
	self.rotationy += self.rotation_incy

	#  use our shader program and enable vertex loading 
	glUseProgram(self.shader_program.program)
	glEnableClientState(GL_VERTEX_ARRAY)
	glEnableClientState(GL_COLOR_ARRAY)

	#  render the triangles into a  virtual buffer object
	self.vbuffer.bind()
	glVertexPointer(3, GL_FLOAT, 24, self.vbuffer)
	glColorPointer(3, GL_FLOAT, 24, self.vbuffer + 12)
	glDrawArrays(GL_TRIANGLES, 0, len(self.vbuffer))
	self.vbuffer.unbind()

	#restore opengl to our previous state
	glDisableClientState(GL_COLOR_ARRAY)
	glDisableClientState(GL_VERTEX_ARRAY)
	shaders.glUseProgram(0)

    def draw(self):
	glEnable(GL_DEPTH_TEST)
	glClearDepth(1.0)
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
	glLoadIdentity()
	if self.mode == 'vbo test cube':
	    self.draw_test()
	if self.mode == 'vbo per cube':
	    self.draw_vbo_per_cube()
	if self.mode == 'vbo all cubes':
	    self.draw_vbo_all_cubes()


if __name__ == '__main__': 
    glexample = scene()    
    GLib.threads_init()
    Gdk.threads_init()
    Gdk.threads_enter()
    Gtk.main()
    Gdk.threads_leave()

The file below is a simple helper to reduce the size of the files, it contains the shader and a point and cube class used in the main program above.

import numpy
from numpy import array
from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays import vbo

from OpenGL._bytes import bytes, _NULL_8_BYTE

def compileShader( source, shaderType ):

    """Compile shader source of given type
	source -- GLSL source-code for the shader
    shaderType -- GLenum GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, etc,
	returns GLuint compiled shader reference
    raises RuntimeError when a compilation failure occurs
    """
    if isinstance(source, str):

	source = [source]
    elif isinstance(source, bytes):

	source = [source.decode('utf-8')]

    shader = glCreateShader(shaderType)
    glShaderSource(shader, source)
    glCompileShader(shader)
    result = glGetShaderiv(shader, GL_COMPILE_STATUS)

    if not(result):
	# TODO: this will be wrong if the user has
	# disabled traditional unpacking array support.
	raise RuntimeError(
	    """Shader compile failure (%s): %s"""%(
		result,
		glGetShaderInfoLog( shader ),
	    ),
	    source,
	    shaderType,
	)
    return shader


class shader:
    vertex = """#version 120
	//attributes in values
	attribute vec3 inPosition;
	attribute vec3 inColor;

	//varying sending to fragment shader
	varying vec4 outColor;
	void main(){
	    vec4 fragmentPos = gl_ModelViewMatrix * gl_Vertex;
	    gl_Position = (gl_ProjectionMatrix * fragmentPos);
	    outColor = vec4(gl_Color.rgb, 1);
	}"""

    fragment = """#version 120
	varying vec4 outColor;
	void main(){
	    gl_FragColor = outColor;
	}"""

    program = None
    def compile(self):
	self.program = shaders.compileProgram(
	    compileShader(self.vertex, GL_VERTEX_SHADER),
	    compileShader(self.fragment, GL_FRAGMENT_SHADER),)

class point:
    __slots__ = ['x', 'y', 'z', 'xyz', 'vertex']

    def __init__(self, p, c=(1, 0, 0)):
	""" Position in 3d space as a tuple or list, and colour in tuple or list format"""
	self.x, self.y, self.z = p
	self.vertex = array([self.x, self.y, self.z, c[0], c[1], c[2]], 'f')


class cube:
    def __init__(self, p1, color, size=0.5):
	self.color = array([1, 0, 0], 'f')
	#self.xyz = p1
	self.points = (
	    point((p1[0] - size, p1[1] + size, p1[2] - size), (color)),
	    point((p1[0] - size, p1[1] + size, p1[2] + size), (color)), 
	    point((p1[0] + size, p1[1] + size, p1[2] + size), (color)),
	    point((p1[0] + size, p1[1] + size, p1[2] - size), (color)),

	    point((p1[0] - size, p1[1] - size, p1[2] - size), (color)),
	    point((p1[0] - size, p1[1] - size, p1[2] + size), (color)), 
	    point((p1[0] + size, p1[1] - size, p1[2] + size), (color)),
	    point((p1[0] + size, p1[1] - size, p1[2] - size), (color)),

	    )

	self.vbuffer = vbo.VBO(array(self.get_data(), 'f'))
	self.vbuffer_size = len(self.get_data())

    def bind(self):
	self.vbuffer.bind()

    def unbind(self):
	self.vbuffer.unbind()

    def get_data(self):
	return (
	    self.points[0].vertex, self.points[2].vertex, self.points[1].vertex, 
	    self.points[0].vertex, self.points[3].vertex, self.points[2].vertex, 

	    self.points[0].vertex, self.points[1].vertex, self.points[5].vertex, 
	    self.points[0].vertex, self.points[5].vertex, self.points[4].vertex,

	    self.points[0].vertex, self.points[7].vertex, self.points[3].vertex, 
	    self.points[0].vertex, self.points[4].vertex, self.points[7].vertex,

	    self.points[6].vertex, self.points[2].vertex, self.points[3].vertex, 
	    self.points[6].vertex, self.points[3].vertex, self.points[7].vertex, 

	    self.points[6].vertex, self.points[1].vertex, self.points[2].vertex,
	    self.points[6].vertex, self.points[5].vertex, self.points[1].vertex,

	    self.points[6].vertex, self.points[4].vertex, self.points[5].vertex,
	    self.points[6].vertex, self.points[7].vertex, self.points[4].vertex,
	)



class cube:
    def __init__(self, p1, color, size=0.5):
	self.color = array([1, 0, 0], 'f')
	#self.xyz = p1
	self.points = (
	    point((p1[0] - size, p1[1] + size, p1[2] - size), (color)),
	    point((p1[0] - size, p1[1] + size, p1[2] + size), (color)), 
	    point((p1[0] + size, p1[1] + size, p1[2] + size), (color)),
	    point((p1[0] + size, p1[1] + size, p1[2] - size), (color)),

	    point((p1[0] - size, p1[1] - size, p1[2] - size), (color)),
	    point((p1[0] - size, p1[1] - size, p1[2] + size), (color)), 
	    point((p1[0] + size, p1[1] - size, p1[2] + size), (color)),
	    point((p1[0] + size, p1[1] - size, p1[2] - size), (color)),

	    )

	self.vbuffer = vbo.VBO(array(self.get_data(), 'f'))
	self.vbuffer_size = len(self.get_data())

    def bind(self):
	self.vbuffer.bind()

    def unbind(self):
	self.vbuffer.unbind()

    def get_data(self):
	return (
	    self.points[0].vertex, self.points[2].vertex, self.points[1].vertex, 
	    self.points[0].vertex, self.points[3].vertex, self.points[2].vertex, 

	    self.points[0].vertex, self.points[1].vertex, self.points[5].vertex, 
	    self.points[0].vertex, self.points[5].vertex, self.points[4].vertex,

	    self.points[0].vertex, self.points[7].vertex, self.points[3].vertex, 
	    self.points[0].vertex, self.points[4].vertex, self.points[7].vertex,

	    self.points[6].vertex, self.points[2].vertex, self.points[3].vertex, 
	    self.points[6].vertex, self.points[3].vertex, self.points[7].vertex, 

	    self.points[6].vertex, self.points[1].vertex, self.points[2].vertex,
	    self.points[6].vertex, self.points[5].vertex, self.points[1].vertex,

	    self.points[6].vertex, self.points[4].vertex, self.points[5].vertex,
	    self.points[6].vertex, self.points[7].vertex, self.points[4].vertex,
	)

GTK-3 Touchscreen events


tut14-touchscreen-events.png

Figure 1: File downloader

The example below demonstrates filtering events for a specific device, in this case a touch screen we then capture touch begin update and end events.

To my knowledge gestures are not supports so you will need to count the begins to determine the number of fingers and handle the updates to determine whats happening.

#!/usr/bin/env python
import sys
from gi.repository import Gtk, GdkX11, Gdk


class gui():

    def __init__(self):
	self.touch_count = 0
	self.window = Gtk.Window()
	self.window.realize()
	self.window.resize(300, 300)
	self.window.set_resizable(True)
	self.window.set_reallocate_redraws(True)
	self.window.set_title("GTK3 touch events")
	self.window.connect('delete_event', Gtk.main_quit)
	self.window.connect('destroy', lambda quit: Gtk.main_quit())

	self.drawing_area = Gtk.DrawingArea()
	#add the type of events we are interested in retrieving, skip this step and your events will never fire
	self.drawing_area.add_events(Gdk.EventMask.TOUCH_MASK)
	self.drawing_area.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
	self.drawing_area.connect('button_press_event', self.touched)
	self.drawing_area.connect('touch-event', self.touched)
	self.drawing_area.set_double_buffered(False)

	self.window.add(self.drawing_area)
	self.window.show_all()

    def touched(self, widget, ev):
	# detect the type of device we can filter events using this
	# if we dont a touch event and mouse event can be triggered from one touch for example
	if ev.get_source_device().get_source() == Gdk.InputSource.TOUCHPAD:
	    print('touchpad device')
	if ev.get_source_device().get_source() == Gdk.InputSource.MOUSE:
	    print('mouse device')
	if ev.get_source_device().get_source() == Gdk.InputSource.TOUCHSCREEN:
	    print('touchscreen device')

	    #from what i can tell there is no support for gestures so you would need another library
	    #or to handle this yourself
	    if ev.touch.type == Gdk.EventType.TOUCH_BEGIN:
		self.touch_count += 1
		print('start %d %s %s' % (self.touch_count, ev.touch.x,ev.touch.y))
	    if ev.touch.type == Gdk.EventType.TOUCH_UPDATE:
		print('UPDATE %d %s %s' % (self.touch_count, ev.touch.x,ev.touch.y))
	    if ev.touch.type == Gdk.EventType.TOUCH_END:
		self.touch_count -= 1
		print('end %d %s %s' % (self.touch_count, ev.touch.x,ev.touch.y))
	    if ev.touch.type == Gdk.EventType.TOUCH_CANCEL:
		self.touch_count -= 1
		print('cancelled')

def main():
    g = gui()
    Gtk.main()

if __name__ == '__main__':
    main()

GTK-3 Example downloader using a listbox


tut12-listbox.png

Figure 1: File downloader

A slightly more complex example, loading our gui from a glade file and dynamically add and remove widgets based on an xml file.

This is the start of a simple file downloader, it reads an xml file and creates a gui dynamically with download buttons for each element in the xml data to retrieve files.

#!/usr/bin/env python
import os
import requests
from io import StringIO, BytesIO
import subprocess
from lxml.html import parse
from gi.repository import Gtk, GLib, Gdk, GdkPixbuf


class application_gui:
    """Tutorial 13 custom treeview list boxes"""
    count = 0

    retrieve_job = None

    def __init__(self):
	#load in our glade interface
	xml = Gtk.Builder()
	xml.add_from_file('tut13.glade')

	#grab our widget using get_object this is the name of the widget from glade, window1 is the default name
	self.window = xml.get_object('winFetcher')

	#load our widgets from the glade file
	self.widgets = {}
	self.widgets['listbox'] = xml.get_object('listbox1')
	self.widgets['progress'] = xml.get_object('listProgress')
	self.widgets['refresh'] = xml.get_object('btnRefresh')
	self.widgets['refresh'].connect('button_press_event', self.refresh)
	self.widgets['close'] = xml.get_object('btnClose')
	self.widgets['close'].connect('button_press_event', self.closeFetcher)

	#wrap the listbox so we can reuse the code, pass in the listbox widget to our wrapper class
	self.listbox = ListBoxSelect(self.widgets['listbox'])

	#connect to events, in this instance just quit our application
	self.window.connect('delete_event', Gtk.main_quit)
	self.window.connect('destroy', lambda quit: Gtk.main_quit())

	#show the window else there is nothing to see :)
	self.openFetcher()
	self.refresh()

    def openFetcher(self):
	self.window.show_all()

    def refresh(self, *args):
	""" get a new xml and start the progress bar"""
	self.listbox.clear()
	self.widgets['progress'].show()
	self.retrieve_job = subprocess.Popen(
	    ['curl', 'file://%s/example.xml' % os.path.abspath('./')],
	    shell=False,
	    stdout=subprocess.PIPE,
	    stderr=subprocess.PIPE)
	GLib.timeout_add_seconds(1, self.update_active_progress_bar)

    def update_active_progress_bar(self):
	""" move the progress bar, when the subprocess returns handle the xml and hide the progress bar"""
	self.widgets['progress'].pulse()
	if self.retrieve_job.poll():
	    return True
	self.widgets['progress'].hide()
	self.update_list()
	return False

    def update_list(self):
	""" parse the xmland grab the elements we are intrested in"""
	nsmap = {'media': 'http://search.yahoo.com/mrss/'}
	results = BytesIO(self.retrieve_job.communicate()[0])
	doc = parse(results).getroot()
	for item in doc.iterfind(".//item", namespaces=nsmap):
	    title = item.find('title').text
	    link = item.find('link').tail
	    description = item.find('description').text
	    image = item.find('thumbnail', namespaces=nsmap).get('url')
	    self.listbox.model_append(image, title, description, link)

    def download(self):
	""" retrieve the xml file in a subprocess using curl """
	self.retrieve_job = subprocess.Popen(
	    ['curl', 'file://%s/example.xml' % os.path.abspath('./')],
	    shell=False,
	    stdout=subprocess.PIPE,
	    stderr=subprocess.PIPE)

    def closeFetcher(self, widget):
	self.window.hide()

class ListBoxSelect:
    """ handle the listbox rows dynamically add and remove widgets, and handle download. """
    listbox = None
    gui_rows = []  # store widgets here so we can destroy them later.

    def __init__(self, listbox):
	""" pass in list box to manage and connect event"""
	self.listbox = listbox
	self.listbox.connect('row-activated', self.selection)

    def selection(self, lbox, lbrow):
	""" row selected we may want to react here"""
	boxrow = lbrow.get_children()[0]
	boxinfo = boxrow.get_children()[1]
	print(boxinfo.get_children()[1].get_text())

    def model_append(self, image, title, description, link):
	""" create new widgets, and connect events for our new row"""
	items = {}
	items['row'] = Gtk.ListBoxRow()
	items['vbox'] = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
	items['label1'] = Gtk.Label(title, xalign=0)
	items['label2'] = Gtk.Label(link, xalign=0)
	items['progress'] = Gtk.ProgressBar()
	items['progress'].hide()
	items['progress'].set_fraction(0) 
	items['vbox'].pack_start(items['label1'], True, False, 0)
	items['vbox'].pack_start(items['label2'], True, False, 0)
	items['vbox'].pack_start(items['progress'], False, False, 0)

	items['hbox'] = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
	items['image'] = Gtk.Image.new_from_file(image)
	items['button'] = Gtk.Button(label="Download")
	items['button'].connect('button_press_event', self.download, items, link)

	items['hbox'].pack_start(items['image'], False, False, 0)
	items['hbox'].pack_start(items['vbox'], True, True, 0)
	items['button_box'] = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)

	items['button_box'].pack_end(items['button'], False, False, 0)
	items['hbox'].pack_start(items['button_box'], False, False, 0)
	items['row'].add(items['hbox'])

	self.listbox.add(items['row'])
	items['row'].show_all()

	self.gui_rows.append(items)

    def download(self, widget, args, items, link):
	""" download button click, change widgets and start the progress bar and download """
	items['button'].hide()
	items['job'] = subprocess.Popen(
	    ['curl', '-O', link],
	    shell=False,
	    stdout=subprocess.PIPE,
	    stderr=subprocess.PIPE)

	GLib.timeout_add_seconds(1, self.update_active_progress_bar, items)

    def update_active_progress_bar(self, widgets):
	""" update progress bar until command finished """
	widgets['progress'].pulse()
	if widgets['job'].poll():
	    return True
	widgets['progress'].hide()
	return False

    def clear(self):
	""" remove all rows so we can pre-populate"""
	for item in self.gui_rows:
	    item['row'].destroy()
	self.gui_rows = []

application = application_gui()
Gtk.main()

GTK-3 OpenGL inside a drawing area


tut12-mixing-opengl-with-gtk.png

Figure 1: Textview widget

Example of rendering opengl inside a gtk drawing area. I have seperated out the opengl code from the main gtk widget this makes it simpler to use in your own applications.

The example below will draw a basic opengl triangle so you know everything is setup and working, it also attaches to the drawing area resize events so you can resize the window.

#!/usr/bin/env python
import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL import GLX
from OpenGL.raw._GLX import struct__XDisplay
from OpenGL import GL
from ctypes import *

import Xlib
from Xlib.display import Display
from gi.repository import Gtk, GdkX11, Gdk


class gtkgl:
    """ these method do not seem to exist in python x11 library lets exploit the c methods """
    xlib = cdll.LoadLibrary('libX11.so')
    xlib.XOpenDisplay.argtypes = [c_char_p]
    xlib.XOpenDisplay.restype = POINTER(struct__XDisplay)
    xdisplay = xlib.XOpenDisplay(None)
    display = Xlib.display.Display()
    attrs = []

    xwindow_id = None
    width = height = 200

    def __init__(self):
	""" lets setup are opengl settings and create the context for our window """
	self.add_attribute(GLX.GLX_RGBA, True)
	self.add_attribute(GLX.GLX_RED_SIZE, 1)
	self.add_attribute(GLX.GLX_GREEN_SIZE, 1)
	self.add_attribute(GLX.GLX_BLUE_SIZE, 1)
	self.add_attribute(GLX.GLX_DOUBLEBUFFER, 0)

	xvinfo = GLX.glXChooseVisual(self.xdisplay, self.display.get_default_screen(), self.get_attributes())
	configs = GLX.glXChooseFBConfig(self.xdisplay, 0, None, byref(c_int()))
	self.context = GLX.glXCreateContext(self.xdisplay, xvinfo, None, True)

    def add_attribute(self, setting, value):
	"""just to nicely add opengl parameters"""
	self.attrs.append(setting)
	self.attrs.append(value)

    def get_attributes(self):
	""" return our parameters in the expected structure"""
	attrs = self.attrs + [0, 0]
	return (c_int * len(attrs))(*attrs)

    def configure(self, wid):
	"""  """
	self.xwindow_id = GdkX11.X11Window.get_xid(wid)
	if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
	    print ('failed configuring context')
	glViewport(0, 0, self.width, self.height)

    def draw_start(self):
	"""make cairo context current for drawing"""
	if(not GLX.glXMakeCurrent(self.xdisplay, self.xwindow_id, self.context)):
	    print ("failed to get the context for drawing")

    def draw_finish(self):
	"""swap buffer when we have finished drawing"""
	GLX.glXSwapBuffers(self.xdisplay, self.xwindow_id)

    def test(self):
	"""Test method to draw something so we can make sure opengl is working and we can see something"""
	self.draw_start()

	glClearColor(0.0, 0.0, 0.0, 0.0)
	glClear(GL_COLOR_BUFFER_BIT)
	glBegin(GL_TRIANGLES)
	glIndexi(0)
	glColor3f(1.0, 0.0, 0.0)
	glVertex2i(0, 1)
	glIndexi(0)
	glColor3f(0.0, 1.0, 0.0)
	glVertex2i(-1, -1)
	glIndexi(0)
	glColor3f(0.0, 0.0, 1.0)
	glVertex2i(1, -1)
	glEnd()

	self.draw_finish()


class gui():
    glwrap = gtkgl()

    def __init__(self):
	""" create a gtk window, attach a drawing area and connect the relevant events"""

	self.window = Gtk.Window()
	self.window.realize()
	self.window.resize(self.glwrap.width, self.glwrap.height)
	self.window.set_resizable(True)
	self.window.set_reallocate_redraws(True)
	self.window.set_title("GTK3 with opengl")
	self.window.connect('delete_event', Gtk.main_quit)
	self.window.connect('destroy', lambda quit: Gtk.main_quit())

	self.drawing_area = Gtk.DrawingArea()
	self.drawing_area.connect('configure_event', self.on_configure_event)
	self.drawing_area.connect('draw', self.on_draw)
	self.drawing_area.set_double_buffered(False)
	self.drawing_area.set_size_request(self.glwrap.width, self.glwrap.height)

	self.window.add(self.drawing_area)
	self.window.show_all()

    def on_configure_event(self, widget, event):
	"""if we recieve a configure event for example a resize, then grab the context settings and resize our scene """
	self.glwrap.configure(widget.get_window())
	self.glwrap.test()
	return True

    def on_draw(self, widget, context):
	"""if we recieve a draw event redraw our test triangle"""
	self.glwrap.test()


def main():
    g = gui()
    Gtk.main()

if __name__ == '__main__':
    main()

GTK-3 Textview interactions


tut11-interactive-python-textview.png

Figure 1: Textview widget

This example shows using the text view widget to get text entered by a use, it makes use of marked text blocks so that only some text can be edited and will dynamically read the current line and run it in python interactively.

You can type app.test() or app.showdrawing(True) as examples of the interaction between the widget and python.

#!/usr/bin/env python
import sys
from StringIO import StringIO
from gi.repository import Gtk, Gdk
import code
import math


class interactiveGtk:

    def __init__(self):
	window = Gtk.Window()
	window.set_default_size(380, 300)
	window.connect("destroy", lambda w: Gtk.main_quit())

	box = Gtk.VBox()

	self.drawingarea = Gtk.DrawingArea()

	#python console using a textview 
	console = interactive_console(Gtk.TextView())
	self.drawingarea.connect("draw", self.area_expose_cb)

	box.add(self.drawingarea)
	box.add(console.textarea)

	window.add(box)
	window.show_all()

	self.drawarea = False

    def show_drawing(self, state):
	"""self.show_drawing(True) to enable showing the lines"""
	self.drawarea = state

    def test(self):
	"""run app.test() when program is running to print this message"""
	print ('hello world')

    def area_expose_cb(self, widget, context):
	"""expose event lets draw, lines will only display if we run self.show_lines first.
	demonstrating running state change of our program"""
	self.style = self.drawingarea.get_style()
	if self.drawarea is True:
	    self.drawing(context, 210, 10)

    def drawing(self, cr, x, y):
	""" draw a circle in the drawing area """
	cr.set_line_width(10)
	cr.set_source_rgb(0.5, 0.8, 0.0)

	cr.translate(20 / 2, 20 / 2)
	cr.arc(50, 50, 50, 0, 2 * math.pi)
	cr.stroke_preserve()

	cr.set_source_rgb(0.3, 0.4, 0.4)
	cr.fill()


class interactive_console:
    editor_chars = '>>>'
    editor_chars_other = '...'
    editor_history = []
    editor_history_position = 0

    def __init__(self, textview):
	#the python editor window
	self.textarea = textview
	self.textarea.connect('key-press-event', self.key_pressed)
	self.console_buffer = self.textarea.get_buffer()

	#setup some characters which can not be changed
	self.console_buffer.set_text(self.editor_chars + 'app.show_drawing(True)')
	self.console_buffer.create_tag("uneditable", editable=False, editable_set=True)
	self.console_buffer.create_mark("input_position", self.console_buffer.get_end_iter(), True)
	self.console_buffer.create_mark("end_position", self.console_buffer.get_end_iter(), False)
	self.console_buffer.apply_tag_by_name("uneditable", self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_offset(len(self.editor_chars)))

	#interactive mode interpreter,
	#pass locals() or globals() so we can access our programs functions and variables
	#using global here so we have access to the app object in the global scope
	self.interpreter = code.InteractiveInterpreter(globals())

    def key_pressed(self, widget, event):
	"""
	    grab key presses, run code from textview on return
	    navigate history if arrow keys are pressed
	"""
	if event.keyval == Gdk.keyval_from_name('Return'):
	    self.execute_line()
	    return True

	if event.keyval == Gdk.keyval_from_name('Up'):
	    self.console_history(-1)
	    return True

	if event.keyval == Gdk.keyval_from_name('Down'):
	    self.console_history(1)
	    return True
	return False

    def execute_line(self):
	"""
	    carriage return was captured so lets process the textview contents for code to run
	"""

	text = self.console_buffer.get_text(self.console_buffer.get_start_iter(), self.console_buffer.get_end_iter(), False)
	source = ''
	block = False
	indent = 0
	#work out code to run if its not a block or a blank line then run what we have,
	#if its a block of code like a for loop or if condition dont run it yet unless the block has finished.
	last_line = ''
	for line in text.split("\n"):
	    line = line[3:].strip('')
	    if line.strip() == '':
		block = False
		indent = 0
	    else:
		if line.endswith(':'):
		    if block is True:
			source += line + "\n\t"
		    else:
			source = line + "\n\t"
		    block = True
		    indent += 1
		else:
		    if line.startswith("\t"):
			source += line + "\n"
		    else:
			block = False
			source = line + "\n"
			indent = 0
		last_line = line
	if last_line.strip() != '':
	    self.append_history(last_line)
	chars = self.editor_chars


	# run the code grabbed from the text buffer and execute it if its not a block
	results = ''
	if block is True:
	    chars = self.editor_chars_other
	else:
	    chars = self.editor_chars
	    results = self.execute_code(source)

	#build text for the editor, and workout which part of the text should be locked
	text_output = text + '\n' + results + chars
	text_output_length = len(text_output)
	if block is True:
	    text_output += "\t" * indent
	self.update_editor(text_output, text_output_length)

    def execute_code(self, source):
	"""
	    run any code sent here and capture output and return the results
	"""
	 # capture output from stdio so we can display the errors in our editor
	result = StringIO()
	sys.stdout = result
	sys.stderr = result
	#run code output will be put into result because we are capturing stdout 
	self.interpreter.runsource(source, "<<console>>")
	# restore stdout so future output is capture to the terminal again
	sys.stdout = sys.__stdout__
	sys.stdout = sys.__stderr__
	return result.getvalue()

    def update_editor(self, text, uneditable_end=0):
	"""
	    pass in text to put in the editor and portion to be locked from editing
	"""
	self.console_buffer.set_text(text)
	self.console_buffer.create_mark("input_position", self.console_buffer.get_end_iter(), True)
	self.console_buffer.create_mark("end_position", self.console_buffer.get_end_iter(), False)
	self.console_buffer.apply_tag_by_name("uneditable", self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_offset(uneditable_end))

    def append_history(self, line):
	"""
	Store command in history drop command if limit has been reached
	"""
	if len(self.editor_history) > 10:
	    self.editor_history.pop()
	self.editor_history.append(line)
	self.editor_history_position = len(self.editor_history)

    def console_history(self, direction=-1):
	""" 
	drop any text on last line and insert text from history based on position
	"""
	# get current text excluding last line as we will update from our history
	text = self.console_buffer.get_text(
	    self.console_buffer.get_start_iter(),
	    self.console_buffer.get_iter_at_line_index(
		self.console_buffer.get_line_count(), 0),
	    False)

	#work out position in history and what should be displayed
	linenumber = self.editor_history_position + direction 
	if linenumber >= 0 and linenumber < len(self.editor_history):
	    self.editor_history_position += direction 
	    self.console_buffer.set_text(text + self.editor_chars + self.editor_history[self.editor_history_position])


app = interactiveGtk()
Gtk.main()

GTK-3 Treeview in treestore mode


tut10-treeview-treestore.png

Figure 1: Treeview in tree mode

The code below will populate the treeview widget with a tree like structure which you can expand and collapse, double clicking will retrieve the selected node in the tree.

#!/usr/bin/env python
from gi.repository import Gtk, GLib


class application_gui:
    """Tutorial 10 text input, display a treeview in expandable tree format."""
    count = 0

    def __init__(self):
	#load in our glade interface
	xml = Gtk.Builder()
	xml.add_from_file('tut10.glade')

	#grab our widget using get_object this is the name of the widget from glade, window1 is the default name
	self.window = xml.get_object('window1')
	self.text = xml.get_object('entry1')

	#load our widgets from the glade file
	self.widgets = {}
	self.widgets['treeview'] = xml.get_object('treeview1')
	treeview(self.widgets['treeview'], self.text)

	#connect to events, in this instance just quit our application
	self.window.connect('delete_event', Gtk.main_quit)
	self.window.connect('destroy', lambda quit: Gtk.main_quit())

	#show the window else there is nothing to see :)
	self.window.show()


class treeview:
    treeview = None
    treemodel = None

    selected = 'workspace'

    def __init__(self, treeview, entry):
	self.entry = entry
	self.treeview = treeview

	self.treeview.connect('row-activated', self.selection)
	self.treeview.connect('button_press_event', self.mouse_click)

	#create a storage model in this case a treemodel
	self.treemodel = Gtk.TreeStore(str)
	self.treeview.set_model(self.treemodel)

	#add columns usually only one in case of the treeview
	column = Gtk.TreeViewColumn("Objects")
	self.treeview.append_column(column)

	#add in a text renderer so we can see the items we add 
	cell = Gtk.CellRendererText()
	column.pack_start(cell, False)
	column.add_attribute(cell, "text", 0)

	self.populate()
	self.menu()

    def populate(self):
	self.treemodel.clear()
	#populate the treeview with a largish tree
	for item1 in range(0, 5):
	    iter_level_1 = self.append_tree('Item ' + str(item1))
	    for item2 in range(0, 5):
		iter_level_2 = self.append_tree('Sub Item ' + str(item2), iter_level_1)
		for item3 in range(0, 5):
		    self.append_tree('Sub Sub Item ' + str(item3), iter_level_2)

    def append_tree(self, name, parent=None):
	"""
	    append to the treeview if parent is null append to root level.
	    if parent is a valid iter (possibly returned from previous append) then append under the parent
	"""
	myiter = self.treemodel.insert_after(parent, None)
	self.treemodel.set_value(myiter, 0, name)
	return myiter

    def menu(self):
	"""
	popover menu shown on right clicking a treeview item.
	"""
	self.treeview_menu = Gtk.Menu()
	for item in range(0, 5):
	    menu_item = Gtk.MenuItem("Menu " + str(item))
	    self.treeview_menu.append(menu_item)

    def mouse_click(self, tv, event):
	if event.button == 3:
	    # right mouse button pressed popup the menu
	    self.treeview_menu.show_all()
	    self.treeview_menu.popup(None, None, None, None, 1, 0)

    def selection(self, tv, treepath, tvcolumn):
	""" 
	    on double click get the value of the item we clicked
	"""
	model = tv.get_model()
	treeiter = model.get_iter(treepath)
	self.selected = model.get_value(treeiter, 0)
	self.entry.set_text(self.selected)



application = application_gui()
Gtk.main()