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 a method for applications to hook into the clipboard to receive notifications when its contents change. This is done by registering one of the application's windows to receive messages when the clipboard changes. These windows are known as clipboard viewers. 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.
Registering and un-registering the clipboard viewer
To register our window as a clipboard viewer we call the SetClipboardViewer API function. This returns the handle of the next window in the viewer chain which we must record for future reference (this handle may be 0 if we are the first viewer in the chain):
var OurWnd: HWND; // handle of window we are registering NextWnd: HWND; // handle of next window in chain ... NextWnd := SetClipboardViewer(OurWnd);
To un-register our viewer window we use the ChangeClipboardChain API function as follows:
ChangeClipboardChain(OurWnd, NextWnd);
Notice how we pass the handle of our window to the function, along with the handle of the next window in the chain.
Responding to clipboard messages
Every clipboard viewer must respond to the two clipboard messages discussed above. The window procedure of the viewer window must handle the messages as follows (where Msg is the message number and LParam and WParam are the parameters passed with the message):
case Msg of ... WM_CHANGECBCHAIN: begin // A window has been detached from clipboard chain if HWND(WParam) = NextWnd then // our next window has changed: record it // we don't pass the message on since we handled it NextWnd := HWND(LParam) else // our next window has not changed: pass message on // if next window exists (i.e. handle <> 0) if NextWnd <> 0 then SendMessage(NextWnd, WM_CHANGECBCHAIN, WParam, LParam); end; WM_DRAWCLIPBOARD: begin // Clipboard content has changed // do something here to handle change in clipboard ... // now pass message along if next window exists if NextWnd <> 0 then SendMessage(NextWnd, WM_DRAWCLIPBOARD, WParam, LParam); end; ... end;
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.
The demo application's source code is presented below. A zip file containing the source is also available for download.
Project file
The demo's project file – CBEdit.dpr – is defined
below:
program CBEdit; uses Forms, FmEditor in 'FmEditor.pas' {EditorForm}; {$R *.RES} begin Application.Initialize; Application.CreateForm(TEditorForm, EditorForm); Application.Run; end.
Form Unit
The form is defined in a file named FmEditor.dfm as follows
(nonessential items have been removed):
object EditorForm: TEditorForm Left = 190 Top = 107 Width = 696 Height = 480 Caption = 'EditorForm' OnCreate = FormCreate OnDestroy = FormDestroy object Panel1: TPanel Align = alTop object btnCut: TButton Left = 8 Top = 8 Caption = 'Cut' OnClick = btnCutClick end object btnCopy: TButton Left = 88 Top = 8 Caption = 'Copy' OnClick = btnCopyClick end object btnPaste: TButton Left = 168 Top = 8 Caption = 'Paste' OnClick = btnPasteClick end end object RichEdit1: TRichEdit Align = alClient OnSelectionChange = RichEdit1SelectionChange end end
Finally, the code associated with the form is stored in
FmEditor.pas and is as follows:
unit FmEditor; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls, Clipbrd; type TEditorForm = class(TForm) Panel1: TPanel; btnCut: TButton; btnCopy: TButton; btnPaste: TButton; RichEdit1: TRichEdit; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure RichEdit1SelectionChange(Sender: TObject); procedure btnCutClick(Sender: TObject); procedure btnCopyClick(Sender: TObject); procedure btnPasteClick(Sender: TObject); private // next window in clipboard viewer chain fNextViewer: HWND; // handles change in clipboard chain procedure WMChangeCBChain(var Msg: TMessage); message WM_CHANGECBCHAIN; // handles change in clipboard content procedure WMDrawClipboard(var Msg: TMessage); message WM_DRAWCLIPBOARD; end; var EditorForm: TEditorForm; implementation {$R *.DFM} { Button event handlers } procedure TEditorForm.btnCopyClick(Sender: TObject); begin RichEdit1.CopyToClipboard; end; procedure TEditorForm.btnCutClick(Sender: TObject); begin RichEdit1.CutToClipboard; end; procedure TEditorForm.btnPasteClick(Sender: TObject); begin RichEdit1.PasteFromClipboard; end; { Window creation and destruction event handlers } procedure TEditorForm.FormCreate(Sender: TObject); begin // Enabled / disable buttons at start-up btnPaste.Enabled := Clipboard.HasFormat(CF_TEXT); btnCut.Enabled := RichEdit1.SelLength > 0; btnCopy.Enabled := btnCut.Enabled; // Register main window in clipboard viewer chain // we record handle of next viewer window in chain fNextViewer := SetClipboardViewer(Self.Handle); end; procedure TEditorForm.FormDestroy(Sender: TObject); begin // We're closing down: remove from clipboard chain ChangeClipboardChain(Self.Handle, fNextViewer); end; { Editor change event hander: updates copy/cut button } procedure TEditorForm.RichEdit1SelectionChange(Sender: TObject); begin btnCut.Enabled := RichEdit1.SelLength > 0; btnCopy.Enabled := RichEdit1.Enabled; end; { Clipboard change event handlers } procedure TEditorForm.WMChangeCBChain(var Msg: TMessage); begin // Clipboard chain changed if HWND(Msg.WParam) = fNextViewer then // next window was removed: record following window as next fNextViewer := HWND(Msg.LParam) else if fNextViewer <> 0 then // no change to next window: pass message on to next window SendMessage(fNextViewer, WM_CHANGECBCHAIN, Msg.WParam, Msg.LParam); end; procedure TEditorForm.WMDrawClipboard(var Msg: TMessage); begin // Clipboard content changed // enable paste button if text on clipboard btnPaste.Enabled := Clipboard.HasFormat(CF_TEXT); // pass message on the next viewer in clipboard chain, if any if fNextViewer <> 0 then SendMessage(fNextViewer, WM_DRAWCLIPBOARD, Msg.WParam, Msg.LParam); end; end.
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.

