Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using Windowless Rich Edit Controls Directly from C#

0.00/5 (No votes)
2 Mar 2019 1  
A technique of invoking and implementing non-standard interfaces of windowsless rich edit controls is demonstrated

Introduction

A windowless rich edit control, or a text services object, which provides the functionality of a rich edit control without providing the window, is usually accessed from the native C/C++ or C++/CLI modules. This article illustrates how this can be done directly from C# assembly compiled as AnyCPU.

Background

Windowless rich edit controls API is declared in the TextServ.h header of Windows SDK. There is a pair of interfaces, ITextServices and ITextHost, and the functions for creating and destroying text services objects. A number of examples written in the native code exist that are working with this API. But a few attempts of using it from C# that I came across were apparently unsuccessful. What is the problem? The developers of these interfaces, in their wisdom, did not add __stdcall modifiers to the methods. As a result, the methods follow default __thiscall calling convention, and the interfaces, being quite normal IUnknown-derived interfaces, are not friendly to .NET interop, because there is currently no way to specify calling convention for the methods of managed interfaces. However, it can be done for delegates. Thus, we can arrange a managed representation of the interface virtual tables to be marshalled from and to the unmanaged code, and use it in a kind of custom RCW. I'll provide a minimal working example, leaving its extension to the interested parties.

Using the Code

ITextHost is a callback interface that is passed from a client to the text services. In this case, an instance of the pseudo object implementing the interface should be created in the unmanaged memory. At first, we declare a formatted class containing managed methods marshaled to the pointers to unmanaged functions with the appropriate calling convention:

[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;

  ...
}

The base IUnknownMethods class contains three delegates for the methods of IUnknown, which use standard calling convention. _this is a pointer to the unmanaged instance that is passed to the methods as a first argument. Then we define a class that implements the interface. It should contain static methods with exactly the same signatures as the delegates in IUnknownMethods and ITextHostVTable. For the purpose of this article, I use a static class that does not implement reference count:

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's methods are assigned to the delegates in an instance of ITextHostVTable, the latter then is marshalled to the unmanaged memory. Marshal automatically creates unmanaged wrappers for the delegates. We also need to create an unmanaged object pointing to this virtual table. Since single instance of the static class is used, we can simply write a pointer to the virtual table in the same memory block:

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);

It represents an unmanaged instance of the ITextHost implementation that can be passed to the CreateTextServices function:

var hr = CreateTextServices(IntPtr.Zero, TextHost.Unmanaged, out var pUnk);

The function returns a pointer to IUnknown that can be queried for ITextServices interface. A managed representation of its virtual table is declared in the same way as ITextHostVTable, but since the unmanaged table already exists at the address returned by the QueryInterface, we just need to marshal it to the managed structure:

_this = pITextServices;
vtable = (ITextServicesVTable)Marshal.PtrToStructure(Marshal.ReadIntPtr(_this), 
          typeof(ITextServicesVTable));

And that is almost all. Just wrap the vtable with the suitable class. The sample class invokes a couple of methods to set and draw "Hello, world!" text.

Points of Interest

I did not elaborate on marshaling the arguments of the interfaces' methods, only those that were necessary for the example were detalized. You can change the marshaling to whatever you need.

If multiple instances of ITextHost implementations are necessary, a separate unmanaged memory block should be allocated for each of them, containing a pointer to the static virtual table and the data that can be used to convert the unmanaged _this pointer to the managed reference.

It seems that ShutdownTextServices function is currently not implemented, and I am not sure what is the correct way to dispose the TextServices object and ITextHost implementation associated with it. So I just release the ITextService interface. It needs to be explored further.

You can also consider finding and dynamically loading necessary rich edit control implementation. Text services are available from version 2.0.

History

  • 2/3/2019: Initial post

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here