1

In Delphi XE, I am capturing CF_UNICODETEXT data from the clipboard. The result is a stream that terminates with two null bytes. To get the actual string that was copied to clipboard, I need to strip the nulls.

This similar so question contains a nice method of converting from TMemoryStream to Delphi's unicode string:

function MemoryStreamToString(M: TMemoryStream): string;
begin
  SetString(Result, M.Memory, M.Size div SizeOf(Char));
end;

In my case, however, this would produce a string including the trailing nulls. I could fix that by limiting the size:

function ClipboardMemoryStreamToString(M: TMemoryStream): string;
begin
  SetString(Result, M.Memory, (M.Size - SizeOf(Char)) div SizeOf(Char));
end;

... but this feels ugly, "special-casey". I wonder if there is a cleaner way to code this, so that anyone (me!) looking at the code later won't immediately ask "Why is the trailing char being dropped from the stream?"

Edit: One way of pre-empting the question is adding a comment. But, other than that?

Community
  • 1
  • 1
Marek Jedliński
  • 7,088
  • 11
  • 47
  • 57
  • It's funny that you mention TMemoryStream. I spent today removing it from my entire codebase (there wen't many references left) due to its memory fragmenting implementation! – David Heffernan Jan 19 '11 at 19:28
  • @David: that's intriguing. What's your preferred substitute? – Marek Jedliński Jan 19 '11 at 21:07
  • my very own hand-crafted `TBlockAllocatedMemoryStream = class(TStream)` does the job with no fragmentation. It also doesn't require contiguous address space and so avoids another pitfall with TMemoryStream. – David Heffernan Jan 19 '11 at 21:12

2 Answers2

5

What's wrong with Clipboard.AsText? It does everything for you with no need for streams, poking at bytes, dealing with null terminators etc.

As for the precise question you raised, I would simply write:

SetString(Result, M.Memory, M.Size div SizeOf(Result[1]) - 1);
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 1
    I'm avoiding the clipbrd unit, because I need to store and put back all the available formats, so I'm dealing only with API and the exact data buffers Windows supplies on the clipboard. Actually, Clipboard.AsText does something neat to solve my problem: Result := PChar(GlobalLock(Data)). The null-terminated stream does conform to PChar format, but I wouldn't have guessed I'd be getting a copy this way. – Marek Jedliński Jan 19 '11 at 18:37
  • If you wish to avoid the `Clipboard` unit then I believe you have now located the canonical solution. However, the `Clipboard` unit can easily be used to read and write multiple formats. Clearly you don't need to use it, but I see no particular reason to avoid it. – David Heffernan Jan 19 '11 at 19:17
  • 1
    @Mood, Saving/restoring the clipboard is possible, but not without unwanted side-effects. You'll not be able to restore complex formats 100% accurately, and you cannot manipulate the clipbaord without causing trouble in other apps. Please see my reply to this question: http://stackoverflow.com/questions/4735559/c-saving-and-then-retriving-data-via-the-clipboard/4735718#4735718 – Chris Thornton Jan 19 '11 at 19:18
  • @Chris: your own well-known product (of which I'm a registered user) does very well what you're saying cannot (or should not) be done. How could that be? :) I'm aiming for something much, *much* simpler, though - just cf_text and cf_unicodetext. I'm not touching bitmaps, OLE objects or even RTF. – Marek Jedliński Jan 19 '11 at 21:05
  • @mood - ;) Well thanks! Have a look at my Application Profile in the options dialog. From there, I let the user pick and choose the formats that are captured. The defaults values are usually just text, HTML, and Bitmap. It never tries to get everything. And I've got lots of retry logic in there, as I expect it to fail. The big difference though, is that I'm not (usually) trying to put the data back onto the clipboard immediately after it arrived in the first place. i.e. I'm avoiding creating a clipboard event, DURING a clipboard event. – Chris Thornton Jan 19 '11 at 21:45
2

If you target CF_UNICODETEXT, you need to specify unicode string specifically:

// For old Delphi versions
{$IFNDEF UNICODE}
type
  UnicodeString = WideString;
{$ENDIF}

// For CF_TEXT
function MemoryStreamToAnsiString(M: TMemoryStream): AnsiString;
begin
  SetString(Result, M.Memory, M.Size);
  if (Result <> '') and (Result[Length(Result)] = #0) then
    SetLength(Result, Length(Result) - 1);
end;

// For CF_UNICODETEXT
function MemoryStreamToUnicodeString(M: TMemoryStream): UnicodeString;
begin
  SetString(Result, M.Memory, M.Size div SizeOf(WideChar));
  if (Result <> '') and (Result[Length(Result)] = #0) then
    SetLength(Result, Length(Result) - 1);
end;

// I'm not sure that you should use this form
function MemoryStreamToString(M: TMemoryStream): String;
begin
  SetString(Result, M.Memory, M.Size div SizeOf(Char));
  if (Result <> '') and (Result[Length(Result)] = #0) then
    SetLength(Result, Length(Result) - 1);
end;

If you're 100% sure that string is zero-terminated, then:

// For CF_TEXT
function MemoryStreamToAnsiString(M: TMemoryStream): AnsiString;
begin
  SetString(Result, M.Memory, M.Size - 1);
end;

// For CF_UNICODETEXT
function MemoryStreamToUnicodeString(M: TMemoryStream): UnicodeString;
begin
  SetString(Result, M.Memory, (M.Size div SizeOf(WideChar)) - 1);
end;

function MemoryStreamToString(M: TMemoryStream): String;
begin
  SetString(Result, M.Memory, (M.Size div SizeOf(Char)) - 1);
end;
Alex
  • 5,477
  • 2
  • 36
  • 56
  • Well, OP is using XE and one assumes is not attempting to write code that compiles on other versions of Delphi. Thus it doesn't need to be complex like this. – David Heffernan Jan 20 '11 at 14:55