Lesson 02.04

Access WPF Form Controls and Get User Input

Learn how to access WPF controls with python code so you can read and modify properties inside your form. This is essential concept to understand how to use x:Name attribute in WPF, so let me show you how to use it.

Lesson 02.04

Access WPF Form Controls and Get User Input

Learn how to access WPF controls with python code so you can read and modify properties inside your form. This is essential concept to understand how to use x:Name attribute in WPF, so let me show you how to use it.

Summary

Get User Inputs from WPF Forms

In the previous lessons you've learnt how to create WPF forms for pyRevit.

You can:

  • Design your XAML Forms

  • Connect them to pyRevit

  • Display in Revit

  • And even add EventTriggers

But there is a crucial piece missing right now. You still don't know how to read user input from your forms…

After all, that's the primary purpose of creating these WPF forms.

So let's explore how to assign an x:Name attribute to your controls and access them in your Python code. This is a key concept in WPF that allows you to reference and manipulate your controls effectively and read whatever values users provide.

Prepare a new Button

Firstly, prepare the button for the lesson. I will duplicate the previous lesson and continue working on the same form.

I will clean up the code a little, because we won't need the events we created, except for the submit button. So here is the code I'm starting this lesson with:

XAML

<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 Text="Input A" Margin="0,0,10,0" FontWeight="Bold"/>
            <TextBox 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   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>
                <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 Content="Check 1" Margin="0,0,10,0"/>
            <CheckBox Content="Check 2" Margin="0,0,10,0"/>
            <CheckBox 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/>
            </DockPanel>

            <ListBox Height="150" SelectedIndex="0">
                <!--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" Margin="0,0,10,0"/>
                <Button Content="Select None" Width="100"/>
            </DockPanel>
              
        </StackPanel>

        <!--Separator-->
        <Separator Margin="5,5,5,12"/>

        <!--Submit Button-->
        <Button Content="Submit!" Width="100" Click="UIe_btn_run" />

Python

# -*- coding: utf-8 -*-
__title__   = "02.03 - First pyRevit Form (Events)"
__doc__     = """Version = 1.0
Date    = 15.09.2024
________________________________________________________________
Description:
Learn how to handle Events in WPF with IronPython.

________________________________________________________________
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 Application, Window
from System.Windows.Controls import CheckBox, Button, TextBox, ListBoxItem
from System import Uri

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

        # Show Form
        self.ShowDialog()

    # ╔╗ ╦ ╦╔╦╗╔╦╗╔═╗╔╗╔  ╔═╗╦  ╦╔═╗╔╗╔╔╦╗╔═╗
    # ╠╩╗║ ║ ║  ║ ║ ║║║║  ║╣ ╚╗╔╝║╣ ║║║ ║ ╚═╗
    # ╚═╝╚═╝ ╩  ╩ ╚═╝╝╚╝  ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╚═╝
    def UIe_btn_run(self, sender, e):
        print('Form Submitted!')
        self.Close()

# Show form to the user
UI = FirstButton()

Assigning x:Name Attributes

And now we are ready to begin the lesson.

To access any controls in your WPF Form to read values or even modify some properties, we need to assign an x:Name attribute to your controls.

This will give them internal name in your WPF class that can be used to reference this element to either read its properties or modify them.

Let's assign this attribute to a few controls and then I explain more.

You can use x:Name like a property and assign any name you want to reference in your python code.

💡I recommend to always start with prefix UI_ as it will help you in debugging and just in general finding all these attribute names.

So, if I create x:Name = "UI_textbox1", it means that I can access this specific control by using self.UI_textbox1 python code inside of my WPF class.

Everything is accessible inside your python class that is based on the WPF Window class because it loads up the XAML code inside of it.

You can do that to any controls:

<DockPanel x:Name="UI_dock" 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" />

💡 Just don't forget to update XAML code in pushbutton.

Accessing Control Values in Python

Let's try to read the <TextBox> control in our python code. I will create a few print statements inside of the Button run method.

So to get the Textbox element itself we can use self.UI_textbox1 and then reading its properties is simple:

def UIe_btn_run(self, sender, e):
    print('Form Submitted!')
    print(self.UI_textbox1)
    print(self.UI_textbox1.Text)
    print(self.UI_textbox1.Foreground)

    self.Close()

Here are the results:

Change WPF Controls with Python

Not only we can access WPF controls to read their properties, but we can also modify them the same way.

Let's use exactly the same element and set it to another value. I will also comment out self.Close() so we can actually see the change when we click on the button.

def UIe_btn_run(self, sender, e):
    print('Form Submitted!')
    print(self.UI_textbox1)
    print(self.UI_textbox1.Text)
    print(self.UI_textbox1.Foreground)

    self.UI_textbox1.Text = "I just changed this text!"

    #self.Close()

If you going to test it, you will first print whatever was written in the form, and then you will modify it to your own message with python code.

XAML - Assign Names to WPF Controls

Let's go back to XAML and assign more x:Name attributes to other elements.

When you do that, notice that you can not have the same name for different elements. This will give you an Invalid Markup Error, so make sure you use unique names.

I will add x:Name attribute to

  • UI_textbox1

  • UI_textbox2

  • UI_combobox

  • UI_check1

  • UI_check2

  • UI_check3

  • UI_search

  • UI_listbox

💡It's recommended to put x:Name property as the first one in your controls. It will help with readability and maintaining your XAML code.

Here is the final 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="450" Width="300" MinHeight="150"
        WindowStartupLocation="CenterScreen" ResizeMode="NoResize">

    <!--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"/>
            </DockPanel>

            <ListBox x:Name="UI_listbox" Height="150" SelectedIndex="0">
                <!--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" Margin="0,0,10,0"/>
                <Button Content="Select None" Width="100"/>
            </DockPanel>
        </StackPanel>



        <!--Separator-->
        <Separator Margin="5,5,5,12"/>

        <!--Submit Button-->
        <Button Content="Submit!" 
                Width="100" 
                Click="UIe_btn_run" />

Best Practice to read WPF properties

Alright, before we are goin to go and get all controls with self.GivenName, I want to mention the best practice for getting WPF values.

Let's talk about @property decorator in python, that would look like this:

@property - is a concept in Object-Oriented Programming(OOP) in python.

It allows you to define properties as methods, which will let you define more logic for getting and setting different values. It has many different uses, but let's keep it simple.

First thing you need to know is that when you create a method and apply property decorator.

For example:

  • 1️⃣You could create a method to get a value in your class, and then assign it to a variable.

  • 2️⃣ Or you could apply @property to the method and then it would become a property.

And in both cases you would get the result by using instance.my_value

# Option 1 - Use Method
class TestClass():
    def __init__(self):

        self.my_value = self.get_value()
        
    def get_value(self):
        x = 1
        y = 2
        return x*y

# Option 2 - Use Property
class TestClass():
    def __init__(self):
        pass

    @property
    def my_value(self):
        x = 1
        y = 2
        return x*y

💡 Keep in mind that this is a very abstract example to explain the functionality of how to use it. You can dive deeper on your own into @property and how it can be used in your own classes.

Read WPF Values

Now let's apply this @property to our WPF class and read all the values from our form. Just make sure the name of your properties are not exact match of what you called you UI elements so you don't override them.

Here is the final python code:

# -*- coding: utf-8 -*-
__title__   = "02.04 - Get User Inputs with xName"
__doc__     = """Version = 1.0
Date    = 15.09.2024
________________________________________________________________
Description:
Learn how to get user input from WPF forms by using xName attribute.
________________________________________________________________
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 Application, Window
from System.Windows.Controls import CheckBox, Button, TextBox, ListBoxItem
from System import Uri

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

        # Show Form
        self.ShowDialog()


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

    # @property.setter
    # def textbox1(self, value):
    #     if type(value) == str:
    #         self.UI_textbox1.Text = value

    @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

    # ╔╗ ╦ ╦╔╦╗╔╦╗╔═╗╔╗╔  ╔═╗╦  ╦╔═╗╔╗╔╔╦╗╔═╗
    # ╠╩╗║ ║ ║  ║ ║ ║║║║  ║╣ ╚╗╔╝║╣ ║║║ ║ ╚═╗
    # ╚═╝╚═╝ ╩  ╩ ╚═╝╝╚╝  ╚═╝ ╚╝ ╚═╝╝╚╝ ╩ ╚═╝
    def UIe_btn_run(self, sender, e):
        print('Form Submitted!')

        print('Printing from inside form:!')
        print('textbox1: {}'.format(self.textbox1))
        print('textbox2: {}'.format(self.textbox2))
        print('check1: {}'.format(self.check1))
        print('check2: {}'.format(self.check2))
        print('check3: {}'.format(self.check3))
        print('search: {}'.format(self.search))

        # self.UI_textbox1.Text = 'I just changed the text!'
        self.Close()


# Show form to the user
UI = FirstButton()

print('-'*50)
print('Printing from outside form:!')
print('textbox1: {}'.format(UI.textbox1))
print('textbox2: {}'.format(UI.textbox2))
print('check1: {}'.format(UI.check1))
print('check2: {}'.format(UI.check2))
print('check3: {}'.format(UI.check3))
print('search: {}'.format(UI.search))

Summary

Now, you've learnt how to use x:Name attribute in WPF to access, read and set values to your controls.

Also, I hope that I didn't overwhelm you with @property or anything else during this lesson. But if I did, please let me know in the community.

Tell me if you found anything confusing during this lesson and if you think that I should re-record it differently. I geniunly want to hear your feedback about this lesson as I feel I might have given too much at once.

It's 100% appreciated as it helps me make better lessons for you.

Homework

Also don't forget to practice. Try adding x:Name attribute to some of your control and get them with the code. Try reading values or even modifying some of the values.

Share your code and insights in the community with others. Together you will learn much more than on your own.

And then, In the next lesson, we'll dive into how to populate data in <ListBox> and how to get user selection from the list.

Until then and 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.