Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Modeless WinForm With Callback From Unmanaged Code

4.84/5 (10 votes)
4 May 2011CPOL7 min read 33.1K   1.3K  
How to use a modeless WinForm from an unmanaged application including callback functionality

Introduction

The two main products the company I work for maintain are legacy applications written in 'C' (yes, I said 'C') and VB6 respectively. Over the years, they both have been extended heavily with C++, MFC, COM, ActiveX, and most recently the .NET framework. The GUI for the 'C' application uses a set of cross-platform tools that are at least 20 years old and have an extremely high learning curve, so development of new UI components are done using the .NET framework and WinForms whenever possible.

Until recently, our libraries included a large number of .NET forms that are displayed as modal dialogs. One of the requirements for a new screen we needed to create was that it be modeless and behave like a child of the main application window. This means that it needs to be contained within the main window and if it were maximized, it would only fill the view portion of the window. As an added “bonus”, some of the buttons on the new form needed to call back into the application to perform complicated tasks like printing or report generation using existing methods in the code.

First Attempt

The first thing I needed was a form to display. I created a simple form with 3 buttons and put it in a class library named ModelessForm.

The second thing that was needed was an entry point in a DLL for the 'C' code to call and display the form. We have an existing wrapper DLL used for this very purpose that's compiled with the CLR option so it can contain both managed and unmanaged code. I added a file containing the following code to provide my entry point and display the form I created in the ModelessForm class library (ModelessForm::DemoForm).

MC++
#include "stdafx.h"
#include <windows.h>

using namespace System;
using namespace System::Windows::Forms;
using namespace ModelessForm;

extern "C" __declspec(dllexport) int ShowModeless(HWND parent)
{
	DemoForm^ form = gcnew DemoForm();
	form->Show( NativeWindow::FromHandle((IntPtr)parent));
		
	return 0;
}

I created a very simple MFC SDI application, added a menu item to show the dialog, and a command handler in the view class to call the exported DLL function.

C++
extern "C" __declspec(dllimport) int ShowModeless(HWND parent);
void CChildView::OnWinformShow()
{
	ShowModeless( GetSafeHwnd() );
}

Too easy! I ran the app, called my new entry point, and my modeless form appeared. I was just about to celebrate when I discovered there were some issues with my implementation – the form wasn't constrained to the calling application's view area and it didn't seem to be processing key strokes. Obviously, it wasn’t going to be as easy as I'd thought.

I did some searching on the internet to see what I'd missed and I found the article "How to make TAB key work in Modeless Winforms" by Elango Ramanathan, which provided a good example of how to implement a message hook that makes use of the Win32 function IsDialogMessage. It seemed like a lot of code just to be able to process keystrokes in a form, but I needed to get this working, so I added the code to my form. I ran the test again and was pleasantly surprised when I saw that my form was indeed processing keystrokes. However, I quickly noticed some strange behavior with the tab order – it didn't match the tab order set in the WinForms designer using the TabIndex property of the controls. It was based on the z-order of the controls, which made sense given that they key strokes were being processed at the windows message level. This solution wasn't going to work either. I needed a way to use a form from unmanaged code while leaving the details of the form like editing with the designer and expected run-time behavior up to the .NET framework and tools.

Better

While searching the internet for information on processing key strokes, I remembered seeing something about running the modeless form from its own thread. This got me to thinking about how WinForms applications are started and their lifetime managed, namely the Run method of the Application object. I wrote a very simple class in the wrapper DLL using C++/CLI as a proof-of-concept and found that by spawning a thread which in turn displays the form with a call to Application.Run, the keys were processed correctly.

Of course, although this worked, it wasn't exactly reusable except in the cut-and-paste sense. I wanted something I could put in one of our shared control libraries that was easy to use and required minimal coding from the consumer. I decided the best way to do this would be to create a base class that implements all of the desired behavior.

C#
public partial class ModelessFormBase : Form
{
	public ModelessFormBase()
	{
		InitializeComponent();
	}

	public void Show(IntPtr parent)
	{
		Thread thread = new Thread(ModelessLifetimeProc);
		thread.Start(parent);
	}

	protected void ModelessLifetimeProc(object parent)
	{
		SetParent(this.Handle, (IntPtr)parent);

		this.Show();

		Application.Run( this );
	}

	[DllImport("user32.dll", SetLastError = true)]
	static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
}

The ModelessFormBase extends the Form class with a new implementation of the Show method that accepts the parent window's Win32 handle as an IntPtr. This method creates the thread which displays the form as modeless using Form.Show and then sets the parent using the Win32 function SetParent. In case you're wondering, this was the only way I could get the modeless form to be constrained to the parent window. Creating a NativeWindow from the parent's handle and passing that to Form.Show didn’t work, nor did setting the Parent property. The lifetime of the form is then managed via a call to Application.Run which won't return until the formed has been closed.

I modified my form so it derived from ModelessFormBase instead of Form and now I had all the behavior I was looking for… except for a callback mechanism to provide notifications to the application.

Callback

I decided that an event would probably be the best way to send notifications from the modeless form to the calling application. To this end, I added the NotifyClient event to the modeless form base class and a method for firing the event. I also created a custom class derived from System.EventArgs to pass an integer notification code to clients subscribed to the event.

In the unmanaged wrapper code, I updated the entry point to accept a callback function from the application that I assigned to a global pointer. I created an event handler for the new event that would call the callback function via the global function pointer passing it the integer notification code specified in the custom EventArgs class.

Although this worked, I was really hoping for something a little simpler. It would be nice to just have the modeless form call the callback function in the application directly without the extra overhead of the events. Lucky for me, Steve, one of my co-workers, had just the solution. He suggested I look at the GetDelegateForFunctionPointer method of the class System.Runtime.InteropServices.Marshal.

This was exactly what I was hoping for! I could now pass the callback function to the modeless form and have it directly notify the application. Here is the final implementation:

C#
public partial class ModelessFormBase : Form
{
	public delegate void NotifyClientCallback(int code);
	protected NotifyClientCallback _notifyClient = null;

	protected void NotifyClient(int code)
	{
		if (null != _notifyClient)
		{
			_notifyClient(code);
		}
	}
	
	public ModelessFormBase()
	{
		InitializeComponent();
	}

	public void Show(IntPtr parent)
	{
		Thread thread = new Thread(ModelessLifetimeProc);
		thread.Start(parent);
	}

	public void Show(IntPtr parent, IntPtr notifyClientFunction)
	{
		_notifyClient = Marshal.GetDelegateForFunctionPointer(
                                     notifyClientFunction, 
                                     typeof(NotifyClientCallback)) 
					as NotifyClientCallback;
		Thread thread = new Thread(ModelessLifetimeProc);
		thread.Start(parent);
	}

	protected void ModelessLifetimeProc(object parent)
	{
		SetParent(this.Handle, (IntPtr)parent);

		this.Show();

		Application.Run( this );
	}

	[DllImport("user32.dll", SetLastError = true)]
	static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
}

To the form base class, I added a delegate with the same signature as the callback function in the application. I added another implementation of the Show method that accepts both the parent window's handle and the callback function pointer and creates the delegate from the function pointer.

I added button click events to the test form derived from ModelessFormBase in order to test the callback function.

C#
public partial class DemoForm : ModelessFormBase
{
	public DemoForm()
	{
		InitializeComponent();
	}

	private void button1_Click(object sender, EventArgs e)
	{
		base.NotifyClient(1);
	}

	private void button2_Click(object sender, EventArgs e)
	{
		base.NotifyClient(2);
	}

	private void button3_Click(object sender, EventArgs e)
	{
		base.NotifyClient(3);
	}
}

I updated the wrapper function to accept a pointer to a callback function and included it in the call to the form's Show method.

MC++
#include "stdafx.h"
#include <windows.h>

using namespace System;
using namespace System::Windows::Forms;
using namespace ModelessForm;

typedef void (*LPF_NOTIFY_CALLBACK)(int);

extern "C" __declspec(dllexport) int ShowModeless
	(HWND parent, LPF_NOTIFY_CALLBACK lpfNotifyCallback)
{
	DemoForm^ form = gcnew DemoForm();
	form->Show( NativeWindow::FromHandle
		((IntPtr)parent), (IntPtr)lpfNotifyCallback );
		
	return 0;
}

And finally, I updated the application to include a callback and to pass the address of the callback to the entry point in the wrapper DLL.

C++
extern "C" __declspec(dllimport) int ShowModeless
	(HWND parent, void (*LPF_NOTIFY_HANDLER)(int));
void OnModelessDialogNotify(int code)
{
	TCHAR message[256];
	_stprintf( message, L"The code is: %d", code );
	MessageBox( NULL, message, L"Modeless Dialog Callback", MB_OK );
}

void CChildView::OnWinformShow()
{
	ShowModeless( GetSafeHwnd(), &OnModelessDialogNotify );
}

Demo

ModelessForm

A C# class library project that contains both the base form, ModelessFormBase and the derived form, DemoForm. Note that I placed these in the same assembly purely to simplify the demo. In the "real world", the base form would probably be in an assembly containing shared UI components.

ModelessFormWrapper

A Win32 DLL project that contains a 'C' style entry point that allows an unmanaged application to display the form. This is compiled with the /CLR option so it supports both managed and unmanaged code.

ModelessFormDemo

A very simple MFC SDI application that calls the wrapper DLL to display the modeless form with the view class as the parent. The form is constrained to the view area including when minimized or maximized. Each of the buttons on the form will display a message when clicked, demonstrating the callback functionality.

Conclusion

Hopefully, this will allow those of you who are working in a legacy code base to be able to implement new UI screens using the power of the .NET Framework.

History

  • 05/03/2011 - Initial release

License

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