65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2019年3月2日

CPOL

3分钟阅读

viewsIcon

8832

downloadIcon

270

演示了一种调用和实现无窗口富文本控件的非标准接口的技术

引言

无窗口富文本编辑控件,或者文本服务对象,它提供 富文本编辑控件 的功能,但没有提供窗口,通常从原生 C/C++ 或 C++/CLI 模块访问。本文阐述了如何直接从编译为 AnyCPU 的 C# 程序集中完成此操作。

背景

无窗口富文本编辑控件 API 在 Windows SDK 的 *TextServ.h* 头文件中声明。 存在一对接口,ITextServicesITextHost,以及用于创建和销毁文本服务对象的函数。 存在许多用原生代码编写的、使用此 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 是一个指向非托管实例的指针,该指针作为第一个参数传递给这些方法。 然后我们定义一个实现该接口的类。 它应该包含与 IUnknownMethodsITextHostVTable 中的委托具有完全相同签名的 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日:首次发布
© . All rights reserved.