28 oct. 2025

[Dynamo += Python] PythonNet série : WPF + IValueConverter




Un guide pratique pour implémenter une interface .NET (IValueConverter) en utilisant PythonNet3 dans un contexte WPF/XAML.


#DynamoBIM #Python #WPF #IValueConverter #PythonNet3 #AutodeskExpertElite #AutodeskCommunity 

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


Cet article s'appuie sur mon article précédent (WPF et PythonNet3) pour vous montrer un autre exemple concret : l'implémentation d'une interface IValueConverter en WPF, directement dans le XAML.


Étant donné que le runtime PythonNet génère les nouveaux types directement en mémoire, l'approche consiste à instancier une classe Python qui herite d'un type .Net (IValueConverter) afin que celui ci soit généré et puisse être réutilisé dans le XAML.

Dans l'exemple ci-dessous, nous allons implémenter un "Converter" qui changera dynamiquement la couleur de fond de la "TextBox" en rouge dès qu'un caractère interdit pour un nom de sous-projet Revit sera saisi, fournissant ainsi un retour utilisateur instantané.

import clr
import sys
import System
from System.Collections.Generic import List
invalid_chars = list(System.IO.Path.GetInvalidFileNameChars())
invalid_chars.remove("\n")
invalid_chars.remove("\t")
invalid_chars.remove("\r")
invalid_chars.extend(["[", "]", "{", "}", ">", "<", ";"])
print(invalid_chars)

clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument

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 *
import System.Windows.Controls.Primitives 
from System.Collections.Generic import List
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
from System.Windows.Data import IMultiValueConverter, Binding, MultiBinding, IValueConverter
from System.Windows.Media import Brushes

class Custom_CheckInvalidCharsConverter(IValueConverter):
    __namespace__ = "UtilsUI.Converters"
    #
    def __init__(self):
        super().__init__()

    def Convert(self, values, targetType, parameter, culture):
        text = values
        if text is not None and all(c not in invalid_chars for c in text):
            return Brushes.White
        else:
            return Brushes.Red

    def ConvertBack(self, value, targetType, parameter, culture):
        raise NotImplementedError()


class InitForm(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"
                xmlns:converter="clr-namespace:UtilsUI.Converters.System.Object__System.Windows.Data;assembly=Python.Runtime.Dynamic"
                Title="UI_SetWorkset" Height="600" Width="600" MinHeight="600" MinWidth="600"
                Background="White">
            <Window.Resources>
                <converter:IValueConverter__Custom_CheckInvalidCharsConverter x:Key="Custom_CheckInvalidCharsConverter" />
            </Window.Resources>
            <Grid Margin="5">
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*" />
                </Grid.ColumnDefinitions>
        
                <!-- Main Panel -->
                <Grid Background="PeachPuff" Grid.Row="0" Margin="5">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="1*" />
                        <ColumnDefinition Width="1*" />
                    </Grid.ColumnDefinitions>
        
                    <!-- Left GroupBox -->
                    <GroupBox Header="Sous Projets à créer (1 par ligne)" Margin="10">
                        <Grid>
                            <TextBox x:Name="RichTextBox1" Margin="5" 
                            VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
                            Background="{Binding RelativeSource={RelativeSource self},Path=Text, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource Custom_CheckInvalidCharsConverter}}"
                            AcceptsReturn="True"/>
                        </Grid>
                    </GroupBox>
        
                    <!-- Right GroupBox -->
                    <GroupBox Header="Sous Projets actuellement dans le projet" Grid.Column="1" Margin="10">
                        <ScrollViewer>
                            <TextBlock x:Name="LabelCurrentWkset" TextWrapping="Wrap" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="10"/>
                        </ScrollViewer>
                    </GroupBox>
                </Grid>
        
                <!-- Bottom Panel -->
                <Button x:Name="ButtonCreateWorksets" Grid.Row="1" Width="200" Height="40" Margin="10"
                    Content="Creer Sous Projets" Tag="CreateWorksets" 
                    HorizontalContentAlignment="Center">
                </Button>
            </Grid>
        </Window>'''
  
    def __init__(self, lstCurrentWkst):
        super().__init__()
        # generate Type in memory
        a = Custom_CheckInvalidCharsConverter()
        #
        self._lstCurrentWkstTxt = "-" + "\n-".join(lstCurrentWkst)
        self.lstCurrentWkst = lstCurrentWkst
        xr = System.Xml.XmlReader.Create(System.IO.StringReader(InitForm.string_xaml))
        root = System.Windows.Markup.XamlReader.Load(xr)
        # copy attributes from the root window to self
        for prop_info in root.GetType().GetProperties():
            if prop_info.CanWrite:
                try:
                    setattr(self, prop_info.Name, prop_info.GetValue(root)) 
                except Exception as ex:
                    print(ex, f"{prop_info.Name=}")
        # out variables
        self.userChoice = None
        self.lstWkstOut = []
        self.InitializeComponent()
        
    def InitializeComponent(self):
        #
        self.ButtonCreateWorksets = LogicalTreeHelper.FindLogicalNode(self, "ButtonCreateWorksets")
        self.ButtonCreateWorksets.Click += self.ButtonCreateWksetClick
        #
        self.LabelCurrentWkset = LogicalTreeHelper.FindLogicalNode(self, "LabelCurrentWkset")
        self.LabelCurrentWkset.Text = self._lstCurrentWkstTxt    
        #
        self.RichTextBox1 = LogicalTreeHelper.FindLogicalNode(self, "RichTextBox1")

    def ButtonCreateWksetClick(self, sender, e):
        try:
            self.userChoice = sender.Tag
            self.lstWkstOut = [x.strip() for x in self.RichTextBox1.Text.split("\n")]
            print(self.lstWkstOut)
            self.Close()
        except Exception as ex:
            print(traceback.format_exc())
            
outWorkSet = []
if doc.IsWorkshared:    
    collCurrentWkSet = FilteredWorksetCollector(doc).OfKind(WorksetKind.UserWorkset).ToWorksets()
    lstCurrentWkst = sorted([x.Name for x in collCurrentWkSet])
    objform  = InitForm(lstCurrentWkst)
    objform.ShowDialog()
    if objform.userChoice == "CreateWorksets":
        #wkstabl =     doc.GetWorksetTable()
        TransactionManager.Instance.EnsureInTransaction(doc)
        for wksetName in objform.lstWkstOut:
            print(wksetName)
            if not System.String.IsNullOrEmpty(wksetName) and wksetName not in objform.lstCurrentWkst:
                newWorkset = Workset.Create(doc, wksetName)
                outWorkSet.append(newWorkset.Name)
        TransactionManager.Instance.TransactionTaskDone()
OUT = outWorkSet


Quelques Explications

  • Création

a = Custom_CheckInvalidCharsConverter()

Cette ligne est cruciale. Elle force la classe Python à être instanciée, ce qui oblige Python.NET à générer les types .NET nécessaires en mémoire pour que le XamlReader  puisse les trouver.

  • Chargement

  xmlns:converter="clr-namespace:UtilsUI.Converters.System.Object__System.Windows.Data;assembly=Python.Runtime.Dynamic"

Cette ligne permet d'utiliser dans le  XAML des classes (typiquement des IValueConverter) qui ne font pas partie de WPF par défaut.

clr-namespace:UtilsUI.Converters.System.Object__System.Windows.Data

Cette partie spécifie la référence à un espace de noms .NET, c'est le nom complet de l'espace de noms .NET dans lequel la classe de conversion est définie.


Il est complété par un double underscore __ suivi de 
System.Windows.Data , car le XAML a besoin d'un type concret, pas seulement du clr-namespace.

L'environnement .NET ajoute souvent des suffixes "mangling" (déformation de nom) à l'espace de noms pour indiquer les types génériques ou les types de base nécessaires, garantissant que le type est unique pour le XAML au moment de l'exécution.

assembly=Python.Runtime.Dynamic

Cette partie spécifie que le code est chargé et exécuté par le runtime Python, qui génère dynamiquement l'assembly puis les types .NET en mémoire pour que WPF puisse le lire.

  • Définition de la classe comme ressource

  <Window.Resources>
     <converter:IValueConverter__Custom_CheckInvalidCharsConverter x:Key="Custom_CheckInvalidCharsConverter" />
  </Window.Resources>
Puis, on utilise cet espace de nom pour déclarer la classe de conversion comme une ressource.


« L’expérience est le nom que chacun donne à ses erreurs. »
Oscar Wilde

0 commentaires:

Enregistrer un commentaire