Simple decorator patterns to help keep qt code clean

I tend to find when someone is new to qt to they block the ui alot espcially in maya.

Here is some examples of decorator you could write to save you some headaches in the future.

__all__ = ("on_qthread", "throttle", "single_shot")

from Qt import QtCore
from functools import wraps


def on_qthread(*decorator_args, signal=None):
    """
    Wrap a function so it runs as a QRunnable in QThreadPool.
    Optionally emit a signal with the result.
    
    Usage:
        @on_qthread
        def foo(...): ...

        @on_qthread(signal=my_signal)
        def bar(...): ...
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            runnable = _Runnable(func, signal, *args, **kwargs)
            QtCore.QThreadPool.globalInstance().start(runnable)
            return runnable
        return wrapper

    # If used without () => @on_qthread
    if len(decorator_args) == 1 and callable(decorator_args[0]):
        return decorator(decorator_args[0])
    return decorator


class _Runnable(QtCore.QRunnable):
    def __init__(self, func, signal, *args, **kwargs):
        super().__init__()
        self.func = func
        self.signal = signal
        self.args = args
        self.kwargs = kwargs

    def run(self):
        result = self.func(*self.args, **self.kwargs)
        if self.signal:
            # emit() if it's a Qt Signal, call() if it's a Python callable
            if hasattr(self.signal, "emit"):
                self.signal.emit(result)
            else:
                self.signal(result)

def throttle(ms=100):
    """Throttle calls to max once every `ms` milliseconds.
    
    Usage:
        @throttle(100)
        def my_function(...):
            ...
    """
    def decorator(func):
        last_call = 0

        @wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal last_call
            now = QtCore.QTime.currentTime().msecsSinceStartOfDay()
            if now - last_call > ms:
                last_call = now
                return func(*args, **kwargs)
        return wrapper
    return decorator


def single_shot(ms):
    """Call function once after `ms` milliseconds.
    
    Usage:
        @single_shot(1000)
        def my_function(...):
            ...
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            QtCore.QTimer.singleShot(ms, lambda: func(*args, **kwargs))
        return wrapper
    return decorator
4 Likes

its great having this code present, but you do need to give a lot more information here on when and how to use this
if you run any OpenMaya, cmds or even generate widgets whatsoever in these threads they will still cause maya to freeze. its not an end all be all solution.
it would work on elements that have nothing to do with maya perce, like going over big data of files, images or numpy arrays.

1 Like

Thanks for comment that’s 100% correct for anyone reading!

Maya code has to be on the main thread so this won’t help.

Yeah in my experience it’s not really the Maya calls that’s slow down tools it’s when someone needs to query p4 or something that causes it.

I will share a real life example to use this when I got a moment.

But yeah high level use stuff like this to query info outside Maya or do something like large computations and then a signal to stuff on the main thread.

Finally had some time to speak a bit more on the code I shared earlier. I kinda dropped those decorator snippets without much context, so here’s the “why” behind them and where they help:

As shared above maya calls need to be on the main thread just gotta be smart when you do things in maya commands!

Sometimes you just needs to take the info and send it to a thread! I hope this helps.

  • @single_shot
    Good for things like that you do not care about in your interfaces like sync a file you do not need much life time management at that point

    @single_shot
    def syncfile(self):
        self.do_syncfile()
    
    
  • @throttle
    Sliders and text edits can spam signals every change. If you’re driving something heavy (search, redraw, scene rebuild), that’s too much. Throttle lets you cap the calls:

    @throttle(200)  # ms
    def on_text_changed(self, text):
        self.search(text)
    
    
  • @on_qthread
    The one that saves your UI from freezing. Normally you’d have to set up a whole QThread/worker/signals dance. This decorator just kicks the work off the main thread for you:
    since it just handles the thread hook up make sure you pull out your singals when you need to do anything on the main thread.

    @on_qthread
    def build_usd(self, path):
        self.expensive_usd_export(path)
    
3 Likes