6 sept. 2024

[Dynamo += Python] Selection via une DataGrid (WPF)





Un exemple de sélection d'éléments au travers d'une interface de type 'DataGrid'

#DynamoBIM #Revit #Python #DataTable #WPF #AutodeskExpertElite #AutodeskCommunity 

if this article is not in your language, use the Google Translate widget ⬈ (bottom of page for Mobile version )


  • Introduction
Dans cet article, nous allons explorer comment créer une interface utilisateur en WPF (Windows Presentation Foundation) pour sélectionner des éléments à l'aide d'un script Python dans Dynamo. Cette approche est particulièrement utile lorsque vous travaillez avec des données complexes et que vous souhaitez offrir à l'utilisateur une interface graphique fluide pour interagir avec ces données.

L'exemple ci-dessous utilise PythonNet ou IronPython pour intégrer le WPF dans Dynamo, permettant la sélection d'éléments au travers d'une grille.


  • Entrées du nœud Python
       1. Liste des noms des colonnes : Il s'agit d'une liste qui représente les en-têtes des colonnes de la grille WPF.

       2. Données d'entrée : Ce sont des données sous forme de liste de listes, au format List[List[objet]].  Chaque sous-liste représente une ligne de données, avec la dernière colonne contenant la valeur de sortie.
    • Exemple de données :
                    [
                        [keyA, keyB, keyC, outValue],
                        [keyA, keyB, keyC, outValue],
                        [keyA, keyB, keyC, outValue]
                    ]

       3. Nom de la colonne de sortie : Il s'agit du nom de la colonne qui contiendra les valeurs de sortie sélectionnées par l'utilisateur.

       4. Paramètre de visibilité : Un paramètre booléen pour cacher ou afficher la colonne de sortie.


    • Aperçu de l'interface utilisateur






    code Python
    
    __author__ = "Cyril POUPIN"
    __license__ = "MIT license"
    __version__ = "1.0.2"
    
    import clr
    import sys
    import System
    
    clr.AddReference('System.Data')
    from System.Data import *
    
    clr.AddReference("System.Xml")
    clr.AddReference("PresentationFramework")
    clr.AddReference("System.Xml")
    clr.AddReference("PresentationCore")
    clr.AddReference("System.Windows")
    import System.Windows.Controls 
    from System.Windows.Controls import *
    from System.IO import StringReader
    from System.Xml import XmlReader
    from System.Windows import LogicalTreeHelper 
    from System.Windows.Markup import XamlReader, XamlWriter
    from System.Windows import Window, Application
    
    import traceback
    
    class MainWindow(Window):
        string_xaml = '''
        <Window 
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            Title="Selection"
            Height="700" MinHeight="700"
            Width="700" MinWidth="780"
            x:Name="MainWindow">
            <Window.Resources>
                <!-- perform Single click checkbox selection in WPF DataGrid like DataGridView.Editmode = EditOnEnter on WinForm -->
                <Style TargetType="DataGridCell">
                  <Style.Triggers>
                    <MultiTrigger>
                      <MultiTrigger.Conditions>
                        <Condition Property="DataGridCell.IsReadOnly" Value="False" />
                        <Condition Property="DataGridCell.IsMouseOver" Value="True" />
                      </MultiTrigger.Conditions>
                      <Setter Property="IsEditing" Value="True" />
                      <Setter Property="Background" Value="LightGreen" />
                    </MultiTrigger>
                  </Style.Triggers>
                </Style>
            </Window.Resources>
            <Grid Width="auto" Height="auto">
                <Grid.RowDefinitions>
                    <RowDefinition Height="30" />
                    <RowDefinition />
                    <RowDefinition Height="60" />
                </Grid.RowDefinitions>
                <Label
                    x:Name="label1"
                    Content="Selection"
                    Grid.Column="0" Grid.Row="0"
                    HorizontalAlignment="Left" VerticalAlignment="Bottom"
                    Margin="8,0,366.6,5"
                    Width="415" Height="25" />
                <DataGrid
                    x:Name="dataGrid"
                    ItemsSource="{Binding}"
                    Grid.Column="0" Grid.Row="1"
                    HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                    Margin="8,3,8,7"
                    SelectionUnit="Cell"
                    CanUserAddRows="False">
                </DataGrid>
                <Button
                    x:Name="buttonCancel"
                    Content="Annuler"
                    Grid.Column="0" Grid.Row="2"
                    HorizontalAlignment="Left" VerticalAlignment="Bottom"
                    Margin="18,13,0,10"
                    Height="30" Width="120">
                </Button>
                <Button
                    x:Name="buttonOK"
                    Content="OK"                
                    Grid.Column="0" Grid.Row="2"
                    HorizontalAlignment="Right" VerticalAlignment="Bottom"
                    Margin="0,12,22,10"
                    Height="30" Width="120">
                </Button>
            </Grid>
        </Window>'''
      
        def __init__(self, tableData, outColumnName, hide_out_Column):
            super().__init__()
            self._tableData = tableData
            self._outColumnName = outColumnName
            self._hide_out_Column = hide_out_Column
            #
            xr = XmlReader.Create(StringReader(MainWindow.string_xaml))
            self.winLoad = XamlReader.Load(xr) 
            self.outSelection = []
            self.InitializeComponent()
            
        def InitializeComponent(self):
            try:
                self.Content = self.winLoad.Content
                #
                self.dataGrid = LogicalTreeHelper.FindLogicalNode(self.winLoad, "dataGrid")
                self.dataGrid.SelectedCellsChanged  += self.DataGrid_CurrentCellChanged
                #
                self.buttonCancel = LogicalTreeHelper.FindLogicalNode(self.winLoad, "buttonCancel")
                self.buttonCancel.Click += self.ButtonCancelClick
                #
                self.buttonOK = LogicalTreeHelper.FindLogicalNode(self.winLoad, "buttonOK")
                self.buttonOK.Click += self.ButtonOKClick
                #
                self.winLoad.Loaded += self.OnLoad
                #
                # set DataContext to Enable Binding with input DataTable
                self.dataGrid.DataContext = self._tableData.DefaultView
            except Exception as ex:
                print(traceback.format_exc())
            
        def DataGrid_CurrentCellChanged(self, sender, e):
            currentDataGrid = sender
            try:
                lst_Selected = [cell.Item["Selection"]  for cell in currentDataGrid.SelectedCells if isinstance(cell.Item["Selection"], (bool, System.Boolean))]
                print("currentDataGrid.SelectedCells", currentDataGrid.SelectedCells.Count, lst_Selected)
                for idx, cell in enumerate(currentDataGrid.SelectedCells):
                    dataGridRowView =cell.Item
                    if currentDataGrid.CurrentCell.Column.DisplayIndex == 0 :
                        # use the last select value and get the reverse 
                        dataGridRowView["Selection"] = not lst_Selected[-1]
                # Refresh DataGrid
                currentDataGrid.Items.Refresh()
            except Exception as ex:
                print(traceback.format_exc())
                
        def OnLoad(self, sender, e):
            print("UI loaded")
            try:
                if self._hide_out_Column:
                    out_column = next((c for c in  self.dataGrid.Columns if str(c.Header) == self._outColumnName ), None)
                    if out_column is not None:
                        self.dataGrid.Columns.get_Item(out_column.DisplayIndex).MaxWidth = 0
                        self.dataGrid.Items.Refresh()
            except Exception as ex:
                print(traceback.format_exc())
    
        def ButtonCancelClick(self, sender, e):
            self.outSelection = []
            self.winLoad.Close()
            
        def ButtonOKClick(self, sender, e):
            try:
                # get result from input Data (Binding)
                self.outSelection = [row[self._outColumnName] for row  in self._tableData.Rows if row["Selection"] == True]
                self.winLoad.Close()
            except Exception as ex:
                print(traceback.format_exc())
                
    def get_DataTableFromList(header_array, data):
        dt = DataTable("CustomData")
        # Create columns
        dt.Columns.Add("Selection", System.Boolean)  # add Column selection
        for item, value in zip(header_array, data[0]):
            try:
                type_value = value.GetType()
            except:
                type_value = type(value)
            dt.Columns.Add(item, type_value)
        # Add rows
        for sublst_values in data:
            sublst_values.insert(0, False) # for Column selection
            dt.Rows.Add(*sublst_values)
        return dt
            
    header_array = IN[0]
    lst_data = IN[1]
    out_ColumnName = IN[2]
    hide_out_Column = IN[3]
                    
    dt = get_DataTableFromList(header_array, lst_data)
    objWindow = MainWindow(dt, out_ColumnName, hide_out_Column)
    objWindow.winLoad.ShowDialog()
    
    OUT = objWindow.outSelection
    
    --


    Quelques Explications 

    • Utilisation du Trigger 
    Le Trigger dans le XAML active l'édition d'une cellule lorsqu'elle n'est pas en lecture seule et que la souris la survole. Cela permet un comportement de sélection en un clic, en activant immédiatement la cellule, et change également le fond en vert clair pour indiquer la sélection.

    • Sélection multiple 
    La méthode DataGrid_CurrentCellChanged permet de gérer la multiple sélection. Elle vérifie l'état de la colonne "Selection" pour chaque cellule sélectionnée et bascule la valeur entre sélectionnée ou non. La méthode met ensuite à jour visuellement la grille avec Items.Refresh().

    • La libraire wpf d'IronPython
    La librairie WPF n'est pas incluse directement avec PythonNet. Toutefois, le code ci-dessus fonctionne avec IronPython3 et PythonNet. Pour IronPython3, vous pouvez l'utiliser en ajoutant les lignes suivantes :
    
    # load ironpython wpf
    clr.AddReference("IronPython.Wpf")
    import wpf
    # some code
    class MyWindow(Window):
    	string_xaml = # xaml content
        def __init__(self):
        	xr = XmlReader.Create(StringReader(MyWindow.string_xaml))
            wpf.LoadComponent(self, xr)
            # ↓↓ No Need this ↓↓ because 'self.dataGrid' already exist 
            # self.dataGrid = LogicalTreeHelper.FindLogicalNode(self.winLoad, "dataGrid")
    
    Ainsi, vous pouvez charger le XAML de manière similaire sans avoir à redéfinir chaque élément UI.

    • WPF dans Python + Dynamo
    Contrairement à WinForm, lorsque vous utilisez WPF dans Dynamo, veillez à bien gérer toutes les erreurs dans votre code Python pour éviter les plantages de l'application hôte. Assurez-vous de toujours capturer les exceptions avec des blocs try-except .

    2 commentaires: