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()