Add SearchBox for ListBox
Let's improve the user experience inside our form by adding search box functionality for our ListBox. This will really make the difference and allow users to easier find the right views. Here is how to do that…
Add SearchBox for ListBox
Let's improve the user experience inside our form by adding search box functionality for our ListBox. This will really make the difference and allow users to easier find the right views. Here is how to do that…
Summary
Add Search Functionality
Lastly let's make our ListBox work with the Search box that we made earlier. This will allow users to quickly filter the right elements in the list to select the right elements.
There are many different ways to achieve this result, and I've tried a lot of them. And in this lesson I will share the best one I've found and it's also the easiest to explain.
So let's dive in and add the search box.
Modifying the XAML Code
We're going to open the XAML code and make a few small changes here. Firstly, we need to introduce an event to our search <TextBox>.
So let's add something like this:
<TextBox x:Name="UI_search" TextChanged="UIe_search_text_changed"/>
This will trigger an event handler that will change displayed items in your listboxes. This way, we'll be able to filter and display only the views that match the written keywords.
Now let's copy the whole XAML code and bring it to our pushbutton in pyRevit. Here is my full XAML code.
<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="150"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
SizeToContent="Height">
<StackPanel Margin="10" >
<DockPanel Margin="5">
<TextBlock x:Name="UI_textblock1" Text="Input A" Margin="0,0,10,0" FontWeight="Bold" />
<TextBox x:Name="UI_textbox1" Text="Default Value..." Foreground="Gray"/>
</DockPanel>
<DockPanel Margin="5">
<TextBlock Text="Input B" Margin="0,0,10,0" FontWeight="Bold"/>
<TextBox x:Name="UI_textbox2"
Text="Default Value..." Foreground="Gray" />
</DockPanel>
<DockPanel Margin="5">
<TextBlock Text="Input C" Margin="0,0,10,0" FontWeight="Bold"/>
<ComboBox x:Name="UI_combobox">
<ComboBoxItem Content="Walls"/>
<ComboBoxItem Content="Floors"/>
<ComboBoxItem Content="Roofs"/>
<ComboBoxItem Content="Windows" IsSelected="True"/>
<ComboBoxItem Content="Doors"/>
</ComboBox>
</DockPanel>
<DockPanel HorizontalAlignment="Center" Margin="5">
<CheckBox x:Name="UI_check1" Content="Check 1" Margin="0,0,10,0" />
<CheckBox x:Name="UI_check2" Content="Check 2" Margin="0,0,10,0" />
<CheckBox x:Name="UI_check3" Content="Check 3" />
</DockPanel>
<StackPanel Margin="5">
<TextBlock Text="Select Views:" FontWeight="Bold" Margin="0,0,0,5"/>
<DockPanel Margin="5">
<TextBlock Text="🔎" Margin="0,0,5,0" />
<TextBox x:Name="UI_search" TextChanged="UIe_search_text_changed"/>
</DockPanel>
<ListBox x:Name="UI_listbox" Height="150" >
<ListBoxItem>
<CheckBox Content="View - A"/>
</ListBoxItem>
<ListBoxItem>
<CheckBox Content="View - B"/>
</ListBoxItem>
<ListBoxItem>
<CheckBox Content="View - C"/>
</ListBoxItem>
</ListBox>
<DockPanel HorizontalAlignment="Center" Margin="0,10,0,0">
<Button Content="Select All" Width="100" Click="UIe_btn_select_all" Margin="0,0,10,0"/>
<Button Content="Select None" Width="100" Click="UIe_btn_select_none"/>
</DockPanel>
</StackPanel>
<Separator Margin="5,5,5,12"/>
<Button Content="Submit!"
Width="100"
Click="UIe_btn_run" />
Python Logic for SearchBox
Now let's look at the python code. Obviously we need to create a method to handle the Event we've created.
I've mentioned mentioned that there are many different ways to achieve the same search functionality, and the easiest way is to play with the visibility of your ListBoxItems
.
Visibility has 3 options:
Visible
Hidden (Still takes space in form)
Collapsed (Doesn't take space in form)
You could also recreate your lists, but then it becomes a hassle to remember what was selected before and after… So it's better to just Hide and Show based on the search keyword.
💡Make sure you import Visibility from .NET library, because Revit API also has another Visibility Class!
So, whenever this event is triggered we are going to do the following:
Get Search Input (+make it lowercase)
If SearchBox Empty:
Iterate Through all Items
Make all items Visible
If SearchBox has value:
Get <CheckBox>
and <TextBlock>
from <ListBoxItem>
Get view name (+make it lowercase)
Check if search value in view name
Adjust Visibility
Property
Lastly, we will also make sure Wildcard search works.
Here is the code for these steps:
def UIe_search_text_changed(self, sender, e):
search_text = self.search.lower()
if search_text:
for listbox_item in self.UI_listbox.Items:
checkbox = listbox_item.Content
textblock = checkbox.Content
view_name = textblock.Text.lower()
if search_text in view_name:
listbox_item.Visibility = Visibility.Visible
else:
listbox_item.Visibility = Visibility.Collapsed
if not search_text:
for listbox_item in self.UI_listbox.Items:
listbox_item.Visibility = Visibility.Visible
Testing the Application
Now let's test it in Revit and see if your SearchBox filters elements in your <ListBox>
Alright, it works, but right now it only takes exact matches.
Instead, I want to use wildcard search. This will allow me to write multiple words and filter views that contain all written words. This is much more powerful search.
Implementing Wildcard Search
To implement Wildcard search we just need to make a minor adjustment.
We will need to:
Split search value into words
Create a check if ALL words are in the view name
That's it…
Here is how to do it with the code:
def UIe_search_text_changed(self, sender, e):
search_text = self.search.lower()
if search_text:
search_words = search_text.split()
for listbox_item in self.UI_listbox.Items:
checkbox = listbox_item.Content
textblock = checkbox.Content
view_name = textblock.Text.lower()
if all(word in view_name for word in search_words):
listbox_item.Visibility = Visibility.Visible
else:
listbox_item.Visibility = Visibility.Collapsed
if not search_text:
for listbox_item in self.UI_listbox.Items:
listbox_item.Visibility = Visibility.Visible
And now you should have a wildcard search that will make it so much more versatile to look for the right elements in your <ListBox>
.
Fix Select All / None Buttons
Lastly, we need to adjust the logic in our Select All/None buttons. Because at the moment they will select or deselect all visible and hidden elements. And we want to make sure that they only affect the visible elements.
And it's very simple, we just need to add an if statement in our listbox_select_all
method to check if ListBoxItem
is Visible
or not.
Here is how to do that:
def listbox_select_all(self, toggle):
""" Set all ListBoxItem to be Selected or Not
:param toggle: True/False"""
for listbox_item in self.UI_listbox.Items:
if listbox_item.Visibility == Visibility.Visible:
checkbox = listbox_item.Content
checkbox.IsChecked = toggle
And now your Select All/None buttons will work as expected and won't disrupt your workflow.
Final Code:
Here is the code from the lesson to avoid ant confusion:
__title__ = "02.06 - ListBox Search"
__doc__ = """Version = 1.0
Date = 15.09.2024
________________________________________________________________
Description:
Learn how to add SearchBox functionality to ListBox using Events.
________________________________________________________________
Author: Erik Frits"""
from Autodesk.Revit.DB import *
from pyrevit import forms
import wpf, os, clr
from pyrevit.forms import select_views
clr.AddReference("System")
from System.Collections.Generic import List
from System.Windows import Application, Window, Visibility
from System.Windows.Controls import CheckBox, Button, TextBox, ListBoxItem, TextBlock
from System import Uri
PATH_SCRIPT = os.path.dirname(__file__)
doc = __revit__.ActiveUIDocument.Document
uidoc = __revit__.ActiveUIDocument
app = __revit__.Application
all_views = FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_Views).WhereElementIsNotElementType().ToElements()
dict_views = {'[{}]_{}'.format(view.ViewType,view.Name) : view for view in all_views}
class FirstButton(Window):
def __init__(self):
path_xaml_file = os.path.join(PATH_SCRIPT, 'ListBoxSearch.xaml')
wpf.LoadComponent(self, path_xaml_file)
self.populate_listbox_with_views()
self.ShowDialog()
def populate_listbox_with_views(self):
self.UI_listbox.Items.Clear()
for view_name, view in dict_views.items():
textblock = TextBlock()
textblock.Text = view_name
checkbox = CheckBox()
checkbox.Content = textblock
checkbox.Tag = view
listbox_item = ListBoxItem()
listbox_item.Content = checkbox
self.UI_listbox.Items.Add(listbox_item)
def listbox_select_all(self, toggle):
""" Set all ListBoxItem to be Selected or Not
:param toggle: True/False"""
for listbox_item in self.UI_listbox.Items:
if listbox_item.Visibility == Visibility.Visible:
checkbox = listbox_item.Content
checkbox.IsChecked = toggle
@property
def textbox1(self):
return self.UI_textbox1.Text
@property
def textbox2(self):
return self.UI_textbox2.Text
@property
def search(self):
return self.UI_search.Text
@property
def combobox_item(self):
return self.UI_combobox.SelectedItem
@property
def check1(self):
return self.UI_check1.IsChecked
@property
def check2(self):
return self.UI_check2.IsChecked
@property
def check3(self):
return self.UI_check3.IsChecked
@property
def selected_listbox_items(self):
selected_views = []
for listbox_item in self.UI_listbox.Items:
checkbox = listbox_item.Content
if checkbox.IsChecked:
selected_views.append(checkbox.Tag)
return selected_views
def UIe_btn_run(self, sender, e):
print('Form Submitted!')
self.Close()
def UIe_btn_select_all(self, sender, e):
self.listbox_select_all(True)
def UIe_btn_select_none(self, sender, e):
self.listbox_select_all(False)
def UIe_search_text_changed(self, sender, e):
search_text = self.search.lower()
if search_text:
search_words = search_text.split()
for listbox_item in self.UI_listbox.Items:
checkbox = listbox_item.Content
textblock = checkbox.Content
view_name = textblock.Text.lower()
if all(word in view_name for word in search_words):
listbox_item.Visibility = Visibility.Visible
else:
listbox_item.Visibility = Visibility.Collapsed
if not search_text:
for listbox_item in self.UI_listbox.Items:
listbox_item.Visibility = Visibility.Visible
def UIe_search_text_changed(self, sender, e):
search_text = self.search.lower()
if search_text:
search_words = search_text.split()
for listbox_item in self.UI_listbox.Items:
checkbox = listbox_item.Content
textblock = checkbox.Content
view_name = textblock.Text.lower()
if all(word in view_name for word in search_words):
listbox_item.Visibility = Visibility.Visible
else:
listbox_item.Visibility = Visibility.Collapsed
if not search_text:
for listbox_item in self.UI_listbox.Items:
listbox_item.Visibility = Visibility.Visible
UI = FirstButton()
print(UI.selected_listbox_items)
<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="150"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
SizeToContent="Height">
<!--Stack Panel for all elements-->
<StackPanel Margin="10" >
<!--First Label and TextBox-->
<DockPanel Margin="5">
<TextBlock x:Name="UI_textblock1" Text="Input A" Margin="0,0,10,0" FontWeight="Bold" />
<TextBox x:Name="UI_textbox1" Text="Default Value..." Foreground="Gray"/>
</DockPanel>
<!--Second Label and TextBox-->
<DockPanel Margin="5">
<TextBlock Text="Input B" Margin="0,0,10,0" FontWeight="Bold"/>
<TextBox x:Name="UI_textbox2"
Text="Default Value..." Foreground="Gray" />
</DockPanel>
<!--Third Label with ComboBox-->
<DockPanel Margin="5">
<TextBlock Text="Input C" Margin="0,0,10,0" FontWeight="Bold"/>
<ComboBox x:Name="UI_combobox">
<ComboBoxItem Content="Walls"/>
<ComboBoxItem Content="Floors"/>
<ComboBoxItem Content="Roofs"/>
<ComboBoxItem Content="Windows" IsSelected="True"/>
<ComboBoxItem Content="Doors"/>
</ComboBox>
</DockPanel>
<!--Checkboxes-->
<DockPanel HorizontalAlignment="Center" Margin="5">
<CheckBox x:Name="UI_check1" Content="Check 1" Margin="0,0,10,0" />
<CheckBox x:Name="UI_check2" Content="Check 2" Margin="0,0,10,0" />
<CheckBox x:Name="UI_check3" Content="Check 3" />
</DockPanel>
<!--ListBox of Random Views-->
<StackPanel Margin="5">
<!--ListBox Label-->
<TextBlock Text="Select Views:" FontWeight="Bold" Margin="0,0,0,5"/>
<!--Search Box-->
<DockPanel Margin="5">
<TextBlock Text="🔎" Margin="0,0,5,0" />
<TextBox x:Name="UI_search" TextChanged="UIe_search_text_changed"/>
</DockPanel>
<ListBox x:Name="UI_listbox" Height="150" >
<!--ListBox Item with a CheckBox-->
<ListBoxItem>
<CheckBox Content="View - A"/>
</ListBoxItem>
<ListBoxItem>
<CheckBox Content="View - B"/>
</ListBoxItem>
<ListBoxItem>
<CheckBox Content="View - C"/>
</ListBoxItem>
</ListBox>
<DockPanel HorizontalAlignment="Center" Margin="0,10,0,0">
<Button Content="Select All" Width="100" Click="UIe_btn_select_all" Margin="0,0,10,0"/>
<Button Content="Select None" Width="100" Click="UIe_btn_select_none"/>
</DockPanel>
</StackPanel>
<!--Separator-->
<Separator Margin="5,5,5,12"/>
<!--Submit Button-->
<Button Content="Submit!"
Width="100"
Click="UIe_btn_run" />
</StackPanel>
</Window>
Conclusion
Alright, and that's it for this lesson, and I hope that you followed me along and implemented this powerful search functionality inside your forms. It's the thing that is really going to improve your user experience.
Our form is getting more advanced and it already includes everything we might need in our WPF forms. But I also need to show you how to populate items inside your <ComboBox>
, and that's what we will focus on in the next lesson.
I wish you Happy Coding and we will see each other 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.