直接从 C# 使用无窗口富文本控件





5.00/5 (3投票s)
演示了一种调用和实现无窗口富文本控件的非标准接口的技术
引言
无窗口富文本编辑控件,或者文本服务对象,它提供 富文本编辑控件 的功能,但没有提供窗口,通常从原生 C/C++ 或 C++/CLI 模块访问。本文阐述了如何直接从编译为 AnyCPU 的 C# 程序集中完成此操作。
背景
无窗口富文本编辑控件 API 在 Windows SDK 的 *TextServ.h* 头文件中声明。 存在一对接口,ITextServices
和 ITextHost
,以及用于创建和销毁文本服务对象的函数。 存在许多用原生代码编写的、使用此 API 的示例。 但是,我遇到的一些从 C# 使用它的尝试显然没有成功。 问题是什么? 这些接口的开发者,明智地没有向方法添加 __stdcall
修饰符。 因此,这些方法遵循默认的 __thiscall
调用约定,并且这些接口,作为非常正常的 IUnknown
派生接口,对 .NET 互操作不友好,因为目前无法为托管接口的方法指定调用约定。 但是,可以为委托完成。 因此,我们可以安排一个托管的接口虚拟表表示,以便从非托管代码封送和封送至非托管代码,并在一种自定义 RCW 中使用它。 我将提供一个最小的工作示例,将其扩展留给有兴趣的人。
Using the Code
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
方法。 就本文而言,我使用了一个不实现引用计数的 static
类
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 自动为委托创建非托管包装器。 我们还需要创建一个指向此虚拟表的非托管对象。 由于使用了 static
类的单个实例,因此我们可以简单地在同一内存块中写入指向虚拟表的指针
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);
它表示 ITextHost
实现的一个非托管实例,可以将其传递给 CreateTextServices
函数
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
接口。 这需要进一步探索。
您还可以考虑查找和动态加载必要的富文本编辑控件实现。 文本服务从 2.0 版本开始可用。
历史
- 2019年2月3日:首次发布