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.
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