Skip over navigation

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;
Listing 1

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;
Listing 2

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// ...
Listing 3

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;
Listing 4

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;
Listing 5

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// ...
Listing 6

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.
Listing 7

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
Listing 8

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.
Listing 9

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.

  1. 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".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.