How to get notified when the content of the clipboard changes
Why do it?
It is often useful to know when the clipboard's contents have changed. For example you may wish to enable or disable a Paste button on a toolbar according to whether the clipboard contains data in a format supported by your application or not. Alternatively you may wish to display details of the items on the clipboard.
How it's done
Overview
Windows provides two different methods that can be used to get notifications when the clipboard contents change.
The preferred method is to use the "listener" API. However this is not supported on all versions of Windows - support started with Windows Vista. On older OSs the older clipboard viewer chain API needs to be used. This older API is unreliable since it depends on each application that uses it to maintain the "chain" of viewers. All it needs is one badly behaved (or crashed) application to corrupt the chain. The newer API is managed by Windows and doesn't suffer from this major design headache.
Using the clipboard listener API
This API is quite simple to use and if your app is running on Windows Vista or later, it's the one you should use. You need to register one of the application's windows to receive a notification from Windows when the clipboard format changes. That window receives a WM_CLIPBOARDUPDATE message when the content changes.
To register window to use to receive notification you call the AddClipboardFormatListener API function, passing it the handle of the window. Before the application terminates you must also call the RemoveClipboardFormatListener function, passing the same window handle.
The last step is to handle the WM_CLIPBOARDUPDATE message in the registered window's message loop and take whatever action you need to take when the clipboard changes.
Assuming you have a standard Delphi VCL application and you want to use the main form's window to receive change notifications, you can use code like the following.
To register the main form window insert the following code into your main form's OnCreate event handler:
1procedure TMainForm.FormCreate(Sender: TObject);
2begin
3 // ... other code
4 AddClipboardFormatListener(Handle);
5 // ... other code
6end;
To un-register the window insert the following code into the form's OnDestroy handler:
1procedure TMainForm.FormDestroy(Sender: TObject);
2begin
3 // ... other code
4 RemoveClipboardFormatListener(Handle);
5 // ... other code
6end;
Finally you need to handle the WM_CLIPBOARDUPDATE message. To do this create a new message handler in you main form class like this:
1// ...
2
3interface
4
5// ...
6
7type
8 TMainForm = class(TForm)
9 private
10 // ...
11 procedure WMClipboardUpdate(var Msg: TMessage);
12 message WM_CLIPBOARDUPDATE;
13 // ...
14 end;
15
16// ...
17
18implementation
19
20// ...
21
22procedure TMainForm.WMClipboardUpdate(var Msg: TMessage);
23begin
24 // Do require clipboard change processing here
25end;
26
27// ...
Using the clipboard viewer chain
When using this API you also need to register a window to receive notifications. The API is more convoluted than the "listener" API discussed above.
The registered window is known as a clipboard viewer. Windows maintains a linked list of clipboard viewers – the clipboard chain. Each viewer window is responsible for passing on notifications to any window that follows it in the chain. Since Windows relies on the cooperation of viewer applications, a badly behaved application can bring down the whole notification system. It is therefore important to play by the rules. The viewer window should also be un-registered before the associated application terminates.
When the content of the clipboard changes, Windows notifies them first window in the chain by sending it a WM_DRAWCLIPBOARD message. This window is responsible for passing the message to the next registered viewer window, and so on down the list. Consequently it can be seen that each clipboard viewer needs to record the identity of the next window in the chain.
Windows also notifies the windows in the clipboard chain when clipboard viewers are removed from the list. This is done by passing a WM_CHANGECBCHAIN message to the first window in the chain. The parameters of WM_CHANGECBCHAIN identify the window being removed along with the window that follows it in the chain. The message is passed along the chain until the window preceding that being removed is found. That window then updates its record of the next window in the chain.
In summary, here are the key steps in creating, managing and deleting a clipboard viewer:
- Register the viewer window by calling the SetClipboardViewer API function. This function returns the handle of the next window in the chain (or 0 if no such window).
- Keep track of viewer windows that are removed by processing the WM_CHANGECBCHAIN message and passing it along the chain or updating the record of the next window in the chain as necessary.
- Respond to clipboard changes by handling the WM_DRAWCLIPBOARD message and passing the message along the chain.
- Before closing the application remove the viewer window from the clipboard chain by calling the ChangeClipboardChain API function.
As before we assume that we have a Delphi VCL application that will use it's main form to receive notifications of clipboard changes. Here's an outline of the required code.
To register the main form window as a clipboard viewer we call the SetClipboardViewer API function in the application's FormCreate event handler. SetClipboardViewer returns the handle of the next window in the viewer chain which we must record in a form field for future reference. The handle may be 0 if we are the first viewer in the chain. Here is an outline of the code:
1// ...
2
3interface
4
5// ...
6
7type
8 TMainForm = class(TForm)
9 private
10 // ...
11 fNextCBViewWnd: HWND;
12 // ...
13 end;
14
15// ...
16
17implementation
18
19// ...
20
21procedure TMainForm.FormCreate(Sender: TObject);
22begin
23 // ... other code
24 fNextCBViewWnd := SetClipboardViewer(Handle);
25 // ... other code
26end;
We need to un-register our viewer window when the application is closing. In the FormDestroy event handler we add the required call to ChangeClipboardChain, as follows:
1procedure TMainForm.FormDestroy(Sender: TObject);
2begin
3 // ... other code
4 ChangeClipboardChain(Handle, fNextCBViewWnd);
5 // ... other code
6end;
Finally we need to handle the WM_CHANGECBCHAIN and WM_DRAWCLIPBOARD messages. We declare a message handler method for each in the form's interface and implement the methods as follows:
1// ...
2
3interface
4
5// ...
6
7type
8 TMainForm = class(TForm)
9 private
10 // ...
11 procedure WMChangeCBChain(var Msg: TMessage); message WM_CHANGECBCHAIN;
12 procedure WMDrawClipboard(var Msg: TMessage); message WM_DRAWCLIPBOARD;
13 // ...
14 end;
15
16// ...
17
18implementation
19
20// ...
21
22procedure TMainForm.WMChangeCBChain(var Msg: TMessage);
23begin
24 // A window has been detached from clipboard chain
25 if HWND(Msg.WParam) = fNextCBViewWnd then
26 // our next window has changed: record it
27 // we don't pass the message on since we handled it
28 fNextCBViewWnd := HWND(Msg.LParam)
29 else
30 // our next window has not changed: pass message on
31 // if next window exists (i.e. handle <> 0)
32 if fNextCBViewWnd <> 0 then
33 SendMessage(fNextCBViewWnd, WM_CHANGECBCHAIN, Msg.WParam, Msg.LParam);
34end;
35
36procedure TMainForm.WMDrawClipboard(var Msg: TMessage);
37begin
38 // Clipboard content has changed
39 //
40 // do something here to respond to change in clipboard
41 //
42 // now pass message along to next window in chain, if it exists
43 if fNextCBViewWnd <> 0 then
44 SendMessage(fNextCBViewWnd, WM_DRAWCLIPBOARD, Msg.WParam, Msg.LParam);
45end;
46
47// ...
The WParam value of WM_CHANGECBCHAIN is the handle of the window to be removed while the LParam value is the handle of the window that follows it in the chain (or 0 if there is no following window). If the window that follows ours in the chain is the one to be removed we replace our record of its handle with the handle of the following window. In this case the message has been handled and there is no need to pass it further down the chain. However, if the window being removed is not the next window in the chain we simply forward the message on to the next window (if it exists).
When a WM_DRAWCLIPBOARD message is received we first take some suitable action in response to the change and then pass the message and parameters on to the next window (if any) in the chain.
Example project
An example project is presented below. This project implements a very basic text editor that has buttons to cut, copy and paste text. The Paste button is enabled only when there is text on the clipboard. In order to implement the Paste button functionality, the program's main window is registered as a clipboard viewer. The Copy and Cut buttons are enabled only when some text is selected in the edit control. These buttons are presented only for completeness and their workings are not relevant to the purpose of this article.
Project file
The demo's project file, CBEdit.dpr
, is defined below:
1program CBEdit;
2
3uses
4 Forms,
5 FmEditor in 'FmEditor.pas' {EditorForm};
6
7{$R *.RES}
8
9begin
10 Application.Initialize;
11 Application.CreateForm(TEditorForm, EditorForm);
12 Application.Run;
13end.
Form unit
The form is defined in a file named FmEditor.dfm
as follows (non-essential items have been removed):
1object EditorForm: TEditorForm
2 Left = 190
3 Top = 107
4 Width = 696
5 Height = 480
6 Caption = 'EditorForm'
7 OnCreate = FormCreate
8 OnDestroy = FormDestroy
9 object Panel1: TPanel
10 Align = alTop
11 object btnCut: TButton
12 Left = 8
13 Top = 8
14 Caption = 'Cut'
15 OnClick = btnCutClick
16 end
17 object btnCopy: TButton
18 Left = 88
19 Top = 8
20 Caption = 'Copy'
21 OnClick = btnCopyClick
22 end
23 object btnPaste: TButton
24 Left = 168
25 Top = 8
26 Caption = 'Paste'
27 OnClick = btnPasteClick
28 end
29 end
30 object RichEdit1: TRichEdit
31 Align = alClient
32 OnSelectionChange = RichEdit1SelectionChange
33 end
34end
Finally, the code associated with the form is stored in FmEditor.pas
and is as follows:
1unit FmEditor;
2
3interface
4
5uses
6 Forms, StdCtrls, ComCtrls, Classes, Controls, Windows, Messages;
7
8const
9 // WM_CLIPBOARDUPDATE is not defined in the Messages unit of all supported
10 // versions of Delphi, so we defined it here for safety.
11 WM_CLIPBOARDUPDATE = $031D;
12
13type
14 TEditorForm = class(TForm)
15 btnCut: TButton;
16 btnCopy: TButton;
17 btnPaste: TButton;
18 RichEdit1: TRichEdit;
19 procedure FormCreate(Sender: TObject);
20 procedure FormDestroy(Sender: TObject);
21 procedure RichEdit1SelectionChange(Sender: TObject);
22 procedure btnCutClick(Sender: TObject);
23 procedure btnCopyClick(Sender: TObject);
24 procedure btnPasteClick(Sender: TObject);
25 private
26 // Flag indicating if the new style clipboard format listener API is
27 // available on the current OS.
28 fUseNewAPI: Boolean;
29 // Handle of next clipboard viewer handle in chain. Used only when old
30 // clipboard viewer API is in use, i.e. when fUseNewAPI is False.
31 fNextCBViewWnd: HWND;
32 // References to AddClipboardFormatListener and
33 // RemoveClipboardFormatListenerAPI functions. These references are nil if
34 // the functions are not supported by the OS, i.e. if fUseNewAPI is False.
35 fAddClipboardFormatListener: function(hwnd: HWND): BOOL; stdcall;
36 fRemoveClipboardFormatListener: function(hwnd: HWND): BOOL; stdcall;
37 // References to SetClipboardViewer and ChangeClipboardChain API functions.
38 // These references is are if the newer clipboard format listener API is
39 // available, i.e. if fUseNewAPI is True.
40 fSetClipboardViewer: function (hWndNewViewer: HWND): HWND; stdcall;
41 fChangeClipboardChain: function(hWndRemove, hWndNewNext: HWND): BOOL;
42 stdcall;
43 // Message handlers
44 procedure WMClipboardUpdate(var Msg: TMessage); message WM_CLIPBOARDUPDATE;
45 procedure WMChangeCBChain(var Msg: TMessage); message WM_CHANGECBCHAIN;
46 procedure WMDrawClipboard(var Msg: TMessage); message WM_DRAWCLIPBOARD;
47 end;
48
49var
50 EditorForm: TEditorForm;
51
52implementation
53
54uses
55 SysUtils, Clipbrd;
56
57{$R *.dfm}
58
59procedure TEditorForm.btnCopyClick(Sender: TObject);
60begin
61 RichEdit1.CopyToClipboard;
62end;
63
64procedure TEditorForm.btnCutClick(Sender: TObject);
65begin
66 RichEdit1.CutToClipboard;
67end;
68
69procedure TEditorForm.btnPasteClick(Sender: TObject);
70begin
71 RichEdit1.PasteFromClipboard;
72end;
73
74procedure TEditorForm.FormCreate(Sender: TObject);
75const
76 cUserKernelLib = 'user32.dll';
77begin
78 // Enable / disable buttons at start-up
79 btnPaste.Enabled := Clipboard.HasFormat(CF_TEXT);
80 btnCut.Enabled := RichEdit1.SelLength > 0;
81 btnCopy.Enabled := btnCut.Enabled;
82 // Load required API functions: 1st try to load modern clipboard listener API
83 // functions. If that fails try to load old-style clipboard viewer API
84 // functions. This should never fail, but we raise an exception if the
85 // impossible happens!
86 fAddClipboardFormatListener := GetProcAddress(
87 GetModuleHandle(cUserKernelLib), 'AddClipboardFormatListener'
88 );
89 fRemoveClipboardFormatListener := GetProcAddress(
90 GetModuleHandle(cUserKernelLib), 'RemoveClipboardFormatListener'
91 );
92 fUseNewAPI := Assigned(fAddClipboardFormatListener)
93 and Assigned(fRemoveClipboardFormatListener);
94 if not fUseNewAPI then
95 begin
96 fSetClipboardViewer := GetProcAddress(
97 GetModuleHandle(cUserKernelLib), 'SetClipboardViewer'
98 );
99 fChangeClipboardChain := GetProcAddress(
100 GetModuleHandle(cUserKernelLib), 'ChangeClipboardChain'
101 );
102 Assert(Assigned(fSetClipboardViewer) and Assigned(fChangeClipboardChain));
103 end;
104 if fUseNewAPI then
105 begin
106 // Register window as clipboard listener
107 if not fAddClipboardFormatListener(Self.Handle) then
108 RaiseLastOSError; // On early Delphis use RaiseLastWin32Error instead
109 end
110 else
111 begin
112 // Register window as clipboard viewer, storing handle of next window in
113 // chain
114 fNextCBViewWnd := fSetClipboardViewer(Self.Handle);
115 end;
116end;
117
118procedure TEditorForm.FormDestroy(Sender: TObject);
119begin
120 // Remove clipboard listener or viewer
121 if fUseNewAPI then
122 fRemoveClipboardFormatListener(Self.Handle)
123 else
124 fChangeClipboardChain(Self.Handle, fNextCBViewWnd);
125end;
126
127procedure TEditorForm.RichEdit1SelectionChange(Sender: TObject);
128begin
129 btnCut.Enabled := RichEdit1.SelLength > 0;
130 btnCopy.Enabled := btnCut.Enabled;
131end;
132
133procedure TEditorForm.WMChangeCBChain(var Msg: TMessage);
134begin
135 Assert(not fUseNewAPI);
136 // Windows is detaching a clipboard viewer
137 if HWND(Msg.WParam) = fNextCBViewWnd then
138 // window being detached is next one: record new "next" window
139 fNextCBViewWnd := HWND(Msg.LParam)
140 else if fNextCBViewWnd <> 0 then
141 // window being detached is not next: pass message along
142 SendMessage(fNextCBViewWnd, Msg.Msg, Msg.WParam, Msg.LParam);
143end;
144
145procedure TEditorForm.WMClipboardUpdate(var Msg: TMessage);
146begin
147 // Clipboard content changed: enable paste button if text is on clipboard
148 btnPaste.Enabled := Clipboard.HasFormat(CF_TEXT);
149end;
150
151procedure TEditorForm.WMDrawClipboard(var Msg: TMessage);
152begin
153 Assert(not fUseNewAPI);
154 // Clipboard content changed
155 // enable paste button if text on clipboard
156 btnPaste.Enabled := Clipboard.HasFormat(CF_TEXT);
157 // pass on message to any next window in viewer chain
158 if fNextCBViewWnd <> 0 then
159 SendMessage(fNextCBViewWnd, Msg.Msg, Msg.WParam, Msg.LParam);
160end;
161
162end.
This application first tries to load the clipboard listener API functions from the OS kernel. If this fails we are running on an OS that does not support this API so we fall back to using the old clipboard viewer chain API and load the required functions from the kernel. We use an assertion to check that the old API has been found as exepected.
The fUseNewAPI field is used to inform the app which API is being used. If we're using the new API we add our forms window handle as a listener, otherwise we hook into the clipboard viewer chain. The necessary tidying up is done when the form is destroyed.
The remainder of the code needs little further explanation. We enable the cut and copy buttons only when some text is selected in the rich edit control. The paste button is enabled or disabled each time the clipboard contents change, depending on whether the CF_TEXT format is available on the clipboard. The code in three custom message handlers was explained earlier in the article.
Demo program
A demo program to accompany this article, based on the example project above, can be found in the delphidabbler/article-demos
Git repository on GitHub.
You can view the code in the article-09
sub-directory. Alternatively download a zip file containing all the demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.
See the demo's README.md file for details.
This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
The demo is open source. See the demo's LICENSE.md file for licensing details.
Component
A clipboard viewer component that triggers an event whenever the clipboard changes is available from this site. This component uses a hidden window which is registered as the clipboard viewer with Windows. The use of such a hidden window is described in article #1.
Feedback
I hope you found this article useful.
If you have any observations, comments, or have found any errors there are two places you can report them.
- For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #9".
- For bugs in the demo code see the
article-demo
project'sREADME.md
file for details of how to report them.