Net4x.MdiInActiveX
2.5.0.26264
dotnet add package Net4x.MdiInActiveX --version 2.5.0.26264
NuGet\Install-Package Net4x.MdiInActiveX -Version 2.5.0.26264
<PackageReference Include="Net4x.MdiInActiveX" Version="2.5.0.26264" />
<PackageVersion Include="Net4x.MdiInActiveX" Version="2.5.0.26264" />
<PackageReference Include="Net4x.MdiInActiveX" />
paket add Net4x.MdiInActiveX --version 2.5.0.26264
#r "nuget: Net4x.MdiInActiveX, 2.5.0.26264"
#:package Net4x.MdiInActiveX@2.5.0.26264
#addin nuget:?package=Net4x.MdiInActiveX&version=2.5.0.26264
#tool nuget:?package=Net4x.MdiInActiveX&version=2.5.0.26264
Net4x.MdiInActiveX
Host ordinary WinForms Forms as real MDI children of an MDI frame that the .NET code does not own —
typically an MDI frame belonging to a legacy VB6/ActiveX host application, or an MDI frame living on a
different thread.
A standard WinForms Form can only become an MDI child by setting MdiParent, which requires the parent
Form object itself. This library removes that requirement: it creates a native MDI child window
(WS_EX_MDICHILD) inside the target MDI client, reparents your form's HWND into it, and forwards captions,
icons, sizing, activation and MDI keyboard accelerators between the two windows. The result behaves like a
first-class MDI child — Ctrl+F6 window cycling, Ctrl+F4 close, maximize, minimize and restore — while the
form itself remains a plain WinForms form you design in the designer.
- Target frameworks:
net40,net6.0-windows,net8.0-windows,net10.0-windows. Thenet40assembly is consumable from any later 4.x project; the Windows Desktop targets carry the same library built against modern Windows Forms. - Platform: Windows only; 32-bit and 64-bit processes are both supported.
- Signed: strong-named assembly
Net4x.MdiInActiveX.
Install
Install-Package Net4x.MdiInActiveX
<PackageReference Include="Net4x.MdiInActiveX" Version="2.5.0.*" />
Brings in Net4x.CoreLibrary.Logging (diagnostics) and Net4x.Configuration.Library (command-line
argument reading).
Quick start
1. Derive your form from MdiChildWindow
The simplest path. MdiChildWindow attaches the MdiActiveX control for you on Load and disposes it on
Closed. Call Start() before base.OnLoad to leave design mode and enable the MDI attachment.
using MdiInActiveX.Forms;
public partial class OrdersForm : MdiChildWindow
{
public OrdersForm()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
Start(); // without this the form stays a normal form (design mode)
base.OnLoad(e);
}
}
// Show it — it appears as an MDI child of the host's MDI frame
new OrdersForm().ShowDialog();
Note the design-mode guard: a MdiChildWindow that never calls Start() behaves as an ordinary form, so
the Windows Forms designer can still open your derived form.
2. Or attach the control to an existing form
using MdiInActiveX.Extenders;
var mdiActiveX = myForm.AddMdiActiveX(); // adds the control, wires ParentForm.Load
var frame = myForm.AddMdiActiveX(true); // also sets IsMdiContainer = true on myForm
MdiActiveX is a zero-sized, transparent UserControl. Once its ParentForm fires Load it calls
Init(), which locates the MDI frame, creates the native MDI child, strips the form's border
(FormBorderStyle.None) and reparents it.
3. Running your own MDI frame on a background STA thread
MdiUtility starts a ParentForm (an IsMdiContainer frame) on a dedicated STA thread, waits until its
handle exists, and points the frame lookup at it — useful for tests, for hosting inside a non-WinForms
process, or when the real frame is not reachable from the calling thread.
using MdiInActiveX.Utility;
var mdi = new MdiUtility();
mdi.DoWork(() =>
{
var frm = new OrdersForm();
frm.ShowDialog();
});
The DoWork(Func<ParentForm>, Action, bool) overload lets you supply your own frame form; pass
changeGetMdiFrame: false to keep the default frame discovery instead of forcing the created frame.
4. Hosting an application that is already running
MdiWindowHost gives the same MDI child to a window this library did not create — including one belonging
to another process. HostedApplication starts an application and hosts the window it opens.
using MdiInActiveX.Hosting;
HostingOutcome outcome;
var app = HostedApplication.Start("charmap.exe", null, TimeSpan.FromSeconds(10), "Character Map",
maximized: true, out outcome);
// with a command line:
var chrome = HostedApplication.Start(pathToChrome, "--profile-directory=Default",
TimeSpan.FromSeconds(20), "Chrome", true, out outcome);
if (app != null)
{
// Its window is now an MDI child of the frame; the application still runs in its own process.
}
// or, for a window you already have a handle for:
using (var host = MdiWindowHost.Attach(someWindowHandle, "Hosted", maximized: true))
{
host.Restore(); // and Maximize() to put it back
}
The application is started hidden, so its window never appears on the desktop on its way to the frame:
it is shown for the first time already inside its MDI child. An application that ignores the show command it
was started with will still flash briefly — that choice belongs to the application, not to its launcher.
maximized decides whether the MDI child covers the whole workspace from the outset.
The window is borrowed, never owned: disposing the host puts it back where it was found — same parent, same
styles, same rectangle — and never destroys it. HostedApplication.Close() also asks the application to
close itself.
A hosted window has no window state of its own left to be in — the MDI child holding it is what is maximized, minimized or restored — but the application does not know that. Asked to maximize, it works out the rectangle a top-level window would take on the monitor and applies it inside its new parent, which lands it somewhere else entirely. So an application that maximizes itself maximizes its MDI child instead, one that minimizes itself minimizes it, and one that moves or resizes itself for any other reason is put back over the client area.
The MDI child shows the hosted window's caption, and it shows that window's icon beside it: the window's
own icon if it has one, otherwise its class's, otherwise the icon of the executable it belongs to — which is
the only answer for a plain dialog such as Character Map, and is what "the application's icon" means. The
child is given a copy, so it keeps drawing correctly no matter what the other application does with its own.
A form hosted through MdiActiveX is treated the same way, from the moment it is attached and again
whenever it changes its Icon.
Two things a caller has to expect. Process.MainWindowHandle does not answer this question for an
application started through a launcher stub, so the window is found by watching for one that appeared.
A window only counts as an application's own if it has a caption, is not a tool window and is not owned by
another window — a process puts up plenty of titled windows that are none of those, and picking one of them
gets an empty frame. Pass a handle straight to MdiWindowHost.Attach to host a window that fails that test
on purpose.
And Windows refuses to reparent some windows at all — a packaged (Store/UWP) application's frame among
them, which is what calc.exe starts on current Windows. SetParent fails on it with
ERROR_INVALID_PARAMETER, and so does taking ownership of it; only moving it is allowed. Such a window is
therefore followed instead: it stays top-level and is kept laid over the client area of the MDI child
standing in for it, so it looks and behaves as though it were in the frame. MdiWindowHost.IsFollowing and
HostingOutcome.HostedByFollowing say when that happened, and it is worth saying to a user, because a
followed window is not clipped by the frame and is raised with it rather than by it. Pass
allowFollowing: false to have such a window refused outright instead.
How the MDI frame is found
MdiInActiveX.Utility.Functions.Instance.GetMdiFrame is a replaceable Func<IntPtr>. The default
implementation:
- walks top-level desktop windows looking for the window property
pMDIFrame(the marker a VB6 MDI form sets on itself), then - falls back to enumerating the current process's thread windows for a class name ending in
MdiForm.
If neither succeeds, Init() logs the failure and closes the form — the library shows no dialogs of its
own. To attach to a frame you locate yourself, install your own resolver before the first child is
created:
Functions.Instance.GetMdiFrame = () => myKnownFrameHandle;
Using it from COM (VB6 and other ActiveX hosts)
The control is published to COM as a real ActiveX control, so a container can site it on a form the way the VB6 predecessor of this library was used.
| ProgId | MdiInActiveX.MdiActiveX |
| Legacy ProgId | CboMdiInActiveX.CboMDIActiveX — registered as an alias, so a host that asks for the old name by name gets this control |
| Interface | IMdiActiveX — Init(), ChildHandle, ChildWindowHandle, MdiChildHandle, WindowState, Version, Copyright |
Window handles cross the COM boundary as 32-bit integers, which is what a VB6 Long is; Windows keeps USER
handles 32-bit significant even in a 64-bit process. A host that owns the window to be hosted assigns its
hWnd to ChildHandle and then calls Init().
Only MdiActiveX is exposed: the assembly is [ComVisible(false)] and types opt in individually, so the
type library carries the control and its interface rather than every public type in the assembly.
Registering
Registration writes to HKEY_CLASSES_ROOT, so it needs administrator rights, and it has to be done in
both registry views — a 32-bit host such as VB6 cannot see a component registered only in the 64-bit
view, and a 64-bit host cannot see the other. Run both RegAsm flavours:
%windir%\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe Net4x.MdiInActiveX.dll /codebase /tlb
%windir%\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe Net4x.MdiInActiveX.dll /codebase /tlb
/codebase records the path the DLL was registered from, which is how COM finds it outside the GAC — so
register it again if the library moves. Add /unregister to undo it; unregistering hands the legacy ProgId
back to whatever owned it before.
Register the net40 assembly from the package: RegAsm is a .NET Framework tool, and a COM host loading
this control expects a .NET Framework assembly. The net6.0/net8.0/net10.0 assemblies are for a modern
Windows Forms application that references the package directly.
The source repository also carries Tools\MdiInActiveX.ComRegister, a .NET 4.0 console application that does
both views in one step, asks for elevation itself, and can report the current state without it:
MdiInActiveX.ComRegister.exe register (elevates itself)
MdiInActiveX.ComRegister.exe --unregister remove the registration
MdiInActiveX.ComRegister.exe --status report what is registered; needs no elevation
It is a development tool and is not part of this package.
Public API
| Type | Purpose |
|---|---|
MdiInActiveX.MdiActiveX |
The control that performs the attachment. Init(), ChildHandle, MyFormHandle, HWndParent, WindowState (0 normal / 1 minimized / 2 maximized), static event InstanceCreated, static IntPtr MdiFrame, Version, Copyright. Its RegisterActiveXControl / UnregisterActiveXControl are called by RegAsm, never by application code. |
MdiInActiveX.Interop.IMdiActiveX |
The interface published to COM. A .NET caller uses the control directly and does not need it. |
MdiInActiveX.Interop.ComIdentity |
The published COM identities — ProgIds, class, interface and type library GUIDs — as constants, for code that has to name them. |
MdiInActiveX.Forms.MdiChildWindow |
Base Form that self-attaches on load; call Start() to leave design mode. |
MdiInActiveX.Forms.ParentForm |
Ready-made MDI frame form used as the host in MdiUtility. |
MdiInActiveX.Extenders.MdiActiveXExtender |
Form.AddMdiActiveX(bool forceMdi = false) extension method. |
MdiInActiveX.Utility.Functions |
Singleton with the GetMdiFrame hook plus ResizeChild(hwnd). |
MdiInActiveX.Utility.ClassUtility |
Registers/unregisters the window class named by its MdiChildClassName constant (MDIActiveXClassNet) and owns the native child's window procedure. Shared per process and reference-counted, so the class outlives individual attachments. A host can use that constant to tell this library's MDI children apart from its own. |
MdiInActiveX.Utility.UnhandlerExceptionLogger |
LogUnhandledExceptions() — routes Application.ThreadException and AppDomain.UnhandledException to the logger. |
MdiInActiveX.Utility.MdiUtility |
Runs a ParentForm MDI frame on a background STA thread. |
MdiInActiveX.Utility.WindowEnumerator |
GetChildWindows(IntPtr parent) helper. |
MdiInActiveX.Hosting.MdiWindowHost |
Hosts an existing window — including another application's — as an MDI child, and gives it back on dispose. |
MdiInActiveX.Hosting.HostedApplication |
Starts an application and hosts the window it opens. Reports a HostingOutcome. |
MdiInActiveX.Hosting.ApplicationWindows |
Finds the window an application opened, and recognises a packaged application's frame. |
What the control forwards
Once attached, the subclassed window procedure keeps the two windows in sync:
WM_SETTEXT/WM_STYLECHANGED→ copies the form caption onto the native MDI child.WM_SETICON→ copies the form icon onto the native MDI child.WM_SIZE→ resizes the native child to match, or refits the form when the child is maximized.WM_NCLBUTTONDOWNon caption/border hit-test areas → routed to the MDI child or, when maximized, to the frame, so dragging and resizing behave normally.Ctrl+F6/Ctrl+Shift+F6→SC_NEXTWINDOW/SC_PREVWINDOW;Ctrl+F4→WM_CLOSE;Alt+accelerator → forwarded to the frame's menu asWM_SYSCHAR.
Maximizing a child
A child keeps its own caption bar when it is maximized, rather than merging it into the frame's menu bar the
way a plain MDI child would — the frame may not have one, and its buttons would then have nowhere to appear.
The state itself is real: the child carries WS_MAXIMIZE, so its caption offers Restore, and a maximized
child is re-fitted to the workspace whenever the frame is resized.
A child that already fills the workspace when it attaches starts maximized; a smaller one starts in the
normal state. Afterwards the caption buttons, a double click on the caption and MdiActiveX.WindowState
(0 normal, 1 minimized, 2 maximized) all move between the states.
Closing a child
Closing the MDI child from the frame — its system menu, the close button, Ctrl+F4 — reaches the hosted
form as a normal close, so FormClosing/FormClosed run and a cancelled close is honoured. The other
system commands (minimize, maximize, restore) are handled by the MDI child rather than by the form.
Lifetime and cleanup
Disposing the MdiActiveX control restores the original window procedure, hands the hosted window back —
unparenting it and undoing its MDI child styles — and only then destroys the native MDI child window, so
Windows Forms is never left holding a handle that has been destroyed underneath it. The window class is
released once the last attachment lets go of it.
MdiChildWindow does all of this for you in OnClosed; if you attached the control manually, dispose it
(or the hosting form) yourself. A subclass is also removed when its window is destroyed, so a form torn down
without a matching dispose does not leave one behind.
Closing the frame while children are still attached is handled too — including closing it from its own close button, which destroys the MDI client before its children have finished being destroyed. A borrowed window is handed back rather than destroyed along with it.
Diagnostics
Attachment failures (no MDI frame found, MDI child window not created) are reported through
CoreLibrary.Logging and the form is closed; the library shows no dialogs of its own. Route unhandled
exceptions to the same log with UnhandlerExceptionLogger.LogUnhandledExceptions().
An exception is never allowed to escape the window procedures this library installs: it would unwind through native frames into whatever pumps the message loop and end the host process. They are caught, logged, and the message is given its default handling instead.
Passing -InitiallyThrowException:true on the command line makes the static constructor raise and log a
test exception — a quick way to confirm logging is wired up.
Known quirks
MdiInActiveX.MessageHooking(global keyboard/mouse hooks) is present in the source tree but excluded from compilation; those types are not in the shipped assembly.- The static
MdiActiveX.InstanceCreatedevent is named that way becauseControlalready definesCreated. It was calledCreatedbefore 2.5.0. WindowEnumeratormoved fromMdiInActiveX.MdiChildWindowtoMdiInActiveX.Utilityin 2.5.0: the old namespace shared its name withMdiInActiveX.Forms.MdiChildWindow, which hid the type from any consumer whose own code sits under anMdiInActiveX.*namespace.ClassUtility.SimulateMaximizeis obsolete. Maximizing is a real window state now, so it simply forwards to it; use the caption buttons,WM_MDIMAXIMIZE, orMdiActiveX.WindowState.
License
Apache-2.0. Copyright (c) Piero Viano.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0-windows7.0 is compatible. net7.0-windows was computed. net8.0-windows was computed. net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. net10.0-windows7.0 is compatible. |
| .NET Framework | net40 is compatible. net403 was computed. net45 was computed. net451 was computed. net452 was computed. net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
-
.NETFramework 4.0
- Net4x.Configuration.Library (>= 2.5.0.26236)
- Net4x.CoreLibrary.Logging (>= 2.5.0.26236)
-
net10.0-windows7.0
- Net4x.Configuration.Library (>= 2.5.0.26236)
- Net4x.CoreLibrary.Logging (>= 2.5.0.26236)
-
net6.0-windows7.0
- Net4x.Configuration.Library (>= 2.5.0.26236)
- Net4x.CoreLibrary.Logging (>= 2.5.0.26236)
-
net8.0-windows7.0
- Net4x.Configuration.Library (>= 2.5.0.26236)
- Net4x.CoreLibrary.Logging (>= 2.5.0.26236)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.0.26264 | 105 | 9/21/2026 |
| 2.5.0.26253 | 97 | 9/10/2026 |
| 2.5.0 | 233 | 4/4/2025 |