Event Handlers to change selection from Combobox

Hi All, @sanzoghenzo

I’m implementing a UI using WPF to create grids in Revit. To allow users to switch between numeric values (‘1, 2, 3, …’) and alphabetic values (‘A, B, C, …’) for grid names as shown below:

grids

I used a ComboBox to display symbol types in a dropdown list; however, I’m struggling to set up event handlers to manage the selection for each grid orientation (X and Y) and to prevent the user from selecting the same symbols for both.

Note: As shown in the image above, I want to retain the content describing numeric and alphabetic symbols as they are in the dropdown, while using the actual symbol values in the code-behind.

Pls check my code below:

Grid script + Xaml Layout
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, os, clr
clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")

from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
import System.Windows.Controls
from System.Windows.Controls import StackPanel, DockPanel, TextBlock, TextBox, Button, Separator

from Autodesk.Revit.DB import *
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application

class Grids(Window):
    LAYOUT = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Grids"
            Height="Auto"
            Width="300"
            SizeToContent ="Height" 
            ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen">
            <StackPanel Margin="10">
                <!--Input LX-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Lx =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_Lx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Input LY-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Ly =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_Ly" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Grid Extension -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Extension des Axes X, Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   e =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_extension" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along X axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dx =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_dx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along Y axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dy =" Width="50" Height="25" Margin="0,0,5,0" RenderTransformOrigin="1.733,0.495" />
                        <TextBox x:Name="input_dy" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--bubbles symbol -->
                <StackPanel>
                    <TextBlock Text="Choix de symbols pour Lx, Ly:" FontWeight="Bold"/>
                    <!-- Symbol Type for Lx -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Lx:" Width="120"/>
                        <ComboBox x:Name="selected_symbol_x" Width="150" HorizontalAlignment="Right" >
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                
                    <!-- Symbol Type for Ly -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Ly:" Width="120"/>
                        <ComboBox x:Name="selected_symbol_y" Width="150" HorizontalAlignment="Right" >
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                
                </StackPanel>
                
                <!--Separator, OK and Cancel Button-->
                <Separator Margin="0,0,0,10"/>
                <DockPanel HorizontalAlignment="Center">
                    <Button Content="OK" Width="75" Height="25" Margin="0,0,10,0" Click="OK_Clicked" />
                    <Button Content="Cancel" Width="75" Height="25" Click="Cancel_Clicked" />
                </DockPanel>            
            </StackPanel>
        </Window>'''
    def __init__(self):
        wpf.LoadComponent(self, StringReader(Grids.LAYOUT))
        """Symbol sets"""
        self.all_symbols_numeric = ['1', '2', '3', '4', '5']  # Numeric
        self.all_symbols_alpha = ['A', 'B', 'C', 'D', 'E']  # Alphabetic

        """I'm struggling with how to handle the event triggered 
        by a user selection to correctly populate the symbols Sx and Sy in self.data 
        while preventing the user from selecting the same symbols for both Sx and Sy"""
        self.selected_symbol_x = None
        self.selected_symbol_y = None
        self.data = {}
        self.ShowDialog()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.input_Lx.Text:
            forms.alert("Please enter a valid value for : Lx")
        elif not self.input_Ly.Text:
            forms.alert("Please enter a valid value for : Ly")
        elif not self.input_extension.Text:
            forms.alert("Please enter a valid value for : e")
        elif not self.input_dx.Text:
            forms.alert("Please enter a valid value for : dx")
        elif not self.input_dy.Text:
            forms.alert("Please enter a valid value for : dy")
        elif self.select_symbol_x.ItemsSource is None:
            forms.alert("Please select symbols for  : Sx")
        elif self.select_symbol_y.ItemsSource is None:
            forms.alert("Please select symbols for  : Sy")
        if self.select_symbol_x.SelectedItem == self.select_symbol_y.SelectedItem:
            forms.alert("Symbols for Lx and Ly must be different.")

        else:
            try:
                self.data = {
                    "Lx": _input_to_meters(self.input_Lx.Text),
                    "Ly": _input_to_meters(self.input_Ly.Text),
                    "e": _input_to_meters(self.input_extension.Text),
                    "dx": _input_to_meters(self.input_dx.Text),
                    "dy": _input_to_meters(self.input_dy.Text),
                    "Sx": [self.select_symbol_x.SelectedItem],
                    "Sy": [self.select_symbol_y.SelectedItem]
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        # self.data
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()
def _input_to_meters(value):
    """Convert a textual value to meters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Meters)

def create_Lines_along_X(Lx, Ly, dy, e):
    #Create lines for X-oriented grids
    Start_pts_X = [XYZ(-(Lx / 2) - e, y, 0)
        for y in [i * dy - Ly / 2 for i in range(int(Ly / dy) + 1)]]

    End_pts_X = [XYZ((Lx / 2) + e, y, 0)
        for y in [i * dy - Ly / 2 for i in range(int(Ly / dy) + 1)]]

    if Ly % dy != 0:
        last_y = Ly / 2
        Start_pts_X.append(XYZ(-(Lx / 2) - e, last_y, 0))
        End_pts_X.append(XYZ((Lx / 2) + e, last_y, 0))

    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_X, End_pts_X)]

def create_Lines_along_Y(Lx, Ly, dx, e):
    #Create lines for Y-oriented grids
    Start_pts_Y = [XYZ(x, -(Ly / 2) - e, 0)
        for x in [i * dx - Lx / 2 for i in range(int(Lx / dx) + 1)]]

    End_pts_Y = [XYZ(x, (Ly / 2) + e, 0)
        for x in [i * dx - Lx / 2 for i in range(int(Lx / dx) + 1)]]

    if Lx % dx != 0:
        last_x = Lx / 2
        Start_pts_Y.append(XYZ(last_x, -(Ly / 2) - e, 0))
        End_pts_Y.append(XYZ(last_x, (Ly / 2) + e, 0))

    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_Y, End_pts_Y)]

def create_grids(lines_x, lines_y, Sx, Sy, doc):
    with Transaction(doc, "Create Grids") as t:
        t.Start()
        grids_X = [Grid.Create(doc, line) for line in lines_x]
        grids_Y = [Grid.Create(doc, line) for line in lines_y]
        for i, grid in enumerate(grids_X):
            if i < len(Sx):
                grid.Name = Sx[i]
        for i, grid in enumerate(grids_Y):
            if i < len(Sy):
                grid.Name = Sy[i]
        t.Commit()


def main(doc=None):
    #Main entrypoint for the script.
    doc = doc or revit.doc
    form = Grids()
    if not form.data:
        return
    data = form.data
    lines_x = create_Lines_along_X(data["Lx"], data["Ly"], data["dy"], data["e"])
    lines_y = create_Lines_along_X(data["Lx"], data["Ly"], data["dx"], data["e"])
    create_grids(lines_x, lines_y, data["Sx"], data["Sy"], doc)


if __name__ == "__main__":
    main()

Thanks.

I know this isn’t what you asked, but since there are only two mutually exclusive options, can’t you just ask something like “lettered grid axis” with “x” and “y” options? I would even turn the combobox into radio buttons to make the ui understandable at first glance

1 Like

@sanzoghenzo

I excluded the use of radio buttons (which is a good idea), and as I mentioned above, I prefer a combo box with a dropdown to display the content description to the user.

To avoid complications in the code and to allow for switching between numeric and alphabetic symbols for Lx and Ly, I made changes to the XAML code. I limited myself to a single combo box for Symbol_x and used a text box for Symbol_y, which is filled with the opposite value of self.Symbol_x.SelectedItem and displayed back in the UI.

...
                    <!-- Symbol Type for X Axis -->
                    <DockPanel Margin="0,0,0,10" >
                        <TextBlock Text="Type de symbole Lx:" />
                        <ComboBox x:Name="Symbol_x" HorizontalAlignment="Right" SelectionChanged="Symbol_x_SelectionChanged" Width="110">
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Ly:" />
                        <TextBox x:Name="Symbol_y" Width="110" HorizontalAlignment="Right"/>
...

However, I am still struggling with how to correctly implement the Symbol_x_SelectionChanged function to get the real values for self.selected_symbol_x and self.selected_symbol_y according to the SelectionChanged event.

Please check my edited code below:

edited code
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, os, clr
clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")

from System.IO import StringReader
from System.Windows.Markup import XamlReader, XamlWriter
from System.Windows import Window, Application
import System.Windows.Controls
from System.Windows.Controls import StackPanel, DockPanel, TextBlock, TextBox, Button, Separator

from Autodesk.Revit.DB import *
uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application

class Grids(Window):
    LAYOUT = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Grids"
            Height="Auto"
            Width="300"
            SizeToContent ="Height" 
            ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen">
            <StackPanel Margin="10">
                <!--Input LX-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Lx =" Width="50" Height="25" Margin="0,0,5,0"  />
                        <TextBox x:Name="input_Lx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Input LY-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Ly =" Width="50" Height="25" Margin="0,0,5,0"  />
                        <TextBox x:Name="input_Ly" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Grid Extension -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Extension des Axes X, Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   e =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_extension" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along X axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along Y axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dy =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dy" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--bubbles symbol -->
                <StackPanel>
                    <TextBlock Text="Choix de symbols pour Lx, Ly:" FontWeight="Bold" Margin="0,0,0,10" />
                    
                    <!-- Symbol Type for X Axis -->
                    <DockPanel Margin="0,0,0,10" >
                        <TextBlock Text="Type de symbole Lx:" />
                        <ComboBox x:Name="Symbol_x" HorizontalAlignment="Right" SelectionChanged="Symbol_x_SelectionChanged" Width="110">
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Ly:" />
                        <TextBox x:Name="Symbol_y" Width="110" HorizontalAlignment="Right"/>
                    </DockPanel>
                </StackPanel>
                
                <!--Separator, OK and Cancel Button-->
                <Separator Margin="0,0,0,10"/>
                <DockPanel HorizontalAlignment="Center">
                    <Button Content="OK" Width="75" Height="25" Margin="0,0,10,0" Click="OK_Clicked" />
                    <Button Content="Cancel" Width="75" Height="25" Click="Cancel_Clicked" />
                </DockPanel>            
            </StackPanel>
        </Window>'''
    def __init__(self):
        wpf.LoadComponent(self, StringReader(Grids.LAYOUT))
        """Symbol sets"""
        self.all_symbols_numeric = ['1', '2', '3', '4', '5']  # Numeric
        self.all_symbols_alpha = ['A', 'B', 'C', 'D', 'E']  # Alphabetic

        """I'm struggling with how to handle the event triggered 
        by a user selection to correctly populate the symbols Sx and Sy in self.data 
        while preventing the user from selecting the same symbols for both Sx and Sy"""
        self.selected_symbol_x = None
        self.selected_symbol_y = None
        self.data = {}
        self.ShowDialog()

    def Symbol_x_SelectionChanged(self, sender, e):
        if self.Symbol_x.SelectedItem == self.Symbol_x.Items[0]:  # Check index for numeric selection
            self.Symbol_y.Text = "A B C ..."
            self.selected_symbol_x = self.all_symbols_numeric
            self.selected_symbol_y = self.all_symbols_alpha
        else:
            self.Symbol_x.SelectedItem == self.Symbol_x.Items[1]  # Check index for alphabetic selection
            self.Symbol_y.Text = "1 2 3 ..."
            self.selected_symbol_x = self.all_symbols_alpha
            self.selected_symbol_y = self.all_symbols_numeric

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.input_Lx.Text:
            forms.alert("Please enter a valid value for : Lx")
        elif not self.input_Ly.Text:
            forms.alert("Please enter a valid value for : Ly")
        elif not self.input_extension.Text:
            forms.alert("Please enter a valid value for : e")
        elif not self.input_dx.Text:
            forms.alert("Please enter a valid value for : dx")
        elif not self.input_dy.Text:
            forms.alert("Please enter a valid value for : dy")
        elif self.Symbol_x.ItemsSource is None:
            forms.alert("Please select symbols for  : Sx")
        else:
            try:
                self.data = {
                    "Lx": _input_to_meters(self.input_Lx.Text),
                    "Ly": _input_to_meters(self.input_Ly.Text),
                    "e": _input_to_meters(self.input_extension.Text),
                    "dx": _input_to_meters(self.input_dx.Text),
                    "dy": _input_to_meters(self.input_dy.Text),
                    "Sx": self.selected_symbol_x,
                    "Sy": self.selected_symbol_y
                }
            except ValueError:
                forms.alert(
                    "Invalid input detected. "
                    "Please ensure all inputs are numeric where required."
                )

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        # self.data
        if not self.data:
            return
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.Close()
def _input_to_meters(value):
    """Convert a textual value to meters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Meters)

def create_Lines_along_X(Lx, Ly, dy, e):
    #Create lines for X-oriented grids
    Start_pts_X = [XYZ(-(Lx / 2) - e, y, 0)
        for y in [i * dy - Ly / 2 for i in range(int(Ly / dy) + 1)]]

    End_pts_X = [XYZ((Lx / 2) + e, y, 0)
        for y in [i * dy - Ly / 2 for i in range(int(Ly / dy) + 1)]]

    if Ly % dy != 0:
        last_y = Ly / 2
        Start_pts_X.append(XYZ(-(Lx / 2) - e, last_y, 0))
        End_pts_X.append(XYZ((Lx / 2) + e, last_y, 0))

    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_X, End_pts_X)]

def create_Lines_along_Y(Lx, Ly, dx, e):
    #Create lines for Y-oriented grids
    Start_pts_Y = [XYZ(x, -(Ly / 2) - e, 0)
        for x in [i * dx - Lx / 2 for i in range(int(Lx / dx) + 1)]]

    End_pts_Y = [XYZ(x, (Ly / 2) + e, 0)
        for x in [i * dx - Lx / 2 for i in range(int(Lx / dx) + 1)]]

    if Lx % dx != 0:
        last_x = Lx / 2
        Start_pts_Y.append(XYZ(last_x, -(Ly / 2) - e, 0))
        End_pts_Y.append(XYZ(last_x, (Ly / 2) + e, 0))

    return [Line.CreateBound(start, end) for start, end in zip(Start_pts_Y, End_pts_Y)]

def create_grids(lines_x, lines_y, Sx, Sy, doc):
    with Transaction(doc, "Create Grids") as t:
        t.Start()
        grids_X = [Grid.Create(doc, line) for line in lines_x]
        grids_Y = [Grid.Create(doc, line) for line in lines_y]
        for i, grid in enumerate(grids_X):
            if i < len(Sx):
                grid.Name = Sx[i]
        for i, grid in enumerate(grids_Y):
            if i < len(Sy):
                grid.Name = Sy[i]
        t.Commit()


def main(doc=None):
    #Main entrypoint for the script.
    doc = doc or revit.doc
    form = Grids()
    if not form.data:
        return
    data = form.data
    lines_x = create_Lines_along_X(data["Lx"], data["Ly"], data["dy"], data["e"])
    lines_y = create_Lines_along_X(data["Lx"], data["Ly"], data["dx"], data["e"])
    create_grids(lines_x, lines_y, data["Sx"], data["Sy"], doc)


if __name__ == "__main__":
    main()

Thanks.

I’m still thinking you’re overcomplicating things for little to no gain in user experience, but if you persist in following this way, you’ll have to learn a big deal of WPF and XAML.
WPF was created to separate the presentation from the logic, and to make it easy to switch the windows interface with another thing; this is possible by using bindings, your keyword for further search on this topic.
I don’t have a ready made solution for you since I’m not that good at WPF, stackoverflow ot chatgpt could give you a solution.

Something to note: since you’re keeping everything inside a class, and do the logic with “behind code”, you could have just used the Windows.Forms to avoid having to struggle with wpf/xaml binding.

Other notes:

  • all_symbols_numeric and all_symbols_alpha don’t need to be are attributes of the window class; they are constants, and only needed in the last function, so you can just store the combobox selected item value to populate your data dictionary and do the mapping inside create_grids (or just before it). To make it more general, you could also leverage the string constants
  • You can use zip to loop throug multiple lists, and it will automatically stop when the shortest one ends:
 for grid, sx_i in zip(grids_X, Sx):
     grid.Name = sx_i

Why calculating 2 times the same values? Extract the x coordinates into a separate list that you can use for both. Even better, you can create a function that can be used for both x and y axis, with generic arguments l, d, or to make more explicit length, step. You can move inside it the last item logic, too

you should invest in @ErikFrits course @REDO_79
it will get you covered for wpf and xaml
Learn Revit API

1 Like

Thanks for your advice, @Jean-Marc , but I prefer to continue my self-taught journey and advance slowly, just as @ErikFrits did.

I resolved my issue as expected by implementing this code.

Grids
# -*- coding: UTF-8 -*-
from pyrevit import forms, revit
import wpf, clr

clr.AddReference("PresentationFramework")
clr.AddReference("System.Xml")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")

from System.IO import StringReader
from System.Windows.Markup import XamlReader
from System.Windows import Window
from Autodesk.Revit.DB import *
from string import ascii_uppercase


uidoc = __revit__.ActiveUIDocument
doc = __revit__.ActiveUIDocument.Document
app = __revit__.Application


class Grids(Window):
    LAYOUT = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Grids"
            Height="Auto"
            Width="300"
            SizeToContent ="Height" 
            ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen">
            <StackPanel Margin="10">
                <!--Input LX-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Lx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_Lx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Input LY-->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Longueur Total suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   Ly =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_Ly" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Grid Extension -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Extension des Axes X, Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   e =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_extension" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along X axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant X (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dx =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dx" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--Spacing along Y axis -->
                <StackPanel Margin="0,0,0,10">
                    <TextBlock Text ="Espacement suivant Y (m):" Width="170" Height="25" HorizontalAlignment="Left" FontSize="12" FontWeight="Bold" />
                    <DockPanel Width="200" HorizontalAlignment="Left" >
                        <TextBlock Text ="   dy =" Width="50" Height="25" Margin="0,0,5,0" />
                        <TextBox x:Name="input_dy" Width="75" Margin="0,0,5,0" />
                        <TextBlock Text="m" Width="25" HorizontalAlignment="Left" />
                    </DockPanel>
                </StackPanel>
                
                <!--bubbles symbol -->
                <StackPanel>
                    <TextBlock Text="Choix de symbols pour Lx, Ly:" FontWeight="Bold" Margin="0,0,0,10" />
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10" >
                        <TextBlock Text="Type de symbole Sx:" />
                        <ComboBox x:Name="Symbol_x" HorizontalAlignment="Right" SelectionChanged="Symbol_x_SelectionChanged" Width="110">
                            <ComboBoxItem Content="1 2 3 ..." />
                            <ComboBoxItem Content="A B C ..." />
                        </ComboBox>
                    </DockPanel>
                    
                    <!-- Symbol Type for Y Axis -->
                    <DockPanel Margin="0,0,0,10">
                        <TextBlock Text="Type de symbole Sy:" />
                        <TextBox x:Name="Symbol_y" Width="110" HorizontalAlignment="Right"/>
                    </DockPanel>
                </StackPanel>
                
                <!--Separator, OK and Cancel Button-->
                <Separator Margin="0,0,0,10"/>
                <DockPanel HorizontalAlignment="Center">
                    <Button Content="OK" Width="75" Height="25" Margin="0,0,10,0" Click="OK_Clicked" />
                    <Button Content="Cancel" Width="75" Height="25" Click="Cancel_Clicked" />
                </DockPanel>            
            </StackPanel>
        </Window>'''

    def __init__(self):
        wpf.LoadComponent(self, StringReader(Grids.LAYOUT))
        self.data = {}
        self.ShowDialog()

    def Symbol_x_SelectionChanged(self, sender, e):
        selected_item = self.Symbol_x.SelectedItem
        if selected_item.Content == "1 2 3 ...":
            self.Symbol_y.Text = self.Symbol_x.Items[1].Content.ToString()
        else:
            self.Symbol_y.Text = self.Symbol_x.Items[0].Content.ToString()

    def _collect_input_data(self):
        """Collect and convert input data from the UI"""
        # Identify which key has missing or invalid input and alert the user
        if not self.input_Lx.Text:
            forms.alert("Please enter a valid value for : Lx")
        elif not self.input_Ly.Text:
            forms.alert("Please enter a valid value for : Ly")
        elif not self.input_extension.Text:
            forms.alert("Please enter a valid value for : e")
        elif not self.input_dx.Text:
            forms.alert("Please enter a valid value for : dx")
        elif not self.input_dy.Text:
            forms.alert("Please enter a valid value for : dy")
        else:
            try:
                self.data = {
                    "Lx": _input_to_meters(self.input_Lx.Text),
                    "Ly": _input_to_meters(self.input_Ly.Text),
                    "e": _input_to_meters(self.input_extension.Text),
                    "dx": _input_to_meters(self.input_dx.Text),
                    "dy": _input_to_meters(self.input_dy.Text),
                }
            except (ValueError, AttributeError):
                forms.alert("Invalid input or missing selections. Please correct and try again.")

    def symbols_alpha(self, length_x, length_y):
        """Generate Sx and Sy symbols based on lengths of grids"""
        Sx = []
        Sy = []
        if self.Symbol_x.SelectedItem == self.Symbol_x.Items[0]:
            for i in range(length_y):
                if i < len(ascii_uppercase):
                    Sy.append(ascii_uppercase[i])
            Sx = []
        else:
            for i in range(length_x):
                if i < len(ascii_uppercase):
                    Sx.append(ascii_uppercase[i])
            Sy = []
        return Sx, Sy

    def OK_Clicked(self, sender, e):
        self._collect_input_data()
        if not self.data:
            return
        n1 = int(self.data["Lx"] // self.data["dx"])
        if not isclose(self.data["Lx"], n1 * self.data["dx"]):
            length_x = n1 + 2
        else:
            length_x = n1 + 1
        print(length_x)
        n2 = int(self.data["Ly"] // self.data["dy"])
        if not isclose(self.data["Ly"], n2 * self.data["dy"]):
            length_y = n2 + 2
        else:
            length_y = n2 + 1

        # Populate Sx and Sy dynamically based on grid lengths
        self.data["Sx"], self.data["Sy"] = self.symbols_alpha(length_x, length_y)
        print(self.data["Sx"])
        print(self.data["Sy"])
        self.Close()

    def Cancel_Clicked(self, sender, e):
        self.data = {}
        self.Close()


def _input_to_meters(value):
    """Convert a textual value to meters."""
    return UnitUtils.ConvertToInternalUnits(float(value), UnitTypeId.Meters)

def isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
    return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)


def create_lines(lx, ly, d, e, y_axis=False):
    """Create lines along X, Y axis """
    if y_axis:
        lx, ly = ly, lx
    n = lx // d
    points_X = []
    i = 0
    while i <= int(n):
        points_X.append(d * i - lx / 2)
        i += 1
    if not isclose(lx, n * d):
        points_X.append(lx / 2)
    points_Y = [-ly / 2 - e, ly / 2 + e]

    if y_axis:
        return [Line.CreateBound(*(XYZ(x, y, 0) for x in points_Y)) for y in points_X]

    return [Line.CreateBound(*(XYZ(x, y, 0) for y in points_Y)) for x in points_X]



def create_grids(lines_x, lines_y, Sx, Sy, doc):
    with Transaction(doc, "Create Grids") as t:
        t.Start()
        grids_X = [Grid.Create(doc, line) for line in lines_x]
        print(len(grids_X))
        grids_Y = [Grid.Create(doc, line) for line in lines_y]
        print(len(grids_Y))
        if Sx:  # If Sx is populated, assign names to grids_X
            print(len(Sx))
            for grid, symbol in zip(grids_X, Sx):
                grid.Name = symbol
            for idx, grid in enumerate(grids_Y):
                grid.Name = str(idx+1)
        if Sy:  # If Sy is populated, assign names to grids_Y
            print(len(Sy))
            for grid, symbol in zip(grids_Y, Sy):
                grid.Name = symbol
            for idx, grid in enumerate(grids_X):
                grid.Name = str(idx+1)
        t.Commit()


def main(doc=None):
    doc = doc or revit.doc
    form = Grids()
    if not form.data:
        return
    data = form.data
    lines_x = create_lines(data["Lx"], data["Ly"], data["dx"], data["e"])
    lines_y = create_lines(data["Lx"], data["Ly"], data["dy"], data["e"], y_axis=True)
    create_grids(lines_x, lines_y, data["Sx"], data["Sy"], doc)


if __name__ == "__main__":
    main()

Thanks.

@Jean-Marc Thanks for mentioning LearnRevitAPI :muscle:

@REDO_79 It’s True. We can learn anything on our own, but it means that we’ve decided to pay more with time upfront. Obviously, I’m biased since we talk about my own course… but I would save months of struggle if something like this existed when I needed it.

Luckily, today you get Chat GPT that can save you a lot of troubles in comparison to how we had to learn it before, but step-by-step tutorials can save you even more time. So it’s about what you value more right now time/money.

1 Like