Bien comprendre le fonctionnement du AllowTransparency en WPF

21. mars 2011

Lors d’un débat hautement philosophique dans un troquet parisien (mon dieu, je m’embourgeoise à une vitesse…), je discutais avec des amis (D**k et Mi**u, pour respecter leur anonymat) de la possibilité de changer la valeur de AllowTransparency au runtime.

Mais avant d’aller plus loin, remettons sur la table le fonctionnement de cette propriété. Son but est simple : activer l’opacité par pixel (per-pixel opacity) qui permet en plus de la couleur de chaque pixel d’y associer une valeur d’alpha qui permet de rendre ce dernier plus ou moins transparent.

Windows supporte deux possibilités pour faire de la transparence au niveau des fenêtres:

 

Les fenêtres Layered en mode System Redirected Content (SRC)


Dans ce mode, Windows crée une image pour la fenêtre et chaque appel au GDI effectué par la fenêtre ou les contrôles est redirigé vers l’image en question. Ce mode est transparent pour la fenêtre qui ne sait pas que ces ordres de dessins sont redirigés. Toutefois il n’est pas possible dans ce mode d’avoir de per-pixel opacity car les appels GDI n’en contiennent pas. Tout au plus on peut activer une transparence globale pour la fenêtre.

Dans notre cas, nous pourrions essayer ce code sur notre fenêtre WPF:

[DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

[DllImport("user32.dll")]
static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, 
byte bAlpha, uint dwFlags); const int GWL_ID = -12; const int GWL_STYLE = -16; const int GWL_EXSTYLE = -20; const int LWA_ALPHA = 0x2; const int WS_EX_LAYERED = 0x80000; public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { WindowInteropHelper helper = new WindowInteropHelper(this); SetWindowLong(helper.Handle, GWL_EXSTYLE,
GetWindowLong(helper.Handle, GWL_EXSTYLE) ^ WS_EX_LAYERED); SetLayeredWindowAttributes(helper.Handle, 0, 128, LWA_ALPHA); }

Cela marcherait très bien pour une fenêtre Windows Forms ou n’importe quelle fenêtre Win32 mais cela n’a aucun effet en WPF. En effet, WPF surveille la valeur du style de sa fenêtre (au sens Win32 du terme) et donc rejette toutes modifications telles que le SetWindowLong ci-dessus.

Il n’est donc PAS POSSIBLE d’activer le AllowTransparency une fois qu’une fenêtre a été affichée. La seule solution consisterait à réinistancier la fenêtre courante et à modifier la valeur de AllowTransparency AVANT de faire le window.Show (ou à construire une fenêtre avec AllowTransparency=true et à transférer l’arbre des éléments WPF).

 

Les fenêtres Layered en mode Application Provided Content (APC)


Vous l’aurez compris, WPF a fait le choix de ce mode.

Ainsi quand on active AllowTransparency=true, la fenêtre Win32 qui sert de support à WPF est créée en mode Layered APC. Dans ce mode c’est l’application qui va crée le bitmap et qui sera responsable de le mettre à jour.

Ce mode supporte le per-pixel opacity et le bitmap est donc généré au format RGBA par WPF et envoyé à l’OS comme dessin de la fenêtre. Pour ce faire il est obligatoire que le WindowStyle soit égal à None car WPF ne saurait pas dessiner la zone non-cliente (la zone de titre, les bordures ou même les effets de distorsion d’Aero) qui sont à la charge de Windows.

Il n’y a donc plus aucun WM_PAINT et tout le mécanisme Win32 de dessin est totalement zappé. Ceci explique par exemple que si l’on a un ActiveX ou un contrôle Win32 dans sa fenêtre, il n’apparaitra pas puisque lui continue à attendre les WM_PAINT et à écrire dans le device context standard (Je referai un post pour contourner ce problème).

C’est un mode performant et qui correspond bien à WPF puisque ce dernier contrôle tout et peut envoyer ses données en RGBA (ce qui est son mode de fonctionnement interne).

Hélas, comme nous l’avons vu plus haut, ce mode n’est pas dynamique et doit être fixer une fois pour toute à cause de l’inflexibilité de WPF sur ce point.

 

Mettre en place une region


Si toutefois, l’effet recherché est de ne pas avoir une fenêtre rectangulaire, mais de faire une fenêtre aux formes bizarres de manière dynamique, il reste la solution de définir une région:

[DllImport("gdi32.dll")]
static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2,
    int cx, int cy);

[DllImport("user32.dll")]
static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);

public MainWindow()
{
    InitializeComponent();
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    WindowInteropHelper helper = new WindowInteropHelper(this);
    SetWindowRgn(helper.Handle, CreateRoundRectRgn(0, 0, 300, 300, 8, 8), true);
}

En effet, même si WPF fait sa sauce pour dessiner le contenu, notre fenêtre de part le fait qu’elle vit sous Windows doit se conformer aux fonctionnements standards et doit donc passer par le Windows Manager. Elle peut donc avoir une région définie (ce que l’on appelle aussi “Airspace”).

 

En conclusion, si l’on veut faire du per-pixel opacity, la seule solution reste le AllowTransparency et ce dernier ne peut être activé qu’avant l’affichage de la fenêtre.

.Net, WPF

Fuites de mémoire avec WPF 4.0 et de grosses listes d’images

15. mars 2011

Dans le cadre d’UrzaGatherer (http://urzagatherer.codeplex.com), j’ai découvert un problème bien embêtant avec une liste d’images en haute définition.

En effet j’ai une ListBox qui me présente les cartes d’une collection dans un WrapPanel (qui porte une jolie animation):

image

Or, l’utilisateur en se promenant de collections en collections va faire changer de nombreuses fois la source de la liste et va donc déclencher de nombreuses fois le chargement de ces grosses images.

Et j’ai constaté que lorsque je réaffecte le DataContext de ma liste à une nouvelle source, les anciennes données semblent ne pas être nettoyées de la mémoire.

Ainsi, UrzaGatherer peut allègrement occuper 2Go de RAM après avoir visité une dizaine de collections (chaque collection peut peser jusqu’à 300 Mo).

Je me suis donc mis en quête de la fuite.

Mais avant d’en venir à cette dernière regardons déjà comment le binding est fait et comment la liste est construite:

 

1 - Notre ListBox


La liste est donc une ListBox avec comme ItemPanel un WrapPanel dans lequel j’ai glissé un behavior en provenance de Blend pour faire un effet sympa d’animations:

            <ListBox ItemsSource="{Binding}" x:Name="imagesList">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Image Source="{Binding Bitmap, Mode=OneWay}"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal" ItemHeight="300">
                            <Interactivity:Interaction.Behaviors>
                                <Layout:FluidMoveBehavior Duration="00:00:00.5">
                                </Layout:FluidMoveBehavior>
                            </Interactivity:Interaction.Behaviors>
                        </WrapPanel>
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
            </ListBox>

 

On peut voir que l’ItemTemplate se branche sur la propriété Bitmap de notre source.

2 - La source de données


Cette dernière est effectivement composée d’une liste de cartes qui portent une propriété Bitmap que je construis ainsi:

public BitmapImage Bitmap
{
    get
    {
        if (CompletePath == null)
            return null;

        BitmapImage bitmapImage = new BitmapImage();
        bitmapImage.BeginInit();
        bitmapImage.CreateOptions = BitmapCreateOptions.None;
        bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        bitmapImage.UriSource = new Uri(CompletePath);
        bitmapImage.EndInit();
        bitmapImage.Freeze();

        return bitmapImage;
    }
}

A partir du chemin CompletePath, je produis une BitmapImage. L’objectif étant de contrôler le cache et de pouvoir créer les images dans un thread séparé pour les affecter par la suite. Toutefois, dans notre exemple, je simplifie cette étape en branchant directement le contrôle Image sur mon Bitmap sans passer par la création asynchrone.

 

3 – La fuite (les fuites?)


A partir de ce code relativement simple, il me suffit de brancher 3 ou 4 collections de cartes pour voir que la mémoire n’est jamais libérée.

Mes investigations m’ont permis de voir deux soucis:

  • Le behavior de Blend garde des références sur les images et de ce fait bloque le Garbage Collector et donc laisse les images en mémoire
  • L’utilisation d’une BitmapImage semble également laisser des traces en mémoire et malgré mes efforts je n’ai pas pu libérer ces ressources. La seule solution : faire un binding vers le chemin (CompletePath) plutôt que vers ma Bitmap.

 

Je me suis donc résigner à ne plus utiliser ni le behavior ni le chargement asynchrone. Toutefois pour ce dernier point, j’ai pu faire un contournement ainsi:

  • Créer une variable BindPath sur mes cartes
  • L’initialiser à null et mettre le binding du contrôle Image dessus
  • Binder la liste (donc pour le moment aucune image ne s’affiche puisque BindPath = null)
  • Faire une boucle dans une Task qui toutes les 20 millisecondes prend une carte et met sa propriété BindPath à CompletePath. De ce fait via le fait que mes cartes sont INotityPropertyChanged, la ListBox se met à jour progressivement et donne un effet progressif agréable (plutot qu’un bon vieux figeage de l’interface à l’ancienne)

 

Si vous aussi vous trouvez des fuites dans l’utilisation de WPF 4.0, n’hésitez pas à me le signaler Sourire

WPF, .Net, UrzaGatherer

Le truc à la con du jour : GridSplitter et Grid, comment sauver les tailles des colonnes?

5. janvier 2011

Alors voila le topo: j’ai une grid avec des splitters dedans pour changer la taille des colonnes:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition MinWidth="200" Name="column0"/>
        <ColumnDefinition MinWidth="200" Name="column1"/>
        <ColumnDefinition MinWidth="200" Name="column2"/>
    </Grid.ColumnDefinitions>
    <Rectangle Grid.Column="0" Fill="Red"/>
    <Rectangle Grid.Column="1" Fill="Blue"/>
    <Rectangle Grid.Column="2" Fill="Green"/>
    <GridSplitter Grid.Column="0" VerticalAlignment="Stretch" 
HorizontalAlignment="Right" Width="4"/> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch"
HorizontalAlignment="Right" Width="4"/> </Grid>

Je voudrais à la fermeture sauver la taille de mes colonnes et pouvoir les refixer lors du chargement suivant.

Du coup, bêtement, lors du chargement je faisais ça:

column0.SetValue(ColumnDefinition.WidthProperty, new GridLength(300));
column1.SetValue(ColumnDefinition.WidthProperty, new GridLength(300));
column2.SetValue(ColumnDefinition.WidthProperty, new GridLength(300));

Et bien je vous le donne en mille : ça ne marche pas! En effet, lorsque je bouge par la suite mes colonnes, la taille des colonnes semblent vouloir rester constante et le comportement global est inadapté.

La solution, comme toujours, est simple. En effet, les colonnes ont une propriété Width (et Height) qui n’est pas qu’un double mais un objet de type GridLength. Or ce dernier permet d’exprimer des tailles absolues (comme ci-dessus) mais aussi des tailles relatives (le célèbre “300*”).

Ainsi, pour conserver le comportement des colonnes tout en remettant les tailles de départ correctement il suffisait d’exprimer les tailles en relatif:

column0.SetValue(ColumnDefinition.WidthProperty, new GridLength(300, 
GridUnitType.Star)); column1.SetValue(ColumnDefinition.WidthProperty, new GridLength(300,
GridUnitType.Star)); column2.SetValue(ColumnDefinition.WidthProperty, new GridLength(300,
GridUnitType.Star));

WPF

Tutorial : Utilisation simple de LINQ pour gérer le tri d’une listbox

10. novembre 2010

Voici notre problématique du jour : avoir une listbox affichant une liste d’objets avec la nécessité de trier selon 3 critères chacun portant sur une propriété différente de nos objets.

Pour notre exemple, les objets sont de ce type:

public class UserProxy
{
    public Guid ID
    { set; get; }
    public string Login
    { set; get; }
    public DateTime Date
    { set; get; }
    public DateTime LastConnection
    { set; get; }

    public string Mail { get; set; }
}

Le but est de pouvoir trier par Login, Date et LastConnection.

Mise en place de la ListBox


Rien de compliqué ici, une simple ListBox avec un DataTemplate permettant d’afficher le Login et la LastConnection:

<ListBox Grid.Row="1" x:Name="lstUsers" >
<ListBox.ItemTemplate> <DataTemplate DataType="UserProxy"> <Grid ToolTip="{Binding Mail}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Image Source="user.png" Width="20" Height="20" Margin="4"/> <TextBlock Text="{Binding Login}" FontSize="24" Grid.Column="1"
Margin="10,0" VerticalAlignment="Center"/> <TextBlock Text="{Binding LastConnectionString}" FontSize="14"
Grid.Column="2" Margin="5,0" FontStyle="Italic" VerticalAlignment="Center"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> <ListBox.FocusVisualStyle> <Style TargetType="ListBoxItem"> <Setter Property="Foreground" Value="White" /> </Style> </ListBox.FocusVisualStyle> </ListBox>

Binding de base

Partant du principe que notre liste d’utilisateurs s’appelle users, le binding sur la liste se définit donc comme suit:

lstUsers.ItemsSource = users

Ajout de la notion de triage


Pour la notion de triage nous allons faire appel à LINQ. En effet, ce dernier introduit les méthodes d’extension OrderBy et OrderByDescending qui s’appliquent aux objets implémentant l’interface IEnumerable<>.

Ainsi le code avec la gestion du tri devient:

lstUsers.ItemsSource = users.OrderBy(u=>u.Login);

On peut le voir sur le code ci-dessus, la méthode d’extension OrderBy prend donc en paramètre un délégué générique dans lequel j’ai mis une expression lamba prenant un UserProxy en paramètre et retournant une chaine de caractères.

La signature précise de OrderBy est d’ailleurs un délégué de type System.Func<TSource, TKey> c’est à dire un délégué prenant un objet de classe TSource en paramètre et retournant un objet de classe TKey (qui sera utilisé comme clef pour le tri).

Partant de ce principe et grâce aux expressions lambdas, nous pouvons définir trois variables comme suit:

readonly Func<UserProxy, object> nameKeySelector  = (u=>u.Login);
readonly Func<UserProxy, object> dateKeySelector = (u=>u.Date);
readonly Func<UserProxy, object> connectionKeySelector = (u => u.LastConnection);

Grâce à ces trois variables, nous allons fournir une méthode qui saura gérer le tri ascendant et descendant et qui surtout saura prendre en charge nos expressions:

        void SortDatas(Func<UserProxy, object> keySelector)
        {
            if (users == null)
                return;

            if (keySelector == null)
                keySelector = previousKeySelector;

            if (keySelector == previousKeySelector)
            {
                asc = !asc;
            }
            else
            {
                asc = true;
            }

            previousKeySelector = keySelector;

            if (asc)
                lstUsers.ItemsSource = users.OrderBy(keySelector);
            else
            {
                lstUsers.ItemsSource = users.OrderByDescending(keySelector);
            }
        }

Pour que la méthode fonctionne il faut juste rajouter deux variables supplémentaires:

        bool asc = true;
        Func<UserProxy, object> previousKeySelector;

L’usage de cette méthode devient donc relativement simple puisqu’il suffit de l’appeler avec l’une de nos expressions pour effectuer le tri associé (et si on l’appelle plusieurs fois de suite avec la même expression, le sens de tri sera inversé):

        private void dateSort_Click(object sender, RoutedEventArgs e)
        {
            SortDatas(dateKeySelector);
        }

        private void nameSort_Click(object sender, RoutedEventArgs e)
        {
            SortDatas(nameKeySelector);
        }

        private void connectionSort_Click(object sender, RoutedEventArgs e)
        {
            SortDatas(connectionKeySelector);
        }

Et le tour est joué Sourire

.Net, WPF, Tutorial

Vivez le keynote de la PDC avec Bewise

28. octobre 2010

Si vous ne savez pas quoi faire ce soir, je vous propose de venir avec moi voir la retranscription simultanée du keynote de la PDC dans les locaux de Microsoft à Toulouse.

Toute la team Bewise sera là et on pourra discuter ensemble de toutes les annonces qui ne manqueront pas de voir le jour.

Pour s’inscrire c’est par là:

http://www.facebook.com/event.php?eid=125098587545246&index=1

L’adresse :
Microsoft - 1 Rue Marie Curie - Parc Technologique du Canal - 31520 Ramonville St-Agne

Windows Phone, .Net, Windows Forms, Windows Mobile, WPF, Visual Studio, Silverlight, DirectX, Bewise

Le truc à la con du jour : Lancement de plusieurs fenêtres en série dans WPF

4. juin 2010

Un truc bien débile qui m’est arrivé aujourd’hui. Au sein d’une application WPF qui poutre (http://urzagatherer.codeplex.com), je voulais, lors du lancement ouvrir une première fenêtre avant la fenêtre principale.

Le code ressemble donc à ça dans le constructeur de mon App:

            InstallDatabaseWindow databaseWindow = new InstallDatabaseWindow();
            databaseWindow.ShowDialog();

            MainWindow mainWindow = new MainWindow();
            mainWindow.Show();

Rien de bien formidable me direz-vous? Et bien si vous faites un test, vous verrez que le Show sur votre mainWindow.Show() plantera généreusement avec le message suivant:

“Cannot set Visibility or call Show, ShowDialog, or WindowInteropHelper.EnsureHandle after a Window has closed.”

Bon, au premier abord, je me suis dit que j’ai du merdé dans mon constructeur.

En fait, pas du tout (je me disais aussi que ce n’était pas possible) l’explication est plus sioux : Par défaut, une application WPF possède une propriété ShutdownMode qui est par défaut réglée sur OnLastWindowClose. En gros dès qu’il n’y a plus de fenêtres en vie, l’application va se fermer gentillement.

De ce fait, dans le cas du lancement de plusieurs fenêtre en série, dès que la première se ferme, l’application vérifie sa propriété ShutdownMode et se retrouve à se fermer puisque la fenêtre suivante n’a pas encore été instanciée! Ce qui fait que lorsque l’on va faire le Show() suivant, comme l’application est en cours de fermeture, la fenêtre va recevoir un Close et donc ne pourra pas s’ouvrir puisqu’elle a déjà été fermée :)

Deux solutions:

  • Instancier ses fenêtres toutes en simultanée avant de faire apparaitre la première
  • Mettre l’application sur le mode ShutdownMode.OnExplicitShutdown et s’abonner à l’événement Closed de la dernière fenêtre pour appeler la méthode Shutdown() sur l’application

 

Et le tour est joué…

.Net, WPF

De l’utilisation intelligente d’Entity Framework 4.0

31. mai 2010

Un framework a beau être super puissant, il ne peut pas empêcher l'a mauvaise utilisation. Il peut tenter par de nombreux moyens de la réduire mais il ne peut l’empêcher.

Du moins pas sans se brider par la suite.

J’en veux pour preuve l’utilisation d’Entity Framework 4.0. Ce framework permet de manière extrêmement élégante de requêter une source de données. Et, bien utilisé, il permet aussi d’être très performant.

Voici un exemple issu d’UrzaGatherer: Mon modèle possède une collection de Card qui, entre autres, contient une propriété Check qui permet de définir si l’utilisateur détient complètement la carte en question. Dans le cadre de la treeview qui affiche les collections de cartes, je voulais modifier le texte de chaque collection en y ajoutant le nombre de cartes manquantes le cas échéant.

Pour se faire et comme tout est issu du binding dans UrzaGatherer, j’ai développé un ValueConverter utilisé ainsi:

<TextBlock Text="{Binding Converter={StaticResource CountConverter}, Mode=OneWay
, IsAsync=True}"
/>

Le code du converter était le suivant:

    public class CountConverter : IValueConverter
    {
    public object Convert(object value, Type targetType, object parameter, 
CultureInfo culture) { Expansion expansion = (Expansion) value; var query = from card in MainWindow.Entities.Cards where card.ExpansionID == expansion.ID select card; int check = 0; List<Card> cards = query.ToList(); int count = cards.Count; foreach (Card card in cards) { if (card.Check) check++; } int missing = count - check; if (missing == 0) return string.Format("- {0} cards", count); return string.Format("({0}/{1} - Missing : {2})", check, count, missing); } public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture) { return value; } }

Dans ce converter, on voit donc une requête LINQ sur les cartes en question, puis une transformation en liste pour faire notre comptage.

Et c’est là qu’apparait la mauvaise utilisation. En effet, la conversion en liste va effectivement faire la requête sur la base de données et créer les entités en mémoire. Ces entités qui peuvent être lourdes (c’est le cas ici ou chaque carte porte plusieurs images).

Tout ça pour juste obtenir un comptage.

Il faut garder à l’esprit que EF4 est efficace tant qu’on reste dans le monde des expressions LINQ, c’est à dire tant qu’on reste au final dans le monde SQL.

Ici par exemple, la solution pour être bien plus efficace est simple : Il ne faut pas utiliser de listes locales mais tout traiter en LINQ:

public object Convert(object value, Type targetType, object parameter, 
CultureInfo culture) { Expansion expansion = (Expansion) value; var query = from card in MainWindow.Entities.Cards where card.ExpansionID == expansion.ID select card; int check = query.Count(c => c.Check); int count = query.Count(); int missing = count - check; if (missing == 0) return string.Format("- {0} cards", count); return string.Format("({0}/{1} - Missing : {2})", check, count, missing); }

La différence est subtile mais ici tout se passera sur SQL Server. Aucune entité ne sera créée ni ramenée côté client. Les méthodes Count() issues de LINQ vont générer des requêtes efficaces côté serveur contrairement à la méthode ToList() qui va faire un bon gros SELECT des familles pour tout reconstruire en mémoire.

Donc en conclusion et en ce qui concerne Entity Framework 4.0 : Retardez toujours au plus tard la récupération des entités!!

.Net, WPF ,

UrzaGatherer

31. mai 2010

Parmi mes nombreux vices, j’ai notamment le plaisir d’être collectionneur de cartes Magic.

Or cette collection est extrêmement riche et complexe à gérer. J’ai donc développé en WPF4.0, une application de gestion associée.

Elle utilise SQL Server 2008 R2 express pour stocker les dizaines de milliers d’images. Le système s’appuie sur Entity Framework 4.0 (qui d’ailleurs est une tuerie).

Je mettrai dans quelques temps le code source de l’application sur Codeplex.

image image

image image

WPF, .Net, UrzaGatherer

Le truc à la con du jour : Sélectionner un item dans une treeview WPF

11. avril 2010

C’est ballot mais contrairement à Windows Forms, il n’y a pas un support direct de la sélection d’un item dans une treeview WPF.

Toutefois, la solution est relativement simple, puisqu’elle passe par la fonctionnalité des treeviews qui permet de récupérer les TreeViewItems depuis les objets liés : ItemContainerGenerator.

Ainsi, en respectant la hierarchie de la treeview on peut parcourir l’arbre à la recherche du container de l’item.

Cela donne donc le code suivant:

public static bool SetSelected(ItemsControl parent, object child)
{
    if (parent == null || child == null)
    {
        return false;
    }

    TreeViewItem childNode = parent.ItemContainerGenerator.ContainerFromItem(child) as TreeViewItem;

    if (childNode != null)
    {
        childNode.Focus();
        return childNode.IsSelected = true;
    }

    if (parent.Items.Count > 0)
    {
        foreach (object childItem in parent.Items)
        {
            ItemsControl childControl = parent.ItemContainerGenerator.ContainerFromItem(childItem) 
as ItemsControl; if (SetSelected(childControl, child)) { return true; } } } return false; }

Et le tour est joué.

.Net, WPF

Composition d’images avec WPF

8. avril 2010

Dans le cadre d’un projet, j’ai eu besoin de générer une image contenant d’autres images composées ainsi que du texte. Ceci devait être fait dans un thread séparé pour des raisons de performances.

Pour cela, il existe une technologie bien sympatique en WPF, le RenderTargetBitmap. Ce dernier permet en effet de dessiner des Visual directement dans son contenu. Pour ce faire on s’appuie sur la classe DrawingVisual qui est un peu l’équivalent de la classe Graphics en Windows Forms/GDI+.

Cette dernière permet par exemple de dessiner des images (et donc de les redimensionner) ainsi que du texte (notamment).

Cela donne donc ceci:

FormattedText text = new FormattedText("Message à caractère informatif",
new CultureInfo("en-us"), FlowDirection.LeftToRight,
new Typeface(new FontFamily("Arial"), FontStyles.Normal, FontWeights.Bold, new FontStretch())
, 18, Brushes.Black); DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext = drawingVisual.RenderOpen(); drawingContext.DrawImage(image, new Rect(0, 0, renderTarget.PixelWidth, renderTarget.PixelHeight)); drawingContext.DrawText(text, new Point(renderTarget.PixelWidth - text.Width - 45, 350)); text.SetForegroundBrush(Brushes.White); drawingContext.DrawText(text, new Point(renderTarget.PixelWidth - text.Width - 45 - 1, 349)); drawingContext.Close(); renderTarget.Render(drawingVisual);

 

Par la suite, ce RenderTargetBitmap peut servir de source à un contrôle Image ou peut être sauvegarder sur le disque via la classe BmpBitmapEncoder:

string picturePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
, "urza.bmp"); FileStream stream = new FileStream(picturePath, FileMode.Create); BmpBitmapEncoder encoder = new BmpBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(renderTarget)); encoder.Save(stream);
 
Et le tour est joué!

.Net, WPF