Global ref to Single Modeless Window

I have a nicely working non-modal window (everything displays correctly, and buttons in the xaml are able to trigger Revit actions, etc).

The only minor problem is that if I click the pushbutton on the toolbar a second time, it opens a second instance of the same window. In other programming languages, I would implement a singleton (or similar) pattern, to track whether or not a window is open, and hold onto a reference to the open window, thereby ensuring a second window would not be created.

In the pyRevit documentation, there is a passing reference to leaving instances of non-modal windows in memory see here

I tried two approaches and neither seemed to work.

Approach 1:

__persistentengine__ = True

log = script.get_logger()

if '__window__' in globals():
  log.warning("Window was found in globals at the start of the script")
  window = globals()['__window__']
  window.show()

else:
  log.warning("Window was not found in globals at the start of the script")
  window = script.load_ui(ux.MyWindow(), 'ui.xaml')
  globals()['__window__'] = window
  window.show()

if '__window__' in globals():
  log.warning("Window was found in globals at the end of the script")
  window = globals()['__window__']
  window.show()

Result of approach 1… the script always results in the log showing this:

WARNING [DSO] Window was not found in globals at the start of the script
WARNING [DSO] Window was found in globals at the end of the script

Approach 2: Attach the window object to some other object dynamically

__persistentengine__ = True

if hasattr(EXEC_PARAMS.command_uibutton, 'window'):
  window = EXEC_PARAMS.command_uibutton.window
  window.show()

else:
  window = EXEC_PARAMS.command_uibutton.load_ui(ux.MyWindow(), 'ui.xaml')
  setattr(EXEC_PARAMS.command_uibutton, 'window', window)
  window.show()

Result of approach 2…

AttributeError: 'RibbonButton' object has no attribute 'load_ui'

Other objects (like pyrevit.script) either have this error too or just don’t seem to persist between pushbutton clicks.

I tried finding examples of working solutions on github but came up blank. It would be greatly appreciated if someone could point out what I’m doing wrong or misunderstanding… Thanks!

Update: I dug around the pyRevit github code, and I think I found a way to confirm whether the engine is persistent, and it seems like it is not…

from pyrevit import script
from pyrevit import EXEC_PARAMS

from pyrevit.runtime import types as runtime_types
from pyrevit.runtime.types import ScriptConsoleManager, ScriptEngineManager

from ux import MyWindow

__persistentengine__ = True
__cleanengine__ = False

print(EXEC_PARAMS.script_runtime)
print(EXEC_PARAMS.script_runtime.ScriptRuntimeConfigs)
print(EXEC_PARAMS.script_runtime.ScriptRuntimeConfigs.EngineConfigs)

Result:

<PyRevitLabs.PyRevit.Runtime.ScriptRuntime object at 0x00000000000013C4 [PyRevitLabs.PyRevit.Runtime.ScriptRuntime]>
<PyRevitLabs.PyRevit.Runtime.ScriptRuntimeConfigs object at 0x00000000000013C5 [PyRevitLabs.PyRevit.Runtime.ScriptRuntimeConfigs]>
{"clean": true, "persistent": false, "full_frame": false}

After troubleshooting a thousand different things, I tried removing the bundle.yaml from the folder. This causes pyRevit to pay attention to the parameters defined in the script, so this line:

print(EXEC_PARAMS.script_runtime.ScriptRuntimeConfigs.EngineConfigs)

Printed this:

{"clean": true, "persistent": true, "full_frame": false}

I deleted the faulty bundle file and rewrote it, and now it seems to be working (it is setting my script engine to be persistent).

1 Like

I finally worked out how to ensure there is only one copy of a specific custom (non-modal) UI window open at a time… Documenting it here for posterity.

As near as I can figure out, its not possible to store a reference to the created window in python (in a way that survives the end of script.py). So instead, at the start of the script, I check Revit to see what “Window” objects the Revit “Window” “Owns”

However, even that took a bit of figuring out, due to what I think is a bug in the pyRevit window logic.

from pyrevit import script
from pyrevit.revit import ui
from pyrevit.framework import Interop


log = script.get_logger()

revit_window = ui.get_mainwindow()
owned_windows = revit_window.OwnedWindows

print('Revit window: %s' % revit_window)
print('Revit window handle: %s' % Interop.WindowInteropHelper(revit_window).Handle)
print('Revit window owns %s windows.' % len(owned_windows))
print('\n\n\n\n')

my_script_window_is_open = False

for owned_window in owned_windows:
  # Optional, but printing this info helped me find the solution
  print("Owned window's handle: %s" % Interop.WindowInteropHelper(owned_window).Handle)
  print("Owned window's owner: %s" % Interop.WindowInteropHelper(owned_window).Owner)
  print("Owned window's title: %s" % owned_window.Title)
  print("Owned window's UID: %s" % owned_window.Uid)
  print('\n\n\n')  


  # My window's "title" is "My PyRevit Window"
  # There might be a smarter way to check this, but it shows the general idea
  if owned_window.Title == "My PyRevit Window":
    pdso_window_is_open = True
    # Instead of creating another copy, activate the existing one
    owned_window.Activate()
    # Stop the for loop
    break


  if my_script_window_is_open == False:
    user_interface = script.load_ui(MyWindow(), 'ui.xaml')

    # This was VERY important. Running this, you can see the owned window 
    # doesn't get assigned a handle until AFTER it is shown. pyRevit assigns 
    # ownership during window __init__ - which is too early.
    print("Owner of new user interface window: %s" % Interop.WindowInteropHelper(user_interface).Owner)
    print("User interface handle: %s" % Interop.WindowInteropHelper(user_interface).Handle)
    print('\n\n\n\n')

    # Showing the user interface causes it to have a handle
    user_interface.show()
    
    # Once the window has a handle, assigning ownership works. 
    user_interface.Owner = revit_window
    print("Owner of new user interface window: %s" % Interop.WindowInteropHelper(user_interface).Owner)
    print("User interface handle: %s" % Interop.WindowInteropHelper(user_interface).Handle)
    print('\n\n\n\n')

The good news is that closing the created window automatically removes it from the “owned windows” collection.

1 Like