Tag Archives: image

[WPF] Empêcher l’utilisateur de coller une image dans un RichTextBox

Le contrôle RichTextBox de WPF est assez puissant, et très pratique quand on a besoin d’accepter une saisie en texte riche. Cependant, l’une de ses fonctionnalités peut devenir problématique : l’utilisateur peut coller une image. Selon ce qu’on veut faire du texte saisi par l’utilisateur, ce n’est pas forcément souhaitable.

Quand j’ai cherché sur le web un moyen d’empêcher cela, les seules solutions que j’ai trouvées suggéraient d’intercepter la frappe de touches Ctrl-V, et de bloquer l’événement si le presse-papiers contient une image. Cette approche présente plusieurs problèmes:

  • elle n’empêche pas l’utilisateur de coller via le menu contextuel
  • elle ne fonctionne pas si le raccourci de la commande a été modifié
  • elle n’empêche pas l’utilisateur d’insérer une image par glisser-déposer

Puisque cette solution ne me convenait pas, j’ai utilisé le site .NET Framework Reference Source pour chercher un moyen d’intercepter l’opération de collage elle-même. J’ai suivi le code à partir de la propriété ApplicationCommands.Paste, et j’ai finalement trouvé l’événement attaché DataObject.Pasting. Ce n’est pas un endroit où j’aurais pensé à chercher, mais quand on y réfléchit, c’est finalement assez logique. Cet événement peut être utilisé pour intercepter une opération de collage ou de glisser-déposer, et permet au gestionnaire de l’événement de faire différentes choses:

  • annuler purement et simplement l’opération
  • changer le format de données qui sera collé depuis le presse-papiers
  • remplacer le DataObject utilisé pour le collage

Dans mon cas, je voulais juste empêcher le collage ou le glisser-déposer d’une image, donc j’ai simplement annulé l’opération quand le FormatToApply était "Bitmap", comme illustré ci-dessous.

XAML:

<RichTextBox DataObject.Pasting="RichTextBox1_Pasting" ... />

Code-behind:

private void RichTextBox1_Pasting(object sender, DataObjectPastingEventArgs e)
{
    if (e.FormatToApply == "Bitmap")
    {
        e.CancelCommand();
    }
}

Bien sûr, il est également possible de gérer ça plus intelligemment. Par exemple, si le DataObject contient plusieurs formats, on pourrait créer un nouveau DataObject avec uniquement les formats acceptables. Comme ça l’utilisateur peut encore coller quelque chose, tant que ce n’est pas une image.

Afficher des suggestions de résultat dans une SearchBox WinRT : bug concernant l’image

Aujourd’hui je me suis heurté à un bug bizarre qui m’a fait perdre une heure ou deux, donc je me suis dit que ça méritait d’écrire un billet à ce sujet au cas où quelqu’un d’autre rencontrerait le même problème.

Le contrôle SearchBox a été ajouté dans Windows 8.1 pour permettre des scénarios de recherche directement dans une application Windows Store. L’une de ses fonctionnalités est l’affichage de suggestions basées sur la saisie de l’utilisateur. Il y a trois sortes de suggestions :

  • Les suggestions d’historique sont les requêtes précédemment effectuées par l’utilisateur. C’est géré automatiquement, donc vous n’avez aucun code à écrire pour que ça marche.
  • Les suggestions de recherche permettent de proposer des termes de recherche en fonction de ce que l’utilisateur a déjà saisi ; si l’utilisateur en sélectionne une, le texte actuel de la recherche est remplacé par celui de la suggestion, et valider la requête lancera la recherche avec ce texte.
  • Les suggestions de résultat sont des suggestions pour des résultats exacts. L’utilisateur peut directement choisir un de ces résultats sans lancer une recherche complète.

Pour fournir des suggestions, il faut gérer l’évènement SuggestionsRequested de la SearchBox, et ajouter des suggestions à l’aide des méthodes AppendQuerySuggestion et AppendResultSuggestion. Concentrons-nous sur les suggestions de résultat.

La méthode AppendResultSuggestion prend plusieurs paramètres, dont l’un représente l’image à afficher pour la suggestion. Il est obligatoire (passer null lèvera une exception), et il est de type IRandomAccessStreamReference, c’est-à-dire quelque chose qui peut fournir un flux. Je trouve ça un peu étrange, vu qu’il aurait été plus naturel de passer une ImageSource, mais c’est comme ça… J’ai donc cherché une classe qui implémente l’interface IRandomAccessStreamReference, et le premier candidat évident que j’ai trouvé était la classe StorageFile, qui représente un fichier. J’ai donc écrit le code suivant :

private async void SearchBox_SuggestionsRequested(SearchBox sender, SearchBoxSuggestionsRequestedEventArgs args)
{
    var deferral = args.Request.GetDeferral();
    try
    {
        var imageUri = new Uri("ms-appx:///test.png");
        var imageRef = await StorageFile.GetFileFromApplicationUriAsync(imageUri);
        args.Request.SearchSuggestionCollection.AppendQuerySuggestion("test");
        args.Request.SearchSuggestionCollection.AppendSearchSeparator("Foo Bar");
        args.Request.SearchSuggestionCollection.AppendResultSuggestion("foo", "Details", "foo", imageRef, "Result");
        args.Request.SearchSuggestionCollection.AppendResultSuggestion("bar", "Details", "bar", imageRef, "Result");
        args.Request.SearchSuggestionCollection.AppendResultSuggestion("baz", "Details", "baz", imageRef, "Result");
    }
    finally
    {
        deferral.Complete();
    }
}

Ce code s’exécute sans aucune erreur, et les suggestions sont affichées… mais l’image n’apparait pas !

https://i2.wp.com/i.stack.imgur.com/BiF0g.png?w=474

J’ai passé un long moment à tout revérifier, à faire plein de petits changements pour essayer de trouver l’origine du problème, j’ai même fait ma propre implémentation de IRandomAccessStreamReference… en vain.

J’ai finalement posté mon problème sur Stack Overflow, et quelqu’un m’a gentiment fourni la solution, qui était très simple : au lieu d’utiliser StorageFile, il faut utiliser RandomAccessStreamReference (ça semble assez évident une fois qu’on sait que ça existe). Le code devient donc :

private void SearchBox_SuggestionsRequested(SearchBox sender, SearchBoxSuggestionsRequestedEventArgs args)
{
    var imageUri = new Uri("ms-appx:///test.png");
    var imageRef = RandomAccessStreamReference.CreateFromUri(imageUri);
    args.Request.SearchSuggestionCollection.AppendQuerySuggestion("test");
    args.Request.SearchSuggestionCollection.AppendSearchSeparator("Foo Bar");
    args.Request.SearchSuggestionCollection.AppendResultSuggestion("foo", "Details", "foo", imageRef, "Result");
    args.Request.SearchSuggestionCollection.AppendResultSuggestion("bar", "Details", "bar", imageRef, "Result");
    args.Request.SearchSuggestionCollection.AppendResultSuggestion("baz", "Details", "baz", imageRef, "Result");
}

(Notez que la méthode n’est plus asynchrone, il n’y a donc plus besoin d’utiliser l’objet deferral).

Les suggestions sont maintenant affichées comme je le voulais, avec l’image :

https://i1.wp.com/i.imgur.com/cjmogKp.png?w=474

La leçon à tirer de cette histoire est que, bien que le paramètre image soit de type IRandomAccessStreamReference, il ne semble pas accepter autre chose qu’une instance de la classe RandomAccessStreamReference. Si vous passez n’importe quelle autre implémentation de l’interface, cela échoue silencieusement et l’image n’est pas affichée. C’est clairement un bug : si le type déclaré du paramètre dans la signature de la méthode et une interface, la méthode devrait accepter n’importe quelle implémentation de cette interface ; sinon, la signature devrait déclarer le type concret. J’ai signalé le bug sur Connect, avec un peu de chance ce sera corrigé dans une future version.

En espérant que ce soit utile à quelqu’un !

[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…