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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Net4x.MdiInActiveX" Version="2.5.0.26264" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Net4x.MdiInActiveX" Version="2.5.0.26264" />
                    
Directory.Packages.props
<PackageReference Include="Net4x.MdiInActiveX" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Net4x.MdiInActiveX --version 2.5.0.26264
                    
#r "nuget: Net4x.MdiInActiveX, 2.5.0.26264"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Net4x.MdiInActiveX@2.5.0.26264
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Net4x.MdiInActiveX&version=2.5.0.26264
                    
Install as a Cake Addin
#tool nuget:?package=Net4x.MdiInActiveX&version=2.5.0.26264
                    
Install as a Cake Tool

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. The net40 assembly 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:

  1. walks top-level desktop windows looking for the window property pMDIFrame (the marker a VB6 MDI form sets on itself), then
  2. 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_NCLBUTTONDOWN on 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 as WM_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.InstanceCreated event is named that way because Control already defines Created. It was called Created before 2.5.0.
  • WindowEnumerator moved from MdiInActiveX.MdiChildWindow to MdiInActiveX.Utility in 2.5.0: the old namespace shared its name with MdiInActiveX.Forms.MdiChildWindow, which hid the type from any consumer whose own code sits under an MdiInActiveX.* namespace.
  • ClassUtility.SimulateMaximize is obsolete. Maximizing is a real window state now, so it simply forwards to it; use the caption buttons, WM_MDIMAXIMIZE, or MdiActiveX.WindowState.

License

Apache-2.0. Copyright (c) Piero Viano.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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