在从 C++ 调用的 C# DLL 中使用表单并不容易,但是一旦编写了一些实用程序代码,它就会相当健壮。对 C++ 代码的回调非常容易。
要执行 Forms(或 WPF),NativeWindow类是您的朋友。您需要比 NativeWindow 提供的更多功能,因此需要派生。下面的代码显示了一个派生自 NativeWindow 并提供BeginInvoke()调用和 Windows 消息事件处理程序的实现。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
/// <summary>
/// A <see cref="NativeWindow"/> for the main application window. Used
/// to be able to run things on the UI thread and manage window message
/// callbacks.
/// </summary>
public class NativeWindowWithCallbacks : NativeWindow, IDisposable
{
/// <summary>
/// Used to synchronize access to <see cref="NativeWindow.Handle"/>.
/// </summary>
private readonly object handleLock = new object();
/// <summary>
/// Queue of methods to run on the UI thread.
/// </summary>
private readonly Queue<MethodArgs> queue = new Queue<MethodArgs>();
/// <summary>
/// The message handlers.
/// </summary>
private readonly Dictionary<int, MessageHandler> messageHandlers =
new Dictionary<int, MessageHandler>();
/// <summary>
/// Windows message number to prompt running methods on the UI thread.
/// </summary>
private readonly int runOnUiThreadWindowsMessageNumber =
Win32.RegisterWindowMessage(
"NativeWindowWithCallbacksInvokeOnGuiThread");
/// <summary>
/// Handles the message.
/// </summary>
/// <param name="sender">
/// The this.
/// </param>
/// <param name="m">
/// The message.
/// </param>
/// <returns>
/// True if done processing; false otherwise. Normally, returning
/// true will stop other handlers from being called, but, for
/// some messages (like WM_DESTROY), the return value has no effect.
/// </returns>
public delegate bool MessageHandler(object sender, ref Message m);
/// <summary>
/// Gets a value indicating whether the caller must call BeginInvoke
/// when making UI calls (like <see cref="Control.InvokeRequired"/>).
/// </summary>
/// <returns>
/// True if not running on the UI thread.
/// </returns>
/// <remarks>
/// This can get called prior to detecting the main window (likely if
/// the main window has yet to be created). In this case, this method
/// will return true even if the main window subsequently gets
/// created on the current thread. This behavior works for queuing up
/// methods that will update the main window which is likely the only
/// reason for invoking methods on the UI thread anyway.
/// </remarks>
public bool InvokeRequired
{
get
{
int pid;
return this.Handle != IntPtr.Zero
&& Win32.GetWindowThreadProcessId(
new HandleRef(this, this.Handle), out pid)
!= Win32.GetCurrentThreadId();
}
}
/// <summary>
/// Like <see cref="Control.BeginInvoke(Delegate,Object[])"/> but
/// probably not as good.
/// </summary>
/// <param name="method">
/// The method.
/// </param>
/// <param name="args">
/// The arguments.
/// </param>
/// <remarks>
/// This can get called prior to finding the main window (likely if
/// the main window has yet to be created). In this case, the method
/// will get queued and called upon detection of the main window.
/// </remarks>
public void BeginInvoke(Delegate method, params object[] args)
{
// TODO: ExecutionContext ec = ExecutionContext.Capture();
// TODO: then ExecutionContext.Run(ec, ...)
// TODO: in WndProc for more accurate security
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);
}
}
/// <summary>
/// Returns the handle of the main window menu.
/// </summary>
/// <returns>
/// The handle of the main window menu; Handle <see cref="IntPtr.Zero"/>
/// on failure.
/// </returns>
public HandleRef MenuHandle()
{
return new HandleRef(
this,
this.Handle != IntPtr.Zero
? Win32.GetMenu(new HandleRef(this, this.Handle))
: IntPtr.Zero);
}
/// <summary>
/// When the instance gets disposed.
/// </summary>
public void Dispose()
{
this.ReleaseHandle();
}
/// <summary>
/// Sets the handle.
/// </summary>
/// <param name="handle">
/// The handle.
/// </param>
/// <param name="onlyIfNotSet">
/// If true, will not assign to an already assigned handle.
/// </param>
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();
}
}
/// <summary>
/// Adds a message handler for the given message number.
/// </summary>
/// <param name="messageNumber">
/// The message number.
/// </param>
/// <param name="messageHandler">
/// The message handler.
/// </param>
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());
}
}
}
/// <summary>
/// Processes the window messages.
/// </summary>
/// <param name="m">
/// The m.
/// </param>
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 WM_DESTROY (messageNumber == 2),
// ignore return value
if (cb(this, ref m) && messageNumber != 2)
{
return; // done processing
}
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("{0}", ex));
}
}
}
}
base.WndProc(ref m);
}
/// <summary>
/// Empty any existing backlog of things to run on the user interface
/// thread.
/// </summary>
private void EmptyUiBacklog()
{
// Check to see if there is a backlog of
// methods to run on the UI thread. If there
// is than notify the UI thread about them.
bool haveBacklog;
lock (this.queue)
{
haveBacklog = this.queue.Any();
}
if (haveBacklog)
{
Win32.PostMessage(
new HandleRef(this, this.Handle),
this.runOnUiThreadWindowsMessageNumber,
IntPtr.Zero,
IntPtr.Zero);
}
}
/// <summary>
/// Holds a method and its arguments.
/// </summary>
private class MethodArgs
{
/// <summary>
/// Gets or sets the method arguments.
/// </summary>
public object[] Args { get; set; }
/// <summary>
/// Gets or sets Method.
/// </summary>
public Delegate Method { get; set; }
}
}
上述代码的主要原因是获得其中实现的BeginInvoke()调用 - 您需要该调用在 GUI 线程上创建自己的表单。但是,您需要有一个窗口句柄才能对 GUI 线程进行回调。最简单的方法是让 C++ 代码传入窗口句柄(作为 IntPtr 到达),但您也可以使用以下内容:
过程。获取当前进程()。主窗口句柄;
即使您在从 C++ 调用的 C# 中,也可以获得主窗口的句柄。请注意,C++ 代码可能会更改主窗口句柄并使 C# 代码无效(当然,这可以通过侦听原始句柄上的正确 Windows 消息来捕获 - 您也可以使用上面的代码执行此操作)。
抱歉,没有显示上面的 Win32 调用声明。您可以通过搜索网络获取它们的 P/Invoke 声明。(我的 Win32 类很大。)
就 C++ 代码中的回调而言 - 只要您使回调相当简单,您就可以使用Marshal.GetDelegateForFunctionPointer将传入的函数指针(变成 IntPtr)转换为常规的旧 C# 委托。
因此,至少回调 C++ 非常容易(只要您正确定义了委托声明)。例如,如果您有一个接受char const *并返回 void 的 C++ 函数,您的委托声明将类似于:
public delegate void MyCallback([MarshalAs(UnmanagedType.LPStr)] string myText);
这涵盖了基础知识。使用带有传入窗口句柄的上述类在NativeWindowWithCallbacks.BeginInvoke()调用中创建您自己的基于表单的窗口。现在,如果您想使用 C++ 窗口代码,例如,在 C++ 代码管理的窗口中添加菜单项条目,事情又变得更加复杂。.Net 控件代码不喜欢与它没有创建的任何窗口进行交互。因此,要添加一个菜单项,您最终会编写包含大量 Win32 P/Invokes 的代码来执行与编写 C 代码时相同的调用。上面的NativeWindowWithCallbacks类将再次派上用场。