Context errors with PyQt App inside blender

Hi guys,

WARNING: Potential long post. sorry!

I’ve started writing a blender API for our pipeline tool that we use to get data published between maya, max, houdini, nuke etc and I’ve run into a couple of issues along the way. Some of which I’ve solved but there is one regarding context that has got me stumped. I’m sure someone that understands more how the context system works will be able to shed more light on it.

First of all I should give a little history on the PyQt side of things as it may be causing a scoping issue, or something similar, that I’m not aware of. Below are the some of the ways I’ve tried to run a ‘stable’ PyQt app in blender.

  • Running the QApplication from the python console / text edit window. This is how I would launch it in maya or any other application that ships with PyQt. I’m assuming it works fine in maya because they’re managing the PyQt event loop along with their own and its all co-operating nicely. In blender, it would hang and crash and wasn’t nice at all.
  • Running the QApplication from a seperate thread. This worked, to a degree. In certain configurations the QT window would hang and not receive input, other configs it worked fine. The worst thing though is when the API was running functions on the scene it would crash at random times. It also wouldn’t block blenders event loop and you could see the scene updating as the functions were being run. It looked cool, but I’m assuming that updating was the reason why i was getting random crashes. Its a similar way to one of the methods houdini uses to launch PyQt windows in their app. Defails are here: http://www.sidefx.com/docs/houdini12.0/hom/cookbook/pyqt/part2/
  • Running the QApplication in blenders main thread but instead of calling QApplication.exec() I created a modal timer operator to send the posted events and process them at a regular interval. This is my current implementation and it is the best so far. The user interface works great, always responds to mouse clicks and updates. When the API is running functions blender blocks and waits for my stuff to finish which is exactly what I need. Once again I used a houdini approach as a guideline: http://www.sidefx.com/docs/houdini12.0/hom/cookbook/pyqt/part1/

The only problem with my current implementation is ‘some’ calls to bpy.context.*** don’t work. It says its missing attributes. AAARRRRGGGHHH!!!

I’ve stripped the thing right back to is basic components for testing and I’m still getting the same issue on a simple window that exports the selected OBJ’s. I’ve posted the code below for people to look at and test out. I’ve supplied an alternative line that works for getting the selected objects however I still get errors in other places that I can’t fix in the same way.

If you comment out the line that fails and use the new alternative it will work, however inside the obj export python file, it does a context.selected_objects and it seems to work. So blender is obviously passing the correct context to the obj export operator. Is the context that is being passed into my PyQt operator incorrect? is that how the system works?? If so is there a way for me to change the context to what i need?? How can i set my operator up to have the receive the correct context?

I can’t do a bpy.ops.objects.mode_set in my API as its poll command fails saying incorrect context. The mode is already correct anyway, I’m just trying to get it to refresh/update itself.

You can see other lines commented out in my supplied code where I’ve tried to find some info regarding the current context. Its accessing the same piece of memory as the blender python console, its in the same mode as the main blender console, it just has attributes missing.

It has to be the way I’m launching the Qt window that is causing the issue. The only downside is that the window performs perfectly when launched in this way and its a shame that this context issue is causing me problems. I’ve actually used a similar method to how houdini launch PyQt windows in their app. Check out their code here:

Without the window, the backend works fantastic. Animate your characters & camera in maya, publish them out and build them into blender, do all your sweet cloth sims, rigid body sims and cache all that out and bring it into max for rendering, or take it back to maya if you need. Its really cool to see, but it’d be cooler if it had the UI to go with. Command lines are for nerds, not artists!

Any help would be really appreciated guys.

Thanks
Jase


import bpy
import PyQt4.QtGui
import PyQt4.QtCore




class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(279, 108)
        self.centralwidget = PyQt4.QtGui.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = PyQt4.QtGui.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.lineEdit = PyQt4.QtGui.QLineEdit(self.centralwidget)
        self.lineEdit.setObjectName("lineEdit")
        self.verticalLayout.addWidget(self.lineEdit)
        self.pushButton = PyQt4.QtGui.QPushButton(self.centralwidget)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout.addWidget(self.pushButton)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = PyQt4.QtGui.QMenuBar(MainWindow)
        self.menubar.setGeometry(PyQt4.QtCore.QRect(0, 0, 279, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = PyQt4.QtGui.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)


        self.retranslateUi(MainWindow)
        PyQt4.QtCore.QMetaObject.connectSlotsByName(MainWindow)


    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(PyQt4.QtGui.QApplication.translate("MainWindow", "AAARRRGGGHHH!!!!", None, PyQt4.QtGui.QApplication.UnicodeUTF8))
        self.lineEdit.setText(PyQt4.QtGui.QApplication.translate("MainWindow", "c:/", None, PyQt4.QtGui.QApplication.UnicodeUTF8))
        self.pushButton.setText(PyQt4.QtGui.QApplication.translate("MainWindow", "export", None, PyQt4.QtGui.QApplication.UnicodeUTF8))


class qt_window(PyQt4.QtGui.QMainWindow):
    def __init__(self):
        PyQt4.QtGui.QMainWindow.__init__(self)


        self.controls = Ui_MainWindow()
        self.controls.setupUi(self)


        self.controls.pushButton.clicked.connect(self.go)
        self.show()


    def go(self):
        export_dir = str(self.controls.lineEdit.text())


        # print(dir(bpy.context))
        # print(bpy.context)
        # print(bpy.context.mode)


        ''' This is the line that fails! :'( '''
        sel_objs = bpy.context.selected_objects


        ''' Alternative that works '''
        # sel_objs = [obj for obj in bpy.data.objects if obj.select]


        for obj in sel_objs:
            for other_obj in bpy.data.objects:
                other_obj.select=False


            obj.select = True    
            file_name = export_dir + '/' + obj.name + '.obj'
            bpy.ops.export_scene.obj(filepath=file_name, path_mode='ABSOLUTE')


class PyQtEventLoopOp(bpy.types.Operator):
    bl_idname = "wm.pyqt_event_loop"
    bl_label = "PyQt Event Loop"
    _timer = None
    _window = None


    def modal(self, context, event):
        if event.type == 'TIMER':
            #Process the events for my qt stuff
            self._event_loop.processEvents()
            self._application.sendPostedEvents(None, 0) 
        return {'PASS_THROUGH'}


    def execute(self, context):
        self._application = PyQt4.QtGui.QApplication.instance()
        if self._application is None:
            self._application = PyQt4.QtGui.QApplication(['blender'])
        self._event_loop = PyQt4.QtCore.QEventLoop()
        
        # exec("return_inst = qt_window()", locals(), globals())
        self.window = qt_window()


        # Standard Blender Op Stuff
        self._timer = context.window_manager.event_timer_add(0.1, context.window)
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}


def register():
    bpy.utils.register_class(PyQtEventLoopOp)
def unregister():
    bpy.utils.unregister_class(PyQtEventLoopOp)


try:
    unregister()
except:
    pass


register()
bpy.ops.wm.pyqt_event_loop()

Hey, just a follow up.

I seem to have found a way to get the ops to work as I need by overriding the context myself.

bpy.ops.export_scene.obj({“selected_objects”:[obj1, obj2, etc]}, filepath…

the downside is that i can’t conveniently use the context to find the selected objects, active object etc. Thats ok though, I can work around that. I usually know the name of the object anyway. It would be nice to know why I’m losing the context all together though. Its weird.

Jase

Just a guess, but the context is related to Blender and PyQt is “out-of-context” thus has no context. Faking the context can work for some operations, however, your operations that need a specific context such as screen space or windows are more difficult to fake.

yeah i can see where you’re coming from. For what I’m doing its normally using import / export operators and basic things like selections/active object. So the majority of things should be ok.

With execnet you may be able to run PyQt in a different process, but I haven’t tried it out yet.
I also have an external GUI written in PyQt. If you manage to solve this, one way or another, please post it :slight_smile:

I have had some bad experiences with Blender + pre-compiled libraries, there is always a chance that the binary is incompatible (different compile flags, 32 bit vs 64 bit, etc) .

You need to return “RUNNING_MODAL” for blender to not wait for the Qt window to finish:
http://www.pasteall.org/45859/python

execnet would not be required in this case
-Alex