WPF

De Banane Atomic
Aller à la navigationAller à la recherche

Généralités

Permet la création d'interfaces graphiques. Remplace Windows Forms.

  • Affichage vectoriel, cela permet d'augmenter la taille des objets en fonction de la résolution de l'écran sans effet de pixellisation.
  • Séparation entre le code de l'interface graphique (XAML) et le code des fonctionnalités (C#).

Historique

WPF Version Release .NET Version Visual Studio Version Major Features
3.0 2006-11 3.0 Initial Release.
3.5 2007-11 3.5 VS 2008 Changes and improvements in: Application model, data binding, controls, documents, annotations, and 3-D UI elements.
3.5 SP1 2008-08 3.5 SP1 Native splash screen support, New WebBrowser control, DirectX pixel shader support.
Faster startup time and improved performance for Bitmap effects.
4.0 2010-04 4.0 VS 2010 New controls: Calendar, DataGrid, and DatePicker.
Multi-Touch and Manipulation
4.5 2012-08 4.5 VS 2012 New Ribbon control, New INotifyDataErrorInfo interface
4.5.1 2013-10 4.5.1 VS 2013
4.5.2 2014-05 4.5.2
4.6 2015-07 4.6 VS 2015 Transparent child window support
HDPI and Touch improvements

Assemblages nécessaires

  • PresentationCore
  • PresentationFramework
  • System.Xaml
  • WindowsBase

Assemblages additionnels

Nom du paquet NuGet Assemblages installés
Expression.Blend.Sdk.WPF
  • Microsoft.Expression.Interactions v4.5 (MouseDragElementBehavior, interactivity:DataTrigger)
  • System.Windows.Interactivity v4.5 (Behavior, InvokeCommandAction, interactivity:EventTrigger)
System.Windows.Interactivity.WPF
  • Microsoft.Expression.Interactions v4.0
  • System.Windows.Interactivity v4.0
Blend.Interactivity.WPF
  • Microsoft.Expression.Interactions v3.5
  • System.Windows.Interactivity v3.5

Visual et Logical Tree

  • Visual Tree: rendu final
  • Logical Tree: code XAML

Snoop

Permet d'afficher l'arborescence visuelle d'une fenêtre WPF.
Crtl + Shift → sélectionner un élément visuel

Récupérer les Templates des composants de base

Csharp.svg
// récupérer le template par défaut de button1 définit dans le XAML
using (var xw = new XmlTextWriter(@"ButtonDefaultTemplate.xml", null))
{
    xw.Formatting = Formatting.Indented;
    System.Windows.Markup.XamlWriter.Save(button1.Template, xw);
}

WPF Control Template Viewer

Forcer la culture locale dans WPF

Par défaut, WPF utilise la culture US English.

Csharp.svg
FrameworkElement.LanguageProperty.OverrideMetadata(
    typeof(FrameworkElement), new FrameworkPropertyMetadata(
        XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));

Erreurs

Impossible de créer le type inconnu ...

Xaml.svg
<MainControl xmlns:xxx="clr-namespace:Namespace">
<!-- Ajouter l'assembly car la définition du namespace n'est pas suffisante -->
<MainControl xmlns:xxx="clr-namespace:Namespace;assembly=Assembly">

Astuces

Icônes / Glyphes

NuGet → FontAwesome.WPF

Xaml.svg
<Window xmlns:fa="http://schemas.fontawesome.io/icons/">
    <fa:ImageAwesome Icon="Refresh" Spin="True" Height="16" Width="16" />

Clipboard

Csharp.svg
Clipboard.SetText("Hello");

Control à définir depuis le code-behind

Xaml.svg
<!-- Permet de définir le Control à afficher depuis le code-behind -->
<ContentControl Content="{Binding Path=AutreControl}"/>

UserControl internal

MyUserControl.xaml.cs
internal partial class MyUserControl : UserControl
{ }
MyUserControl.xaml
<UserControl x:Class="MyNamespace.MyUserControl"
             x:ClassModifier="internal">

Un Enum dans du XAML

Xaml.svg
<MainControl xmlns:current="clr-namespace:CurrentNameSpace"
             Propriete="{x:Static Member=current:MaClasse+MonEnum.Element1}">
Csharp.svg
namespace CurrentNameSpace
{
    public class MaClasse
    {
        // public ou internal au minimum
        internal enum MonEnum { Element1, Element2 }
    }
}

Image

Xaml.svg
<Image Source="C:\chemin\image.jpg" />

<!-- si la taille de l'image est réduite, utiliser DecodePixelWidth ou DecodePixelHeight
pour mettre en cache l'image réduite et non l'originale, ceci afin d'économiser de la mémoire -->
<Image Width="100">
    <Image.Source>
        <BitmapImage DecodePixelWidth="100" UriSource="C:\chemin\image.jpg" />
    </Image.Source>
</Image>

<!-- ajouter l'image au projet:
Build action → Content
Copy to Output Directory → Copy if newer
L'image n'est pas embarqué dans l'assemblage -->
<Image Source="dossier/image.jpg" />
<!-- depuis un autre assemblage -->
<Image Source="/AssemblyName;component/dossier/image.jpg" />
Le format SVG n'est pas supporter nativement, il faut le convertir en XAML (avec Inkscape).

Binding et web API

Csharp.svg
private ImageSource _imageBoxStream;
public ImageSource ImageBoxStream
{
    get
    {
        return _imageBoxStream;
    }
    set
    {
        Set(() => ImageBoxStream, ref _imageBoxStream, value, true);
    }
}

var request = new HttpRequestMessage(HttpMethod.Get, url);
var response = await _httpClient.SendAsync(request);

using (var stream = await response.Content.ReadAsStreamAsync())
{
    var bitmapImage = new BitmapImage();
    bitmapImage.BeginInit();
    bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
    bitmapImage.StreamSource = stream;
    bitmapImage.EndInit();
    ImageBoxStream = bitmapImage;
}
Xaml.svg
<Image Source="{Binding ImageBoxStream}" Stretch="None" />

Image base64

Csharp.svg
var base64string = "data:image/jpeg;base64,XXXX";
var match = Regex.Match(response, @"data.+,(?<base64>.+)");
if (match.Success)
{
    byte[] binaryData = Convert.FromBase64String(match.Groups["base64"].Value);
    var bitmapImage = new BitmapImage();
    bitmapImage.BeginInit();
    bitmapImage.StreamSource = new MemoryStream(binaryData);
    bitmapImage.EndInit();
    ImageBoxStream = bitmapImage;
}

OriginalSource vs Source sur RoutedEventArgs

OriginalSource correspond à l'objet visuel alors que Source correspond à l'objet XAML.

Xaml.svg
<Button Content="Text" 
        PreviewMouseDown="Button_PreviewMouseDown"/>
Csharp.svg
private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    var source = e.Source; // Button
    var originalSource = e.OriginalSource; // ButtonChrome
}

Arbre visuel correspondant :

Button
ButtonChrome
ContentPresenter
TextBlock

Get children of a specific type

Csharp.svg
public static IEnumerable<T> FindChildren<T>(this DependencyObject parent, Func<T, bool> predicate)
    where T : DependencyObject
{
    var children = new List<DependencyObject>();

    if ((parent is Visual) || (parent is Visual3D))
    {
        var visualChildrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int childIndex = 0; childIndex < visualChildrenCount; childIndex++)
        {
            children.Add(VisualTreeHelper.GetChild(parent, childIndex));
        }
    }
    foreach (var logicalChild in LogicalTreeHelper.GetChildren(parent).OfType<DependencyObject>())
    {
        if (!children.Contains(logicalChild))
        {
            children.Add(logicalChild);
        }
    }

    foreach (var child in children)
    {
        var typedChild = child as T;
        if ((typedChild != null) && predicate.Invoke(typedChild))
        {
            yield return typedChild;
        }

        foreach (var foundDescendant in FindChildren(child, predicate))
        {
            yield return foundDescendant;
        }
    }

    yield break;
}

Get parent of a specific type

Csharp.svg
public static T GetParent<T>(DependencyObject child) where T : DependencyObject
{
    var parentObject = VisualTreeHelper.GetParent(child);
    if (parentObject == null)
    {
        return null;
    }

    if (parentObject is T matchingParent)
    {
        return matchingParent;
    }
    
    return GetParent<T>(parentObject);
}

Listen to the changes of a property

MyViewModel.cs
internal sealed class MyViewModel : BindableBase
{
    private readonly IDisposable myPropertySubscription;
    private string myProperty;

    public MyViewModel(MyViewContext context)
    {
        this.context = context;
        this.myPropertySubscription = this.context.AsWeakObservableOfPropertyChanged(x => x.MyProperty).Subscribe(this.OnMyPropertyChanged);
        this.RefreshFromContext();
    }

    public string MyProperty
    {
        get => this.myProperty;
        set => this.SetProperty(ref this.myProperty, value);
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
        this.myPropertySubscription.Dispose();
    }

    private void OnMyPropertyChanged(string myProp) => this.RefreshFromContext();

    private void RefreshFromContext()
    {
        this.MyProperty = this.context.MyProperty;
    }
}
NotifyPropertyChangedExtensions.cs
public static class NotifyPropertyChangedExtensions
{
    public static IObservable<TProperty> AsObservableOfPropertyChanged<T, TProperty>(this T source, Expression<Func<T, TProperty>> propertyExpression)
        where T : INotifyPropertyChanged
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException(nameof(propertyExpression));
        }

        var propertyName = ((MemberExpression)propertyExpression.Body).Member.Name;
        var propertyGetter = propertyExpression.Compile();

        return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
                handler => handler.Invoke,
                handler => source.PropertyChanged += handler,
                handler => source.PropertyChanged -= handler)
            .Where(e => e.EventArgs.PropertyName == propertyName)
            .Select(e => propertyGetter.Invoke(source));
    }

    public static IObservable<TProperty> AsWeakObservableOfPropertyChanged<T, TProperty>(this T source, Expression<Func<T, TProperty>> propertyExpression)
        where T : INotifyPropertyChanged
    {
        return GetWeakObservableOfPropertyChanged(source, propertyExpression, false);
    }

    public static IObservable<TProperty> AsWeakObservableOfPropertyChangedAndCurrentValue<T, TProperty>(this T source, Expression<Func<T, TProperty>> propertyExpression)
        where T : INotifyPropertyChanged
    {
        return GetWeakObservableOfPropertyChanged(source, propertyExpression, true);
    }

    private static IObservable<TProperty> GetWeakObservableOfPropertyChanged<T, TProperty>(this T source, Expression<Func<T, TProperty>> propertyExpression, bool startWithCurrentValue)
        where T : INotifyPropertyChanged
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException(nameof(propertyExpression));
        }

        var propertyName = ((MemberExpression)propertyExpression.Body).Member.Name;
        var propertyGetter = propertyExpression.Compile();

        var observer = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
                handler => handler.Invoke,
                handler => PropertyChangedEventManager.AddHandler(source, new EventHandler<PropertyChangedEventArgs>(handler), propertyName),
                handler => PropertyChangedEventManager.RemoveHandler(source, new EventHandler<PropertyChangedEventArgs>(handler), propertyName))
            .Where(e => e.EventArgs.PropertyName == propertyName)
            .Select(e => propertyGetter.Invoke(source));

        if (startWithCurrentValue)
        {
            observer = Observable.Return(propertyGetter.Invoke(source)).Concat(observer);
        }

        return observer;
    }
}

WPF vs UWP

WPF

  • OS antérieur à Windows 10
  • accès bas niveau au noyau et au matériel
  • applications non-sanboxées, accès aux APIs système
  • APIs graphique: radial brush, clipping mask
  • Fenêtre transparente
  • Accès plus fin à la bar de titre et au chrome des fenêtres
  • intégration de WindowsForm
  • chargement de code dynamique: plugins
  • applications complexes à plusieurs fenêtres

UWP

  • distribution via le store
  • applications sanboxées
  • code à destination de différents supports: smartphone, Xbox