Multiprocessing Qt/PySide and Panda

Hi all,

I’m sure you’ve seen the Nemesis#13’s code for running WX and Panda together with multiprocessing (and if you haven’t, you should go check it out here: [Multiprocess wxPython + Panda3D)).

What Nemesis did was really cool (even tho it feels so simple and obvious now, doesn’t it?), but for some reason I really don’t like WX (don’t ask why, it might well be completely irrational :wink: ). Thus, I thought about doing the same with other libraries.

I started (successfully) with GTK, but it has the disadvantage of not being cross platform, or at least not making it easy since GTK3. So I decided to take Qt and PySide for a test drive with Nemesis’ ways and here’s the result for those of you who, like me, prefer Qt:

import sys
import platform

from PySide.QtCore import *
from PySide.QtGui import *

from multiprocessing import Process, Pipe

from panda3d.core import loadPrcFileData
from panda3d.core import WindowProperties
from direct.showbase.ShowBase import ShowBase

class FrontEnd(QWidget):
    def __init__(self, pipe):
        QWidget.__init__(self)
        
        self.viewportWidget = QWidget()
        self.testButton = QPushButton("Test layout")
        
        self.testLayout = QVBoxLayout()
        self.testLayout.addWidget(self.viewportWidget)
        self.testLayout.addWidget(self.testButton)
        
        self.setLayout(self.testLayout)
        
        self.pipe = pipe
        
        self.resize(800, 600)
        self.setWindowTitle('Simple')
        self.show()
    
    def getViewportID(self):
        wid = self.viewportWidget.winId()
        if platform.system() == "Windows":
            import ctypes
            ctypes.pythonapi.PyCObject_AsVoidPtr.restype = ctypes.c_void_p
            ctypes.pythonapi.PyCObject_AsVoidPtr.argtypes = [ ctypes.py_object ]
            wid = ctypes.pythonapi.PyCObject_AsVoidPtr(wid)
        return wid
    
    def resizeEvent(self, e):
        self.pipe.send(["resize", e.size().width(), e.size().height()])

class BackEnd(object):
    def __init__(self, viewportID, pipe):
        self.pipe = pipe
        
        loadPrcFileData("", "window-type none")
        
        ShowBase()
        wp = WindowProperties()
        wp.setOrigin(0, 0)
        wp.setSize(800, 600)
        wp.setParentWindow(viewportID)
        base.openDefaultWindow(props = wp, gsg = None)
        
        s = loader.loadModel("smiley.egg")
        s.reparentTo(render)
        s.setY(5)
        
        base.setFrameRateMeter(True)
        base.taskMgr.add(self.checkPipe, "check pipe")
        
        run()
    
    def closeCallback(self):
        sys.exit()
    
    def resizeCallback(self, viewportSizeH, viewportSizeV):
        wp = WindowProperties()
        wp.setOrigin(0, 0)
        wp.setSize(viewportSizeH, viewportSizeV)
        base.win.requestProperties(wp)
    
    def checkPipe(self, task):
        while self.pipe.poll():
            request = self.pipe.recv()
            method = request[0]
            args = request[1:]
            
            getattr(self, method + "Callback")(*args)
        
        return task.cont

class Controller(object):
    def __init__(self):
        self.pipe, remote_pipe = Pipe()
        
        self.app = QApplication([])
        
        self.frontEnd = FrontEnd(self.pipe)
        
        self.backEnd = Process(
            target = BackEnd,
            args = (self.frontEnd.getViewportID(), remote_pipe)
        )
        
        self.app.aboutToQuit.connect(self.onDestroy)
        
        self.backEnd.start()
        self.app.exec_()
    
    def onDestroy(self, *args):
        print "Destroy"
        self.pipe.send(["close"])
        self.backEnd.join(1)
        if self.backEnd.is_alive():
            self.backEnd.terminate()

if __name__ == "__main__":
    c = Controller()

I tested this on Linux (Ubuntu) and Windows. You will obviously need PySide to run the code.

Just a note for anyone who tries to do serious stuff with this – make sure you don’t abuse the pipe. Pushing 60 messages/second in there doesn’t work – I tried ;D. Use Tasks.

Thanks for the code! Especially for the windows specific window handle stuff, which I wouldn’t have figured out in ages. :smiley:

Did you further test Qt with Panda3D? WX is driving me crazy… I have to push quite some ridiculously low-level and non-obvious UI events through the pipe and my GUI has lots of bugs and glitches.

E.g. sliding the bar of a splitter or MDI when panda is rendering in a sub-panel results in flickering. Or while the Panda window is focused you simply can't invoke window menus or any shortcuts (called accelerators) from it, so things like alt+letter for invoking a menu won't work at all and invoking an accelerator like ctrl+s or ctrl+q requires you to send all keys from panda3d to wx and in wx you loop through the menus, looking for accelerators and calling them there.

Little things making a coder’s life hard :frowning:

I’d like to give Qt a chance now.
Do you by chance have some other examples? Any hints or the like?

Thanks in advance.

I wouldn’t have figured it out either. Uncle Google did :wink:.

Sorry, but I don’t have any other snippets at hand.

I can give you something I don’t really use, and thus it’s not bound with too much CopperEd-specific code. I wrote that out of curiosity after our conversation on IRC. The basic code to handle key events going from Panda to Qt – the callback calls an actual key event, same as directly pressing a key in Qt, so it’s a “just works” case.

Note that this code is a raw prototype and is not entirely correct, secure, complete, optimized or anything else :wink:. But last time I checked, it worked.

Obviously, this code, assuming it won’t eat anyone’s pets, is useful and I will eventually put it to use because, when inside Panda, you obviously can’t call Qt shortcuts directly as well – the keys are just stuck with Panda, so this is the only way to send them to Qt. But, compared to what you described on IRC regarding WX, it’s really easy with Qt.

Panda side:

base.buttonThrowers[0].node().setButtonDownEvent("button")

def keyboardEvents(self, keyname):
    self.pipeSend("keyEvent", keyname)    

Qt side:

def keyEventCallback(self, definition):
        definitionSplit = definition.split("-")
        
        try:
            modifiers = definitionSplit[:-1]
            key = definitionSplit[-1]
        except:
            modifiers = []
            key = definition
        
        blocked = ["lalt", "lcontrol", "lshift", "ralt", "rcontrol", "rshift"]
        if key in blocked:
            return
        
        if "mouse" in key:
            return
        
        print modifiers, key
        
        finalMod = Qt.NoModifier
        for modifier in modifiers:
            newMod = eval("Qt." + modifier.capitalize() + "Modifier")
            finalMod |= newMod
        
        finalKey = eval("Qt.Key_" + key.capitalize())
        
        print finalMod, finalKey
        
        e = QKeyEvent(QEvent.KeyPress, finalKey, finalMod)
        self.app.postEvent(self, e)

Other than that, just ask away.

Thank you. I’ll investigate the whole thing during the next few weeks or months.
The problem with wx is that firing key events from code simply wasn’t considered. In the newest developers version they added an UI Action Simulator, which tries to fill that gap, but it turned out it isn’t usable for anything more than filling an input field.

I’m curious about how well Panda3D works in an MDI environment in Qt. (MDI means multiple windows in one - think of photoshop or older office programs.)