techartorg / bqt

A Blender add-on to support & manage Qt Widgets in Blender (PySide2)

Home Page:https://github.com/techartorg/bqt/wiki

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

figure out why bqt works without event loop

hannesdelbeke opened this issue · comments

when using qt without bqt, creating a QApplication and exec it in Blender freezes Blender input, untill we close the widget.
You can test this with following pseudo code

app = QApplication()
w = QWidget()
w.show()
app._exec()

to solve this, we usually create an event loop in QT scripts.

At some point in refactoring BQT, I removed the event loop since it wasnt needed to prevent blender from freezing.
But I don't understand why. It would be good to figure out what part of the code in BQT prevents Blender from freezing.

pointed out in #70, where we look at adding bqt support to linux.

actual test code

from PySide2 import QtWidgets

def show():
    app = QtWidgets.QApplication.instance()
    
    exec = False
    if not app:
        exec = True
        app = QtWidgets.QApplication()

    window = QtWidgets.QWidget()
    window.show()

    if exec:
        app.exec_()

    return window

show()

this freezes blender the first time i run it.
but when i close the widget and run it a second time it's fine.
likely related to fact that we don't exec again in the show method. and use the QApp instance instead

This is VERY high level, and very handwavy, but hopefully this helps.

So what exec_ is doing under the hood, is basically

while True:
    msg = get_message_from_os()
    if msg in stuff_we_care_about:
        process_message(msg)
    sleep(0.001)

Where it's listening for the various OS events/messages, and then processing the important ones and otherwise trying to do a minimum amount of work to maintain the illusion of realtime interaction.

So why does this freeze blender? Well because it has its own similar loop, and when you run this code inside that loop, it never gets to continue until the QApplication is closed.

Why does this work in blender? Well that's because OS native message handling is pretty universally understood, and when the user interacts with a Qt widget, those messages get processed by blender's loop, and when messages come in from the OS aimed at Qt widget, the widget IDs are part of that message, and the message processing shunts them to the correct part of the UI.

Also, I've used this same kind of amalgam of framework message loops to link up code written in wxpython, pyside2, and pywin32 that was all hosted inside a custom C++ application with its own internal python interpreter.

thanks for this breakdown 🙏

So why does this freeze blender? Well because it has its own similar loop, and when you run this code inside that loop, it never gets to continue until the QApplication is closed.

It makes sense that Blender is blocked, since QApp has it's own infinite while loop to process events.

What I don't get is why it starts working after we close the widget. Since we close the widget, not the QApplication.
When I check after closing the widget, QtWidgets.QApplication.instance() doesn't return None, implying it stays in active.

I would have expected that closing the widget closes the widget, but continues the QApps while loop. Processing it's qt events etc.

It likely relates to the last part of your post, any recommendations on where to read up on these things?

todo add to wiki when we find solution https://github.com/techartorg/bqt/wiki

This is wild speculation, but my guess would be that because blender is the primary owner of the process, it is stopping the QApplication from fully exiting. So while its internal message loop stops after you close the last visible widget, the application itself remains.
Hence why calling QApplication.instance() returns an instance, and much like bqt, the message passing routes through blender's message loop.

  • noticed when docking a widget to blender qt window, it freezes untill you move the mouse. implying that the qt repaint / eventloop is not properly hooked up.
  • qt timers however runs smoothly when mouse is not moving.

chatGPT, so pinch of salt.

When you call QApplication.exec_(), it starts the event loop and blocks the current thread until the event loop is exited. This means that any code that comes after the call to exec_() will not be executed until the event loop is exited.

When you close the widget, the event loop is exited and the call to exec_() returns, allowing the code after it to continue executing. However, the QApplication instance is still running in the background, waiting for events to process.

When you open the same widget again, a new event loop is started and the QApplication instance continues processing events. This is why both the widget and the app are responsive.

It's important to note that if you call QApplication.exec_() multiple times on the same thread, it will block the thread until all event loops are exited. This can cause your application to become unresponsive if you have long-running tasks that block the event loop.

follow up regarding threading, since it's a blender gotcha

The behavior of QApplication.exec_() is single-threaded. It starts the event loop and blocks the current thread until the event loop is exited. This means that any code that comes after the call to exec_() will not be executed until the event loop is exited.

However, it's possible to use multiple threads or processes in a PySide2 application by using the QThread and QProcess classes. QThread provides a way to run code in a separate thread, while QProcess provides a way to run an external process.

When using QThread, it's important to note that you should not interact with GUI elements directly from the thread. Instead, you should use signals and slots to communicate between the thread and the main thread.

this is effectively what happens when you close a widget. we can simulate with following code

from PySide2 import QtCore, QtWidgets
app = QtWidgets.QApplication([])
QtCore.QTimer.singleShot(1, app.quit)  # Schedule a call to app.quit() after 1 ms
app.exec_()  # Start the event loop

running this before doing qt stuff without bqt also makes blender responsive.
i assume bqt somehow triggers the quit method from the qapp during it's setup / window wrap.

i had similar behaviour when testing python in unity. with pyblish. it should have blocked, but it didn't the second time

It might be worth putting some prints in

def notify(self, receiver: QObject, event: QEvent) -> bool:
to see what events are actually being passed to the QApplication.
An even more noisy event stream can be hooked into using https://doc.qt.io/qt-5/qcoreapplication.html#installNativeEventFilter, which again might also be helpful.

Might be able to identify if something is swallowing the quit/close events (or if they're even firing at all)

nice, tried that. close event doesn't trigger on startup./
it does trigger when i open a widget and close it.

    def notify(self, receiver: QObject, event: QEvent) -> bool:
        if isinstance(event, QCloseEvent) :
            print("close event", receiver, event)
      # ...

just printing event doesnt give much info. probably need to do something like type(event)

event types on startup
after that its just qtimer events forever.
image

when modifying bqt code to load a normal QApplication on startup, Blender freezes as expected

    global app
    app = QApplication()
    app.exec_()
    # _create_global_app()  # this is what it normall does, creating our custom BlenderQApplication

update 1
hmm going through the code it seems we never do exec_() in bqt

update 2
removing exec from above example actually ends up being enough to show widgets in Blender.
so instead of running exec once, and then pass a quit event. we actually never run it. this makes a lot more sense.
and the event loop from Blender keeps the whole thing running.

seems mystery is solved. only need to check on other OSes, since someone reported it freezes on linux but they might have run exec. haven't seen their code.
no problems reported on the mac side makes me think that might be the case