Retrieve Main Window in PyQt/PySide for DCC Apps

If you’re developing within a DCC and you create a new GUI element in PyQt/PySide, such as a QFrame, QMainWindow, or QDialog, you’re going to want the main window of the DCC app as the parent of the new GUI element. This ensures that when we close the main app, the new widget closes as well. Here are the ways I’ve gotten the main window for the most recent DCCs I’ve used:

3DS MAX 2020

reference

from PySide2 import QtWidgets


def get_qmax_main_window():
    """Get the 3DS MAX main window.
    
    Returns:
        PySide2.QtWidgets.QMainWindow: 'QMainWindow' 3DS MAX main window.
    """
    for w in QtWidgets.QApplication.topLevelWidgets():
        if w.inherits('QMainWindow') and 
                w.metaObject().className() == 'QmaxApplicationWindow':
            return w
    raise RuntimeError('Count not find QmaxApplicationWindow instance.')

Houdini 18

reference

import hou

def getHoudiniMainWindow():
    """Get the Houdini main window.
    
    Returns:
        PySide2.QtWidgets.QWidget: 'QWidget' Houdini main window.
    """
    return hou.qt.mainWindow()

Katana 3

from UI4.App import MainWindow

def getKatanaMainWindow():
    """Get the Katana main window.

    Returns:
        UI4.App.MainWindow.KatanaWindow: 'KatanaWindow' Katana main window.
    """
    return MainWindow.GetMainWindow()

Mari 4

from PySide2.QtWidgets import QApplication

import mari

def getMariMainWindow():
    """Get the Mari main window.
    
    Returns:
        PySide2.QtWidgets.QWidget: 'MriMainWindow' Mari main window.
    """

    # Set Mari main window to be in focus.
    mari.app.activateMainWindow()

    # Returns the window that has focus.
    return QApplication.activeWindow()

Maya 2020

from PySide2 import QtWidgets
import shiboken2

import maya.OpenMayaUI as apiUI

def getMayaMainWindow():
    """Get the Maya main window.
    
    Returns: 
        PySide2.QtWidgets.QWidget:  'TmainWindow' Maya main window.
    """

    ptr = apiUI.MQtUtil.mainWindow()
    if ptr is not None:
        return shiboken2.wrapInstance(long(ptr), QtWidgets.QWidget)

Nuke 12

from PySide2 import QtWidgets

def getNukeMainWindow():
    """Get the Nuke main window.

    Returns:
        PySide2.QtWidgets.QMainWindow: 'DockMainWindow' Nuke 
            main window.
    """
    for w in QtWidgets.QApplication.topLevelWidgets():
        if w.inherits('QMainWindow') and w.metaObject().className() == \
                'Foundry::UI::DockMainWindow':
            return w
    raise RuntimeError('Could not find DockMainWindow instance')

A simple “QApplication.activeWindow()” would also probably work for Nuke, but if there are multiple instances of QMainWindow, it might get confused. As far as I know, there is only one “Foundry::UI::DockMainWindow“, so it’s best to check for that.

Correct Alphanumeric Sort

If you’re in a node based program like Nuke, Houdini, or Katana, you’ll notice when you create a new node the program tries to name the node with a number at the end, thus avoiding namespace issues. The more nodes of the same name that get created, the higher the number tacked on at the end becomes. So when developing tools for these programs, there is definitely going to be a point where you will have to list nodes in order.

On the surface, this doesn’t seem like that big of an issue. They are just alphanumeric strings, after all. If you do a normal sort with alphanumeric strings, the sort doesn’t behave the way you would expect.

lst = ["var1", "var2", "var3", "var10", "var11"]
lst.sort()
print(lst)
['var1', 'var10', 'var11', 'var2', 'var3']

We humans know 10 is greater than 2, but if that number is in an alphanumeric string, the sort doesn’t care; it’s going to compile all the strings one character at a time, regardless what those characters “mean”.

So when you’re in a situation that requires you to pay attention to concurrent numbers, you need to supply a key to the sort that takes numbers into account.

import re


def alphanum_key(string, case_sensitive=False):
    """Turn a string into a list of string and number chunks.
    Using this key for sorting will order alphanumeric strings properly.
    
    "ab123cd" -> ["ab", 123, "cd"]
    
    Args:
        string (str): Given string.
        case_sensitive (bool): If True, capital letters will go first.
    Returns:
        list[int|str]: Mixed list of strings and integers in the order they
            occurred in the given string.
    """
    def try_int(str_chunk, cs_snstv):
        """Convert string to integer if it's a number.
        In certain cases, ordering filenames takes case-sensitivity 
        into consideration.
        
        Windows default sorting behavior:
        ["abc1", "ABC2", "abc3"]
            
        Linux, Mac, PySide, & Python default sorting behavior:
        ["ABC2", "abc1", "abc3"]
            
        Case-sensitivity is off by default but is provided if the developer
        wishes to use it.
        
        Args:
            str_chunk (str): Given string chunk.
            cs_snstv (bool): If True, capital letters will go first.
        Returns:
            int|str: If the string represents a number, return the number.
                Otherwise, return the string.
        """
        try:
            return int(str_chunk)
        except ValueError:
            if cs_snstv:
                return str_chunk
            # Make it lowercase so the ordering is no longer case-sensitive.
            return str_chunk.lower()
            
    return [try_int(chunk, case_sensitive)
            for chunk in re.split('([0-9]+)', string)]

Now passing alphanum_key() as the sorting key gets us exactly what we were expecting. We’ll mix it up to make sure it’s working.

lst = ["var2", "var1", "var10", "var3", "var11"]
lst.sort(key=lambda s: alphanum_key(s))
print(lst)
['var1', 'var2', 'var3', 'var10', 'var11']

Executing the CurveTool

We ran into an issue one day of trying to run a group on the farm.  One of the nodes within the group was a CurveTool with "Curve Type" ("operation" knob) set to "Max Luma Pixel".

CurveTool

We had to get the CurveTool to run for the WHOLE frame range BEFORE the script could render because there were some expressions that were dependent upon the max and min data for the whole range.  So there were 2 solutions we thought of:

  1. Run the CurveTool before you send to the farm.
  2. Send to the farm and run the curve tool before the render.

The second solution was ideal because the techs didn't want to wait for the CurveTool to finish before they could work on the next script.  Send it to the farm and let the farm worry about it.  But how do you do that?

nuke_execute

According to the API, nuke.execute() can take a node and "execute" it if it has specific code to do so.  For instance, the Write node has a "Render" button that's executed.  So in this case, we use nuke.execute() to run the Curve tool for the frame range we need.  For our purposes we passed in the name of the node and the first and last frame and put in the "beforeRender" knob in the write node that would be used to render.

write_node

This worked... but it was inefficient.  The farm split the render script across a bunch of machines (which is the point of the farm).  On each machine, it was executing the curve tool for the whole frame range before the write node rendered its little section of the render.  Each script was executing the whole range in the curve tool just to render a few frames.

So we thought of a 3 option:

  • Create a job that executes the whole range in the curve tool, save the script, and create a render job using the same script.

So we did just that.  In the CurveTool's Python tab, we put the following code in the "afterFrameRender" knob: 

We ran into some issues with just calling nuke.scriptSave(); using nuke.scriptSaveAs was good because it has an 'overwrite' argument.

But then we ran into another problem: how do we get it to execute on the farm?

Well when we send things to the farm, we're actually sending a command line to execute.  When accessing the command-line help for Nuke, there is an option for supplying the names of the nodes to render:

It says "execute" instead of "render", so that gave us hope.  So we created a job that passed the name of the CurveTool, which is "GroupName.CurveToolName" when it's inside a group.  Once executed on the farm, the script had all it needed to render on multiple machines.

So in the end, I didn't actually use nuke.execute(), but it sent me down the collaborative rabbit hole that led to the solution.  I'm looking into taking this to the next logical step, which is to get the keyframes from the CurveTool written out to separate files or to database entries so the script can be run on several machines at once.  They'll then be collected and will populate the CurveTool before the render happens on multiple machines.  I'm sure we'll find the best way to do it eventually.  Just like with the original CurveTool problem, I know it can be done.  We'll just have to fall down that rabbit hole when we get to it.