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']

Houdini Hotkey Overrides

For whatever reason, I never could find actual documentation on coding hotkey for Houdini. If you’re making a specific environment for the artist to launch Houdini with specific tools, then those hotkeys need to be mandatory, especially when you’re overriding the BIG stuff, like “Open” and “Save”.

Hotkey overrides are spread across 2 files: HotkeyOverrides and MainMenuCommon.xml. These should be located in your HoudiniPath environment variable.

MainMenuCommon.xml adds the ability to customize the main menus. Documentation for it can be found here. In the case I used it, we overrode opening a scene with our own code. You can tell it what menu to put the item in, before or after which item to place your new item, what code to launch with your new item, and (if you’re replacing an item) what item to remove. When adding an item, you give it an id, so you can add a shortcut key later.

<?xml version="1.0" encoding="UTF-8"?>

<mainMenu>
    <addScriptItem id="h.open_scene">
        <label>Open</label>
        <parent>file_menu</parent>
        <insertAfter>h.new</insertAfter>
        <scriptCode>
            <![CDATA[
from houdini_code.scene import open_scene
open_scene()
]]>
        </scriptCode>
    </addScriptItem>

    <removeItem id="h.open"/>
</mainMenu>

HotkeyOverrides adds the actual hotkeys. It is a tab delineated file. The first section is the id you created in the MainMenuCommon.xml. The second section is the label that is used in the menu. The third section is description. The rest of the sections are the key combinations you’d like to assign to that command.

h.open_scene	Open	"Open file"	Alt+O	Ctrl+O