Como monitorear el portapapeles (clipboard) de Windows con Delphi

Clipboard convencional de cartón

Hace unos días tuve la necesidad de escribir una pequeña aplicación que monitoreara el portapapeles de Windows y, si el contenido era una imagen, escribiera automáticamente un archivo con extensión JPG con el contenido del portapapeles; parte del meollo del asunto fue monitorear el portapapeles de Windows, pero afortunadamente es bastante sencillo, y de eso trata este artículo. Antes de iniciar, un poco de antecedentes sobre como monitorear el portapapeles:

Monitoreando el portapapeles

En Windows hay tres métodos para monitorear el portapapeles:

  1. Crear un visor del portapapeles (cualquier versión de Windows)
  2. Obtener el número de secuencia del portapapeles (Windows 2000 y más recientes)
  3. Implementar un oyente (listener) de formato de portapapeles (Windows Vista y recientes)

Vamos a hablar de cada una de ellas e implementarlas todas para ver como funcionan.

Método 1: monitoreando el portapapeles con un visor del portapapeles

Un visor del portapapeles es básicamente una ventana que se ha registrado en una “cadena de visores del portapapeles” que será notificada cuando el contenido de este portapapeles cambie; en pocas palabras, la cadena de visores del portapapeles es una lista interna de Windows de aplicaciones que reciben una notificación cuando el contenido del portapapeles cambia. Implementar un visor es sencillo, y sólo hay que seguir los pasos:

  1. Añadir la ventana a la cadena de visores del portapapeles
  2. Procesar dos mensajes: WM_CHANGEBCHAIN y WM_DRAWCLIPBOARD
  3. Al finalizar la aplicación, removerla ventana de la cadena de visores del portapapeles

Empecemos pues con la implementación; para iniciar, crea un nuevo proyecto en Delphi, y añade un componente TImage, preferentemente alínealo al área de cliente; añade “Clipbrd” a tu “uses”. Es todo lo que necesitaremos.

Para el primer paso de la lista anterior necesitamos de la función SetClipboardViewer(), que es una función definida en Windows.pas y que necesita como parámetro el handle de la ventana que se registrará como visor del portapapeles; retorna el handle del próximo visor (recuerda, es una cadena de visores), mismo que debe de ser almacenado, pues nos servirá después para puentear los mensajes recibidos al resto de la cadena, así como para desconectarnos de la misma.

Añade el siguiente código en la sección private de TForm1:

 ...
  private
    FNextViewer : Cardinal;
  ...

Haz doble clic sobre el evento OnCreate o bien doble clic sobre la forma, para abrir el editor de código para este método, y escribe el siguiente código:

Procedure TForm1.FormCreate(Sender : TObject);
 
Begin
  { Al iniciar la aplicación, conecta la ventana a la cadena de visores }
  FNextViewer:=SetClipboardViewer(Handle);
End;

En este caso Handle corresponde a Form1, y es el handle de la ventana que estamos creando cuando se ejecuta la aplicación. Hasta aquí ya tenemos el mecanismo para añadir nuestra ventana a la cadena de visores del portapapeles, pero aún nos falta responder a los mensajes que nos mandará Windows para procesar los cambios en el mismo portapapeles. Vamos a hacerlo.

Respondiendo a los mensajes WM_CHANGEBCHAIN y WM_DRAWCLIPBOARD

Ahora necesitamos responder a los dos mensajes que recibirá la aplicación que tienen que ver con la cadena de visores del portapapeles; ve a la sección private de la declaración de TForm1, y después de la declaración de FNextViewer añade:

  ...
  private
    FNextViewer : Cardinal;
    Procedure WMDrawClipboard(Var AMessage : TMessage); Message WM_DRAWCLIPBOARD;
    Procedure WMChangeBChain(Var AMessage : TMessage); Message WM_CHANGECBCHAIN;
  ...

Presiona Shift + Ctrl + C para que Delphi cree automáticamente los cuerpos de los métodos que acabamos de añadir. Ve al método WMDrawClipboard() y escribe este código:

Procedure TForm1.WMDrawClipboard(Var AMessage : TMessage); 
 
Begin
  { Si hay un bitmap en el portapapeles, entonces asígnalo a la imagen }
  If IsClipboardFormatAvailable(CF_BITMAP) Then
    Image1.Picture.Assign(Clipboard);
 
  { Ya procesamos el mensaje, ahora sigue la cadena de visores }
  SendMessage(FNextViewer, WM_DRAWCLIPBOARD, 0, 0);
End;

Esa última línea que ves se usa para seguir la cadena de visores del portapapeles: recuerda que estamos en una cadena, y hay que pasar el mensaje al enlace siguiente para que también procesen el cambio. Ahora toca el turno a WMChangeBChain():

Procedure TForm1.WMChangeBChain(Var AMessage : TMessage); 
 
Begin
  { Si la siguiente ventana está cerrando, repara la cadena... }
  If (AMessage.WParam = FNextViewer) then
    FNextViewer:=AMessage.LParam
  Else
    { ...de lo contrario pasa el mensaje al próximo elemento de la cadena. }
    { WParam = handle de la ventana a quitar de la cadena                  }
    { LParam = handle de la siguiente ventana                              }
    SendMessage(FNextViewer, WM_CHANGECBCHAIN, AMessage.WParam, AMessage.LParam);
end;

Finalmente, necesitamos dejar la cadena de visores cuando nuestro programa termine. Para ello, haz doble clic sobre el evento OnDestroy para crear su definición; en ese método escribe el siguiente código:

procedure TForm1.FormDestroy(Sender: TObject);
begin
  { ¡Adiós cadena! Handle = handle de ventana a eliminar (la nuestra), y }
  { FNextViewer = el handle del siguiente visor                          }
  ChangeClipboardChain(Handle, FNextViewer);
end;

Y eso es todo lo que necesitamos; ejecuta tu programa, y prúebalo presionando la tecla “Imp Pant” (o Prt Scr) para capturar la pantalla como un mapa de bits y copiarlo al portapapeles; cuando lo hagas verás como aparece en tu aplicación. Sólo recuerda que este método de (visor de portapapeles) sólo sigue activo por cuestiones de compatibilidad con versiones anteriores de Windows.

Método 2: monitoreando el portapapeles a través del número de secuencia

Este método es algo más sencillo que el anterior, aunque no es realmente de monitoreo; se usa cuando tu aplicación usa el portapapeles como almacenamiento intermedio (o caché) de algún dato, después realiza una operación y posteriormente recupera el dato del portapapeles para compararlo, o bien alguna situación similar. Funciona usando el número de secuencia del portapapeles, un entero de 32 bits sin signo (que sería representado por el tipo Cardinal o LongWord en Delphi) que cambia cada vez que lo hace el contenido del portapapeles. Para usarlo, sólo tienes que usar la función GetClipboardSequenceNumber(), que no recibe parámetros y retorna el número de secuencia del portapapeles; teniendo esto, monitorear el número de secuencia se reduce a:

  1. Copiar algo al portapapeles, obtener el número de secuencia y almacenarlo para referencia
  2. Realizar cualquier procesamiento necesario
  3. Cuando se quiera comparar el contenido, sólo hay que comparar el número de secuencia del portapapeles frente al actual (llamando de nuevo a GetClipboardSequenceNumber()); si son diferentes, el contenido ha cambiado, de lo contrario es el mismo.

Como puedes deducir seguramente, la implementación es trivial, pero hagámosla de todos modos. Crea un nuevo proyecto de Delphi, deposita en la forma un TEdit y dos TButton (con el Caption “Copiar” y “Comprobar”), y añade “Clipbrd” a tu “uses”. Para empezar, ve a la definición de TForm1 y añade en la sección private el siguiente código:

  ...
  private
    FClipSeqNbr : Cardinal;
  ...

Ahora si, haz doble clic en Button1 para abrir el método OnClic, y copia el siguiente código:

procedure TForm1.Button1Click(Sender: TObject);
begin
  { Hagamos una copia al portapapeles para que incremente el número de secuencia }
  Edit1.CopyToClipboard();
 
  { Ahora guardamos nuestro número de secuencia para comparar después }
  FClipSeqNbr:=GetClipboardSequenceNumber();
end;

Posteriormente haz clic en el botón “Comprobar” (el segundo que depositaste), y copia este código:

procedure TForm1.Button2Click(Sender: TObject);
begin
  { Compara el número de secuencia almacenado antes con el actual }
  If (FClipSeqNbr <> GetClipboardSequenceNumber()) Then
    MessageDlg('¡El portapapeles cambió!', mtError, [mbOK], 0)
  Else
    MessageDlg('El portapapeles sigue igual.', mtInformation, [mbOK], 0);
end;

Y eso es todo lo que hace falta. Como puedes ver, no es tanto de monitoreo; y ojo, la documentación oficial de MSDN dice que este método no es de notificación, y que no debería de ser usado en un ciclo de consulta como por ejemplo (entiendo) un thread; para ser notificado debes de usar el método anterior (un visor de portapapeles) o bien un oyente de formato, que es justo lo que sigue.

Método 3: monitoreando el portapapeles con un oyente (listener) de formato

Pensando en el primer método (el visor de portapapeles), este tiene varios problemas; si no sigues la cadena en cuanto a añadir la ventana, eliminar nodos de la cadena o bien dejarla, puedes tener problemas; y ¿que tal si un nodo de la cadena deja de responder? estarías en problemas. Por eso hay un tercer método que es en realidad el método preferido para monitorear el portapapeles, y es a través de un oyente de formato. Ten en cuenta que este método sólo está disponible en Windows Vista y superiores. Implementar un oyente de formato es bastante sencillo; sólo hay que seguir estos pasos:

  1. Registrar una ventana como oyente de formato del portapapeles
  2. Procesar el mensaje WM_CLIPBOARDUPDATE
  3. Dejar la lista de oyentes de formato al cerrar la aplicación

Y voilá, eso es todo. Vamos a implementarlo: inicia un nuevo proyecto de Delphi, y deposita en la forma un componente TImage donde y como tu quieras: de preferencia alínealo al cliente; añade “Clipbrd” a tu “uses”. Ahora haz doble clic sobre la forma para abrir el código del evento OnCreate, y teclea este código:

procedure TForm1.FormCreate(Sender: TObject);
begin
  { Intenta registrarse en la lista de oyentes; de no ser posible, cierra el programa }
  If Not AddClipboardFormatListener(Handle) Then
    Begin
      MessageDlg('¡Ouch! no puedo enlazarme a lista de oyentes. Cerrando...', mtError, [mbOK], 0);
      Close();
    End;
end;

Una pequeña explicación: la función AddClipboardFormatListener() recibe un parámetro que es el handle de la ventana que registraremos como oyente de formato del portapapeles; retorna verdadero si se pudo añadir a la lista de oyentes, de lo contrario falso. Es posible obtener más información de porqué ocurrió el error, pero eso te lo dejo de tarea a ti, amigo lector.

De regreso al código, tenemos un pequeño problema; si tienes Delphi 2009 o menor, las funciones y mensajes usados no están definidos, por lo que tendremos que definir ambos a mano. Si tienes Delphi 2009 o menor, haz lo siguiente; primero que nada, debajo de “Uses…” teclea el siguiente código:

{$IF CompilerVersion < 21}
{ Delphi pre-2010 no tiene este mensaje definido en windows.pas, así que }
{ tendremos que definirlo a mano                                         }
Const
  WM_CLIPBOARDUPDATE = $031D;
{$IFEND}

Esta línea sólo se compilará en Delphi 2009 o menor, y define el mensaje que necesitamos para implementar el método adecuado. Ahora necesitamos las dos funciones antes mencionadas; justo antes de la sección Implementation, copia y pega el siguiente código:

{$IF CompilerVersion < 21}
{ Delphi pre-2010 no tiene definidas las funciones necesarias }
{ en windows.pas así que los definiremos a mano               }
Function AddClipboardFormatListener(AHandle : Cardinal) : Boolean; stdcall; external 'user32.dll';
Function RemoveClipboardFormatListener(AHandle : Cardinal) : Boolean; stdcall; external 'user32.dll';
{$IFEND}

Y ahora si podemos seguir con nuestro trabajo sin preocuparnos por la versión de Delphi.

Ahora tenemos que procesar el mensaje WM_CLIPBOARDUPDATE, que también es trivial; en la definición de la clase TForm1 añade el siguiente código en su sección private:

  ...
  private
    { Private declarations }
    Procedure WMClipboardUpdate(Var AMessage : TMessage); Message WM_CLIPBOARDUPDATE;
  ...

Ahora presiona Shift + Ctrl + C para que Delphi se encargue de crear el código de declaración para este método; ahora sólo añade el siguiente código:

procedure TForm1.WMClipboardUpdate(var AMessage: TMessage);
begin
  { Si hay un bitmap en el portapapeles, despliégalo en Image1 }
  If IsClipboardFormatAvailable(CF_BITMAP) Then
    Image1.Picture.Assign(Clipboard);
end;

Finalmente, sólo necesitamos abandonar la lista de oyentes cuando termine nuestro programa. Para hacerlo, haz doble clic sobre el evento OnDestroy para crear su código, y teclea lo siguiente:

procedure TForm1.FormDestroy(Sender: TObject);
begin
  { Al cerrarse nuestra aplicación, deja la lista de oyentes de formato del portapapeles }
  RemoveClipboardFormatListener(Handle);
end;

Como puedes ver, es un método tremendamente sencillo de implementar y sin tanto problema como los otros. Sólo recuerda que lo podrás usar en Windows Vista y superiores solamente.

Bueno, eso concluye el artículo sobre como monitorear el portapapeles en Windows con Delphi. Te dejo los proyectos listos por si quieres descargarlos: están en Delphi 2009 pero no debes de tener problemas para abrirlos en otras versiones, a excepción del tercero, por la dependencia de varias funciones propias de Windows Vista.

Descarga el código de este artículo

Y por último, si quieren documentarse más del portapapeles por supuesto está la documentación en línea de MSDN en este enlace.

Hasta luego y ¡happy programming! 😀

Esta entrada fue publicada en Delphi. Guarda el enlace permanente.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *