Thursday, February 24, 2005

Delphi: Get the RTF for a paragraph in a rich edit

To the best of my knowledge, the only way to get the RTF for a portion of the contents in a rich edit control---without changing the selection---is as follows:
  1. Obtain an ITextRange as described in a previous post.

  2. Call ITextRange.Copy(), passing it a pointer to a variant. (Otherwise it will copy the text to the clipboard.)

  3. The variant should now contain an IDataObject interface pointer.

  4. Call IDataObject.GetData() requesting CF_RTF.

I tried to follow this plan, passing an Unassigned Variant to ITextRange.Copy(), but found that it was still Unassigned afterwards. I decided to get hold of proper declarations for ITextRange so that I need not go via Variant IDispatch. I found tom_TLB.pas on a Japanese site and took a copy. The file had obviously been produced by some automatic means. It declared ITextRange.Copy() as:

procedure Copy(out pVar: OleVariant); safecall;

Sadly, using tom_TLB.pas did not help: my variant was still Unassigned. I noticed in the documentation for ITextRange.Copy() has some strange requirements for its argument:
pVar->vt = (VT_UNKNOWN | VT_BYREF)
pVar is not null
pVar->ppunkVal is not null

By tracing into Delphi's call I found that Delphi was clearing my Variant and setting it to Unassigned on the way in. Clearly it could never satisfy the requirement that pVar->vt = (VT_UNKNOWN | VT_BYREF). To work around this, I build my variant argument manually:
  1. Change the declaration of ITextRange.Copy to:
    procedure Copy(pVar: PVariantArg); safecall;

  2. Construct the argument manually:
    var
    data_var: IUnknown;
    data_var_arg: TVariantArg;
    begin
    data_var := range.FormattedText; // Copy range to ensure no interference.
    data_var_arg.vt := VT_UNKNOWN or VT_BYREF;
    data_var_arg.wReserved1 := 0;
    data_var_arg.wReserved2 := 0;
    data_var_arg.wReserved2 := 0;
    data_var_arg.punkVal := @data_var;
    range.Copy(@data_var_arg);

  3. Your IDataObject will now be in data_var:
    OleCheck(IUnknown(data_var).QueryInterface(IDataObject, data));

Id love to know of a simpler way.

Friday, February 18, 2005

Delphi: Change rich edit format without changing selection

If you are using Microsoft's Rich Edit control v2+ then you can change a paragraph's format without selecting that paragraph if you use the ITextDocument COM interface.


var
rich_edit_ole: IRichEditOle;
doc_dispatch: IDispatch;
doc, range: Variant;
begin
if RichEdit_GetOleInterface(rich_edit.Handle, rich_edit_ole) then begin
OleCheck(rich_edit_ole.QueryInterface(IDispatch, doc_dispatch));
// Use Variants because we do not have Delphi declarations for
// the ITextDocument and ITextRange interfaces.
doc := doc_dispatch;
range := doc.Range(start_char, end_char);
range.SpaceAfter := 12;
end;

Note that you are not using Rich Edit v2+ if you use Delphi's inbuilt TRichEdit. TRichEdit specifically requests v1, which does not support the Text Object Model (TOM). I use Alexander Obukhov's TRichEdit98.

Thursday, February 17, 2005

Delphi: Stream your objects

Delphi's forms demostrate that it can stream objects to and from files. How can you use this to stream your own objects? It took me a day to blunder through this so I thought I'd share.

  • Derive your objects from TComponent. TPersistent is more trouble than it is worth, but see http://www.undu.com/Articles/990609d.html if you really want to.

  • Register each class that will be saved. e.g.
    initialization
    Classes.RegisterClass(TRichClipboardContent);
  • To save an object to a stream:
    stream.WriteComponent(c);
  • Delphi will save your published properties.

  • Override GetChildren in any objects that have sub-objects you want to save.

  • Also override GetChildOwner in such objects, otherwise all objects will be owned by the root component when they are read back in.

  • To read them back in:
    c := stream.ReadComponent(nil);

Want to see what's in a stream? Convert it to the text format used in modern .dfm files:
var
text_stream: TMemoryStream;
terminator: Char;
begin
text_stream := TMemoryStream.Create;
stream.Seek(soFromBeginning, 0);
ObjectBinaryToText(stream, text_stream);
terminator := #0;
debug_stream.Write(terminator, 1); // Add null terminator
OutputDebugString(PChar(debug_stream.Memory));