Lesson 03.02

How to reuse WPF forms in pyRevit?

As a programmers we need always thing about being efficient with our code. If you think about Copy-Pasting your code, you know you doing it wrong. So let's have a look at how to reuse your WPF forms across all your pyRevit scripts.

Lesson 03.02

How to reuse WPF forms in pyRevit?

As a programmers we need always thing about being efficient with our code. If you think about Copy-Pasting your code, you know you doing it wrong. So let's have a look at how to reuse your WPF forms across all your pyRevit scripts.

Summary

Creating Reusable Forms

In this lesson we will look at how to reuse your WPF forms in pyRevit across all your scripts. This is the ultimate goal for creating custom UI forms. This will save you tons of time as you integrate these forms across all your scripts, similar to how we used pyrevit.forms until now.

It's also going to be a very easy lesson. We've already created an Alert form, so let's make it reusable by leveraging pyRevit library.

Lib Folder structure

Let me quickly recap how to reuse code in pyRevit.

In general, everything is about creating a library folder and then placing your reusable code there like inside of a custom package. Then you will be able to import it in all your scripts because pyRevit looks for lib folder by default.

Also, there are a few things to keep in mind:

  • Place lib inside .extension folder

  • Create __init__.py files inside every folder in lib
    (This will make python read these files as a package…)

  • Write reusable code

P.S.

If you don't know how to use pyRevit lib, you can also check this video from LearnRevitAPI course where I will explain the basics.

Here is the folder structure to remind you:

Setting Up the Folder Structure

Now, let's create the lib folder inside your extension.

  1. Create lib folder

  2. Add __init__.py files

  3. Create folder for your UI forms (e.g. ef_forms)

  4. Copy your Python and XAML script from previous lesson.
    I will also rename my files to Alert.py and Alert.xaml

Once you copy, you can also clean up your python file a little. You won't need pyRevit metatags like __title__, __doc__…

You can also get rid of your comments, and you won't need the main code on the button where you used the class.

💡Tip: Write a doc string to describe your class and how to use it. You can also put your example code from the bottom inside as an example.

Here is my cleaned up code from Alert.py

# -*- coding: utf-8 -*-

# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ IMPORTS
#====================================================================================================
from Autodesk.Revit.DB import *
from pyrevit import forms   # By importing forms you also get references to WPF package! IT'S Very IMPORTANT !!!
import wpf, os, clr, sys    # wpf can be imported only after pyrevit.forms!

# .NET Imports
clr.AddReference("System")
from System.Windows import Window, Visibility
from System import Uri
from System.Windows.Media.Imaging import BitmapImage

# ╦  ╦╔═╗╦═╗╦╔═╗╔╗ ╦  ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ VARIABLES
#====================================================================================================
PATH_SCRIPT = os.path.dirname(__file__)
doc     = __revit__.ActiveUIDocument.Document #type: Document
uidoc   = __revit__.ActiveUIDocument
app     = __revit__.Application

# ╔╦╗╔═╗╦╔╗╔  ╔═╗╔═╗╦═╗╔╦╗
# ║║║╠═╣║║║║  ╠╣ ║ ║╠╦╝║║║
# ╩ ╩╩ ╩╩╝╚╝  ╚  ╚═╝╩╚═╩ ╩ MAIN FORM
#====================================================================================================
# Inherit .NET Window for your UI Form Class

class ef_alert(Window):
    """ EF Alert form that allows you to display
    - Heading, SubMessage, Image and ExitScript.

    e.g.
    UI = ef_alert('Error Message 1', sub_msg='sub_msg',
               image_path='.../img_path/',title= 'Alert Message',
               exitscript=True)"""
    def __init__(self, msg, sub_msg="", image_path="", title='EF Alert Form', exitscript=False):
        # Connect to .xaml File (in the same folder!)
        path_xaml_file = os.path.join(PATH_SCRIPT, 'Alert.xaml')
        wpf.LoadComponent(self, path_xaml_file)

        # Change Properties
        self.Title          = title
        self.UI_msg.Text    = msg

        if sub_msg:
            self.UI_submsg.Text = sub_msg

        if image_path:
            self.UI_img.Source     = BitmapImage(Uri(image_path))
            self.UI_img.Visibility = Visibility.Visible

        self.exitscript = exitscript


        # Show Form
        self.ShowDialog()

    # ╔╗ ╦ ╦╔╦╗╔╦╗╔═╗╔╗╔╔═╗
    # ╠╩╗║ ║ ║  ║ ║ ║║║║╚═╗
    # ╚═╝╚═╝ ╩  ╩ ╚═╝╝╚╝╚═╝ BUTTONS
    #==================================================
    def UIe_btn_run(self, sender, e):
        """Button action: Rename view with given """
        self.Close()

        if self.exitscript:
            sys.exit()

The XAML code hasn't changed, but I will leave it anyway to avoid any confusion.

<Window Title="EF-First WPF Form"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="100"
        WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
        SizeToContent="Height">

    <!--StackPanel for all Elements-->
    <StackPanel Margin="10">

        <!--Title-->
        <TextBlock x:Name="UI_msg"
                   Text="Error Message"
                   Margin="0,0,0,10"
                   TextWrapping="Wrap"
                   FontWeight="Bold"
                   FontSize="16"/>

        <!--SubMessage-->
        <TextBlock x:Name="UI_submsg"
                   TextWrapping="Wrap"
                   Margin="0,0,0,10"/>

        <!--Image-->
        <Image x:Name="UI_img"
               Height="Auto"
               Margin="0,0,0,10"
               Visibility="Collapsed"/>

        <!--Suparator-->
        <Separator Margin="0,0,0,10"/>

        <!--Button-->
        <Button Content="Close"
                Width="100"
                Margin="0,0,0,10"
                Click="UIe_btn_run"/>

Importing Reusable Forms

Once you have placed files inside of your lib folder, you can import them anywhere in your code easily.

I will create a new pushbutton and then write the following code:

from ef_forms.Alert import ef_alert

ef_alert('EF Error Message', sub_msg='Sub Message', title='Reusable Form Works!', exitscript=True)
print('Hello WPF World!')

You can see side by side that we literally importing the class a python file located in a nested folder in the lib.

pyCharm lib Autocomplete

Also it's great idea to setup your Autocomplete for your own lib folder. There are 2 ways to do that.

  1. Add path in Interpreter Settings

    Just follow the steps that we did for Revit API Stubs so your IDE can reference the files in your lib folder.

  1. Mark lib folder as the Source Root

There is also a feature in pyCharm that can mark folders as Source Root. That might not be the real use-case, but it works for autocomplete of packages like this.

💡 Sometimes autocomplete doesn't work right away. It worked half way during the video, but the next day it started to work as it should. I guess it's a bug or something, keep it in mind.

Simplifying Imports

While we are on this topic, let me also show you how you can use __init__ files inside your lib folder to simplify your imports.

It's obvious that using the current method you will have to import all classes and methods always from a new file, and it might be annoying and not as user-friendly.

It would look something like that:

Instead, we can import all classes directly from ef_forms folder by writing

from ef_forms import form_A, form_B, form_C #and so on...

However, to do that we need to use __init__ file placed inside the ef_forms folder.

I won't go in much details, but this file will represent the ef_forms module itself. So, you can go inside the __init__ file and import all your classes one by one from individual files. And then, later you will be able to re-import them in your script directly from ef_forms.

I'm going a bit of ahead of myself, but that's how it would look like in the future, so you get an idea. And by importing all these forms inside of __init__ file we will be able to import them directly from ef_forms

I thought that you might want to know this trick about package management in python.

Homework

Now it's your turn to practice. Go and make your form reusable and don't forget to test it across multiple scripts. This is not complicated and it will save you a lot of time in the future once you get comfortable with it.

Happy Coding!

🙋‍♂️ See you in the next lesson.
- EF

🙋‍♂️ See you in the next lesson.
- EF

Discuss the lesson:

P.S. Sometimes this chat might experience connection issues.
Please be patient or join via Discord app so you can get the most out of this community and get access to even more chats.

Discuss the lesson:

P.S. Sometimes this chat might experience connection issues.
Please be patient or join via Discord app so you can get the most out of this community and get access to even more chats.