C#中使用无窗口富文本编辑控件

在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接口。这需要进一步探索。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485