ReactOS 上 C# System.Windows.Forms 入门






4.90/5 (7投票s)
如何在 ReactOS 中编译和运行第一个使用 System.Windows.Forms 的 C# GUI 应用程序。
下载 WindowsFormsApplication01.zip
引言
本文基于提示 ReactOS 上的 C# 入门。文章最初为 ReactOS 0.4.7 编写,现已更新至 ReactOS 0.4.8...0.4.10。
本文展示了如何避免有关通用对话框的 System.Windows.Forms
包装器的陷阱。我知道——Forms
不是最新技术,但它适用于大多数需求,并且在 ReactOS 上可用。
GUI 可能遇到的最令人不愉快的缺点之一是无法缩放。而(惊喜)Forms
GUI 能够很好地缩放——因此,我可以放心地使用它。
为什么不使用 GTK# 2.12.11?因为我想要创建一个可以无需额外工作/安装即可在 Windows 和 ReactOS 之间共享的解决方案。实际上,MONO 编译的示例应用程序在我的 Windows 10 Enterprise 64 位版本上运行,无需重新编译或任何更改!
背景
本文使用了与提示 ReactOS 上的 C# 入门相同的底层架构(关于为什么选择这个,请参阅提示)。
- 从在 ORACLE VirtualBox 虚拟机版本 5.1.26 中运行的 ReactOS 0.4.7 到在 ORACLE VirtualBox 虚拟机版本 5.2.16 中运行的 ReactOS 0.4.10。
- Microsoft .NET 4.0 作为运行时环境。
- 从 MONO 3.2.3 到 MONO 4.3.2 作为编译器。
- 带有 NppExec 插件的 Notepad++ 用于编辑代码和运行编译器。
提供了一个包含示例应用程序的完整源代码供下载。
我的具有可伸缩 GUI 的示例应用程序结构如下:
并且它在 ReactOS 上缩放效果很好(493px * 209px 和 610px * 292px)
使用代码
示例应用程序侧重于三个通用对话框
- MessageBox
- OpenFileDialog
- SaveFileDialog
MessageBox 类
原始框架的 MessageBox
类的问题在于 ShowCore(...)
中的这段代码:
UnsafeNativeMethods.LoadLibraryFromSystemPathIfAvailable(ExternDll.Shell32)
对于 WinXP 和 ReactOS 0.4.7,LoadLibraryFromSystemPathIfAvailable(...)
方法不起作用。但是 LoadLibrary(...)
方法可以承担这项工作。这需要采用 MessageBox.
ShowCore(...)
方法,因此必须采用整个 MessageBox
类。
框架的大部分原始源代码——包括 MessageBox
类——Microsoft 提供用于“**参考使用**”,参见。
采用的类使用 System.Windows.FormsROS
命名空间而不是 System.Windows.Forms
。
ShowCore(...)
方法包含对 internal
框架方法或类的几个调用,因此必须用反射等效项替换这些调用。这里有一些例子:
/// <summary>Displays a message box with specified owner window, text, caption, buttons, icon,
/// default button and style as well as help info.</summary>
/// <param name="owner">The owner window.</param>
/// <param name="text">The message text to display.</param>
/// <param name="caption">The message box caption.</param>
/// <param name="buttons">The buttons to display.</param>
/// <param name="icon">The icon to display.</param>
/// <param name="defaultButton">The default button, that is invoked in [Return] key.</param>
/// <param name="options">The display options (e.g. right to left).</param>
/// <param name="showHelp">Determine whether to display the 'Help' button.</param>
/// <returns>The <see cref="DialogResult"/> that represents the button that closed the message box.</returns>
private static System.Windows.Forms.DialogResult ShowCore(System.Windows.Forms.IWin32Window owner,
string text, string caption,
System.Windows.Forms.MessageBoxButtons buttons,
System.Windows.Forms.MessageBoxIcon icon,
System.Windows.Forms.MessageBoxDefaultButton defaultButton,
System.Windows.Forms.MessageBoxOptions options,
bool showHelp)
{
// ORIGINALLY: if (!ClientUtils.IsEnumValid(buttons, (int)buttons, (int)MessageBoxButtons.OK, (int)MessageBoxButtons.RetryCancel))
if (!((bool)Reflectables.Call(Reflectables.ClientUtilsType, "IsEnumValid", null, new object[] { buttons, (int)buttons, (int)System.Windows.Forms.MessageBoxButtons.OK, (int)System.Windows.Forms.MessageBoxButtons.RetryCancel }, new Type[] { typeof(Enum), typeof(int), typeof(int), typeof(int) }, false)))
{
throw new InvalidEnumArgumentException("buttons", (int)buttons, typeof(System.Windows.Forms.MessageBoxButtons));
}
// valid values are 0x0 0x10 0x20 0x30 0x40, chop off the last 4 bits and check that it's between 0 and 4.
// ORIGINALLY: if (!WindowsFormsUtils.EnumValidator.IsEnumWithinShiftedRange(icon, /*numBitsToShift*/4, /*min*/0x0,/*max*/0x4))
if (!((bool)Reflectables.Call(Reflectables.WindowsFormsUtilsEnumValidatorType, "IsEnumWithinShiftedRange", null, new object[] { icon, /*numBitsToShift*/4, /*min*/0x0,/*max*/0x4 })))
{
throw new InvalidEnumArgumentException("icon", (int)icon, typeof(System.Windows.Forms.MessageBoxIcon));
}
// valid values are 0x0 0x100, 0x200, chop off the last 8 bits and check that it's between 0 and 2.
// ORIGINALLY: if (!WindowsFormsUtils.EnumValidator.IsEnumWithinShiftedRange(defaultButton, /*numBitsToShift*/8, /*min*/0x0,/*max*/0x2))
if (!((bool)Reflectables.Call(Reflectables.WindowsFormsUtilsEnumValidatorType, "IsEnumWithinShiftedRange", null, new object[] { defaultButton, /*numBitsToShift*/8, /*min*/0x0,/*max*/0x2 })))
{
throw new InvalidEnumArgumentException("defaultButton", (int)defaultButton, typeof(System.Windows.Forms.DialogResult));
}
...
反射
所有用于用反射等效项替换原始框架的 internal
方法或类的辅助属性和方法都集中在 System.Windows.FormsROS.Reflectables
中。Reflectables
类提供了:
ClientUtilsType
属性,用于获取internal
框架类System.Windows.Forms.ClientUtilsType
的类型。IntSecurityType
属性,用于获取internal
框架类System.Windows.Forms.IntSecurity
的类型。UnsafeNativeMethodsType
属性,用于获取internal
框架类System.Windows.Forms.UnsafeNativeMethods
的类型。UnsafeNativeMethodsThemingScopeType
属性,用于获取internal
框架类System.Windows.Forms.UnsafeNativeMethods+ThemingScope
的类型。WindowsFormsUtilsType
属性,用于获取internal
框架类System.Windows.Forms.WindowsFormsUtils
的类型。WindowsFormsUtilsEnumValidatorType
属性,用于获取internal
框架类System.Windows.Forms.WindowsFormsUtils+EnumValidator
的类型。GetPropertyValue(...)
方法,用于通过名称获取internal
框架类的属性值。GetFieldValue(...)
方法,用于通过名称获取internal
框架类的字段值。Call(...)
,用于通过名称执行internal
框架类的方法。TraceMethods(...)
,用于检查internal
框架类的方法。
MessageBox 示例
示例应用程序包含 4 个不同的 MessageBox 调用用于测试目的。
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message only.");
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message and caption.",
"Test Message Box");
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message, caption and " +
"'Yes' 'No' 'Cancel' buttons.", "Test Message Box",
MessageBoxButtons.YesNoCancel);
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message, caption, " +
"'Yes' 'No' 'Cancel' buttons and icon.", "Test Message Box",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);
OpenFileDialog 和 SaveFileDialog 类
这两个对话框都继承自 FileDialog
,而 FileDialog
又继承自 CommonDialog
。原始框架的 OpenFileDialog
和 SaveFileDialog
类的问题在于:
1. CommonDialog.ShowDialog(...)
中的代码
System.Windows.FormsROS.SystemInformation.UserInteractive
调用了 ReactOS 0.4.7 上完全不支持的属性 UserInteractive
。
2. CommonDialog.ShowDialog(...)
中的代码
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc)
在 ReactOS 0.4.7 上需要额外的类型转换。
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc as System.Delegate);
在 ReactOS 0.4.7 上,
3. FileDialog.HookProc(...)
中的代码
System.Runtime.InteropServices.Marshal.StructureToPtr(ofn, notify.lpOFN, true);
System.Runtime.InteropServices.Marshal.StructureToPtr(notify, lparam, true);
需要用一个包装器替换
System.Runtime.InteropServicesROS.Marshal.StructureToPtr(ofn, notify.lpOFN, true);
System.Runtime.InteropServicesROS.Marshal.StructureToPtr(notify, lparam, true);
用于 StructureToPtr()
,
4. FileDialog.RunDialog(...)
中的代码
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc)
在 ReactOS 0.4.7 上需要额外的类型转换。
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc as System.Delegate)
在 ReactOS 0.4.7 上,
5. FileDialog.RunDialog(...)
中的代码
ofn.lStructSize = Marshal.SizeOf(ofn)
在 ReactOS 0.4.7 上需要额外的类型转换。
ofn.lStructSize = Marshal.SizeOf((object)ofn)
在 ReactOS 0.4.7 上。
由于 CommonDialog
类本身就需要调整,因此 CommonDialog
、FileDialog
以及 OpenFileDialog
/SaveFileDialog
的整个继承树都需要被采用。
同样,对于这四个类,框架的原始源代码由 Microsoft 提供用于“**参考使用**”,参见。
采用的类使用 System.Windows.FormsROS
命名空间而不是 System.Windows.Forms
。
顺便说一句
- Microsoft 选择了一种方式,在需要时动态扩展所选文件名的文件名缓冲区。这支持选择大量文件,但这也需要(不安全地)实现一个托管回调
FileDialog.HookProc(...)
,用于处理非托管消息循环 Hook,并处理CDN_SELCHANGE
通知消息代码。 - Microsoft 扩展了通用对话框以提供帮助支持。这需要(不安全地)实现一个托管回调
CommonDialog.OwnerWndProc(...)
,用于处理非托管消息循环 WndProc,并处理自定义_helpMsg
消息。
有一些好的实现,它们只支持少量选定的文件且不提供帮助,但避免了(不安全地)为非托管消息循环 Hook/WndProc 应用托管回调,例如,问题 GetOpenFileName for multiple files 的第一个答案。
OpenFileDialog 和 SaveFileDialog 示例
OpenFileDialog
和 SaveFileDialog
的调用代码几乎是标准的——只有命名空间不同。
private void btnOpen_Click(object sender, EventArgs e)
{
System.Windows.FormsROS.OpenFileDialog fod = new System.Windows.FormsROS.OpenFileDialog();
fod.Multiselect = false;
fod.Filter = "Application (*.exe)|*.exe";
fod.FilterIndex = 0;
fod.ShowDialog();
string fileName = fod.FileName;
if (!string.IsNullOrWhiteSpace(fileName))
txtOpenFile.Text = fileName;
}
private void btnSaveAs_Click(object sender, EventArgs e)
{
System.Windows.FormsROS.SaveFileDialog fod = new System.Windows.FormsROS.SaveFileDialog();
fod.Filter = "Text (*.txt)|*.txt";
fod.FilterIndex = 0;
fod.ShowDialog();
string fileName = fod.FileName;
if (!string.IsNullOrWhiteSpace(fileName))
txtSaveAsFile.Text = fileName;
}
项目编译
Notepad++ 插件 NppExec 支持带有命令执行的脚本。我将示例应用程序分解为两个 DLL 和一个 Windows EXE。
- SystemROS.dll 包含
System
、System.Runtime.InteropServices
和System.Diagnostics
命名空间的采用/扩展。 - System.Windows.FormsROS.dll 包含
System.Windows.Forms
命名空间中对话框的采用。 - WindowsFormsApplication01.exe 包含示例应用程序本身。
编译器脚本(为便于阅读,已添加换行符 ↵)如下:
NPP_SAVE
CD $(CURRENT_DIRECTORY)
SET LOCAL LIBSPATH=C:\Program Files\Mono\lib\mono\4.5
SET LOCAL CSCFLAGS=ReactOS
SET LOCAL SYSTEMROSSRC1=.\System\Runtime\InteropServices\Marshal.cs
SET LOCAL SYSTEMROSSRC2=.\System\StringResources.cs
SET LOCAL SYSTEMROSSRC3=.\System\Diagnostics\LogWriter.cs
SET LOCAL SYSTEMROSSRC="$(SYSTEMROSSRC1)" "$(SYSTEMROSSRC2)" "$(SYSTEMROSSRC3)"
SET LOCAL SYSTEMROSDLL=SystemROS.dll
C:\Program Files\Mono\lib\mono\4.5\mcs.exe /define:$(CSCFLAGS) /target:library /lib:"$(LIBSPATH)"↵
/out:"$(CURRENT_DIRECTORY)\$(SYSTEMROSDLL)" $(SYSTEMROSSRC)
SET LOCAL FORMSROSSRC1=.\System\Windows\FormsROS\CommonDialog.cs
SET LOCAL FORMSROSSRC2=.\System\Windows\FormsROS\FileDialog.cs
SET LOCAL FORMSROSSRC3=.\System\Windows\FormsROS\HelpInfo.cs
SET LOCAL FORMSROSSRC4=.\System\Windows\FormsROS\MessageBox.cs
SET LOCAL FORMSROSSRC5=.\System\Windows\FormsROS\NativeMethods.cs
SET LOCAL FORMSROSSRC6=.\System\Windows\FormsROS\OpenFileDialog.cs
SET LOCAL FORMSROSSRC7=.\System\Windows\FormsROS\Reflectables.cs
SET LOCAL FORMSROSSRC8=.\System\Windows\FormsROS\RuntimeInfo.cs
SET LOCAL FORMSROSSRC9=.\System\Windows\FormsROS\SafeNativeMethods.cs
SET LOCAL FORMSROSSRC10=.\System\Windows\FormsROS\SaveFileDialog.cs
SET LOCAL FORMSROSSRC11=.\System\Windows\FormsROS\SystemInformation.cs
SET LOCAL FORMSROSSRC12=.\System\Windows\FormsROS\UnsafeNativeMethods.cs
SET LOCAL FORMSROSSRC="$(FORMSROSSRC1)" "$(FORMSROSSRC2)" "$(FORMSROSSRC3)" "$(FORMSROSSRC4)"↵
"$(FORMSROSSRC5)" "$(FORMSROSSRC6)" "$(FORMSROSSRC7)" "$(FORMSROSSRC8)"↵
"$(FORMSROSSRC9)" "$(FORMSROSSRC10)" "$(FORMSROSSRC11)" "$(FORMSROSSRC12)"
SET LOCAL FORMSROSDLL=System.Windows.FormsROS.dll
C:\Program Files\Mono\lib\mono\4.5\mcs.exe /define:$(CSCFLAGS) /target:library /lib:"$(LIBSPATH)"↵
/r:$(SYSTEMROSDLL) /r:System.Windows.Forms.dll /r:System.Drawing.dll↵
/out:"$(CURRENT_DIRECTORY)\$(FORMSROSDLL)" $(FORMSROSSRC)
SET LOCAL APPSRC1=Program.cs
SET LOCAL APPSRC2=MainForm.cs
SET LOCAL APPSRC3=MainForm.Designer.cs
SET LOCAL APPSRC="$(APPSRC1)" "$(APPSRC2)" "$(APPSRC3)"
SET LOCAL APPEXE=WindowsFormsApplication01.exe
C:\Program Files\Mono\lib\mono\4.5\mcs.exe /define:$(CSCFLAGS) /target:winexe↵
/r:"System.Windows.Forms.dll","System.Drawing.dll","$(SYSTEMROSDLL)","$(FORMSROSDLL)"↵
/out:"$(APPEXE)" $(APPSRC) $(SYSTEMSRC) $(FORMSSRC)
我创建了 LogWriter 类,作为一种简单的方式来找出 ReactOS 0.4.7 的陷阱。
关注点
我喜欢 GUI 编程,也喜欢挑战。我希望这篇帖子能激发对 ReactOS 的兴趣,并减少接触它的恐惧。生活是丰富多彩的!
历史
2017年12月31日:初稿
2019年2月20日:更新 1