Comment utiliser WebView2 dans une interface WPF avec PythonNet3
Microsoft.Web.WebView2.Core est disponible dans le
contexte Dynamo ? , ce qui permet en conséquence d'utiliser WebView2 dans
vos interfaces WPF.
-
Explication
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).
- Créer ou récupérer un
CoreWebView2Environment. - Initialiser le contrôle de façon asynchrone.
- Attendre que le moteur soit chargé via
CoreWebView2InitializationCompleted. - Charger le HTML.
- Déclarer / Gérer les événements JavaScript/.NET
(création d'un pont viaCoreWebView2.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 |
Le Petit Prince (chapitre 21) Antoine de Saint-Exupéry,





0 commentaires:
Enregistrer un commentaire