One of the nice things about Windows is that it lets us cut and paste items from one application to another. It does so, behind the scenes, with the Clipboard. One of the nice things about Delphi is that it gives us quick and easy access to clipboard functions, formats, events and methods.
For some of the standard operations like Copy and Paste, Delphi provides us with the TClipboard class.
Clipboard stores information in multiple formats so we can transfer data between applications that use different formats. When reading information from the clipboard with Delphi’s TClipboard class, we are limited to standard clipboard formats: text, pictures and metafiles.
Suppose we have two different Delphi applications running, what do you say about defining custom clipboard format in order to send and receive data between those two (Delphi) programs?
Suppose we are trying to code a Paste menu item – we want it to be disabled when there is no, lets say, text in the clipboard. Since the entire process with the clipboard takes place behind the scenes, there is no method of TClipboard class that will inform us that there has been some change in the contents of the clipboard. What we need is to hook in the clipboard notification system, so we can get and respond to events when the clipboard changes.
If we want more flexibility and functionality we have to reach for Win API and messaging system.
An application can be notified of changes in the data stored in the Windows clipboard by registering itself as a Clipboard Viewer.
Clipboard viewers use two API calls and several messages to communicate with the Clipboard viewer chain. SetClipboardViewer adds a window to the beginning of the chain and returns a handle to the next viewer in the chain. ChangeClipboardChain removes a window from the chain.
When a clipboard change occurs, the first window in the clipboard viewer chain is notified via the WM_DrawClipboard message and must pass the message on to the next window. To do this, our application must store the next window along in the chain to forward messages to and also respond to the WM_ChangeCBChain message which is sent whenever any other clipboard viewer on the system is added or removed to ensure the next window along is valid.
Delphi Clipboard Spy
As a demo project we’ll create a simple clipboard viewer application that displays the text in the Windows clipboard in a Memo component. The content of a memo is updated every time when the text in clipboard changes (format: cf_text).
To attach our application to the clipboard viewer notification chain we call the SetClipboardViewer function in the OnCreate event handler procedure of the main form.
procedure TForm1.FormCreate(Sender: TObject) ; begin NextInChain := SetClipboardViewer(Handle) ; end;
The NextInChain variable is of THandle type, declared as global in the form1’s unit. NextInChain represents our program as the next window in the clipboard viewer chain.
... var Form1: TForm1; NextInChain : THandle; implementation uses ClipBrd; ...
To perform a certain task whenever the contents of the clipboard changes we need to receive (and respond) to a WM_DrawClipboard message. The following procedure checks whether there is a “new” text stored in a clipboard and if so pastes this text to a memo component on a form. In any case after responding to WM_DrawClipboard message application must pass it to the next clipboard viewer.
procedure TForm1.WMDrawClipboard(var Msg:TMessage) ; begin if Clipboard.HasFormat(cf_text) then begin Memo1.Lines.Clear; Memo1.PasteFromClipboard end else begin //do something with other clipboard formats end; //pass the message on to the next window if NextInChain <> 0 then SendMessage(NextInChain, WM_DrawClipboard, 0, 0) end;
It may seem that this is all we have to code, but… The WM_ChangeCBChain messages (chain fixing functions) needs to be handled every time any other clipboard viewer chain is changed. The message brings with it the information about the window being removed and the next window in the clipboard viewer chain.
procedure TForm1.WMChangeCBChain(var Msg: TMessage) ; var Remove, Next: THandle; begin Remove := Msg.WParam; Next := Msg.LParam; with Msg do if NextInChain = Remove then NextInChain := Next else if NextInChain <> 0 then SendMessage(NextInChain, WM_ChangeCBChain, Remove, Next) end;
Finally, when we are ready to terminate our application we have to call the ChangeClipboardChain API function to remove our window from the chain of clipboard viewers. Naturally in the OnDestroy event handler:
procedure TForm1.FormDestroy(Sender: TObject) ; begin ChangeClipboardChain(Handle, NextInChain) ; end;
I have merely forgot this one: we, of course, have to declare all those message handling procedure in the, let’s say, private part of the forms interface section:
procedure WMDrawClipboard(var Msg: TMessage) ; message WM_DRAWCLIPBOARD; procedure WMChangeCBChain(var Msg: TMessage) ; message WM_CHANGECBCHAIN;
To see this code in real action, compile and run this project. Try cutting and copying some text to the clipboard…whoa, the text is displayed in our application window! We have produced a real clipboard textual spy.
I paste it my way
Do you know that, for example, MS Word has it’s own format for storing data to the clipboard? Large number of applications paste information in other formats, for example Rich Text Format, HTML format and so forth. Why not use Delphi to create and store our specific formats (like records) in the clipboard. This can be very useful in moving data between two Delphi applications.
Let’s say we have a record type called TMember, declared as:
type TMember = record Name : string; eMail : string; Posts : Cardinal; end;
We want to paste a variable of TMember type to the clipboard.
Before we can write information to the clipboard in a specific format, the format must be registered. We register a new clipboard format by calling the RegisterClipboardFormat API function and specifying a unique name for your new format – this enables us to copy and paste any type of data we want.
First we have to register the CF_TMember format (that holds TMember type variable values), in the OnCreate procedure of the main form, so that we can use it later on during clipboard calls. Newly created clipboard format will be named: ‘OurFormat’ and it will hold data from DelphiGuide variable filled in the OnCreate event and declared at the form level.
Then we send our data to the clipboard.
Sending custom format data to the clipboard is coded in the following way:
- Open the clipboard with OpenClipboard.
- Empty current contents of the clipboard with EmptyClipboard.
- Allocate and lock a global memory block.
- Copy data into the global memory block.
- Unlock the block.
- Transfer the block to the Clipboard with SetClipboardData. (Windows now controls the block).
- Close the clipboard with CloseClipboard. (So other programs can access it).
var DelphiGuide: TMember; ... MemberPointer : ^TMember; ... //fill some dummy data DelphiGuide.Name := 'Zarko Gajic'; DelphiGuide.eMail := 'email@example.com'; DelphiGuide.Posts := 15; OurFormat := RegisterClipboardFormat('CF_TMember') ; if OpenClipboard(Handle) then begin EmptyClipboard; MemberHandle := GlobalAlloc(GMEM_DDESHARE or GMEM_MOVEABLE, SizeOf(TMember)) ; MemberPointer := GlobalLock(MemberHandle) ; MemberPointer^.Name := DelphiGuide.Name; MemberPointer^.eMail := DelphiGuide.eMail; MemberPointer^.Posts := DelphiGuide.Posts; GlobalUnLock(MemberHandle) ; SetClipboardData(OurFormat,MemberHandle) ; CloseClipboard() ; end;
Again, to act whenever the contents of the clipboard changes we need to respond to a WM_DrawClipboard message. We use the HasFormat function to find out whether the clipboard contains data encoded in a specific format.
Getting custom format data from the clipboard is coded in the following way:
- Open the clipboard with OpenClipboard.
- Get a handle to the clipboard data with GetClipboardData.
- Allocate and lock a memory block to hold the clipboard data.
- Lock the clipboard data block.
- Copy data from the clipboard data block to the program’s data block (The data can now be manipulated by the application).
- Unlock both memory blocks.
- Close the clipboard with CloseClipboard.
if Clipboard.HasFormat(OurFormat) then begin if OpenClipboard(Handle) then begin MemberInClip := GetClipboardData(OurFormat) ; MemberPointer := GlobalLock(MemberInClip) ; with AMember do begin Name := MemberPointer^.Name; eMail := MemberPointer^.eMail; Posts := MemberPointer^.Posts; end; GlobalUnLock(MemberInClip) ; CloseClipboard() ; with Memo1.Lines do begin Clear; Add('Clipboard has TMember data:') ; Add(AMember.Name) ; Add(AMember.email) ; Add(IntToStr(AMember.Posts)) ; end; end; end;
Note: in normal situation we’ll have one Delphi application sending data to the clipboard and another Delphi application getting data from it. Since this is the demo project we have only one application running – we send custom formatted data to the clipboard in the OnCreate event for the form. In the same form, we are handling the clipboard change notification, therefore when we start our project, Memo1 component is filled with data from the clipboard – i.e. the data we are sending when the form is created.
Are You in Control Now?
That’s it. We have the clipboard working just as we would like it to. No one can send data to the clipboard without us knowing that something is going on. Even more: we can copy and paste program-specific data to the clipboard.
However, some questions stay unanswered: What is the best way of sending an array of TMember variables to another Delphi application; what will happen if some other application is register ‘CF_TMember’ with some other format, and so on.