PyCEGUI

From CEGUI Wiki - Crazy Eddie's GUI System (Open Source)
Jump to: navigation, search

Written for CEGUI 0.7


Works with versions 0.7.x (obsolete)

Requires at least version
0.7.5

Since CEGUI 0.7.5 official python bindings are provided. The main reason for that is that the new CEGUI tools are written completely in python. As a nice side effect, everybody can use the bindings standalone in their python only apps as well as embedded python apps for scripting and what not.

Where to get the bindings?

We provide ready to go packages for win32 platform since it's fairly hard to get things going there. You can get them at the pypi repository. For other platforms, please just download the SDK and use bindings you find there. You can also use Win32 bindings from the SDK if you use embedded python, it can be easier than installing the aforementioned package.

Base app

I will provide a little base application below so you know the basics of how to use CEGUI in python environment. OpenGL is used in the base app.

Also, note that this script assumes that the default CEGUI resources (lua_scripts, schemes, xml_schemas, etc) are located in a directory named 'datafiles', which itself is located in the same directory as the script itself. That is to say, if the script path is '/home/foo/script.py', there needs to be a path '/home/foo/datafiles' which contains all the resources.

import os, sys
 
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
 
# you must have PyCEGUI python package installed (see pypi repository)
# or you must make it work yourself using binary bindings from SDK
import PyCEGUI
import PyCEGUIOpenGLRenderer
 
CEGUI_PATH = "./"
 
class BaseApp(object):
    def __init__(self):
        glutInit(sys.argv)
        glutInitDisplayMode(GLUT_DEPTH|GLUT_DOUBLE|GLUT_RGBA)
        glutInitWindowSize(1280, 1024)
        glutInitWindowPosition(100, 100)
        glutCreateWindow("Crazy Eddie's GUI Mk-2 - glut Base Application")
        glutSetCursor(GLUT_CURSOR_NONE)
 
        glutDisplayFunc(self.displayFunc)
        glutReshapeFunc(self.reshapeFunc)
        glutMouseFunc(self.mouseFunc)
        glutMotionFunc(self.mouseMotionFunc)
        glutPassiveMotionFunc(self.mouseMotionFunc)
 
        PyCEGUIOpenGLRenderer.OpenGLRenderer.bootstrapSystem()
 
    def __del__(self):
        PyCEGUIOpenGLRenderer.OpenGLRenderer.destroySystem()
 
    def initialiseResources(self):
        rp = PyCEGUI.System.getSingleton().getResourceProvider()
 
        rp.setResourceGroupDirectory("schemes", CEGUI_PATH + "datafiles/schemes")
        rp.setResourceGroupDirectory("imagesets", CEGUI_PATH + "datafiles/imagesets")
        rp.setResourceGroupDirectory("fonts", CEGUI_PATH + "datafiles/fonts")
        rp.setResourceGroupDirectory("layouts", CEGUI_PATH + "datafiles/layouts")
        rp.setResourceGroupDirectory("looknfeels", CEGUI_PATH + "datafiles/looknfeel")
        rp.setResourceGroupDirectory("schemas", CEGUI_PATH + "datafiles/xml_schemas")
 
        PyCEGUI.Imageset.setDefaultResourceGroup("imagesets")
        PyCEGUI.Font.setDefaultResourceGroup("fonts")
        PyCEGUI.Scheme.setDefaultResourceGroup("schemes")
        PyCEGUI.WidgetLookManager.setDefaultResourceGroup("looknfeels")
        PyCEGUI.WindowManager.setDefaultResourceGroup("layouts")
 
        parser = PyCEGUI.System.getSingleton().getXMLParser()
        if parser.isPropertyPresent("SchemaDefaultResourceGroup"):
            parser.setProperty("SchemaDefaultResourceGroup", "schemas")     
 
    def setupUI(self):
        PyCEGUI.SchemeManager.getSingleton().create("VanillaSkin.scheme")
        PyCEGUI.SchemeManager.getSingleton().create("TaharezLook.scheme")
        PyCEGUI.System.getSingleton().setDefaultMouseCursor("Vanilla-Images", "MouseArrow")
 
        root = PyCEGUI.WindowManager.getSingleton().loadWindowLayout("VanillaWindows.layout")
        PyCEGUI.System.getSingleton().setGUISheet(root)
 
        self.wnd = PyCEGUI.WindowManager.getSingleton().createWindow("TaharezLook/FrameWindow", "Demo Window")
        root.addChildWindow(self.wnd)
 
    def run(self):
        self.initialiseResources()
        self.setupUI()
 
        self.lastFrameTime = glutGet(GLUT_ELAPSED_TIME)
        self.updateFPS = 0
        glutMainLoop()
 
    def displayFunc(self):
        thisTime = glutGet(GLUT_ELAPSED_TIME)
        elapsed = (thisTime - self.lastFrameTime) / 1000.0
        self.lastFrameTime = thisTime
        self.updateFPS = self.updateFPS - elapsed
 
        PyCEGUI.System.getSingleton().injectTimePulse(elapsed)
 
        # do rendering for this frame.
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        PyCEGUI.System.getSingleton().renderGUI()
        glutPostRedisplay()
        glutSwapBuffers()
 
    def reshapeFunc(self, width, height):
        glViewport(0, 0, width, height)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(60.0, width / height, 1.0, 50.0)
        glMatrixMode(GL_MODELVIEW)
        PyCEGUI.System.getSingleton().notifyDisplaySizeChanged(PyCEGUI.Size(width, height))
 
    def mouseFunc(self, button, state, x, y):
        if button == GLUT_LEFT_BUTTON:
            if state == GLUT_UP:
                PyCEGUI.System.getSingleton().injectMouseButtonUp(PyCEGUI.LeftButton)
            else:
                PyCEGUI.System.getSingleton().injectMouseButtonDown(PyCEGUI.LeftButton)
 
        elif button == GLUT_RIGHT_BUTTON:
            if state == GLUT_UP:
                PyCEGUI.System.getSingleton().injectMouseButtonUp(PyCEGUI.RightButton)
            else:
                PyCEGUI.System.getSingleton().injectMouseButtonDown(PyCEGUI.RightButton)
 
    def mouseMotionFunc(self, x, y):
        PyCEGUI.System.getSingleton().injectMousePosition(x, y)
 
app = BaseApp()
app.run()
del app

I must say that this app isn't perfect or complete but it will get you going. You might want to make it exception safe and add keyboard injection. If you do that, please post it back here for others to benefit.

Documentation

Since the API is resembling the C++ version as closely as possible (mainly to avoid confusion and as easy switch between C++ and Python as possible), doxygen API docs apply for most of the classes. Doxygen comments are also extracted and added to __doc__ of all the python classes and methods, using python docstrings is therefore possible but not perfect.

Subscriptions

One major thing that isn't documented in doxygen or other wiki docs and that is important in python is how subscriptions work.

Subscribing to listenerClass:

wnd.subscribeEvent(PyCEGUI.Window.EventMouseEnters, listenerClass, "methodInThatClass")

Subscribing to a free function:

wnd.subscribeEvent(PyCEGUI.Window.EventMouseEnters, PythonModule.freeFunction)

Example

An example of this, using a single class for simplicity, might go as follows.

class SingleClassGUI:
 
    # Initialization things
    # - Create/set the GUISheet.
    def __init__(self):
        self.GUISheet = PyCEGUI.WindowManager.getSingleton().createWindow('DefaultWindow', 'root')
        PyCEGUI.System.getSingleton().setGUISheet(self.GUISheet)
        return
 
    # Setup a window with a button to demonstrate subscriptions
    def setupMenu(self):
        menu = PyCEGUI.WindowManager.getSingleton().loadWindowLayout('menu.layout') # See below for 'menu.layout'
 
        # This defines the widget we are accessing (in this case, the widget with path 'Menu/Button') ...
        # the event we are listening for (in this case, the clicking of a push button) ...
        # and the method we want to call when the aforementioned event takes place.
        #
        # Important note: the second argument to `subscribeEvent` is the class which should be used to find the
        # method, as indicated by the third argument. Because this is a single class example, we just use `self`.
        # If, however, this was a more practical example, a different class would be passed in, which would be
        # investigated for the proper method to call.
        menu.getChild('Menu/Button').subscribeEvent(PyCEGUI.PushButton.EventClicked, self, 'buttonClicked')
 
        # Or, it could be written like this, which is more verbose but functionally equivalent
        #button = menu.getChild('Menu/Button')
        #button.subscribeEvent(PyCEGUI.PushButton.EventClicked, self, 'buttonClicked')
 
        # Don't forget to do this
        self.GUISheet.addChildWindow(menu)
        return
 
    # Handler
    # - `args` is context sensitive; which is to say that its value depends on the type of event. See the EventArgs class for details.
    def buttonClicked(self, args):
        print('buttonClicked')
        return

This is the XML layout file which defines how the window frame will appear. There is no doubt more information than necessary here for the example, but the main points (from a coding point of view) to pay attention to are the properties which define the name of the menu, and the button.

<?xml version="1.0" encoding="UTF-8"?>
 
<GUILayout >
    <Window Type="TaharezLook/FrameWindow" Name="Menu" >
        <Property Name="Font" Value="DejaVuSans-10" />
        <Property Name="Text" Value="An example Menu" />
        <Property Name="TitlebarFont" Value="DejaVuSans-10" />
        <Property Name="RollUpEnabled" Value="False" />
        <Property Name="TitlebarEnabled" Value="True" />
        <Property Name="UnifiedAreaRect" Value="{{0.157031,0},{0.194911,0},{0.783984,0},{0.687913,0}}" />
        <Property Name="DragMovingEnabled" Value="False" />
        <Property Name="CloseButtonEnabled" Value="False" />
        <Property Name="EWSizingCursorImage" Value="set:Vanilla-Images image:MouseArrow" />
        <Property Name="InheritsTooltipText" Value="False" />
        <Property Name="NSSizingCursorImage" Value="set:Vanilla-Images image:MouseArrow" />
        <Property Name="NESWSizingCursorImage" Value="set:Vanilla-Images image:MouseArrow" />
        <Property Name="NWSESizingCursorImage" Value="set:Vanilla-Images image:MouseArrow" />
        <Window Type="TaharezLook/Button" Name="Menu/Button" >
            <Property Name="Text" Value="Button" />
            <Property Name="UnifiedAreaRect" Value="{{0.78567,0},{0.620646,0},{0.979596,0},{0.749355,0}}" />
        </Window>
    </Window>
</GUILayout>

The fourth line defines the type of window, and the name of it - this is the name used to access it (and its children) from Python.

Farther down (the next <Window></Window> block) is the line where the button is defined; first there is the type of button, and then the name of it - in this case, 'Menu/Button'; note that this is the name (or path, if you prefer) we use to access it from Python (see the SingleClassGUI.setupMenu method above).

On a final note, this example assumes that some renderer has been setup - via OpenGL, Ogre, etc. Out of the box, this example will not work, because [Py]CEGUI has no renderer setup; once a system has been bootstrapped, this example will display a frame with a button, which when clicked, will print 'buttonClicked' to the console.

Something akin to the following:

# Renderer setup goes here
pass
 
# Make it so.
gui = SingleClassGUI()
gui.setupMenu()
 
# Enter rendering loop goes here

Multi-class example

See here.

User Data

CEGUI has setUserData and getUserData methods in several classes. This is not exposed to Python at all. You can do something much much nicer and comfier in Python though:

a = PyCEGUI.ListboxTextItem("SSSS")
a.myUserData = "AAAAA" # myUserData can be anything, player, ship, whatever
print a.myUserData # prints AAAAA
 
# you can ofcourse have more of these user data custom attributes

These do NOT persist when you pass them to C++ and back, I have to figure out a way to make them persist :-/ --User:Kulik

Implementation

We use py++ and boost::python. It allows rapid development of the bindings and a very easy maintenance. It may be a bit slower than SWIG and definitely slower than hand written bindings but since both of these won't happen, please don't cry about that fact (unless you are willing to create and maintain those). The slowness is very unlikely to be noticeable at all unless you do synthetic tests.

Caveats

C++ allows you to shoot yourself in a lot of ways whilst python is very limited in this area. For example if you create a window, get it and store the reference in python variable, then destroy the window, the python variable will hold a dangling pointer. This is something you have to take care of unfortunately, there is no way for us to handle this unless we affect the C++ interface which would hinder performance for C++ users. It's not that hard to handle this and unless you do something really unusual, you probably aren't going to trigger it.

Listbox

This is of particular concern when populating a listbox widget from Python; consider the following code:

def someMethod(self):
    listBox = someWidget.getChild('aListBox')
    item = PyCEGUI.ListboxTextItem('Some text')
    listBox.addItem(item)
    return

This code will produce a segmentation fault because when the local variable `item` goes out of scope, Python will garbage collect it - more specifically, the listbox will reference a listbox item which no longer exists. A possible solution to this problem is to bind it to an object, such as the following:

def someMethod(self):
    listBox = someWidget.getChild('aListBox')
    self.item = PyCEGUI.ListboxTextItem('Some text')
    listBox.addItem(self.item)
    return

The key difference here is that the `ListboxTextItem` is bound to the instance of the class, thus the implication is that as long as the instance exists, so will the listbox item; they will both be destroyed when the instance in question goes out of scope.

This is a somewhat contrived example, and in all likelihood a practical implication of this would be more complicated.

Panda3D integration

morgul has integrated CEGUI with Panda via these bindings - Pure python Panda CEGUI integration