Benjamin Slack
Benjamin Slack

Reputation: 11

Unreal, Qt, Event Loop Integration

Seeing strange behavior from QApplication when integrating Qt with stock UE 5.4.4 using Python and PySide6. Specifically handling this by doing the following:

  1. create a Python 3.11.8 (matching UE's embedded python) virtual environment
  2. activate the venv
  3. add PySide6 to the venv with pip
  4. Open UE, fresh project (Blueprint, not C++)
  5. In the project settings, enable all the python scripting features
  6. Add the site packages folder from the venv we created to the additional python paths for the project
  7. Restart the editor to pickup the changes.
  8. In the Output Log (UE's version of a Python Terminal), import QtCore and QtWidgets from PySide6. 9a. Confirm that QtWidgets.QApplication.instance() return None, i.e. that no QApp is running 9b. Create a QApplication assigned to the name app so it doesn't get garbage collected.
  9. Create a timer, QtCore.QTimer.singleShot(3000, lambda: print('Timer fired!'))
  10. Wait 3 seconds

Now, what SHOULD happen is nothing. The QApplication has not been exec'd. There should be no event loop running and so no timeout signal would get processed. I've created dozens of QApplications over my years as a Pipeline Developer. To get event processing you either have to: start the event loop with exec; or pump processEvents via some callback. Qt based applications like Maya start the event loop for you, but UE is not a Qt application. It doesn't ship with any Qt libraries. I've consulted with Epic support. They have no clue how or why this is happening.

All that said, if you follow the above procedure, you will see your timer fire after three seconds.

I've done a whole bunch of GoogleFu looking for answers to this. The only leads pointed to a class called QAbstractEventDispatcher which is created and attached to the QApplication has part of its setup. Our created QApp does have one of these, of type QWindowsGuiEventDispatcher. The Qt docs very vaguely state that you can integrate with a host event loop via implementing this class. However, Epic hasn't done that. So the evidence implies that Qt is somehow detecting that the QApplication was created in a thread with an event loop and it's attaching to that event loop. However, such behavior undocumented.

So the question, can anyone out there explain how a QApplication object can detect that it is being instantiated in a thread that has an event loop and attach itself to it?

TLDR: In UE Editor, OutputLog, Python REPL, enter the folowing:

from PySide6 import QtCore, QtWidgets
if QtWidgets.QApplication.instance() is None:
    app = QtWidgets.QApplication()
    QtCore.QTimer.singleShot(3000, lambda: print('Timer fired')

What this should do is absolutely nothing. The application event loop should not be running. To get the timer to fire should require an exec_ call or processEvents call. However, in three seconds of entering that code, the timer WILL fire and you'll see that lambda's message.

Upvotes: 1

Views: 69

Answers (1)

musicamante
musicamante

Reputation: 48444

There could be two reasons for that.

Either the "Python terminal" used in UE is based on iPython (or similar) terminals, or you're using a relatively recent version of PySide (possibly 6.7.3 or greater).

In the first case, such interactive interpreters use a input hook (see PyOS_InputHook) that automatically allows event processing when the prompt becomes idle.

This is very useful in many situations, for instance when showing matplotlib plots while still being able to change values interactively, without the need to write full programs every time.

PyQt has provided such support for years on its own (so, it works by default even in the standard interpreter) and it's also properly documented.

Apparently, PySide introduced this only recently, but unfortunately it's not yet documented, and while it seems that functions to register/unregister (similarly to PyQt) do exist, they don't seem to be public.

If you want to avoid this and the UE interpreter is based on iPython, you may try to check the related documentation to find out how to disable it.

Upvotes: 1

Related Questions