25 oct. 2026

[Dynamo += Python] WPF + WebView2

 


Comment utiliser WebView2 dans une interface WPF avec PythonNet3


#DynamoBIM #Revit #Python #RevitAPI #WPF #WebView2  #Pandas #AutodeskExpertElite #AutodeskCommunity 

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




Savez-vous que l'assembly Microsoft.Web.WebView2.Core est disponible dans le contexte Dynamo ? , ce qui permet en conséquence d'utiliser WebView2 dans vos interfaces WPF.


  • Explication


Par rapport à WebBrowser (obsolète), WebView2 peut sembler plus complexe à programmer, mais il offre la puissance, la sécurité et la compatibilité totale d'un moteur Web moderne (Chromium).


La mise en œuvre de WebView2 nécessite de gérer plusieurs étapes.
  1. Créer ou récupérer un CoreWebView2Environment.
  2. Initialiser le contrôle de façon asynchrone.
  3. Attendre que le moteur soit chargé via CoreWebView2InitializationCompleted.
  4. Charger le HTML.
  5. Déclarer / Gérer les événements JavaScript/.NET
    (création d'un pont via CoreWebView2.WebMessageReceived).

Avec WebView2, nous devons attendre que CoreWebView2 soit prêt. Ce surcoût de code vient du cycle de vie plus explicite du navigateur.

En revanche, une fois le pont configuré, nous disposons d'une interface plus moderne et plus flexible.

L'événement JavaScript chrome.webview.postMessage(...) envoie un "message" à Python/.NET. En retour, .NET peut exécuter du JavaScript ou envoyer un message à la page. Cela facilite la création d'une interface web pilotée par Revit.


  • Exemple

Voici un exemple avec un DataFrame Pandas et la bibliothèque open-source de visualisation de données Plotly.

Nous passons ici par une classe d'extension Pandas pour créer des propriétés dynamiques d'un DataFrame, puis les afficher dans une fenêtre WPF.


vidéo


.


import clr
import sys
import System
#import net library
from System import Array
from System.Collections.Generic import List, IList, Dictionary

from System import Environment
from System.IO import Path, Directory
from System import Action
from System.Threading.Tasks import Task
#import Revit API
clr.AddReference('RevitAPI')
import Autodesk
from Autodesk.Revit.DB import *
import Autodesk.Revit.DB as DB
#import specify namespace
from Autodesk.Revit.DB.Electrical import *

clr.AddReference('RevitAPIUI')
import Autodesk.Revit.UI as RUI
from Autodesk.Revit.UI import *


#import transactionManager and DocumentManager (RevitServices is specific to Dynamo)
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc = DocumentManager.Instance.CurrentDBDocument
uiapp = DocumentManager.Instance.CurrentUIApplication
uidoc = uiapp.ActiveUIDocument

clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)

clr.AddReference("System.Reflection")
from System.Reflection import BindingFlags

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.Media.Imaging import BitmapCacheOption, BitmapCreateOptions, TransformedBitmap
from System.Windows.Media import ScaleTransform
from System.Windows import Application, WindowState

clr.AddReference("Microsoft.Web.WebView2.Core")
from Microsoft.Web.WebView2.Core import CoreWebView2Environment

import os
import tempfile
import time
import json
import traceback
import pandas as pd
import re
import plotly.express as px
import plotly.graph_objects as go


APPDATADIR = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData)


class MainForm(Window):
    __namespace__ = "Dynamo.PandasViewer_WxeZ74"
    #
    init_htmlDf = """\
        <html>
        <head>
        <meta http-equiv="X-UA-Compatible" content="IE=9">
        <style> 
          table, th, td {font-size:10pt; border:1px solid black; border-collapse:collapse; text-align:left;}
          th, td {padding: 5px;}
          tr:nth-child(even) {background: #E0E0E0;}
          tr:hover {background: silver; cursor: pointer;}
        </style>
        </head>
          <body>
          </body>
        </html>
        """
    string_xaml = '''
    <Window
        x:Name="PandasViewerForm"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        Title="PandasViewer"
        Width="600"
        Height="540"
        MinWidth="300"
        MinHeight="200"
        WindowStartupLocation="CenterScreen"
        ResizeMode="CanResizeWithGrip">
    
        <Grid>
            <wv2:WebView2
                x:Name="webBrowser1"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"/>
        </Grid>
    </Window>'''
  
    def __init__(self):
        super().__init__()
        self.htmlDf = self.init_htmlDf
        self._core = None
        self._pending_html = None
        self._initialized = False
        self.start_time = time.time()
        # Solution with PythonNetFixer module
        xr = XmlReader.Create(StringReader(MainForm.string_xaml))
        self.root = XamlReader.Load(xr) 
        # copy attributes from the root window to self (NOT NECESSARY if you used WPF_Utils.LoadComponent(self, string_xaml))
        for prop_info in self.root.GetType().GetProperties():
            if prop_info.CanWrite:
                try:
                    setattr(self, prop_info.Name, prop_info.GetValue(self.root)) 
                except Exception as ex:
                    print(ex, f"{prop_info.Name=}")
        self.InitializeComponent()
        
    def InitializeComponent(self):
        reader = XmlReader.Create(StringReader(MainForm.string_xaml))
        while reader.Read():
            # Check if it's an element node
            if reader.NodeType == System.Xml.XmlNodeType.Element:
                # Check for x:Name attribute
                name = next((reader.GetAttribute(x) for x in ["x:Name", "Name"] if reader.GetAttribute(x) is not None ), None)
                if name:
                    uiElement = LogicalTreeHelper.FindLogicalNode(self.root, name)
                    if uiElement:
                        setattr(self, name, uiElement)
        #
        self.ContentRendered  += self.WindowRendered

        self.Loaded += self.On_loaded
        self.webBrowser1.CoreWebView2InitializationCompleted += self.On_Initialized

            
    def WindowRendered(self, sender, e):
        print(f"UI show in {time.time() - self.start_time} seconds")
        
    def On_loaded(self, sender, e):
        if getattr(self, "_webview_started", False):
            return
        self._webview_started = True
    
        try:
            folder = Path.Combine(APPDATADIR, "DynamoPandasViewer", "WebView2")
            Directory.CreateDirectory(folder)
    
            task = CoreWebView2Environment.CreateAsync(None, folder, None)
    
            def completed(t):
                def apply():
                    if t.IsFaulted:
                        print("WebView2 error:", t.Exception)
                    else:
                        self.webBrowser1.EnsureCoreWebView2Async(t.Result)
    
                self.Dispatcher.BeginInvoke(Action(apply))
    
            self._environment_continuation = task.ContinueWith(
                Action[Task](completed)
            )
        except Exception:
            print(traceback.format_exc())
    
    
    def On_Initialized(self, sender, e):
        if not e.IsSuccess:
            print("WebView2 initialization failed:", e.InitializationException)
            return
    
        self._core = self.webBrowser1.CoreWebView2
        self._core.WebMessageReceived += self.On_web_message
        self.Navigate_to_html(getattr(self, "_pending_html", None) or self.htmlDf)
    
    
    def Navigate_to_html(self, html):
        if self._core is None:
            self._pending_html = html
            return
        #
        path = os.path.join(tempfile.gettempdir(), f"dynamo_plotly_viewer.html")
        #
        with open(path, "w", encoding="utf-8") as f:
            f.write(str(html))
    
        self._core.Navigate("file:///" + path.replace("\\", "/"))
    
    
    def On_web_message(self, sender, e):
        try:
            message = json.loads(e.TryGetWebMessageAsString())
            if message.get("type") != "selectElement":
                return
    
            value = str(message.get("elementId", ""))
            if value.isdigit():
                elemId = ElementId(int(value))
                uidoc.Selection.SetElementIds(List[ElementId]([elemId]))
                uidoc.ShowElements(List[ElementId]([elemId]))
        except Exception:
            print(traceback.format_exc())
        
    #@property
    def get_HtmlText(self):
        return self.htmlDf
        
    def set_HtmlText(self, value):
        self.htmlDf = value
        if self._core is None:
            self._pending_html = value
        else:
            self.Navigate_to_html(self.htmlDf)
    
    HtmlText = clr.clrproperty(str, get_HtmlText, set_HtmlText)
        

    def ResetHtml(self):
        self.HtmlText = self.init_htmlDf
    
    
@pd.api.extensions.register_dataframe_accessor("viewer")
class HtmlAccessor:

    def __init__(self, df_obj):
        self._obj = df_obj

    def _get_base_html(self):
        html_content = getattr(viewerForm, "htmlDf", "")
        if not html_content or "</body>" not in html_content:
            return "<html><body></body></html>"
        return html_content

    def Show(self, n=0, label=""):
        html_df = self._get_base_html()

        df_to_show = self._obj if n == 0 else self._obj.head(n)
        html_to_add = df_to_show.to_html()

        if label:
            html_to_add = f"<p><b>{label}</b></p>" + html_to_add

        updated_html = re.sub(
                r"</body>", html_to_add + r"<br><hr><br></body>", html_df, flags=re.I
        )

        viewerForm.HtmlText = updated_html
        viewerForm.htmlDf = updated_html
        
    def Plot3D(self, contours_points=None):
        
        post_script = """
            document.getElementById('{plot_id}').on('plotly_click', e => {
                const id = e.points[0].customdata?.[0];
                if (id != null && window.chrome?.webview)
                    window.chrome.webview.postMessage(JSON.stringify({
                        type: 'selectElement', elementId: String(id)
                    }));
            });
        """
        df = self._obj
        cfg = df.attrs.get("plot3d", {})
    
        fig = px.scatter_3d(
            df,
            x=cfg.get("x", "X"),
            y=cfg.get("y", "Y"),
            z=cfg.get("z", "Z"),
            color=cfg.get("color", "Name"),
            size=cfg.get("size", "Power"),
            size_max=100,
            hover_data=cfg.get("hover_data", ["ElementId", "Name", "Power"]),
            custom_data=["ElementId"]
        )
    
        for i, pts in enumerate(contours_points or []):
            x, y, z = zip(*[(p.X, p.Y, p.Z) for p in pts])
            fig.add_trace(go.Scatter3d(
                x=x, y=y, z=z, 
                mode="lines",
                name=f"Contour sol {i + 1}",
                line=dict(color="gray", width=1),
                hoverinfo="skip"
            ))
    
        html = fig.to_html(full_html=True, include_plotlyjs=True, post_script=post_script)
    
        html = re.sub(
            r"</body>", lambda m: html + m[0],
            self._get_base_html(), count=1, flags=re.I
        )
    
        viewerForm.HtmlText = viewerForm.htmlDf = html
        

def find_viewer():
    app = Application.Current

    if app is None:
        return None, None

    for window in app.Windows:
        print(window.Title)
        try:
            if window.Title != "PandasViewer":
                continue
            # The WebView2 control is declared in the XAML.
            web = window.FindName("webBrowser1")

            if web is None:
                _web = getattr(window, "webBrowser1", None)
                _htmlText = getattr(window, "HtmlText", None)
            #
            if _web is not None and _htmlText is not None:
                return window, _web

        except Exception:
            continue
    return None, None


viewerForm, webView = find_viewer()

if viewerForm is None :
    viewerForm = MainForm()
    viewerForm.Title = "PandasViewer"
    viewerForm.Show()

else:
    viewerForm.ResetHtml()
    
    if not viewerForm.IsVisible:
        viewerForm.Show()

    if viewerForm.WindowState == WindowState.Minimized:
        viewerForm.WindowState = WindowState.Normal

    viewerForm.Activate()
     
    
def get_floor_contours(xdoc):
    return [
        points
            for f in FilteredElementCollector(xdoc).OfClass(Floor).WhereElementIsNotElementType()
                for r in HostObjectUtils.GetTopFaces(f)
                    for face in [f.GetGeometryObjectFromReference(r)]
                        if face.Area > 50
                            for points in [[p for loop in face.GetEdgesAsCurveLoops() for c in loop for p in c.Tessellate()]]
                                if len(points) >= 3
    ]

lnkinstance = UnwrapElement(IN[0])
doc_lnk = lnkinstance.GetLinkDocument()

all_panels = (FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_ElectricalEquipment).WhereElementIsNotElementType()
            .Where(System.Func[DB.Element, System.Boolean](lambda e : e.Symbol.Family.get_Parameter(BuiltInParameter.FAMILY_CONTENT_PART_TYPE).AsInteger() == 14))\
            .ToList()

)

df = pd.DataFrame([], columns=["ElementId", "X", "Y", "Z", "Power", "Name"])


for panel in all_panels:
    loc = panel.Location.Point
    power_str = panel.get_Parameter(BuiltInParameter.RBS_ELEC_PANEL_TOTALLOAD_PARAM).AsValueString()
    print(power_str)
    power = float(re.search(r"([0-9]+(?:\.[0-9]+)?)\s*", power_str).group(1))
    df.loc[len(df) + 1] = [panel.Id.Value, loc.X, loc.Y, loc.Z, power, panel.Name]
    
df.viewer.Show(label="list of Electrical Panel")   
contours = get_floor_contours(doc_lnk)    
df.viewer.Plot3D(contours)

OUT = repr(df)


  • Comparaison avec WebBrowser


Fonctionnalité WebBrowser classique WebView2
Afficher HTML / CSS Oui Oui
Afficher Plotly 3D Possible, mais moteur ancien Oui, Chromium moderne
Rotation et zoom 3D Dépend de la version d'IE Oui
Clic sur une sphère (plotly_click) Dépend de la compatibilité JavaScript Oui
JavaScript → Python/.NET ObjectForScripting WebMessageReceived
Python/.NET → JavaScript InvokeScript() ExecuteScriptAsync()
Initialisation asynchrone du moteur Non, généralement plus simple Oui
Compatibilité avec les bibliothèques JS modernes Limitée Excellente




« C’est le temps que tu as perdu pour ta rose qui fait ta rose si importante. » 
Le Petit Prince (chapitre 21) 
Antoine de Saint-Exupéry,


0 commentaires:

Enregistrer un commentaire