Skip to content

Commit 61181ac

Browse files
smparkesclaude
andcommitted
Add .NET 8 (coreclr) compatibility for Windows ARM64
pywebview fails to load on Windows ARM64 because pythonnet's .NET Framework (netfx) runtime is not available on ARM64 — the clr-loader dependency chain (NXPorts → dnlib) does not support ARM64: pythonnet/pythonnet#2523 The upstream fallback in r0x0r#1791 successfully switches to coreclr when netfx is unavailable, but pywebview then fails because several components assume .NET Framework. Use FolderBrowserDialog for the folder picker on coreclr, since the internal FileDialogNative types (IFileDialog, FOS_PICKFOLDERS) used by the existing OpenFolderDialog don't exist in .NET 8. On .NET Framework, the existing FileDialogNative reflection code is preserved unchanged, including its support for multiple folder selection. FolderBrowserDialog does not support multi-select; this limitation only affects coreclr users. (.NET 9 adds FolderBrowserDialog.Multiselect which could be used in a future update.) The netfx code is wrapped in an if/else on PYTHONNET_RUNTIME, which adds indentation; ignoring whitespace (https://github.com/r0x0r/pywebview/compare/master...smparkes:fix/dotnet8-coreclr?expand=1&w=1) makes the diff much simpler to review. Update bundled WebView2 interop DLLs from Microsoft.Web.WebView2 NuGet package 1.0.2957.106 to 1.0.3240.44 (netcoreapp3.0 target): https://www.nuget.org/packages/Microsoft.Web.WebView2/1.0.3240.44 The old DLLs referenced System.Windows.Forms.ContextMenu, which was removed in .NET 6+: https://learn.microsoft.com/en-us/dotnet/core/compatibility/windows-forms/6.0/apis-throw-argumentnullexception The new DLLs use ContextMenuStrip instead, which exists on both .NET Framework and .NET 8, so this change is safe for all platforms. Skip the .NET Framework registry check in _is_chromium() when running on coreclr, since coreclr does not register in the .NET Framework registry (HKLM\...\NDP\v4\Full). Also fix a potential UnboundLocalError by initializing net_key before the try block and guarding CloseKey. Add explicit clr.AddReference('Microsoft.Win32.SystemEvents') when PYTHONNET_RUNTIME=coreclr. This assembly is auto-loaded on .NET Framework but requires an explicit reference on coreclr. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 86d08c2 commit 61181ac

File tree

3 files changed

+106
-69
lines changed

3 files changed

+106
-69
lines changed
28 KB
Binary file not shown.
536 Bytes
Binary file not shown.

webview/platforms/winforms.py

Lines changed: 106 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
clr.AddReference('System.Collections')
2828
clr.AddReference('System.Threading')
2929
clr.AddReference('System.Reflection')
30+
if os.environ.get('PYTHONNET_RUNTIME') == 'coreclr':
31+
clr.AddReference('Microsoft.Win32.SystemEvents')
3032

3133
import System.Windows.Forms as WinForms # noqa: E402
3234
from Microsoft.Win32 import SystemEvents # noqa: E402
@@ -84,14 +86,17 @@ def edge_build(key_type, key, description=''):
8486

8587
return '0'
8688

89+
is_coreclr = os.environ.get('PYTHONNET_RUNTIME') == 'coreclr'
90+
net_key = None
8791
try:
88-
net_key = winreg.OpenKey(
89-
winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
90-
)
91-
version, _ = winreg.QueryValueEx(net_key, 'Release')
92+
if not is_coreclr:
93+
net_key = winreg.OpenKey(
94+
winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full'
95+
)
96+
version, _ = winreg.QueryValueEx(net_key, 'Release')
9297

93-
if version < 394802: # .NET 4.6.2
94-
return False
98+
if version < 394802: # .NET 4.6.2
99+
return False
95100

96101
build_versions = [
97102
{
@@ -121,7 +126,8 @@ def edge_build(key_type, key, description=''):
121126
except Exception as e:
122127
logger.exception(e)
123128
finally:
124-
winreg.CloseKey(net_key)
129+
if net_key is not None:
130+
winreg.CloseKey(net_key)
125131

126132
return False
127133

@@ -487,10 +493,8 @@ def _set_window_menu():
487493
def create_action_item(menu_line_item):
488494
action_item = WinForms.ToolStripMenuItem(menu_line_item.title)
489495
# Don't run action function on main thread
490-
action_item.Click += (
491-
lambda _, __, menu_line_item=menu_line_item: threading.Thread(
492-
target=menu_line_item.function
493-
).start()
496+
action_item.Click += lambda _, __, menu_line_item=menu_line_item: (
497+
threading.Thread(target=menu_line_item.function).start()
494498
)
495499
return action_item
496500

@@ -641,70 +645,103 @@ def alert(message):
641645
WinForms.MessageBox.Show(str(message))
642646

643647

644-
class OpenFolderDialog:
645-
foldersFilter = 'Folders|\n'
646-
flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
647-
windowsFormsAssembly = Assembly.LoadWithPartialName('System.Windows.Forms')
648-
iFileDialogType = windowsFormsAssembly.GetType(
649-
'System.Windows.Forms.FileDialogNative+IFileDialog'
650-
)
651-
OpenFileDialogType = windowsFormsAssembly.GetType('System.Windows.Forms.OpenFileDialog')
652-
FileDialogType = windowsFormsAssembly.GetType('System.Windows.Forms.FileDialog')
653-
createVistaDialogMethodInfo = OpenFileDialogType.GetMethod('CreateVistaDialog', flags)
654-
onBeforeVistaDialogMethodInfo = OpenFileDialogType.GetMethod('OnBeforeVistaDialog', flags)
655-
getOptionsMethodInfo = FileDialogType.GetMethod('GetOptions', flags)
656-
setOptionsMethodInfo = iFileDialogType.GetMethod('SetOptions', flags)
657-
fosPickFoldersBitFlag = (
658-
windowsFormsAssembly.GetType('System.Windows.Forms.FileDialogNative+FOS')
659-
.GetField('FOS_PICKFOLDERS')
660-
.GetValue(None)
661-
)
648+
if os.environ.get('PYTHONNET_RUNTIME') != 'coreclr':
649+
650+
class OpenFolderDialog:
651+
"""Folder picker dialog using FileDialogNative reflection.
652+
653+
Used on .NET Framework where the internal FileDialogNative types
654+
are available. Supports multiple folder selection.
655+
"""
662656

663-
vistaDialogEventsConstructorInfo = windowsFormsAssembly.GetType(
664-
'System.Windows.Forms.FileDialog+VistaDialogEvents'
665-
).GetConstructor(flags, None, [FileDialogType], [])
666-
adviseMethodInfo = iFileDialogType.GetMethod('Advise')
667-
unadviseMethodInfo = iFileDialogType.GetMethod('Unadvise')
668-
showMethodInfo = iFileDialogType.GetMethod('Show')
669-
670-
@classmethod
671-
def show(cls, parent=None, initialDirectory=None, allow_multiple=False, title=None):
672-
openFileDialog = WinForms.OpenFileDialog()
673-
openFileDialog.InitialDirectory = initialDirectory
674-
openFileDialog.Title = title
675-
openFileDialog.Filter = OpenFolderDialog.foldersFilter
676-
openFileDialog.AddExtension = False
677-
openFileDialog.CheckFileExists = False
678-
openFileDialog.DereferenceLinks = True
679-
openFileDialog.Multiselect = allow_multiple
680-
openFileDialog.RestoreDirectory = True
681-
682-
iFileDialog = OpenFolderDialog.createVistaDialogMethodInfo.Invoke(openFileDialog, [])
683-
OpenFolderDialog.onBeforeVistaDialogMethodInfo.Invoke(openFileDialog, [iFileDialog])
684-
options = OpenFolderDialog.getOptionsMethodInfo.Invoke(openFileDialog, [])
685-
options = options.op_BitwiseOr(OpenFolderDialog.fosPickFoldersBitFlag)
686-
OpenFolderDialog.setOptionsMethodInfo.Invoke(iFileDialog, [options])
687-
adviseParametersWithOutputConnectionToken = Array[Object](
688-
[
689-
OpenFolderDialog.vistaDialogEventsConstructorInfo.Invoke([openFileDialog]),
690-
UInt32(0),
691-
]
657+
foldersFilter = 'Folders|\n'
658+
flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
659+
windowsFormsAssembly = Assembly.LoadWithPartialName('System.Windows.Forms')
660+
iFileDialogType = windowsFormsAssembly.GetType(
661+
'System.Windows.Forms.FileDialogNative+IFileDialog'
692662
)
693-
OpenFolderDialog.adviseMethodInfo.Invoke(
694-
iFileDialog, adviseParametersWithOutputConnectionToken
663+
OpenFileDialogType = windowsFormsAssembly.GetType('System.Windows.Forms.OpenFileDialog')
664+
FileDialogType = windowsFormsAssembly.GetType('System.Windows.Forms.FileDialog')
665+
createVistaDialogMethodInfo = OpenFileDialogType.GetMethod('CreateVistaDialog', flags)
666+
onBeforeVistaDialogMethodInfo = OpenFileDialogType.GetMethod('OnBeforeVistaDialog', flags)
667+
getOptionsMethodInfo = FileDialogType.GetMethod('GetOptions', flags)
668+
setOptionsMethodInfo = iFileDialogType.GetMethod('SetOptions', flags)
669+
fosPickFoldersBitFlag = (
670+
windowsFormsAssembly.GetType('System.Windows.Forms.FileDialogNative+FOS')
671+
.GetField('FOS_PICKFOLDERS')
672+
.GetValue(None)
695673
)
696-
dwCookie = adviseParametersWithOutputConnectionToken.GetValue(1)
697-
try:
698-
result = OpenFolderDialog.showMethodInfo.Invoke(
699-
iFileDialog, [parent.Handle if parent else None]
674+
675+
vistaDialogEventsConstructorInfo = windowsFormsAssembly.GetType(
676+
'System.Windows.Forms.FileDialog+VistaDialogEvents'
677+
).GetConstructor(flags, None, [FileDialogType], [])
678+
adviseMethodInfo = iFileDialogType.GetMethod('Advise')
679+
unadviseMethodInfo = iFileDialogType.GetMethod('Unadvise')
680+
showMethodInfo = iFileDialogType.GetMethod('Show')
681+
682+
@classmethod
683+
def show(cls, parent=None, initialDirectory=None, allow_multiple=False, title=None):
684+
openFileDialog = WinForms.OpenFileDialog()
685+
openFileDialog.InitialDirectory = initialDirectory
686+
openFileDialog.Title = title
687+
openFileDialog.Filter = OpenFolderDialog.foldersFilter
688+
openFileDialog.AddExtension = False
689+
openFileDialog.CheckFileExists = False
690+
openFileDialog.DereferenceLinks = True
691+
openFileDialog.Multiselect = allow_multiple
692+
openFileDialog.RestoreDirectory = True
693+
694+
iFileDialog = OpenFolderDialog.createVistaDialogMethodInfo.Invoke(openFileDialog, [])
695+
OpenFolderDialog.onBeforeVistaDialogMethodInfo.Invoke(openFileDialog, [iFileDialog])
696+
options = OpenFolderDialog.getOptionsMethodInfo.Invoke(openFileDialog, [])
697+
options = options.op_BitwiseOr(OpenFolderDialog.fosPickFoldersBitFlag)
698+
OpenFolderDialog.setOptionsMethodInfo.Invoke(iFileDialog, [options])
699+
adviseParametersWithOutputConnectionToken = Array[Object](
700+
[
701+
OpenFolderDialog.vistaDialogEventsConstructorInfo.Invoke([openFileDialog]),
702+
UInt32(0),
703+
]
704+
)
705+
OpenFolderDialog.adviseMethodInfo.Invoke(
706+
iFileDialog, adviseParametersWithOutputConnectionToken
700707
)
701-
if result == 0:
702-
return tuple(openFileDialog.FileNames)
708+
dwCookie = adviseParametersWithOutputConnectionToken.GetValue(1)
709+
try:
710+
result = OpenFolderDialog.showMethodInfo.Invoke(
711+
iFileDialog, [parent.Handle if parent else None]
712+
)
713+
if result == 0:
714+
return tuple(openFileDialog.FileNames)
703715

704-
return None
716+
return None
717+
718+
finally:
719+
OpenFolderDialog.unadviseMethodInfo.Invoke(iFileDialog, [UInt32(dwCookie)])
720+
721+
else:
722+
723+
class OpenFolderDialog:
724+
"""Folder picker dialog using FolderBrowserDialog.
705725
706-
finally:
707-
OpenFolderDialog.unadviseMethodInfo.Invoke(iFileDialog, [UInt32(dwCookie)])
726+
Used on coreclr (.NET 8) where the internal FileDialogNative types
727+
don't exist. FolderBrowserDialog does not support multiple folder
728+
selection; the allow_multiple parameter is accepted but ignored.
729+
"""
730+
731+
@classmethod
732+
def show(cls, parent=None, initialDirectory=None, allow_multiple=False, title=None):
733+
dlg = WinForms.FolderBrowserDialog()
734+
if title:
735+
dlg.Description = title
736+
dlg.UseDescriptionForTitle = True
737+
if initialDirectory:
738+
dlg.SelectedPath = initialDirectory
739+
740+
result = dlg.ShowDialog()
741+
if result == WinForms.DialogResult.OK:
742+
return (dlg.SelectedPath,)
743+
744+
return None
708745

709746

710747
_main_window_created = Event()

0 commit comments

Comments
 (0)