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.