[WPF] Coller une image du presse-papier (bug dans Clipboard.GetImage)

Hum… 2 mois depuis mon précédent (et premier) post… il faudra que j’essaie d’être un peu plus régulier à l’avenir 😉

Si vous avez déjà essayé d’utiliser la méthode Clipboard.GetImage avec WPF, vous avez dû avoir une mauvaise surprise… En effet, cette méthode renvoie un InteropBitmap qui, dans certains cas (voire tout le temps), refuse de s’afficher dans un contrôle Image : aucune exception n’est levée, la taille de l’image est correcte, mais… l’affichage reste désespérément vide, ou alors l’image est méconnaissable.

Pourtant, si on enregistre l’image dans un stream et qu’on la relit à partir du stream, on obtient une image parfaitement utilisable. Mais bon, je trouve cette méthode assez lourde (décodage – réencodage – redécodage de l’image). J’ai donc cherché une solution pour récupérer « manuellement » l’image dans le presse-papier.

Si on regarde les formats d’image disponibles dans le presse-papier (Clipboard.GetDataObject().GetFormats()), on voit que ça varie selon l’origine de l’image (capture d’écran, copie dans Paint…). Le seul format qui semble être toujours présent est DeviceIndependentBitmap (DIB). J’ai donc cherché, à récupérer le MemoryStream pour ce format, et à le décoder en BitmapSource :

        private ImageSource ImageFromClipboardDib()
        {
            MemoryStream ms = Clipboard.GetData("DeviceIndependentBitmap") as MemoryStream;
            BitmapImage bmp = new BitmapImage();
            bmp.BeginInit();
            bmp.StreamSource = ms;
            bmp.EndInit();
            return bmp;
        }

Malheureusement, ce code renvoie une magnifique NotSupportedException : « Impossible de trouver un composant d’image adapté pour terminer l’opération.». En clair, il ne sait pas comment décoder ça… c’est pourtant un format assez simple a priori, et très répandu. J’ai donc un peu creusé la question et étudié la structure d’un DIB. En simplifiant un peu, un fichier bitmap « classique » (.bmp) est composé des sections suivantes :

  • En-tête de fichier (structure BITMAPFILEHEADER)
  • En-tête de bitmap (structure BITMAPINFO)
  • Palette (tableau de RGBQUAD)
  • Données de l’image

En observant le contenu du DIB dans le presse-papier, on voit qu’il présente la même structure, mais sans le BITMAPFILEHEADER… l’astuce est donc d’ajouter cet en-tête au début du buffer, et de passer le tout à BitmapImage ou BitmapFrame. Pas bien difficile a priori… là où ça se corse, c’est qu’il faut renseigner dans cet en-tête certaines valeurs qu’on ne peut obtenir qu’en lisant le contenu de l’image. Il faut notamment indiquer à quelle position débutent les données de l’image proprement dite, et donc connaitre la taille des en-têtes et de la palette. Le code suivant effectue ce traitement, et renvoie une ImageSource à partir du presse-papier :

        private ImageSource ImageFromClipboardDib()
        {
            MemoryStream ms = Clipboard.GetData("DeviceIndependentBitmap") as MemoryStream;
            if (ms != null)
            {
                byte[] dibBuffer = new byte[ms.Length];
                ms.Read(dibBuffer, 0, dibBuffer.Length);

                BITMAPINFOHEADER infoHeader =
                    BinaryStructConverter.FromByteArray<BITMAPINFOHEADER>(dibBuffer);

                int fileHeaderSize = Marshal.SizeOf(typeof(BITMAPFILEHEADER));
                int infoHeaderSize = infoHeader.biSize;
                int fileSize = fileHeaderSize + infoHeader.biSize + infoHeader.biSizeImage;

                BITMAPFILEHEADER fileHeader = new BITMAPFILEHEADER();
                fileHeader.bfType = BITMAPFILEHEADER.BM;
                fileHeader.bfSize = fileSize;
                fileHeader.bfReserved1 = 0;
                fileHeader.bfReserved2 = 0;
                fileHeader.bfOffBits = fileHeaderSize + infoHeaderSize + infoHeader.biClrUsed * 4;

                byte[] fileHeaderBytes =
                    BinaryStructConverter.ToByteArray<BITMAPFILEHEADER>(fileHeader);

                MemoryStream msBitmap = new MemoryStream();
                msBitmap.Write(fileHeaderBytes, 0, fileHeaderSize);
                msBitmap.Write(dibBuffer, 0, dibBuffer.Length);
                msBitmap.Seek(0, SeekOrigin.Begin);

                return BitmapFrame.Create(msBitmap);
            }
            return null;
        }

Définition des structures BITMAPFILEHEADER et BITMAPINFOHEADER :

        [StructLayout(LayoutKind.Sequential, Pack = 2)]
        private struct BITMAPFILEHEADER
        {
            public static readonly short BM = 0x4d42; // BM

            public short bfType;
            public int bfSize;
            public short bfReserved1;
            public short bfReserved2;
            public int bfOffBits;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct BITMAPINFOHEADER
        {
            public int biSize;
            public int biWidth;
            public int biHeight;
            public short biPlanes;
            public short biBitCount;
            public int biCompression;
            public int biSizeImage;
            public int biXPelsPerMeter;
            public int biYPelsPerMeter;
            public int biClrUsed;
            public int biClrImportant;
        }

Classe utilitaire pour convertir des structures en binaire :

    public static class BinaryStructConverter
    {
        public static T FromByteArray<T>(byte[] bytes) where T : struct
        {
            IntPtr ptr = IntPtr.Zero;
            try
            {
                int size = Marshal.SizeOf(typeof(T));
                ptr = Marshal.AllocHGlobal(size);
                Marshal.Copy(bytes, 0, ptr, size);
                object obj = Marshal.PtrToStructure(ptr, typeof(T));
                return (T)obj;
            }
            finally
            {
                if (ptr != IntPtr.Zero)
                    Marshal.FreeHGlobal(ptr);
            }
        }

        public static byte[] ToByteArray<T>(T obj) where T : struct
        {
            IntPtr ptr = IntPtr.Zero;
            try
            {
                int size = Marshal.SizeOf(typeof(T));
                ptr = Marshal.AllocHGlobal(size);
                Marshal.StructureToPtr(obj, ptr, true);
                byte[] bytes = new byte[size];
                Marshal.Copy(ptr, bytes, 0, size);
                return bytes;
            }
            finally
            {
                if (ptr != IntPtr.Zero)
                    Marshal.FreeHGlobal(ptr);
            }
        }
    }

L’image obtenue par ce code peut être utilisée sans problème dans un contrôle Image.

Comme quoi, WPF a beau être une technologie « dernier cri », il faut encore parfois mettre les mains dans le cambouis ! Espérons que Microsoft corrigera ce bug dans la prochaine version…

[WPF] Binding sur les paramètres d’application à l’aide d’une Markup extension

Voilà, c’est fait, j’ai créé mon blog sur .NET… j’ai mis le temps, mais j’ai fini par y venir 😉

Je me présente rapidement : Thomas Levesque, 27 ans, ingénieur de formation. Je suis passionné depuis toujours par l’informatique, et plus particulièrement par la technologie .NET, que je suis de très près depuis ses débuts. Comme je suis du genre curieux, je passe pas mal de temps à fouiner dans les docs MSDN et sur le net pour m’auto-former sur les dernières nouveautés du framework. Aujourd’hui, je travaille comme développeur C# pour une PME en région parisienne.

Mais assez parlé de moi, venons-en au sujet qui nous intéresse : le binding sur les paramètres d’application en WPF.

Pour l’utilisateur d’une application, il est plus confortable de ne pas avoir à redéfinir sans arrêt ses préférences (dimensions de la fenêtre, activation de telle ou telle option…) : d’où l’utilité des paramètres d’application, introduits dans la version 2.0 du framework. Pour le développeur, c’est un peu pénible… même avec la classe Settings définie par Visual Studio, il reste encore pas mal de code à écrire pour lire et appliquer les paramètres au démarrage de l’appli, puis les enregistrer avant de quitter.

Dans Windows Forms, on pouvait définir des bindings entre les propriétés des contrôles et les paramètres d’application, mais ça restait peu pratique, et finalement assez peu utilisé (du moins c’est l’impression que j’en ai…).

Avec WPF, ça devient beaucoup plus intéressant… bien qu’il n’y ait pas de documentation “officielle” sur cette pratique, il est tout à fait possible de définir dans le code XAML des bindings sur les paramètres d’application. Par exemple, pour binder les dimensions de la fenêtre sur des paramètres, la méthode qui est présentée sur de nombreux blogs est la suivante :

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:p="clr-namespace:WpfApplication1.Properties"
        Title="Window1"
        Height="{Binding Source={x:Static p:Settings.Default}, Path=Height, Mode=TwoWay}"
        Width="{Binding Source={x:Static p:Settings.Default}, Path=Width, Mode=TwoWay}"
        Left="{Binding Source={x:Static p:Settings.Default}, Path=Left, Mode=TwoWay}"
        Top="{Binding Source={x:Static p:Settings.Default}, Path=Top, Mode=TwoWay}">

(Dans cet exemple, Height, Width, Top et Left sont des paramètres de l’application)

Cette méthode a l’avantage de fonctionner, mais franchement, est-ce que vous vous voyez écrire ça des dizaines de fois, pour chaque paramètre de l’application ? C’est long à écrire, peu intuitif, répétitif, et ça rend le code globalement peu lisible…

Loin de moi l’idée de dénigrer ceux qui ont eu cette idée, bien sûr… mais il est très facile d’améliorer cette méthode, en créant notre propre « markup extension ». Nous allons donc définir une classe qui va hériter de Binding, et servir spécifiquement à lier des propriétés à des paramètres de l’application.

Une « markup extension » (qu’on pourrait traduire approximativement par « balise d’extension ») est un objet qu’on peut utiliser dans le code XAML pour récupérer une valeur. On en utilise en permanence en WPF : Binding, StaticResource, DynamicResource sont des exemples de markup extensions.

On peut facilement définir sa propre markup extension en créant une classe dérivée de MarkupExtension, qui doit implémenter une méthode ProvideValue. En l’occurrence, l’essentiel de ce qu’on veut faire est déjà implémenté dans la classe Binding (qui hérite indirectement de MarkupExtension). Nous allons donc hériter directement de Binding, et simplement initialiser les propriétés qui nous intéressent pour se binder sur les paramètres :

using System.Windows.Data;

namespace WpfApplication1
{
    public class SettingBindingExtension : Binding
    {
        public SettingBindingExtension()
        {
            Initialize();
        }

        public SettingBindingExtension(string path)
            :base(path)
        {
            Initialize();
        }

        private void Initialize()
        {
            this.Source = WpfApplication1.Properties.Settings.Default;
            this.Mode = BindingMode.TwoWay;
        }
    }
}

Notez le suffixe « Extension » à la fin du nom de la classe : par convention, la plupart des markup extensions ont ce suffixe (Binding est une exception qui confirme la règle…). Ce suffixe pourra être omis quand on utilisera la classe en XAML (un peu comme les attributs, dont on omet le suffixe « Attribute » ).

Dans cette classe, on a défini 2 constructeurs, qui correspondent à ceux de Binding. On ne redéfinit pas la méthode ProvideValue, car celle de la classe Binding nous convient parfaitement (et d’ailleurs elle est marquée comme finale (sealed), si bien qu’on ne pourrait pas la redéfinir…). Le code « intéressant », si j’ose dire, est dans la méthode Initialize. On y définit la propriété Source comme étant les paramètres de notre appli, de façon à ce que le Path indiqué pour le binding renvoie le paramètre qui nous intéresse, et Mode = TwoWay, de façon à ce que les paramètres soient automatiquement mis à jour à partir de l’interface. L’intérêt étant de ne pas avoir à redéfinir ces propriétés à chaque fois…

Pour utiliser cette classe, c’est tout simple ! Reprenons l’exemple précédent, en remplaçant les Binding par notre extension SettingBinding :

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:WpfApplication1"
        Title="Window1"
        Height="{my:SettingBinding Height}"
        Width="{my:SettingBinding Width}"
        Left="{my:SettingBinding Left}"
        Top="{my:SettingBinding Top}">

C’est tout de même plus lisible et plus facile à utiliser…

Ah, et bien sûr, pour que ça marche, on n’oublie pas d’enregistrer les paramètres dans l’évènement Exit de l’application…

        private void Application_Exit(object sender, ExitEventArgs e)
        {
            WpfApplication1.Properties.Settings.Default.Save();
        }

Et voilà, les dimensions de la fenêtre seront enregistrées et restaurées à chaque lancement de l’application, sans qu’on ait rien de plus à coder !

Télécharger les sources

Mise à jour : Si vous voulez en savoir plus sur les markup extensions, je vous invite à lire le tutoriel que j’ai écrit suite à ce billet : Les markup extensions en WPF

css.php