Dec 182018
 

I just now had the need to transmit GPS (WGS 84) coordinates from one program to another. First, I simply copied longitude and latitude separately using the clipboard, which works fine but is really time consuming when you have to do that very often.

I could have just copied both values as text to clipboard using e.g. <tab> as the separator, but that would have required some code on both sides anyway, so I thought, why not use a custom clipboard format? I had never done that before and I wanted to learn how to do it.

Windows allows all programs to register custom clipboard formats by calling the RegisterClipboardFormat Windows API function. It simply takes a PChar (PAnsiChar / PWideChar, depending on which variant you call) and returns a handle for the new format. This handle can then be used to read and write entries of that format to and from the clipboard.

  MyClipFormat := RegisterClipboardFormat(PChar('MyClipFormat'));

The name of the clipboard format must be unique for that format, so e.g. you could prefix it with your company’s domain name to be sure. Multiple programs can register the same format if they know how to read and write it and can then exchange data this way.

Once such a format has been registered, it can be used with the standard Clipboard.HasFormat method.

  if Clipboard.HasFormat(MyClipFormat) then begin
    // Yes! There is something for us on the clipboard.
  end;

Actually writing and reading a custom format involves calls to the Windows API function GlobalAlloc, GlobalLock and GlobalUnlock as well as using the Clipboard.SetAsHandle and Clipboard.GetAsHandle methods.

But first, we need to define that format. In my case I decided to transfer the GPS coordinates as an AnsiString in the following format:

'WGS84'#9 + LongitudeString + #9 + LatitudeString
So, I use a fixed prefix followed by a tab character and then the longitude and latitude as strings, again separated by a tab character. I could simply have transferred the binary representation of e.g. two doubles but I wanted to use a format that was easy to debug. Since this is the clipboard of a single computer, I don’t care about the decimal separator. It should be the same on both sides, the writer and the reader.

Writing is the more complicated part, so let’s implement that first.

procedure WriteMyClipFormat(_Longitude, _Latitude: double);
var
  s: AnsiString;
  MemHandle: HGLOBAL;
  MemPtr: Pointer;
begin
  s := AnsiString(Format('WGS84'#9'%.5f'#9'%.5f', [_Longitude, _Latitude]));
  // Get a moveable memmory handle.
  // (according to the Windows API documentation of
  // SetClipboardData it must be moveable.)
  MemHandle := GlobalAlloc(GMEM_MOVEABLE, Length(s) + 1);
  // To be able to write to such a handle, we must convert it to a pointer.
  MemPtr := GlobalLock(MemHandle);
  try
    StrCopy(MemPtr, PAnsiChar(s));
  finally
    GlobalUnlock(MemHandle);
  end;
  // Now we call SetAsHandle to write it to the clipboard.
  Clipboard.SetAsHandle(MyClipFormat, MemHandle);
  // Since the memory is now stored in the clipboard,
  // we can not free it. So don't call GlobalFree here!
end;

That takes care of the writing part. Now for reading.

function TryReadMyClipFormat(out _Longitude, _Latitude: double): boolean;
var
  MemHandle: THandle;
  MemPtr: Pointer;
  WgsCoords: string;
  s: string;
begin
  Result := Clipboard.HasFormat(MyClipFormat);
  if not Result then begin
    // There is nothing in that format on the clipboard.
    Exit; //==&amp;gt;
  end;

  Clipboard.Open;
  try
    // Get the content.
    MemHandle := Clipboard.GetAsHandle(MyClipFormat);
    // Convert the memory handle to a pointer so we can access it.
    MemPtr := GlobalLock(MemHandle);
    // It's a PAnsiChar, so we can simply assign it to a string,
    // converting it from AnsiString to String on the way
    WgsCoords := string(PAnsiChar(MemPtr));
    GlobalUnlock(MemHandle);
  finally
    Clipboard.Close;
  end;
  // WgsCoords now contains the coordinates encoded in the
  // way described above.
  // We now use ExtractStr (from my u_dzStringUtils unit)
  // to get the three parts and convert the numbers to float.
  s := ExtractStr(WgsCoords, #9);
  Result := (s = 'WGS84');
  if not Result then begin
    // Something went wrong. It is supposed to have this prefix.
    Exit; //==&amp;gt;
  end;

  s := ExtractStr(WgsCoords, #9);
  Result := TryStrToFloat(s, _Longitude);
  if not Result then begin
    // The longitude could not be converted to a float.
    Exit; //==&amp;gt;
  end;

  s := ExtractStr(WgsCoords, #9);
  Result := TryStrToFloat(s, _Latitude);
end;

That takes care of the reading part.

As mentioned above, we could have simply transfered the a memory block containing the binary representation of two doubles instead. I just like my code complex and unreadable. 😉

Also, it would easily be possible to wrap all this into a class which registers the custom clipboard format in its constructor and has a Write and TryRead Method. We could even have multiple classes for different clipboard data types.

Discussion about this in the international Delphi Praxis forum.

 Posted by on 2018-12-18 at 18:13