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
1 Like

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.