Search code examples
delphimigrationdelphi-xe2clipboard

Copying/pasting with TMemoryStream and TClipboard in D2009+


I have a method that reads the data in the cells of a row of a TStringGrid, and copies it to the clipboard. And I have a corresponding method to paste the data from the clipboard into an empty row in the TStringGrid.

These methods were written for D7, but are broken after migration to XE2.

procedure TfrmBaseRamEditor.CopyLine(Sender: TObject; StrGridTemp: TStringGrid;
                                     Row, Column: Integer);
var
  Stream: TMemoryStream;
  MemHandle: THandle;
  MemBlock: Pointer;
  i, Len: Integer;
  RowStr: String;
begin
  Stream := nil;
  try
    Stream := TMemoryStream.Create;

    // The intermediate format to write to the stream.
    // Separate each item by horizontal tab character.
    RowStr := '';
    for i := 0 to (StrGridTemp.ColCount - 1) do
      RowStr := RowStr + StrGridTemp.Cells[i, Row] + #9;
    // Write all elements in a string.
    Len := Length(RowStr);
    Stream.Write(Len, SizeOf(Len));
    Stream.Write(PChar(RowStr)^, Length(RowStr));
    // Request Memory for the clipboard.
    MemHandle := GlobalAlloc(GMEM_DDESHARE, Stream.SIZE);
    MemBlock := GlobalLock(MemHandle);
    try
      // Copy the contents of the stream into memory.
      Stream.Seek(0, soFromBeginning);
      Stream.Read(MemBlock^, Stream.SIZE);
    finally
      GlobalUnlock(MemHandle);
    end;
    // Pass the memory to the clipboard in the correct format.
    Clipboard.Open;
    Clipboard.SetAsHandle(TClipboardFormat, MemHandle);
    Clipboard.Close;
  finally
    Stream.Free;
  end;
end;

procedure TfrmBaseRamEditor.PasteLine(Sender: TObject; StrGridTemp: TStringGrid;
                                      Row, Column: Integer);
var
  Stream: TMemoryStream;
  MemHandle: THandle;
  MemBlock: Pointer;
  ASize, Len, i: Integer;
  TempStr: String;
begin
  Clipboard.Open;
  try
    // If something is in the clipboard in the correct format.
    if Clipboard.HasFormat(TClipboardFormat) then
    begin
      MemHandle := Clipboard.GetAsHandle(TClipboardFormat);
      if MemHandle <> 0 then
      begin
        // Detect size (number of bytes).
        ASize := GlobalSize(MemHandle);
        Stream := nil;
        try
          Stream := TMemoryStream.Create;
          // Lock the contents of the clipboard.
          MemBlock := GlobalLock(MemHandle);
          try
            // Copy the data into the stream.
            Stream.Write(MemBlock^, ASize);
          finally
            GlobalUnlock(MemHandle);
          end;
          Stream.Seek(0, soFromBeginning);
          Stream.Read(Len, SizeOf(Len));
          SetLength(TempStr, Len);
          Stream.Read(PChar(TempStr)^, Stream.SIZE);
          for i := 0 to StrGridTemp.RowCount do
            StrGridTemp.Cells[i, Row] := NextStr(TempStr, #9);
        finally
          Stream.Free;
        end;
      end;
    end;
  finally
    Clipboard.Close;
  end;
end;

The problem manifests when I copy a row with some values, then paste it into an empty row. The first cell is pasted correctly, but the second cell contains garbage characters (and nothing is pasted in the 3rd column onwards). I know why nothing is pasted in 3rd column onwards: because the "horizontal tab" character which separates the columns is corrupted along with the cell contents.

I've looked through "Delphi and Unicode" by Marco Cantu, but haven't been able to figure out where it's all going wrong.


Solution

  • Char is an alias for WideChar. So in CopyLine

    Stream.Write(PChar(RowStr)^, Length(RowStr));
    

    only writes half the string. It should be

    Stream.Write(PChar(RowStr)^, Length(RowStr)*SizeOf(Char));
    

    In PasteLine I find this line odd:

    Stream.Read(PChar(TempStr)^, Stream.SIZE);
    

    Since you've already consumed some of the string you are attempting to read past the end. I'd write it like this:

    Stream.Read(PChar(TempStr)^, Len*SizeOf(Char));
    

    Note that if you use the same custom clipboard format identifier as your ANSI program then you'll have encoding mismatches if you copy from one and paste into the other. You might be wise to register under a different clipboard format for your new Unicode format.


    Some other comments:

    Stream := nil;
    try
      Stream := TMemoryStream.Create;
      ...
    finally
      Stream.Free;
    end;
    

    should be written as:

    Stream := TMemoryStream.Create;
    try
      ...
    finally
      Stream.Free;
    end;
    

    If the constructor raises an exception, the try block will not be entered.

    You don't really need to write out the string length. You can rely on the stream size when reading to know how long the string is.

    In CopyLine, the clipboard Open and Close calls should be protected by a try/finally block.