Delphi tip: Pasting OLE objects into TRichEdits

Posted: (EET/GMT+2)

 

The new TRichEdit control in Delphi 2 is a wonderful thing. It supports fonts, colors, paragraph formatting, printing, and so on. This all might seem simple, but actually the rich editor control is very advanced. For example, it allows you to embed OLE objects into it, just like that. The real pity is that the Delphi's TRichEdit component doesn't support OLE. But don't worry. After you've finished this How To, you will know how to paste OLE objects into your TRichEdits - and it doesn't matter if they are simple bitmaps or complicated spreadsheet documents!

By default, the TRichEdit accepts only text. This applies to the user (pressing Ctrl+V) and the programmer (sending a WM_PASTE message) alike. If you want to extend this, you have to implement some OLE interfaces.

OLE interfaces? Yes, I know they can sound a bit scary. But actually, they are quite simple. Honestly speaking, they are just plain Delphi classes with predefined function names and parameters. The interface you need to implement is called IRichEditOLECallback. It defines ten new methods, and because it is a descendant of IUnknown, a total of 13 methods are defined. To accept objects from the Clipboard, you need to implement at least three of them plus two IUnknown methods.

For this documents purposes, I only give you a stripped version of the IRichEditOLECallback definition:

IRichEditOleCallback = Class(IUnknown)
  Function GetNewStorage(Var lplpstg : IStorage) : LongInt;
                         Virtual; StdCall; Abstract;
  Function GetInPlaceContext(Var A,B,C) : LongInt;
                             Virtual; StdCall; Abstract;
  Function ShowContainer(Var A) : LongInt;
                         Virtual; StdCall; Abstract;
  Function QueryInsertObject(Var lpclsid : TCLSID; lpstg : IStorage;
                             cp : Integer) : LongInt;
                             Virtual; StdCall; Abstract;
  Function DeleteObject(Var A) : LongInt; Virtual; StdCall; Abstract;
  Function QueryAcceptData(lpdataobj : IDataObject;
                           lpcfFormat : PClipFormat; reco : Integer;
                           fReally : Bool; hMetaPict : HGLOBAL)
                           : LongInt; Virtual; StdCall; Abstract;
End;

The methods we need to implement are GetNewStorage, QueryInsertObject and QueryAcceptData. Note that for example GetInPlaceContext must be defined (even with totally incorrect parameters) because the order of definition is important. The methods IRichEditOLECallback are defined as "virtual stdcall abstract" because we don't directly instantiate this object. Instead, we define an descendant class, as follows:

IMyRichEditCallback = Class(IRichEditOLECallback)
  Function AddRef : LongInt; Override;
  Function Release : LongInt; Override;
  Function GetNewStorage(Var lplpstg : IStorage) : LongInt; Override;
  Function QueryInsertObject(Var lpclsid : TCLSID; lpstg : IStorage;
                             cp : Integer) : LongInt; Override;
  Function QueryAcceptData(lpdataobj : IDataObject;
                           lpcfFormat : PClipFormat; reco : Integer;
                           fReally : Bool;
                           hMetaPict : HGLOBAL) : LongInt; Override;
End;

Here, the AddRef and Release methods are new. These are IUnknown methods which we must implement (thus the "override" directive). Normally, OLE objects are reference counted, which means that an object can be used multiple times without consuming additional memory. In this How To we ignore this fact. Instead, we just implement them the simplest possible way. The IMyRichEditCallback actually looks like this:

Function IMyRichEditCallback.AddRef : LongInt;
Begin
  Result := 1;
End;

Function IMyRichEditCallback.Release : LongInt;
Begin
  Result := 0;
End;

Function IMyRichEditCallback.GetNewStorage(Var lplpstg : IStorage) : LongInt;
Begin
  Result := StgCreateDocFile('c:\mystorage.stg',
                             stgm_ReadWrite or stgm_Direct or
                             stgm_Create or stgm_Share_Exclusive,
                             0,lplpstg);
End;

Function IMyRichEditCallback.QueryInsertObject(Var lpclsid : TCLSID;
                                               lpstg : IStorage;
                                               cp : Integer) : LongInt;
Begin
  Result := S_OK;
End;

Function IMyRichEditCallback.QueryAcceptData(lpdataobj : IDataObject;
                                             lpcfFormat : PClipFormat;
                                             reco : Integer;
                                             fReally : Bool;
                                             hMetaPict : HGLOBAL) : LongInt;
Begin
  Result := S_OK;
End;

As you can see, the methods are actually quite simple. For example, QueryInsertObject and QueryAcceptData simply return S_OK as their result, meaning that everything is all right, and that the OLE object insertion can continue. The most interesting method is the GetNewStorage:

Result := StgCreateDocFile('c:\mystorage.stg',
                           stgm_ReadWrite or stgm_Direct or
                           stgm_Create or stgm_Share_Exclusive,
                           0,lplpstg);

If you look the StgCreateDocFile in the Win32 API Help file, you can see that it "creates a new storage object using the OLE-provided compound file implementation for the IStorage interface." Simply put, it creates something called a compound (structured storage) file, and returns a pointer to an object representing the file. This object then is used internally by the rich editor control to save the data pasted from the Clipboard.

Structured storage, compound files, and OLE are difficult topics, and beyond the scope of this topic. But for this document, it doesn't matter whether you understand them or not. The only thing you need to know is that you implement an interface, which the rich editor uses to get information about how it should handle the object on the Windows Clipboard.

After you have implemented the interface, you only need to register the interface with the rich editor:

Var
  MyCallback : IMyRichEditCallback;
  ...

{$WARNINGS OFF}
  MyCallback := IMyRichEditCallback.Create;
{$WARNINGS ON}
  SendMessage(RichEdit1.Handle,em_SetOLECallback,0,LongInt(MyCallback));

Here, we instantiate our object, and the register it with the EM_SETOLECALLBACK message, which is defined in the RichEdit unit. The reason that the $WARNINGS directive is used around the object creation is that we haven't implemented all the abstract methods in IRichEditOLECallback, and thus the compiler warns us with a reason.

When we have registered the interface, we are ready to paste the object on the Clipboard. As you can see, this is really trivial:

procedure TForm1.Button1Click(Sender: TObject);
begin
  SendMessage(RichEdit1.Handle,wm_Paste,0,0);
end;

For example, if you had a bitmap drawn with Pain(t) on the Clipboard, the bitmap would be inserted to the cursor position. If an error occurs, for example because there is nothing to paste or if there is not enough memory, you will hear a beep (made by the rich editor common control).

Please note that the above implementation is very far from perfect. For example, if you try to double-click the embedded object to edit it, you will get a nasty crash. However, writing a full-blown rich editor with OLE support is a difficult task. I have only tried to give you some simple insight on how you can paste objects without digging deep into OLE. However, I'm currently working on a extended TRichEdit component, which will support OLE objects without much pain. So stay tuned!