Lesson 03.03

Create SelectFromDict Form

Let's create the second most used form you will reuse - SelectFromDict. You would be surprised how much you can do with this form only.

Lesson 03.03

Create SelectFromDict Form

Let's create the second most used form you will reuse - SelectFromDict. You would be surprised how much you can do with this form only.

Summary

✨The 2nd Most Popular Form

We've made Alerts, which is probably the most reusable form for any pyRevit user.

And now we are going to look at the second most used form - SelectFromList, but in my case, I like to call it SelectFromDict, because I prefer providing dict to have better control over visible names and returned values.

We've already used <ListBox> in the 2nd module, so you know already all the steps. However, since it's such popular control, it will be great to practice it once more and put your skills to the test.

XAML - SelectFromDict

We are going to start with our XAML code and create the Front-End of our form. Also, it's a great place for you to practice it on your own.

Take this starting template with the skeleton of the code and try to make this form yourself. And don't worry to get stuck, that will actually mean that you found what you need to learn to be able to do it in the future.

<Window Title="EF-SelectFromDict"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip"
        SizeToContent="Height">

        <StackPanel Margin="10">
        
        <!--Listbox Label-->

        <!--SearchBox-->
        
        <!--ListBox-->
            
            <!--Item A-->

            <!--Item B-->
        
        <!--ListBox Buttons-->
        
        <!--Separator-->

        <!--Run Button-->

    </StackPanel>

</Window>

Spoiler Alert!

You are about to see the complete XAML code, so think twice if you are ready to see it. It's the perfect opportunity for you to practice, and if you fail you will get to see the complete solution and my step by step explanation.


And now here is the snippet (pss, I warned you.)

<Window Title="EF-SelectFromDict"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip"
        SizeToContent="Height">

    <StackPanel Margin="10">
        
        <!--Listbox Label-->
        <TextBlock x:Name="UI_label" Text="Select Elements:" FontWeight="Bold" Margin="0,0,0,10"/>

        <!--SearchBox-->
        <DockPanel Margin="0,0,0,10">
            <TextBlock Text="🔎" Margin="0,0,5,0"/>
            <TextBox x:Name="UI_search" TextChanged="UIe_search_changed"/>
        </DockPanel>
        
        <!--ListBox-->
        <ListBox x:Name="UI_listbox" Height="250" Margin="0,0,0,10">
            
            <!--Item A-->
            <ListBoxItem>
                <CheckBox>
                    <TextBlock Text="Element_A"/>
                </CheckBox>
            </ListBoxItem>

            <!--Item B-->
            <ListBoxItem>
                <CheckBox>
                    <TextBlock Text="Element B"/>
                </CheckBox>
            </ListBoxItem>

        </ListBox>
        
        <!--ListBox Buttons-->
        <DockPanel HorizontalAlignment="Center" Margin="0,0,0,10">
            <Button Click="UIe_btn_all" Content="Select All"  Width="75" Margin="0,0,10,0"/>
            <Button Click="UIe_btn_none" Content="Select None" Width="75"/>
        </DockPanel>
       
        <!--Separator-->
        <Separator Margin="0,0,0,10"/>

        <!--Run Button-->
        <Button x:Name="UI_btn_run" Click="UIe_btn_run" Content="Select" Width="160"

    <

Reveal

<Window Title="EF-SelectFromDict"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip"
        SizeToContent="Height">

    <StackPanel Margin="10">
        
        <!--Listbox Label-->
        <TextBlock x:Name="UI_label" Text="Select Elements:" FontWeight="Bold" Margin="0,0,0,10"/>

        <!--SearchBox-->
        <DockPanel Margin="0,0,0,10">
            <TextBlock Text="🔎" Margin="0,0,5,0"/>
            <TextBox x:Name="UI_search" TextChanged="UIe_search_changed"/>
        </DockPanel>
        
        <!--ListBox-->
        <ListBox x:Name="UI_listbox" Height="250" Margin="0,0,0,10">
            
            <!--Item A-->
            <ListBoxItem>
                <CheckBox>
                    <TextBlock Text="Element_A"/>
                </CheckBox>
            </ListBoxItem>

            <!--Item B-->
            <ListBoxItem>
                <CheckBox>
                    <TextBlock Text="Element B"/>
                </CheckBox>
            </ListBoxItem>

        </ListBox>
        
        <!--ListBox Buttons-->
        <DockPanel HorizontalAlignment="Center" Margin="0,0,0,10">
            <Button Click="UIe_btn_all" Content="Select All"  Width="75" Margin="0,0,10,0"/>
            <Button Click="UIe_btn_none" Content="Select None" Width="75"/>
        </DockPanel>
       
        <!--Separator-->
        <Separator Margin="0,0,0,10"/>

        <!--Run Button-->
        <Button x:Name="UI_btn_run" Click="UIe_btn_run" Content="Select" Width="160"

    <

Reveal

And once you are done, you should have a form with this front-end.


Now copy the whole XAML code and paste it inside a xaml file in your pushbutton. And then we are ready to start with python code.

Setup python Code

Now let's create a base WPF class so we can display XAML code inside of Revit before we will start writing out all the functionality. For now we don't care about functionality, we just want to display our XAML code inside of Revit.

💡Remember that we need to create EventHandler for each Event that we've defined in the XAML code. So I will add the placeholders to avoid error messages.

# -*- coding: utf-8 -*-
__title__   = "03.03 - SelectFromDict"
__doc__     = """Version = 1.0
Date    = 24.10.2024
________________________________________________________________
Description:
Learn how to create SelectFromDict form.

________________________________________________________________
Author: Erik Frits"""

# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ 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         # wpf can be imported only after pyrevit.forms!


# .NET Imports
clr.AddReference("System")
from System.Collections.Generic import List
from System.Windows import Window, Visibility
from System.Windows.Controls import CheckBox, Button, TextBox, ListBoxItem, TextBlock

# ╦  ╦╔═╗╦═╗╦╔═╗╔╗ ╦  ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ 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 SelectFromDict(Window):
    def __init__(self):
        # Connect to .xaml File (in the same folder!)
        path_xaml_file = os.path.join(PATH_SCRIPT, 'SelectFromDict.xaml')
        wpf.LoadComponent(self, path_xaml_file)

        # Show Form
        self.ShowDialog()

    # ╔═╗╦  ╦╔═╗╔╗╔╔╦╗╔═╗
    # ║╣ ╚╗╔╝║╣ ║║║ ║ ╚═╗
    # ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╚═╝
    def UIe_search_changed(self, sender, e):
        pass

    def UIe_btn_all(self, sender, e):
        pass

    def UIe_btn_none(self, sender, e):
        pass


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

# ╦ ╦╔═╗╔═╗  ╔═╗╔═╗╦═╗╔╦╗
# ║ ║╚═╗║╣   ╠╣ ║ ║╠╦╝║║║
# ╚═╝╚═╝╚═╝  ╚  ╚═╝╩╚═╩ ╩
#====================================================================================================
UI = SelectFromDict()

And now if you test it in Revit, you are supposed to see your form.

Make sure you fix any errors before moving further.

Provide Arguments

Now we need to add some arguments to our class.
We will need:

  • items

  • label

  • title

  • btn_name


And we can also make some of them work right away, because they are quite simple. We just need to reference the correct <Control> and override the value.

# Inherit .NET Window for your UI Form Class
class SelectFromDict(Window):
    def __init__(self, items, label='', title='', btn_name=''):
        # Connect to .xaml File (in the same folder!)
        path_xaml_file = os.path.join(PATH_SCRIPT, 'SelectFromDict.xaml')
        wpf.LoadComponent(self, path_xaml_file)

        # Change Values in the form
        if label:    self.UI_label.Text = label
        if title:    self.Title = title
        if btn_name: self.UI_btn_run.Content = btn_name

        # Show Form
        self.ShowDialog()

    # OTHER METHODS...
Populate items in ListBox

Now we are getting to the main section of the form - <ListBoxItems>.

First of all, it's good to have a look at the working XAML code to understand the logic you are about to recreate.

In our case, we will create a
TextBlock -> CheckBox -> ListBoxItem

- TextBlock will store the visible name in the form
- CheckBox will store the actual element and collect user input
- ListBoxItem is necessary to add items to the ListBox


So let's put all of it now into a populate_listbox method.

    def populate_listbox(self, items):
        """Add Items to ListBox by create ListBoxItem-CheckBox-TextBlock."""
        #🟧 Clear ListBox
        self.UI_listbox.Items.Clear()

        for item_name, item in sorted(items.items()):
            #🟦 Create TextBlock
            textblock      = TextBlock()
            textblock.Text = item_name

            #🟦 Create CheckBox
            check         = CheckBox()
            check.Content = textblock
            check.Tag     = item

            #🟦 Create ListBoxItem
            list_item         = ListBoxItem()
            list_item.Content = check

            #🟧 Add To ListBox
            self.UI_listbox.Items.Add(list_item)

💡 Don't forget to use this method inside __init__ statement.


Make sure to test your form to see if you are adding elements to your form. That's important before going to the next step.

We can get all views in the project and try to use this class.

#🔵 Get Views
all_views = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).WhereElementIsNotElementType().ToElements()
dict_views = {v.Name : v for v in all_views}

#🔶 Text The Form
UI = SelectFromDict(dict_views, 'Select Views', 'Select Views Form', 'Select Views.')
Add Search Functionality

Now let's add the search functionality.

We've already defined the TextChanged Event in the XAML Code, so now we need to read the provided text and see if we have any matches in the <ListBox>.

As you remember, there are lots of ways to achieve this functionality, but changing Visibility of the <ListBoxItems> is by far the easiest and the best option to use.


So this method will:

  • Get User Input + lower()

  • split() into multiple words for wildcard search

  • Then get element name (item -> checkbox -> textblock -> text)

  • And modify Visiblity based on the check result.

Here is the code:

    def UIe_search_changed(self, sender, e):
        search_input = self.UI_search.Text.lower()
        if search_input:
            search_words = search_input.split()

            for item in self.UI_listbox.Items:
                checkbox  = item.Content
                textblock = checkbox.Content
                item_name = textblock.Text.lower()

                if all(word in item_name for word in search_words):
                    item.Visibility = Visibility.Visible
                else:
                    item.Visibility = Visibility.Collapsed

        else:
            for item in self.UI_listbox.Items:
                item.Visibility = Visibility.Visible
Select All/None Buttons

Now let's also make our Select All and None buttons work. It's an optional feature, but I like to have it under my ListBoxes. It's not hard to add it, and sometimes it can save a lot of time or frustrations.

We just need to iterate through all visible items and change their IsChecked property to True/False.

Also we will create a method to reuse the functionality for both buttons, because they need opposite boolean values.

Here is the code:

def listbox_select_all(self, toggle):
    for item in self.UI_listbox.Items:
        if item.Visibility == Visibility.Visible:
            checkbox = item.Content
            checkbox.IsChecked = toggle

def UIe_btn_all(self, sender, e):
    self.listbox_select_all(True)

def UIe_btn_none(self, sender, e):
    self.listbox_select_all(False)

Get Selected Items

Lastly, we need to get our results from the form.

Iterate through items in the ListBox and check their IsChecked value. And if they are checked we can get the resulting element from Tag property.

Additionally we can also check the visible name of the element if you want to return the list of selected names.


In my case, I will create 2 @properties

  • selected_items

  • selected_names

Second one is not necessary, but it might be useful.

Here is the code:

    @property
    def selected_items(self):
        selected_items = []

        for item in self.UI_listbox.Items:
            checkbox = item.Content
            if checkbox.IsChecked:
                item = checkbox.Tag
                selected_items.append(item)

        return selected_items

    @property
    def selected_names(self):
        selected_names = []
        for item in self.UI_listbox.Items:
            checkbox = item.Content
            if checkbox.IsChecked:
                textblock = checkbox.Content
                item_name = textblock.Text
                selected_names.append(item_name)

        return selected_names
Final Test

Now let's put it to the final test and try to get selected items from the form.

Here is a simple code we will use.

all_views = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).WhereElementIsNotElementType().ToElements()
dict_views = {v.Name : v for v in all_views}

UI = SelectFromDict(dict_views, 'Select Views', 'Select Views Form', 'Select Views.')
print(UI.selected_items)
print('-'*100)
print(UI.selected_names)

Final Code

Just to avoid any confusion, here is the final XAML and Python code.

<Window Title="EF-SelectFromDict"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="Auto" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="CanResizeWithGrip"
        SizeToContent="Height">

    <StackPanel Margin="10">
        
        <!--Listbox Label-->
        <TextBlock x:Name="UI_label" Text="Select Elements:" FontWeight="Bold" Margin="0,0,0,10"/>

        <!--SearchBox-->
        <DockPanel Margin="0,0,0,10">
            <TextBlock Text="🔎" Margin="0,0,5,0"/>
            <TextBox x:Name="UI_search" TextChanged="UIe_search_changed"/>
        </DockPanel>
        
        <!--ListBox-->
        <ListBox x:Name="UI_listbox" Height="250" Margin="0,0,0,10">
            
            <!--Item A-->
            <ListBoxItem>
                <CheckBox>
                    <TextBlock Text="Element_A"/>
                </CheckBox>
            </ListBoxItem>

            <!--Item B-->
            <ListBoxItem>
                <CheckBox>
                    <TextBlock Text="Element B"/>
                </CheckBox>
            </ListBoxItem>

        </ListBox>
        
        <!--ListBox Buttons-->
        <DockPanel HorizontalAlignment="Center" Margin="0,0,0,10">
            <Button Click="UIe_btn_all" Content="Select All"  Width="75" Margin="0,0,10,0"/>
            <Button Click="UIe_btn_none" Content="Select None" Width="75"/>
        </DockPanel>
       
        <!--Separator-->
        <Separator Margin="0,0,0,10"/>

        <!--Run Button-->
        <Button x:Name="UI_btn_run" Click="UIe_btn_run" Content="Select" Width="160"/>


    </StackPanel>
    
    
</Window>
# -*- coding: utf-8 -*-
__title__   = "03.03 - SelectFromDict"
__doc__     = """Version = 1.0
Date    = 24.10.2024
________________________________________________________________
Description:
Learn how to create SelectFromDict form.

________________________________________________________________
Author: Erik Frits"""

# ╦╔╦╗╔═╗╔═╗╦═╗╔╦╗╔═╗
# ║║║║╠═╝║ ║╠╦╝ ║ ╚═╗
# ╩╩ ╩╩  ╚═╝╩╚═ ╩ ╚═╝ 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         # wpf can be imported only after pyrevit.forms!
from select import select

# .NET Imports
clr.AddReference("System")
from System.Collections.Generic import List
from System.Windows import Window, Visibility
from System.Windows.Controls import CheckBox, Button, TextBox, ListBoxItem, TextBlock

# ╦  ╦╔═╗╦═╗╦╔═╗╔╗ ╦  ╔═╗╔═╗
# ╚╗╔╝╠═╣╠╦╝║╠═╣╠╩╗║  ║╣ ╚═╗
#  ╚╝ ╩ ╩╩╚═╩╩ ╩╚═╝╩═╝╚═╝╚═╝ 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 SelectFromDict(Window):
    def __init__(self, items, label='', title='', btn_name=''):
        # Connect to .xaml File (in the same folder!)
        path_xaml_file = os.path.join(PATH_SCRIPT, 'SelectFromDict.xaml')
        wpf.LoadComponent(self, path_xaml_file)


        # Change Values in the form
        if label:    self.UI_label.Text = label
        if title:    self.Title = title
        if btn_name: self.UI_btn_run.Content = btn_name

        self.populate_listbox(items)

        # Show Form
        self.ShowDialog()

    # ╔═╗╦═╗╔═╗╔═╗╔═╗╦═╗╔╦╗╦ ╦
    # ╠═╝╠╦╝║ ║╠═╝║╣ ╠╦╝ ║ ╚╦╝
    # ╩  ╩╚═╚═╝╩  ╚═╝╩╚═ ╩  ╩
    @property
    def selected_items(self):
        selected_items = []

        for item in self.UI_listbox.Items:
            checkbox = item.Content
            if checkbox.IsChecked:
                item = checkbox.Tag
                selected_items.append(item)

        return selected_items

    @property
    def selected_names(self):
        selected_names = []
        for item in self.UI_listbox.Items:
            checkbox = item.Content
            if checkbox.IsChecked:
                textblock = checkbox.Content
                item_name = textblock.Text
                selected_names.append(item_name)

        return selected_names


    # ╔╦╗╔═╗╔╦╗╦ ╦╔═╗╔╦╗
    # ║║║║╣  ║ ╠═╣║ ║ ║║
    # ╩ ╩╚═╝ ╩ ╩ ╩╚═╝═╩╝
    def populate_listbox(self, items):
        """Add Items to ListBox by create ListBoxItem-CheckBox-TextBlock."""
        #🟧 Clear ListBox
        self.UI_listbox.Items.Clear()

        for item_name, item in sorted(items.items()):
            #🟦 Create TextBlock
            textblock      = TextBlock()
            textblock.Text = item_name

            #🟦 Create CheckBox
            check         = CheckBox()
            check.Content = textblock
            check.Tag     = item

            #🟦 Create ListBoxItem
            list_item         = ListBoxItem()
            list_item.Content = check

            #🟧 Add To ListBox
            self.UI_listbox.Items.Add(list_item)




    # ╔═╗╦  ╦╔═╗╔╗╔╔╦╗╔═╗
    # ║╣ ╚╗╔╝║╣ ║║║ ║ ╚═╗
    # ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╚═╝
    def UIe_search_changed(self, sender, e):
        search_input = self.UI_search.Text.lower()
        if search_input:
            search_words = search_input.split()

            for item in self.UI_listbox.Items:
                checkbox  = item.Content
                textblock = checkbox.Content
                item_name = textblock.Text.lower()

                if all(word in item_name for word in search_words):
                    item.Visibility = Visibility.Visible
                else:
                    item.Visibility = Visibility.Collapsed

        else:
            for item in self.UI_listbox.Items:
                item.Visibility = Visibility.Visible

    def listbox_select_all(self, toggle):
        for item in self.UI_listbox.Items:
            if item.Visibility == Visibility.Visible:
                checkbox = item.Content
                checkbox.IsChecked = toggle

    def UIe_btn_all(self, sender, e):
        self.listbox_select_all(True)

    def UIe_btn_none(self, sender, e):
        self.listbox_select_all(False)

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

# ╦ ╦╔═╗╔═╗  ╔═╗╔═╗╦═╗╔╦╗
# ║ ║╚═╗║╣   ╠╣ ║ ║╠╦╝║║║
# ╚═╝╚═╝╚═╝  ╚  ╚═╝╩╚═╩ ╩
#====================================================================================================

#🔵 Get Views
all_views = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).WhereElementIsNotElementType().ToElements()
dict_views = {v.Name : v for v in all_views}

#🔶 Text The Form
UI = SelectFromDict(dict_views, 'Select Views', 'Select Views Form', 'Select Views.')

print(UI.selected_items)
print('-'*100)
print(UI.selected_names)

💡P.S. Let me know if it's too inconvenient to copy code from smaller windows.

Conclusion

🥳Congratulations!

This will be a really useful form in your future tools, and you will often come back to copy certain parts to reuse this functionality in other forms.

Make sure you follow along and make this form work. Because in the next lesson we will need to make this form reusable and also prapare a bunch of reusable functions for your future scripts.


Happy Coding and see you soon.

🙋‍♂️ 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.