miércoles, 15 de abril de 2015

Windows Forms. Imágenes Windows MetaFile y Clipboard

Debido a las limitaciones de la implementación del objeto DataObject que utiliza el .NET Framework para manejar los datos del Portapapeles de Windows nos podemos encontrar con que nuestra aplicación no reconozca las imágenes de tipo Windows MetaFile que otras aplicaciones han colocado en el Portapapeles.

Y, en la otra dirección, nos podemos encontrar también con que otras aplicaciones no son capaces de reconocer las imágenes de este tipo que colocamos en el Portapapeles desde nuestra aplicación.

En este artículo mostraré cómo podemos utilizar la API de Win32 para crearnos una clase helper que nos permita salvar este problema.


Creando el proyecto de prueba


Para mostrar tanto el problema como la solución voy a crear un nuevo Proyecto Windows Forms en el Visual Studio llamado ClipboardMetaFile.

Al formulario que Visual Studio crea por defecto le he añadido un control PictureBox (picImagen) y dos botones (btnCopiar y btnPegar).

En el evento Click del botón btnPegar he añadido el siguiente código para copiar la imagen contenida en el Portapapeles al control PictureBox.

        private void btnPegar_Click(object sender, EventArgs e)
        {
            if (Clipboard.ContainsImage())
                picImagen.Image = Clipboard.GetImage();
        }
    Private Sub btnPegar_Click(sender As Object, e As EventArgs) Handles btnPegar.Click
        If Clipboard.ContainsImage() Then
            picImagen.Image = Clipboard.GetImage()
        End If
    End Sub

El código simplemente comprueba si el contenido del Portapapeles se corresponde con una imagen y, si es así, la recupera y asigna al control PictureBox.

Si arrancamos nuestra aplicación y pulsamos la tecla Impr Pant para cargar un pantallazo en el Portapapeles, podemos comprobar que al pulsar el botón Pegar la imagen capturada se carga en nuestro control.

Pantallazo cargado en el formulario


Sin embargo, si abrimos el WordPad, insertamos una imagen desde un archivo, la copiamos al Portapapeles e intentamos repetir la operación veremos que la imagen no se carga en nuestro formulario.

Si ponemos un punto de interrupción en el código del botón Pegar veremos que el método ContainsImage de la clase Clipboard no reconoce el contenido del Portapapeles como una imagen, aunque sí que reconoce el formato del contenido como MetaFilePict (Metaarchivo de Windows).

No reconoce el metaarchivo de Windows como imagen


La clase ClipboardHelper


Para solucionar el problema voy a crear una clase helper que devuelva la imagen del Portapapeles utilizando la API de Windows cuando el contenido de éste se corresponda con un Metaarchivo de Windows.

He añadido al proyecto una nueva clase ClipboardHelper con el siguiente código:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ClibboardMetaFile
{
    public static class ClipboardHelper
    {
        [DllImport("user32.dll")]
        static extern bool OpenClipboard(IntPtr hWndNewOwner);
        [DllImport("user32.dll")]
        static extern bool CloseClipboard();
        [DllImport("user32.dll")]
        static extern IntPtr GetClipboardData(uint uFormat);
        [DllImport("user32.dll")]
        static extern bool IsClipboardFormatAvailable(uint uFormat);

        const uint CF_ENHMETAFILE = 14;

        public static bool ContainsImage()
        {
            return Clipboard.ContainsImage() || IsClipboardFormatAvailable(CF_ENHMETAFILE);
        }

        public static Image GetImage()
        {
            if (!ContainsImage()) return null;

            if (Clipboard.ContainsImage())
                return Clipboard.GetImage();
            else
            {
                Metafile mf = null;
                if (OpenClipboard(IntPtr.Zero))
                {
                    IntPtr ptr = IntPtr.Zero;
                    if (IsClipboardFormatAvailable(CF_ENHMETAFILE))
                    {
                        ptr = GetClipboardData(CF_ENHMETAFILE);
                        if (!ptr.Equals(IntPtr.Zero))
                            mf = new Metafile(ptr, true);
                    }
                    CloseClipboard();
                }
                return mf;
            }
        }
    }
}
Imports System.Runtime.InteropServices
Imports System.Drawing.Imaging

Public Class ClipboardHelper

    <DllImport("user32.dll", EntryPoint:="OpenClipboard", _
       SetLastError:=True, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
    Shared Function OpenClipboard(ByVal hWnd As IntPtr) As Boolean
    End Function
    <DllImport("user32.dll", EntryPoint:="CloseClipboard", _
       SetLastError:=True, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
    Shared Function CloseClipboard() As Boolean
    End Function
    <DllImport("user32.dll", EntryPoint:="GetClipboardData", _
        SetLastError:=True, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
    Shared Function GetClipboardData(uFormat As UInteger) As IntPtr
    End Function
    <DllImport("user32.dll", EntryPoint:="IsClipboardFormatAvailable", _
        SetLastError:=True, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)> _
    Shared Function IsClipboardFormatAvailable(uFormat As UInteger) As Boolean
    End Function

    Const CF_ENHMETAFILE As UInteger = 14

    Public Shared Function ContainsImage() As Boolean
        Return Clipboard.ContainsImage() Or IsClipboardFormatAvailable(CF_ENHMETAFILE)
    End Function

    Public Shared Function GetImage() As Image
        If Not ContainsImage() Then Return Nothing

        If Clipboard.ContainsImage() Then
            Return Clipboard.GetImage()
        Else
            Dim mf As Metafile = Nothing
            If OpenClipboard(IntPtr.Zero) Then
                Dim ptr As IntPtr = IntPtr.Zero
                If IsClipboardFormatAvailable(CF_ENHMETAFILE) Then
                    ptr = GetClipboardData(CF_ENHMETAFILE)
                    If Not ptr.Equals(IntPtr.Zero) Then
                        mf = New Metafile(ptr, True)
                    End If
                End If
                CloseClipboard()
            End If
            Return mf
        End If
    End Function

End Class



La clase tiene dos métodos. Uno para comprobar si el contenido del Portapapeles se corresponde con una imagen válida y otro para recuperar la imagen.

El método ContainsImage comprueba si el Portapapeles contiene una imagen reconocida por la clase Clipboard o un Metaarchivo de Windows (usando el método IsClipboardFormatAvailable de la librería user32.dll.

El método GetImage devuelve la imagen del Portapapeles a través del método GetImage de la clase Clipboard (si es un formato reconocido por ésta) o a través del método GetClipboardData de la librería user32.dll (si se trata de un MetaArchivo de Windows).

He modificado también el código del evento Click del botón Pegar para hacer uso de los métodos de la nueva clase:

        private void btnPegar_Click(object sender, EventArgs e)
        {
            if (ClipboardHelper.ContainsImage())
                picImagen.Image = ClipboardHelper.GetImage();
        }
    Private Sub btnPegar_Click(sender As Object, e As EventArgs) Handles btnPegar.Click
        If ClipboardHelper.ContainsImage() Then
            picImagen.Image = ClipboardHelper.GetImage()
        End If
    End Sub

Si repetimos la prueba con el WordPad comprobaremos que ahora si nos permite pegar las imágenes independientemente de la aplicación origen.

Voy a repetir el proceso con el botón Copiar. Para empezar voy a incluir código en el evento Click del botón para copiar al Portapapeles la imagen del control PictureBox utilizando la clase Clipboard.

        private void btnCopiar_Click(object sender, EventArgs e)
        {
            if (picImagen.Image != null)
                Clipboard.SetImage(picImagen.Image);
        }
    Private Sub btnCopiar_Click(sender As Object, e As EventArgs) Handles btnCopiar.Click
        If picImagen.Image IsNot Nothing Then
            Clipboard.SetImage(picImagen.Image)
        End If
    End Sub

Podemos realizar las mismas pruebas que en el caso anterior. Si pegamos en el formulario una imagen obtenida a partir de una captura de pantalla, pusamos el botón Copiar y probamos a pegar la imagen en otra aplicación (por ejemplo en el WordPad) veremos que se copia correctamente. En cambio si insertamos una imagen en el WordPad desde un archivo, la copiamos y pegamos en nuestro formulario, y la copiamos de nuevo a través del botón Copiar veremos que ya no podemos pegarla en otras aplicaciones (ni siquiera en la nuestra, podemos hacer la prueba pulsando el botón Pegar a continuación).

Así que voy a modificar la clase ClipboardHelper para añdir un nuevo método SetImage. En esta ocasión en lugar de utilizar la API de Windows lo que haré es convertir la imagen a formato png antes de mandarla al Portapapeles.

        public static void SetImage(Image image)
        {
            if (image == null) return;

            if (image is Metafile)
            {
                MemoryStream stream = new MemoryStream();
                image.Save(stream, ImageFormat.Png);
                Image png = Image.FromStream(stream);
                Clipboard.SetImage(png);
            }
            else
                Clipboard.SetImage(image);
        }
    Public Shared Sub SetImage(imag As Image)
        If imag Is Nothing Then Return

        If TypeOf imag Is Metafile Then
            Dim stream As MemoryStream = New MemoryStream()
            imag.Save(stream, ImageFormat.Png)
            Dim png As Image = Image.FromStream(stream)
            Clipboard.SetImage(png)
        Else
            Clipboard.SetImage(imag)
        End If
    End Sub

De esta forma las imágenes generadas por nuestra aplicación estarán en un formato más universal. Podemos optar por convertir todas las imágenes independientemente del formato origen o convertirlas a cualquier otro formato estándar.

Por último, únicamente quedaría modificar el código del evento Click del botón Copiar para probar la funcionalidad completa.

        private void btnCopiar_Click(object sender, EventArgs e)
        {
            if (picImagen.Image != null)
                ClipboardHelper.SetImage(picImagen.Image);
        }
    Private Sub btnCopiar_Click(sender As Object, e As EventArgs) Handles btnCopiar.Click
        If picImagen.Image IsNot Nothing Then
            ClipboardHelper.SetImage(picImagen.Image)
        End If
    End Sub

Existen ejemplos internet para copiar las imágenes en formato de metaarchivo en el Portapapeles utilizando la API de Windows (se puede ver una en PRB: Metafiles on Clipboard Are Not Visible to All Applications). En cualquier caso, si optas por esta solución, deberías tener en cuenta un par de cosas:

  • El formato de metaarchivo de Windows no es un formato tan extendido como pueden ser otros formatos de imagen y, dependiendo de dónde quiera pegar el usuario la imagen, podría tener problemas de incompatibilidad.
  • Por otra lado, aunque nunca he puesto demasiado empeño en conseguirlo (porque nunca he tenido un interés especial en que mis aplicaciones generen datos en este formato), cuando lo he intentado nunca he conseguido implementar una solución completamente libre de errores.

El código completo de este artículo, tanto en C# como en Visual Basic, está disponible en:

Códigos de muestra - Ejemplos MSDN

No hay comentarios:

Publicar un comentario