zacharyparmley.com

OSX Native Tricks with DearPyGui

OSX, AppKit & DearPyGUI


Dear PyGui

Dear PyGui is a GUI library for python built on top of DearImGUI. Out of the box you get an awful lot of features for building rich GUIs quickly - I'm a big fan.

Lately I've been working on a project that pushes up against some limitations of Dear PyGui and I found myself reaching for external tools. Here are a few learnings I find worth a share. Do note that I'm working exclusively in OSX, and that's the OS these tips are for.

Native File Dialogs

One feature of DPG I've always struggled with is the inbuilt file and directory selector. Lovely that it exists - and for some use cases it's more than adequate - but it's got some quirks. When prompting for a new file to save to, for instance, I've never figured out how to preserve the file extension the user enters. And anyway, native file dialogs just look better to me.

I've tried several libraries for native file dialogs. Not all of them play nicely with DearPyGUI... Some fail to launch, some raise exceptions. Eventually I found crossfiledialog. crossfiledialog just works, at least on OSX, plus it's super simple to interface with.

Prompting for a file to open is as easy as crossfiledialog.open_file(). One caveat - cancels raise exceptions. So I find myself using this pattern:

def open_file() -> str | None:
    try:
        file_path = crossfiledialog.open_file()
    except crossfiledialog.exception.FileDialogException:
        return None
    return file_path

An example using crossfiledialog with Dear PyGui:

import crossfiledialog
import dearpygui.dearpygui as dpg

def cb_open_file():
    try:
        file_path = crossfiledialog.open_file()
    except crossfiledialog.exception.FileDialogException:
        return

    dpg.push_container_stack('selected_files')
    dpg.add_text(file_path)
    dpg.pop_container_stack()

dpg.create_context()
dpg.create_viewport()

with dpg.window(tag='window'):
    dpg.add_button(label="Select File", callback=cb_open_file)
    dpg.add_child_window(tag='selected_files')

dpg.set_primary_window('window', True)

dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()

Changing the Mouse Pointer

Dear PyGui doesn't include a way to change the mouse pointer. The standard pointer-arrow is often fine, but sometimes a nice grabby-hand or text-selection-ibeam make for better user experience. For this I reach for pyobjc.

from AppKit import NSCursor

NSCursor.openHandCursor().push()

There is a trick to this with Dear PyGui - the cursor is cleared on every frame. Therefore, to show a custom cursor, you'll have to set it on every frame. Here's a simple example showing the various cursors documented here:

import itertools

import dearpygui.dearpygui as dpg
from AppKit import NSCursor


CURSORS = [
    'arrowCursor', 'IBeamCursor', 'crosshairCursor', 'closedHandCursor',
    'openHandCursor', 'pointingHandCursor', 'resizeLeftCursor',
    'resizeRightCursor', 'resizeLeftRightCursor', 'resizeUpCursor',
    'resizeDownCursor', 'resizeUpDownCursor', 'disappearingItemCursor',
    'IBeamCursorForVerticalLayout', 'operationNotAllowedCursor',
    'dragLinkCursor', 'dragCopyCursor', 'contextualMenuCursor',
]

dpg.create_context()
dpg.create_viewport(title='Cursor Demo', width=550, height=675)

with dpg.window(tag='window'):
    for batch in itertools.batched(CURSORS, n=4):
        with dpg.group(horizontal=True):
            for cursor in batch:
                with dpg.child_window(width=125, height=125, tag=cursor):
                    dpg.add_text(cursor)

dpg.set_primary_window('window', True)
dpg.setup_dearpygui()
dpg.show_viewport()
while dpg.is_dearpygui_running():
    dpg.render_dearpygui_frame()
    for cursor in CURSORS:
        if dpg.is_item_hovered(cursor):
            getattr(NSCursor, cursor)().push()

dpg.destroy_context()

Modifier Keys

Dear PyGui provides convenient wiring for key_down and key_up events handling and you can certainly use those to track the state of command/control/option/shift keys. As a bonus they're cross-platform. But AppKit gives us a mac-native way to check modifier key state which I like to use:

from AppKit import (
    NSEvent,
    NSEventModifierFlagOption
    NSEventModifierFlagControl,
    NSEventModifierFlagCommand,
    NSEventModifierFlagShift,
)

if NSEvent.modifierFlags() & NSEventModifierFlagOption:
    print('Option is pressed!')

if NSEvent.modifierFlags() & NSEventModifierFlagControl:
    print('Control is pressed!')

# You get the idea...

Because the flags are just ints, it's fairly straightforward to encapsulate the logic with an IntEnum:

from AppKit import (
    NSEvent,
    NSEventModifierFlagOption,
    NSEventModifierFlagControl,
    NSEventModifierFlagCommand,
    NSEventModifierFlagShift,
    NSEventModifierFlagFunction,
)

class ModKey(enum.IntEnum):
    COMMAND = NSEventModifierFlagCommand
    CONTROL = NSEventModifierFlagControl
    OPTION = NSEventModifierFlagOption
    SHIFT = NSEventModifierFlagShift
    FUNCTION = NSEventModifierFlagFunction

    def pressed(self) -> bool:
        return (NSEvent.modifierFlags() & self) == self

Now, on key events in Dear PyGui, using such an enum is straight forward. Here's a silly little example:

import enum

from AppKit import (
    NSEvent,
    NSEventModifierFlagOption,
    NSEventModifierFlagControl,
    NSEventModifierFlagCommand,
    NSEventModifierFlagShift,
    NSEventModifierFlagFunction,
)
import dearpygui.dearpygui as dpg


class ModKey(enum.IntEnum):
    COMMAND = NSEventModifierFlagCommand
    CONTROL = NSEventModifierFlagControl
    OPTION = NSEventModifierFlagOption
    SHIFT = NSEventModifierFlagShift
    FUNCTION = NSEventModifierFlagFunction

    def pressed(self) -> bool:
        return (NSEvent.modifierFlags() & self) == self


dpg.create_context()

def cb_key_press():
    modifiers = [member.name for member in ModKey if member.pressed()]
    dpg.set_value('console', '+'.join(modifiers))

with dpg.handler_registry():
    dpg.add_key_press_handler(callback=cb_key_press)

with dpg.window(width=500, height=300):
    dpg.add_text('Press modifier keys')
    dpg.add_text('', tag='console')

dpg.create_viewport(title='Custom Title', width=800, height=600)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()