revit-apipyrevit

pyRevit WPF non-modal trouble


So I'm just starting to dip my toes into WPF in pyRevit. I tried to implement the pyrevit.forms.WPFWindow Class like this:

# -*- coding: UTF-8 -*-
"""
Third-Party software credits:
pyRevit: repository at https://github.com/eirannejad/pyRevit
"""

import System, clr, json, sys
clr.AddReference("System.Windows.Forms")
clr.AddReference('IronPython.Wpf')
import wpf
from Autodesk.Revit.DB import *
from pyrevit import revit, script, forms

class FactorySettings(forms.WPFWindow):

    def __init__(self):
        forms.WPFWindow.__init__(self, script.get_bundle_file('settings.xaml'))

    def something_click(self, A, B):
        plantings = FilteredElementCollector(revit.doc) \
            .WhereElementIsElementType() \
            .OfCategory(BuiltInCategory.OST_Planting)

ui = FactorySettings()
ui.show()

This is my xaml:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        mc:Ignorable="d"
        ShowInTaskbar="False" ResizeMode="CanResize"
        WindowStartupLocation="CenterScreen"
        HorizontalContentAlignment="Center"
        Title="Set worksets" Width="300" SizeToContent="Height"
        >
    <StackPanel Margin="0,0,0,0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
        <Button Content="Do something" Name="something" Click="something_click"/>
    </StackPanel>

</Window>

This will open a non-modal window because of the def show(self, modal=False): class-function. The problem for me is that even though my click-function only calls a FilteredElementCollector object, Revit crashes. If I do ui.show(modal=True) it will work, but then I can't do anything in Revit UI. What I really wanted was to do this:

def something_click(self, A, B):
    if self.PHfamSymbol != None:
         with forms.WarningBar(title='Place an instance of the placeholder object.'):
            revit.uidoc.PromptForFamilyInstancePlacement(self.PHfamSymbol)

That will not work because the focus is still on the UI. I tried this:

def something_click(self, A, B):
    if self.PHfamSymbol != None:
        self.Close()
         with forms.WarningBar(title='Place an instance of the placeholder object.'):
            try:
                revit.uidoc.PromptForFamilyInstancePlacement(self.PHfamSymbol)
            except:
                pass

This works, but I need to create a new instance of the UI after completing. Is it possible at all to have a non modal UI with pyRevit?


Solution

  • You need to use an ExternalEvent to interact with Revit in a non modal windows. You have many samples in pyRevitMEP. I made a reusable class here. I use it in many pyRevitMEP script like Element3DRotation. You can also check my old blog [Revit API] Simple Modeless Form (External Event Handler) in pyRevit post which reference more resources and have more comments.

    Sample

    script

    """
    Copyright (c) 2017 Cyril Waechter
    Python scripts for Autodesk Revit
    
    This file is part of pypevitmep repository at https://github.com/CyrilWaechter/pypevitmep
    
    pypevitmep is an extension for pyRevit. It contain free set of scripts for Autodesk Revit:
    you can redistribute it and/or modify it under the terms of the GNU General Public License
    version 3, as published by the Free Software Foundation.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    See this link for a copy of the GNU General Public License protecting this package.
    https://github.com/CyrilWaechter/pypevitmep/blob/master/LICENSE
    """
    from Autodesk.Revit.UI import IExternalEventHandler, ExternalEvent
    from Autodesk.Revit.DB import Transaction
    from Autodesk.Revit.Exceptions import InvalidOperationException
    import rpw
    from pyrevit.forms import WPFWindow
    doc = rpw.revit.doc
    uidoc = rpw.revit.uidoc
    
    __doc__ = "A simple modeless form sample"
    __title__ = "Modeless Form"
    __author__ = "Cyril Waechter"
    __persistentengine__ = True
    
    # Simple function we want to run
    def delete_elements():
        with rpw.db.Transaction("Delete selection"):
            for elid in uidoc.Selection.GetElementIds():
                doc.Delete(elid)
    
    # Create a subclass of IExternalEventHandler
    class SimpleEventHandler(IExternalEventHandler):
        """
        Simple IExternalEventHandler sample
        """
    
        # __init__ is used to make function from outside of the class to be executed by the handler. \
        # Instructions could be simply written under Execute method only
        def __init__(self, do_this):
            self.do_this = do_this
    
        # Execute method run in Revit API environment.
        def Execute(self, uiapp):
            try:
                self.do_this()
            except InvalidOperationException:
                # If you don't catch this exeption Revit may crash.
                print "InvalidOperationException catched"
    
        def GetName(self):
            return "simple function executed by an IExternalEventHandler in a Form"
    
    
    # Now we need to make an instance of this handler. Moreover, it shows that the same class could be used to for
    # different functions using different handler class instances
    simple_event_handler = SimpleEventHandler(delete_elements)
    # We now need to create the ExternalEvent
    ext_event = ExternalEvent.Create(simple_event_handler)
    
    # A simple WPF form used to call the ExternalEvent
    class ModelessForm(WPFWindow):
        """
        Simple modeless form sample
        """
    
        def __init__(self, xaml_file_name):
            WPFWindow.__init__(self, xaml_file_name)
            self.simple_text.Text = "Hello World"
            self.Show()
    
        def delete_click(self, sender, e):
            # This Raise() method launch a signal to Revit to tell him you want to do something in the API context
            ext_event.Raise()
    
    # Let's launch our beautiful and useful form !
    modeless_form = ModelessForm("ModelessForm.xaml")
    

    xaml file

    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Delete things:" Height="150" Width="300" ShowInTaskbar="False" Topmost="True"
            WindowStartupLocation="CenterScreen" ScrollViewer.VerticalScrollBarVisibility="Disabled" HorizontalContentAlignment="Center">
        <StackPanel Margin="20" HorizontalAlignment="Stretch">
            <TextBlock x:Name="simple_text" Text="" Grid.Column="0" HorizontalAlignment="Center" FontWeight="Bold"/>
            <Button Content="Delete selected elements" Height="30" Margin="10,10" Click="delete_click"/>
        </StackPanel>
    </Window>