Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Introduction to System.Windows.Forms on ReactOS with C#

4.90/5 (7 votes)
19 Feb 2019CPOL5 min read 12.9K   88  
How to compile and run the first C# GUI application with System.Windows.Forms in ReactOS.

Image 1 Download WindowsFormsApplication01.zip

Introduction

This article is based on the Tip Introduction to C# on ReactOS. The article was originally written for ReactOS 0.4.7 and ist updated meanwhile for ReactOS 0.4.8...0.4.10.

This article shows how to avoid pitfalls regarding the System.Windows.Forms wrapper around the common dialogs. I know - Forms is not the latest technology, but it works for most of the requirements and it's available on ReactOS.

One of the most unpleasant drawbacks, a GUI can have, is not to scale. And (surprise) a Forms GUI is able to scale very well - consequently, I can utilize it in good conscience.

Why not GTK# 2.12.11? Because i want to create a solution, that can be shared between Windows and ReactOS without additional effort/installations. And in fact, the MONO compiled sample application runs on my Windows 10 Enterprise 64 Bit edition without re-compilation or any change!

Background

This article utilizes the same infrastructure as the Tip Introduction to C# on ReactOS (for details why exactly this selection, see the tip).

  • From ReactOS 0.4.7 running in ORACLE VirtualBox VM version 5.1.26 up to ReactOS 0.4.10 running in ORACLE VirtualBox VM version 5.2.16.
  • Microsoft .NET 4.0 as the runtime environment.
  • From MONO 3.2.3 to MONO 4.3.2 as the compiler.
  • Notepad++ with NppExec plugin to edit the code and run the compiler.

The complete source code of a sample application is provided for download.

My sample application with scalable GUI is structured as follows:

Image 2

And it scales great on ReactOS (493px * 209px and 610px * 292px):

Image 3

Image 4

Using the code

The sample application is focused to three common dialogs:

  • MessageBox
  • OpenFileDialog
  • SaveFileDialog

The MessageBox class

The problem of the original framework's MessageBox class is this code within ShowCore(...):

C#
UnsafeNativeMethods.LoadLibraryFromSystemPathIfAvailable(ExternDll.Shell32)

The LoadLibraryFromSystemPathIfAvailable(...) method doesn't work for WinXP nor for ReactOS 0.4.7. But the LoadLibrary(...) method can take up the job. This requires to adopt the MessageBox.ShowCore(...) method and therefore the complete MessageBox class has to be adopted.

The most of the framework's original source code - including the MessageBox class - is made available by Microsoft for "reference use", see.

The adopted class uses the namespace System.Windows.FormsROS instead of System.Windows.Forms.

The ShowCore(...) method contains several calls to internal framework methods or classes, so that these calls must be replaced by reflection equivalents. Here some examples:

C#
/// <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));
    }

...

The reflection

All the helper properties and methods to replace original framework's internal methods or classes by reflection equivalents are centralized in System.Windows.FormsROS.Reflectables. The Reflectables class provides:

  • ClientUtilsType property to get the type of the internal framework class System.Windows.Forms.ClientUtilsType
  • IntSecurityType property to get the type of the internal framework class System.Windows.Forms.IntSecurity
  • UnsafeNativeMethodsType property to get the type of the internal framework class System.Windows.Forms.UnsafeNativeMethods
  • UnsafeNativeMethodsThemingScopeType property to get the type of the internal framework class System.Windows.Forms.UnsafeNativeMethods+ThemingScope
  • WindowsFormsUtilsType property to get the type of the internal framework class System.Windows.Forms.WindowsFormsUtils
  • WindowsFormsUtilsEnumValidatorType property to get the type of the internal framework class System.Windows.Forms.WindowsFormsUtils+EnumValidator
  • GetPropertyValue(...) method to to get the value of an internal framework class property by it's name
  • GetFieldValue(...) method to to get the value of an internal framework class field by it's name
  • Call(...) to execute an internal framework class method by it's name
  • TraceMethods(...) to investigate the methods of an internal framework class

The MessageBox samples

The sample application contains 4 different MessageBox calls for test purposes:

C#
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message only.");

Image 5

C#
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message and caption.",
                                        "Test Message Box");

Image 6

C#
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message, caption and " +
                                        "'Yes' 'No' 'Cancel' buttons.", "Test Message Box",
                                        MessageBoxButtons.YesNoCancel);

Image 7

C#
System.Windows.FormsROS.MessageBox.Show("Hello - Message box with message, caption, " +
                                        "'Yes' 'No' 'Cancel' buttons and icon.", "Test Message Box",
                                        MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);

Image 8

The OpenFileDialog and SaveFileDialog classes

Both dialog boxes inherit from FileDialog, that inherits from CommonDialog. The problem of the original framework's OpenFileDialog and SaveFileDialog classes are

1.   the code within CommonDialog.ShowDialog(...)

C#
System.Windows.FormsROS.SystemInformation.UserInteractive

      that calls the, on ReactOS 0.4.7, completely unsupported property UserInteractive,

2.   the code within CommonDialog.ShowDialog(...)

C#
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc)

      that requires an additional cast

C#
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc as System.Delegate);

     on ReactOS 0.4.7,

3.   the code within FileDialog.HookProc(...)

C#
System.Runtime.InteropServices.Marshal.StructureToPtr(ofn, notify.lpOFN, true);
System.Runtime.InteropServices.Marshal.StructureToPtr(notify, lparam, true);

      that is to replace with a wrapper

C#
System.Runtime.InteropServicesROS.Marshal.StructureToPtr(ofn, notify.lpOFN, true);
System.Runtime.InteropServicesROS.Marshal.StructureToPtr(notify, lparam, true);

     for StructureToPtr(),

4.   the code within FileDialog.RunDialog(...)

C#
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc)

      that requires an additional cast

C#
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc as System.Delegate)

     on ReactOS 0.4.7 and

5.   the code within FileDialog.RunDialog(...)

C#
ofn.lStructSize = Marshal.SizeOf(ofn)

      that requires an additional cast

C#
ofn.lStructSize = Marshal.SizeOf((object)ofn)

     on ReactOS 0.4.7.

Since already the CommonDialog class needs adjustments, the complete inheritance tree of CommonDialog, FileDialog and OpenFileDialog/SaveFileDialog requires to be adopted.

Also for these four classes the framework's original source code is made available by Microsoft for "reference use", see.

The adopted classes uses the namespace System.Windows.FormsROS instead of System.Windows.Forms.

By the way:

  1. Microsoft has chosen the way to extend the file name buffer of selected file names dynamically, if required. This supports the selection of a huge amount of files but it also requires the (unsafe) implementation of a managed callback FileDialog.HookProc(...) for the unmanaged message loop Hook, processing the CDN_SELCHANGE notification message code.
  2. Microsoft extends the common dialogs to provide help support. This requires the (unsafe) implementation of a managed callback CommonDialog.OwnerWndProc(...) for the unmanaged message loop WndProc, processing the custom _helpMsg message.

There are good implementations that support only a small amout of selected files and no help but avoid the (unsafe) application of a managed callback for the unmanaged message loop Hook/WndProc, e. g. the question's GetOpenFileName for multiple files first answer.

The OpenFileDialog and SaveFileDialog samples

The caller code for OpenFileDialog and SaveFileDialog is almost standard - only the namespaces differ:

C#
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;
}

Image 9 Image 10

Project compilation

The Notepad++ plugin NppExec supports scripting with command execution. I've structured the sample application into two DLLs and one Windows EXE:

  • SystemROS.dll contains the adoptions/extensions of the System, System.Runtime.InteropServices and System.Diagnostics namespaces.
  • System.Windows.FormsROS.dll contains the adoptions of the dialogs from System.Windows.Forms namespace.
  • WindowsFormsApplication01.exe contains the sample application itself.

The compiler script (line breaks added at ↵ for better readability) looks like:

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)

I created the LogWriter class as a simple way to figure out the pitfals of ReactOS 0.4.7.

Points of Interest

I love to deal with GUI programming and i love callanges. I hope this post encourages interest in ReactOS and reduces fear of contact. Life is colorful!

History

31th December 2017: Initial article version
20th February 2019: Update 1

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)