在Windows应用程序开发中,富文本编辑控件是一种常见的UI组件,它允许用户输入和编辑格式化文本。然而,标准的富文本编辑控件通常会创建一个窗口,这在某些情况下可能不是想要的。幸运的是,Windows SDK提供了一种无窗口的富文本编辑控件API,它允许直接从原生C/C++或C++/CLI模块中访问。本文将展示如何在C#程序中使用这种无窗口的富文本编辑控件。
无窗口富文本编辑控件的API在Windows SDK的TextServ.h头文件中声明。它包含一对接口:ITextServices和ITextHost,以及用于创建和销毁文本服务对象的函数。虽然有许多使用这个API的原生代码示例,但发现使用C#访问这个API的尝试似乎都不太成功。问题在于这些接口的开发者没有为方法添加__stdcall修饰符。因此,这些方法遵循默认的__thiscall调用约定,而接口本身作为普通的IUnknown派生接口,对.NET互操作并不友好,因为目前没有办法为托管接口的方法指定调用约定。然而,可以为委托指定调用约定。因此,可以安排托管接口虚表的托管表示形式,以便从非托管代码中进行封送,并在自定义的RCW中使用。将提供一个最小的工作示例,并将其扩展留给感兴趣的人。
ITextHost是一个回调接口,由客户端传递给文本服务。在这种情况下,应该在非托管内存中创建一个实现该接口的伪对象实例。首先,声明一个格式化类,其中包含托管方法,这些方法被封送到具有适当调用约定的非托管函数的指针:
[StructLayout(LayoutKind.Sequential)]
class ITextHostVTable : IUnknownMethods
{
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate IntPtr TxGetDCDelegate(IntPtr _this);
TxGetDCDelegate TxGetDC;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate int TxReleaseDCDelegate(IntPtr _this, IntPtr hdc);
TxReleaseDCDelegate TxReleaseDC;
// ...
}
基类IUnknownMethods包含三个委托,用于IUnknown的方法,这些方法使用标准调用约定。_this是一个指向非托管实例的指针,作为方法的第一个参数传递。然后定义一个实现接口的类。它应该包含与IUnknownMethods和ITextHostVTable中的委托完全相同的静态方法签名。对于本文的目的,使用了一个不实现引用计数的静态类:
static class TextHost
{
// ...
public static int QueryInterface(IntPtr _this, ref Guid iid, out IntPtr ppv)
{
if (iid == IID_ITextHost || iid == IID_IUnknown)
{
ppv = _this;
return S_OK;
}
ppv = IntPtr.Zero;
return E_NOINTERFACE;
}
public static uint AddRef(IntPtr _this)
{
return 1;
}
public static uint Release(IntPtr _this)
{
return 1;
}
public static IntPtr TxGetDC(IntPtr _this)
{
return IntPtr.Zero;
}
public static int TxReleaseDC(IntPtr _this, IntPtr hdc)
{
return 0;
}
// ...
}
TextHost的方法被分配给ITextHostVTable的一个实例,后者然后被封送到非托管内存。Marshal自动为委托创建非托管包装器。还需要创建一个指向这个虚表的非托管对象。由于使用了静态类的单个实例,可以简单地在相同的内存块中写入指向虚表的指针:
vtable = new ITextHostVTable();
var ptrsize = Marshal.SizeOf(typeof(IntPtr));
Unmanaged = Marshal.AllocHGlobal(ptrsize + Marshal.SizeOf(vtable));
var pVTable = IntPtr.Add(Unmanaged, ptrsize);
Marshal.WriteIntPtr(Unmanaged, pVTable);
Marshal.StructureToPtr(vtable, pVTable, false);
它表示一个可以传递给CreateTextServices函数的ITextHost实现的非托管实例:
var hr = CreateTextServices(IntPtr.Zero, TextHost.Unmanaged, out var pUnk);
该函数返回一个指向IUnknown的指针,可以为其查询ITextServices接口。ITextHostVTable的托管表示形式以相同的方式声明,但由于非托管表已经存在于QueryInterface返回的地址中,只需要将其封送到托管结构:
_this = pITextServices;
vtable = (ITextServicesVTable)Marshal.PtrToStructure(Marshal.ReadIntPtr(_this), typeof(ITextServicesVTable));
这几乎就是全部了。只需用合适的类包装vtable。示例类调用了几个方法来设置和绘制"Hello, world!"文本。
没有详细说明接口方法的参数封送,只详细说明了示例中必要的那些。可以根据需要更改封送。
如果需要多个ITextHost实现的实例,则应该为每个实例分配一个单独的非托管内存块,其中包含指向静态虚表的指针和可以用来将非托管_this指针转换为托管引用的数据。
似乎目前还没有实现ShutdownTextServices函数,不确定正确处理TextServices对象和与之关联的ITextHost实现的方法。所以仅释放了ITextService接口。这需要进一步探索。