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:
And it scales great on ReactOS (493px * 209px and 610px * 292px):
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(...)
:
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:
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)
{
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));
}
if (!((bool)Reflectables.Call(Reflectables.WindowsFormsUtilsEnumValidatorType, "IsEnumWithinShiftedRange", null, new object[] { icon, 4, 0x0,0x4 })))
{
throw new InvalidEnumArgumentException("icon", (int)icon, typeof(System.Windows.Forms.MessageBoxIcon));
}
if (!((bool)Reflectables.Call(Reflectables.WindowsFormsUtilsEnumValidatorType, "IsEnumWithinShiftedRange", null, new object[] { defaultButton, 8, 0x0,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:
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);
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(...)
System.Windows.FormsROS.SystemInformation.UserInteractive
that calls the, on ReactOS 0.4.7, completely unsupported property UserInteractive
,
2. the code within CommonDialog.ShowDialog(...)
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc)
that requires an additional cast
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc as System.Delegate);
on ReactOS 0.4.7,
3. the code within FileDialog.HookProc(...)
System.Runtime.InteropServices.Marshal.StructureToPtr(ofn, notify.lpOFN, true);
System.Runtime.InteropServices.Marshal.StructureToPtr(notify, lparam, true);
that is to replace with a wrapper
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(...)
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc)
that requires an additional cast
subclassWndProcPtr = Marshal.GetFunctionPointerForDelegate(subclassWndProc as System.Delegate)
on ReactOS 0.4.7 and
5. the code within FileDialog.RunDialog(...)
ofn.lStructSize = Marshal.SizeOf(ofn)
that requires an additional cast
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:
- 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. - 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:
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;
}
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