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