在从C++调用的C# DLL中使用表单并不容易,但是一旦编写了一些实用程序代码,它就可以相当健壮。回调到C++代码非常容易。
要使用表单(或WPF),NativeWindow类是您的朋友。您希望获得比NativeWindow更多的功能,因此需要派生。下面的代码显示了一个从NativeWindow派生并提供BeginInvoke()调用和窗口消息事件处理程序的实现。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class NativeWindowWithCallbacks : NativeWindow, IDisposable
{
private readonly object handleLock = new object();
private readonly Queue<MethodArgs> queue = new Queue<MethodArgs>();
private readonly Dictionary<int, MessageHandler> messageHandlers =
new Dictionary<int, MessageHandler>();
private readonly int runOnUiThreadWindowsMessageNumber =
Win32.RegisterWindowMessage(
"NativeWindowWithCallbacksInvokeOnGuiThread");
public delegate bool MessageHandler(object sender, ref Message m);
public bool InvokeRequired
{
get
{
int pid;
return this.Handle != IntPtr.Zero
&& Win32.GetWindowThreadProcessId(
new HandleRef(this, this.Handle), out pid)
!= Win32.GetCurrentThreadId();
}
}
public void BeginInvoke(Delegate method, params object[] args)
{
lock (this.queue)
{
this.queue.Enqueue(
new MethodArgs { Method = method, Args = args });
}
if (this.Handle != IntPtr.Zero)
{
Win32.PostMessage(
new HandleRef(this, this.Handle),
this.runOnUiThreadWindowsMessageNumber,
IntPtr.Zero,
IntPtr.Zero);
}
}
public HandleRef MenuHandle()
{
return new HandleRef(
this,
this.Handle != IntPtr.Zero
? Win32.GetMenu(new HandleRef(this, this.Handle))
: IntPtr.Zero);
}
public void Dispose()
{
this.ReleaseHandle();
}
public void AssignHandle(IntPtr handle, bool onlyIfNotSet)
{
bool emptyBacklog = false;
lock (this.handleLock)
{
if (this.Handle != handle
&& (!onlyIfNotSet || this.Handle != IntPtr.Zero))
{
base.AssignHandle(handle);
emptyBacklog = true;
}
}
if (emptyBacklog)
{
this.EmptyUiBacklog();
}
}
public void AddMessageHandler(
int messageNumber,
MessageHandler messageHandler)
{
lock (this.messageHandlers)
{
if (this.messageHandlers.ContainsKey(messageNumber))
{
this.messageHandlers[messageNumber] += messageHandler;
}
else
{
this.messageHandlers.Add(
messageNumber, (MessageHandler)messageHandler.Clone());
}
}
}
protected override void WndProc(ref Message m)
{
if (m.Msg == this.runOnUiThreadWindowsMessageNumber && m.Msg != 0)
{
for (;;)
{
MethodArgs ma;
lock (this.queue)
{
if (!this.queue.Any())
{
break;
}
ma = this.queue.Dequeue();
}
ma.Method.DynamicInvoke(ma.Args);
}
return;
}
int messageNumber = m.Msg;
MessageHandler mh;
if (this.messageHandlers.TryGetValue(messageNumber, out mh))
{
if (mh != null)
{
foreach (MessageHandler cb in mh.GetInvocationList())
{
try
{
if (cb(this, ref m) && messageNumber != 2)
{
return;
}
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("{0}", ex));
}
}
}
}
base.WndProc(ref m);
}
private void EmptyUiBacklog()
{
bool haveBacklog;
lock (this.queue)
{
haveBacklog = this.queue.Any();
}
if (haveBacklog)
{
Win32.PostMessage(
new HandleRef(this, this.Handle),
this.runOnUiThreadWindowsMessageNumber,
IntPtr.Zero,
IntPtr.Zero);
}
}
private class MethodArgs
{
public object[] Args { get; set; }
public Delegate Method { get; set; }
}
}
以上代码的主要目的是获取在内部实现的
BeginInvoke()调用 - 您需要该调用来在GUI线程上创建自己的窗体。但是,在您可以在GUI线程上回调之前,您需要拥有一个窗口句柄。最简单的方法是让C++代码传递窗口句柄(作为IntPtr到达),但您也可以使用类似以下内容的东西:
Process.GetCurrentProcess().MainWindowHandle;
即使在从C++调用C#时,也可以获取到主窗口的句柄。请注意,C++代码可能会更改主窗口句柄并留下无效的C#代码(当然,可以通过监听原始句柄上的适当窗口消息来捕获此问题 - 您也可以使用上面的代码执行此操作)。
很抱歉,上面的Win32调用声明未显示。您可以通过搜索网络获取它们的P/Invoke声明。(我的Win32类非常庞大。)
至于回调到C++代码-只要使回调相当简单,您就可以使用Marshal.GetDelegateForFunctionPointer将传递的函数指针(转换为IntPtr)转换为常规的C#委托。
因此,至少回调到C++非常容易(只要正确定义委托声明)。例如,如果您有一个C++函数,该函数接受char const *并返回void,则委托声明将如下所示:
public delegate void MyCallback([MarshalAs(UnmanagedType.LPStr)] string myText);
这涵盖了基础知识。使用上述类和传入的窗口句柄,在NativeWindowWithCallbacks.BeginInvoke()调用中创建自己的基于表单的窗口。现在,如果您想玩C++窗口代码,例如在C++代码管理的窗口中添加菜单项条目,事情会变得更加复杂。 .Net控件代码不喜欢与其未创建的任何窗口进行接口交互。因此,要添加菜单项,您最终需要编写具有大量Win32 P / Invokes的代码,以执行与编写C代码时相同的调用。上述的NativeWindowWithCallbacks类将再次派上用场。