Panda in PyQt

Thanks - this is great!

I’ve been using your hints to create a Panda+PyQt-based viewer for a non-standard 3d geometry file format. However, I’m facing the problem of not being able to control the scene with mouse and keyboard, in Windows, as reported by others.

Has anyone of you ever wondered some workaround for it? I was wondering about intercepting mouse and keboard controls over the viewer widget in Qt’s main loop and then passing them to Panda… what do you think about it? Does Panda provide some way to do this?

Dunno if this is related, but a few weeks ago someone drwr a fix to Panda CVS related to not being able to receive events correctly in embedded windows or so. So maybe it’s a now-fixed Panda bug…

Yes, it’s fixed in Panda CVS now. A workaround until then is to use the Python win32 or ctypes module to call SetFocus(window) whenever you get a mouse event in the Panda window. (The problem is that Panda wasn’t resetting the keyboard focus to itself.)

David

Good news! It’s a long time I don’t check the forums but I’m still using Panda so I’m happy for this.

Is the next version scheduled to be released soon? Just so I don’t try to compile from source and I get a new binary the next day :smiley:

Thanks

1.7.0 will be released before the end of the year. But I could email you a CVS snapshot build (contact me on my email address).

Hi all again. After some time, I get to tinker again with my Panda3d/Pyqt visualizer.

rdb, thanks for your snapshot. Now I’m using the official 1.7.0 but I’m still facing the same problem of not having mouse and keyboard input on windows.

I’ve tried several tricks, including using ctypes to call SetFocus on the window handle obtained by GetWindow. No luck.

Is there any other thing I could try?

P.S. Unrelated, but could be useful for someone. I had to start the QTimer hooked to world.step with timer.start(1). In fact, timer.start(0) failed to produce a running panda main loop. strange.

I was able to get this code working with the latest version of Panda3d/Qt4 on Ubuntu 10.10. However watching the prompt I am getting the follow:
display:x11display(error): BadMatch (invalid parameter attributes)

Which looks to be caused by the windowHandle being passed into openDefaultWindow(windowHandle)

Is anyone else having this issue? Or know how to fix it?

I found out what the error is:

Instead of

base.cam.setPos(0, -28, 6) 

It should be

base.camera.setPos(0, -28, 6) 

The error was very miss leading but once I changed from cam to camera the error went away.

The manual says:

This doesnt make much sense if you ask me.

The point of base.camera is that if you want to have multiple cameras (besides base.cam) you can simply attach them to base.camera, too.

Yeah not really sure, I noticed using base.cam also wasn’t changing my camera location in my scene. Whereas base.camera does.

For my example code with picking working (I need to figureout how to update a Qt textbox with panda3d data :S). Check here: https://github.com/warplydesigned/warply3D it is under custom_widgets/pandawidget.py

whats the difference in attaching them to base.cam?

Hi I’ve been trying to get the code by dinoint working in Windows 7
What is happening is when it gets to this line:

base.openDefaultWindow(props=wp)

it gives me this error

:display:windisplay(warning): SetActiveWindow() failed!
:display:windisplay(warning): SetForegroundWindow() failed!

Does anyone have any ideas what could cause this?

Hi all,

I did never get the examples here working, so I went experimenting and did get something thats working. This I wanted to share with you all. Also did some experimenting to get panda working with QGLWidget. Its working, but the Panda window stay’s open wathever I try. So I hope with these examples that we can have some desent QGLWidget without the Panda window.

without QGLWidget, eg with a normal QWidget

import platform
if platform.architecture()[0] != "32bit":
    raise Exception("Only 32bit architecture is supported")

from pandac.PandaModules import loadPrcFileData
#loadPrcFileData("", "window-type offscreen") # Set Panda to draw its main window in an offscreen buffer
loadPrcFileData("", "load-display pandagl")
loadPrcFileData("", "model-path c:/Panda3D-1.8.1/models")
loadPrcFileData("", "win-size 800 600")
loadPrcFileData("", "show-frame-rate-meter #t")
# PyQt imports
from PyQt4 import QtGui
from PyQt4 import QtCore
import os, sys

# Panda imports
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import WindowProperties, GraphicsOutput,  Texture,  StringStream,  PNMImage
from direct.interval.LerpInterval import LerpHprInterval
from pandac.PandaModules import Point3
# Set up Panda environment
import direct.directbase.DirectStart
from struct import *

P3D_WIN_WIDTH = 800
P3D_WIN_HEIGHT = 600

def GetApplicationPath(file=None):
    import re, os
    # If file is None return current directory without trailing slash.
    if file is None:
        file = ""
    # Only when relative path.
    if not file.startswith("/") and not file.startswith("\\") and (
            not re.search(r"^[\w-]+:", file)):
        if hasattr(sys, "frozen"):
            path = os.path.dirname(sys.executable)
        elif "__file__" in globals():
            path = os.path.dirname(os.path.realpath(__file__))
        else:
            path = os.getcwd()
        path = path + os.sep + file
        path = re.sub(r"[/\\]+", re.escape(os.sep), path)
        path = re.sub(r"[/\\]+$", "", path)
        return path
    return str(file)

def ExceptHook(excType, excValue, traceObject):
    import traceback, os, time, codecs
    # This hook does the following: in case of exception write it to
    # the "error.log" file, display it to the console, shutdown CEF
    # and exit application immediately by ignoring "finally" (_exit()).
    errorMsg = "\n".join(traceback.format_exception(excType, excValue, traceObject))
    errorFile = GetApplicationPath("error.log")
    try:
        appEncoding = cefpython.g_applicationSettings["string_encoding"]
    except:
        appEncoding = "utf-8"
    if type(errorMsg) == bytes:
        errorMsg = errorMsg.decode(encoding=appEncoding, errors="replace")
    try:
        with codecs.open(errorFile, mode="a", encoding=appEncoding) as fp:
            fp.write("\n[%s] %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), errorMsg))
    except:
        print("WARNING: failed writing to error file: %s" % (errorFile))
    # Convert error message to ascii before printing, otherwise
    # you may get error like this:
    # | UnicodeEncodeError: 'charmap' codec can't encode characters
    errorMsg = errorMsg.encode("ascii", errors="replace")
    errorMsg = errorMsg.decode("ascii", errors="replace")
    print("\n"+errorMsg+"\n")
    os._exit(1)

class MainWindow(QtGui.QMainWindow):
    mainFrame = None

    def __init__(self):
        super(MainWindow, self).__init__(None)
        panHandler = PandaHandler()
        self.mainFrame = MainFrame(panHandler.screenTexture)
        panHandler.bindToWindow(int(self.mainFrame.winId()))
        self.setCentralWidget(self.mainFrame)
        self.resize(800, 600)
        self.setWindowTitle('PyQT example')
        self.setFocusPolicy(QtCore.Qt.StrongFocus)

class MainFrame(QtGui.QWidget):
    def __init__(self, texture,  parent=None):
        super(MainFrame, self).__init__(parent)
       
        #self.setGeometry(50, 50, 800, 600)
        #self.setWindowTitle("PandaQt")
        #self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) # Setup the window so its frameless
        self.pandaTexture = texture
       
        # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop
        pandaTimer = QtCore.QTimer(self)
        self.connect(pandaTimer,  QtCore.SIGNAL("timeout()"), taskMgr.step)
        pandaTimer.start(0)
       
        # Setup another timer that redraws this widget in a specific FPS
        redrawTimer = QtCore.QTimer(self)
        self.connect(redrawTimer,  QtCore.SIGNAL("timeout()"), self, QtCore.SLOT("update()"))
        redrawTimer.start(1000/30)
       
        self.paintSurface = QtGui.QPainter()
        self.rotate = QtGui.QTransform()
        self.rotate.rotate(180)
       
        self.out_image = QtGui.QImage()
   
    def showEvent(self, event):
        self.desktopBg = QtGui.QPixmap.grabWindow(QtGui.QApplication.desktop ().winId(), self.geometry().x(),self.geometry().y(), self.rect().width(), self.rect().height())
   
    # Use the paint event to pull the contents of the panda texture to the widget
    def paintEvent(self,  event):
        if self.pandaTexture.mightHaveRamImage():
            self.pandaTexture.setFormat(Texture.FRgba32)
            #print "Should draw yes?"
            data = self.pandaTexture.getRamImage().getData()
            img = QtGui.QImage(data, self.pandaTexture.getXSize(), self.pandaTexture.getYSize(), QtGui.QImage.Format_ARGB32).mirrored()
            self.paintSurface.begin(self)
            self.paintSurface.drawPixmap(0, 0, self.desktopBg)
            self.paintSurface.drawImage(0, 0, img)
            self.paintSurface.end()
            pixmap = QtGui.QPixmap.fromImage(img)
            self.setMask(pixmap.mask())

class PandaHandler(DirectObject):
   
    def __init__(self):
        base.disableMouse()
        base.camera.setPos(0, -28, 6)
        self.testModel = loader.loadModel('panda.egg.pz')
        self.testModel.reparentTo(render)
        self.rotateInterval = LerpHprInterval(self.testModel, 3, Point3(360, 0, 0))
        self.rotateInterval.loop()
       
        self.screenTexture = Texture()
        self.screenTexture.setMinfilter(Texture.FTLinear)
        self.screenTexture.setFormat(Texture.FRgba32)
        print "Format is", self.screenTexture.getFormat()
        base.win.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam)

    def bindToWindow(self, windowHandle):
        gsg = None
        wp = WindowProperties().getDefault()
        wp.setOrigin(0,0)
        wp.setSize(800, 600)
        if platform.system() != 'Darwin':
            try:
                wp.setParentWindow(windowHandle)
            except OverflowError:
                # Sheesh, a negative value from GetHandle().  This can
                # only happen on 32-bit Windows.
                wp.setParentWindow(windowHandle & 0xffffffff)
        #wp.setParentWindow(windowHandle)
        base.openWindow(props = wp, gsg = gsg, type = 'offscreen', unexposedDraw = False)
        self.wp = wp

if __name__ == '__main__':
    print("PyQt version: %s" % QtCore.PYQT_VERSION_STR)
    print("QtCore version: %s" % QtCore.qVersion())

    sys.excepthook = ExceptHook
    settings = {
        "log_file": GetApplicationPath("debug.log"),
        "release_dcheck_enabled": False # Enable only when debugging.
    }

    app = QtGui.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    app.exec_()

    # Need to destroy QApplication(), otherwise Shutdown() fails.
    # Unset main window also just to be safe.
    del mainWindow
    del app

    sys.exit(0)

and with the QGLWidget

import platform
if platform.architecture()[0] != "32bit":
    raise Exception("Only 32bit architecture is supported")

from pandac.PandaModules import loadPrcFileData
#loadPrcFileData("", "window-type offscreen") # Set Panda to draw its main window in an offscreen buffer
loadPrcFileData("", "load-display pandagl")
loadPrcFileData("", "model-path c:/Panda3D-1.8.1/models")
loadPrcFileData("", "win-size 800 600")
loadPrcFileData("", "show-frame-rate-meter #t")
# PyQt imports
from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4 import QtOpenGL
import OpenGL
OpenGL.ERROR_CHECKING = True
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL import error
oglError = error

import os, sys, platform

# Panda imports
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import WindowProperties, FrameBufferProperties, CallbackGraphicsWindow, GraphicsOutput,  Texture,  StringStream,  PNMImage
from direct.interval.LerpInterval import LerpHprInterval
from pandac.PandaModules import Point3
# Set up Panda environment
import direct.directbase.DirectStart
from struct import *

P3D_WIN_WIDTH = 800
P3D_WIN_HEIGHT = 600

def GetApplicationPath(file=None):
    import re, os
    # If file is None return current directory without trailing slash.
    if file is None:
        file = ""
    # Only when relative path.
    if not file.startswith("/") and not file.startswith("\\") and (
            not re.search(r"^[\w-]+:", file)):
        if hasattr(sys, "frozen"):
            path = os.path.dirname(sys.executable)
        elif "__file__" in globals():
            path = os.path.dirname(os.path.realpath(__file__))
        else:
            path = os.getcwd()
        path = path + os.sep + file
        path = re.sub(r"[/\\]+", re.escape(os.sep), path)
        path = re.sub(r"[/\\]+$", "", path)
        return path
    return str(file)

def ExceptHook(excType, excValue, traceObject):
    import traceback, os, time, codecs
    # This hook does the following: in case of exception write it to
    # the "error.log" file, display it to the console, shutdown CEF
    # and exit application immediately by ignoring "finally" (_exit()).
    errorMsg = "\n".join(traceback.format_exception(excType, excValue, traceObject))
    errorFile = GetApplicationPath("error.log")
    appEncoding = "utf-8"
    if type(errorMsg) == bytes:
        errorMsg = errorMsg.decode(encoding=appEncoding, errors="replace")
    try:
        with codecs.open(errorFile, mode="a", encoding=appEncoding) as fp:
            fp.write("\n[%s] %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), errorMsg))
    except:
        print("WARNING: failed writing to error file: %s" % (errorFile))
    # Convert error message to ascii before printing, otherwise
    # you may get error like this:
    # | UnicodeEncodeError: 'charmap' codec can't encode characters
    errorMsg = errorMsg.encode("ascii", errors="replace")
    errorMsg = errorMsg.decode("ascii", errors="replace")
    print("\n"+errorMsg+"\n")
    os._exit(1)

class MainWindow(QtGui.QMainWindow):
    mainFrame = None

    def __init__(self):
        super(MainWindow, self).__init__(None)
        self.mainFrame = EmbeddedPandaWindow()
        self.setCentralWidget(self.mainFrame)
        self.resize(800, 600)
        self.setWindowTitle('PyQT example')
        self.setFocusPolicy(QtCore.Qt.StrongFocus)

class EmbeddedPandaWindow(QtOpenGL.QGLWidget):
    """ This class implements a Panda3D window that is directly
    embedded within the frame.  It is fully supported on Windows,
    partially supported on Linux, and not at all on OSX. """

    def __init__(self, *args, **kw):
        gsg = None
        if 'gsg' in kw:
            gsg = kw['gsg']
            del kw['gsg']

        fbprops = kw.get('fbprops', None)
        if fbprops == None:
            fbprops = FrameBufferProperties.getDefault()

        QtOpenGL.QGLWidget.__init__(self, *args, **kw)
        
        base.disableMouse()
        base.camera.setPos(0, -28, 6)
        self.testModel = loader.loadModel('panda.egg.pz')
        self.testModel.reparentTo(render)
        self.rotateInterval = LerpHprInterval(self.testModel, 3, Point3(360, 0, 0))
        self.rotateInterval.loop()

        self.callbackWindowDict = {
            'Events' : self.__eventsCallback,
            'Properties' : self.__propertiesCallback,
            'Render' : self.__renderCallback,
            }

        # Make sure we have an OpenGL GraphicsPipe.
        if not base.pipe:
            base.makeDefaultPipe()
        self.pipe = base.pipe
        if self.pipe.getInterfaceName() != 'OpenGL':
            base.makeAllPipes()
            for self.pipe in base.pipeList:
                if self.pipe.getInterfaceName() == 'OpenGL':
                    break

        if self.pipe.getInterfaceName() != 'OpenGL':
            raise StandardError, "Couldn't get an OpenGL pipe."

        self.win = base.openWindow(callbackWindowDict = self.callbackWindowDict, pipe = self.pipe, gsg = gsg, type = 'none', unexposedDraw = True)

        # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop
        pandaTimer = QtCore.QTimer(self)
        self.connect(pandaTimer,  QtCore.SIGNAL("timeout()"), taskMgr.step)
        pandaTimer.start(0)

    def closeEvent(self, event):
        self.cleanup()
        event.Skip()

    def cleanup(self):
        """ Parent windows should call cleanup() to clean up the
        wxPandaWindow explicitly (since we can't catch EVT_CLOSE
        directly). """
        if self.win:
            base.closeWindow(self.win)
            self.win = None

    def onSize(self, event):
        wp = WindowProperties()
        wp.setOrigin(0, 0)
        wp.setSize(*self.GetClientSize())
        self.win.requestProperties(wp)
        event.Skip()

    def __eventsCallback(self, data):
        data.upcall()

    def __propertiesCallback(self, data):
        data.upcall()

    def __renderCallback(self, data):
        cbType = data.getCallbackType()
        if cbType == CallbackGraphicsWindow.RCTBeginFrame:
            if not self.isVisible():
                data.setRenderFlag(False)
                return
            self.updateGL()

            # Don't upcall() in this case.
            return
        
        elif cbType == CallbackGraphicsWindow.RCTEndFlip:
            # Now that we've swapped, ask for a refresh, so we'll
            # get another paint message if the window is still
            # visible onscreen.
            self.updateGL()

        data.upcall()

if __name__ == '__main__':
    print("PyQt version: %s" % QtCore.PYQT_VERSION_STR)
    print("QtCore version: %s" % QtCore.qVersion())

    sys.excepthook = ExceptHook
    settings = {
        "log_file": GetApplicationPath("debug.log"),
        "release_dcheck_enabled": False # Enable only when debugging.
    }

    app = QtGui.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    app.exec_()

    # Need to destroy QApplication(), otherwise Shutdown() fails.
    # Unset main window also just to be safe.
    del mainWindow
    del app

    sys.exit(0)

I hope that some of you can help to make these examples working, so that beginners can benefit from our work…

Happy Panda coding!!!