65.9K
CodeProject 正在变化。 阅读更多。
Home

ReactOS 上 C# System.Windows.Forms 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (7投票s)

2017 年 12 月 31 日

CPOL

5分钟阅读

viewsIcon

13295

downloadIcon

99

如何在 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。原始框架的 OpenFileDialogSaveFileDialog 类的问题在于:

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 类本身就需要调整,因此 CommonDialogFileDialog 以及 OpenFileDialog/SaveFileDialog 的整个继承树都需要被采用。

同样,对于这四个类,框架的原始源代码由 Microsoft 提供用于“**参考使用**”,参见

采用的类使用 System.Windows.FormsROS 命名空间而不是 System.Windows.Forms

顺便说一句

  1. Microsoft 选择了一种方式,在需要时动态扩展所选文件名的文件名缓冲区。这支持选择大量文件,但这也需要(不安全地)实现一个托管回调 FileDialog.HookProc(...),用于处理非托管消息循环 Hook,并处理 CDN_SELCHANGE 通知消息代码。
  2. Microsoft 扩展了通用对话框以提供帮助支持。这需要(不安全地)实现一个托管回调 CommonDialog.OwnerWndProc(...),用于处理非托管消息循环 WndProc,并处理自定义 _helpMsg 消息。

有一些好的实现,它们只支持少量选定的文件且不提供帮助,但避免了(不安全地)为非托管消息循环 Hook/WndProc 应用托管回调,例如,问题 GetOpenFileName for multiple files 的第一个答案。

OpenFileDialog 和 SaveFileDialog 示例

OpenFileDialogSaveFileDialog 的调用代码几乎是标准的——只有命名空间不同。

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 包含 SystemSystem.Runtime.InteropServicesSystem.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

© . All rights reserved.