Étienne Baudoux // Notes

Récupérer la vraie résolution d'un écran en C#

// 10/18/2016

Hello World à tous !

Tu essayes de récupérer la résolution d’écran de tous tes moniteurs en C# et tu t’arraches les cheveux ? Tu es au bon endroit.

Je viens de passer l’après-midi à tenter de récupérer la résolution de tous mes écrans (internes + externes) sans que la taille du texte, apps et autre de Windows, que l’écran principal ou que la rotation d’un écran externe ne vienne me faire démouler un cake.

Le scénario

Dans le cadre d’un projet personnel, je dois développer une application WPF. Je souhaite faire en sorte qu’une même fenêtre de cette application soit ouverte sur chaque écran du PC et prenne une certaine dimension en fonction de la résolution de l’écran ciblé. Il ne me faut donc pas juste récupérer la résolution de l’écran principal (primaire) ou la résolution global (somme de tous les écrans), mais bien récupérer la résolution de tous les écrans connectés au PC.

Pour cela, rien de plus simple, il me suffit d’utiliser l’API suivante :

var allScreens = System.Windows.Forms.Screen.AllScreens;

Oui mais ! Ceux qui ont fait cette API (et je pense que j’irais leur dire bonjour la prochaine fois que je retourne à Redmond) ont dû trouver que c’était trop facile.

Le problème

Je vais tenter d’être clair. Dans mon scénario, j’ai deux écrans :

  1. L’écran interne de mon PC (une Surface Book).
    1. Résolution : 3000 x 2000
    2. Taille du texte, apps et autre de Windows : 200%
    3. Ecran primaire : Oui
  2. Un écran externe (Asus).
    1. Résolution : 1920 x 1080
    2. Taille du texte, apps et autre de Windows : 100%

Le souci, c’est d’avoir des écrans à résolution différente, et surtout, d’avoir la taille du texte, apps et autre différent d’un écran à l’autre.

Je ne suis toujours pas sûr du pourquoi du comment, mais quand je récupère les informations sur mon écran externe via l’API ci-dessus, la résolution de celui-ci est erroné. En effet, au lieu d’avoir une largeur de 1920, l’API me dit que mon écran externe fait 3840 de large... POURQUOIIIII !!??

Et c’est ainsi que je me suis perdu et que Windows a élargie mon Anus...

En fait, la résolution de l’écran externe retournée par cette API est multipliée par deux car… la taille du texte, apps et autre de Windows sur mon écran « primaire » (ici, mon écran interne… pas mon écran externe hein…) est à 200%.

Oui, vous avez bien lu. Toutes les résolutions récupérées via l’API Screen.AllScreens sont multipliés par le Scale de l’écran primaire… SAUF, pour la résolution de l’écran primaire question.

« OK Etienne m’a perdu. »

T’inquiètes je reprends ! Voici mes écrans et ce que me retourne l’API :

Résolution d’écran dans Windows Scale Résolution selon l’API
Ecran primaire 3000 x 2000 200 % 3000 x 2000
Ecran secondaire 1920 x 1080 100 % 3840 x 2160 (WTF !)
Ecran secondaire (bis) 1920 x 1080 125 % 3072 x 1728 (WTF bis !)

Pourquoi la résolution d’écran change en fonction de la taille de rendue des éléments à l’écran ? Je ne le sais toujours pas. Tout ce que je sais, c’est que j’ai eu du mal à récupérer la vraie résolution de chaque écran.

J’ai bien essayé d’utiliser des APIs plus bas niveau pour récupérer la résolution, mais le résultat était similaire.

Du coup, Reverse Engineering. En faisant le lien entre les diverses données du tableau ci-dessus, et en faisant des tests avec plusieurs résolutions, tailles de texte, écrans primaires, positionnement et rotation des écrans, j’ai déduis la formule magique qui permet de récupérer la vraie résolution de n’importe quel écran du PC quelle que soit sa configuration : Vraie Largeur d’un écran secondaire = Largeur selon l’API / Scale de l’écran primaire * Scale de l’écran secondaire

Exemple avec l’écran secondaire dont le Scale est à 125% :

= 3072 / 200 * 125
= 15.36 * 125
= 1920 // BINGO !

Exemple avec l’écran secondaire dont le Scale est à 100% :

= 3840 / 200 * 100
= 19.2 * 100
= 1920 // YOUHOU !

Ainsi, pour connaître la vraie résolution d’un écran secondaire, je dois connaître le Scale de l’écran primaire ainsi que celui de l’écran secondaire.

Le résultat

Après quelques recherches, j’ai trouvé la bonne API… Du moins je pense… Je vous explique.

J’ai trouvé l’API Win32 GetScaleFactorForMonitor qui doit me retourner le Scale du moniteur spécifié. A par que la documentation n’est pas à jour dans le sens ou en réalité l’API retourne un simple nombre entier et n'est pas limitée à l'énumération décrite, elle fonctionne très bien sous Windows 10 Anniversary Update. Je n’ai pas testé sous d’autres Builds mais sachez que chez certains, l’API retourne des valeurs qui ne correspondent pas au véritable Scale de l’écran. Attention donc. Si vous êtes sous une Build antérieur de Windows 10 ou sous Windows 8.1, ça peut vous jouer des tours.

J’ai également trouvé l’API GetDpiForMonitor qui peut être utile, mais j’ai fait sans.

J’ai ensuite rapidement codé l’algorithme suivant qui permet de récupérer les véritables résolutions de chaque écran de votre PC, quelle que soit la taille du texte sur chacun de ces écrans.

/// <summary>
/// Represents information about a monitor
/// </summary>
internal class ScreenInfo
{
    /// <summary>
    /// Gets or sets the index of the monitor
    /// </summary>
    internal int Index { get; set; }

    /// <summary>
    /// Gets or sets wether the monitor is the primary screen.
    /// </summary>
    internal bool Primary { get; set; }

    /// <summary>
    /// Gets or sets the device name
    /// </summary>
    internal string DeviceName { get; set; }

    /// <summary>
    /// Gets or sets the location and the size of the monitor
    /// </summary>
    internal Rect Bounds { get; set; }

    /// <summary>
    /// Gets or sets the scale of the UI. 125 means 125%.
    /// </summary>    
    internal int Scale { get; set; }
}

[StructLayout(LayoutKind.Sequential)]
internal struct Rect
{
    public Rect(int left, int top, int right, int bottom)
    {
        Left = left;
        Top = top;
        Right = right;
        Bottom = bottom;
    }

    internal int Left;
    internal int Top;
    internal int Right;
    internal int Bottom;
}

/// <summary>
/// Retrieves information about all monitors.
/// </summary>
/// <returns>An array of <see cref="ScreenInfo"/></returns>
internal static ScreenInfo[] GetAllScreenInfos()
{
    var result = new List<ScreenInfo>();
    var allScreens = System.Windows.Forms.Screen.AllScreens.ToList();

    var primaryScreenIndex = allScreens.FindIndex(s => s.Primary);
    var primaryScreen = allScreens[primaryScreenIndex];
    var screenScale = GetMonitorScaleFactor(primaryScreen);
    var primaryScreenScaleFactor = screenScale / 100.0;

    result.Add(new ScreenInfo
    {
        Index = primaryScreenIndex,
        DeviceName = primaryScreen.DeviceName,
        Scale = screenScale,
        Primary = true,
        Bounds = new Rect(primaryScreen.Bounds.Left, primaryScreen.Bounds.Top, primaryScreen.Bounds.Width, primaryScreen.Bounds.Height)
    });

    for (var i = 0; i < allScreens.Count; i++)
    {
        if (i == primaryScreenIndex)
        {
            continue;
        }

        var screen = allScreens[i];
        screenScale = GetMonitorScaleFactor(screen);
        double left = screen.Bounds.Left;
        if (screen.Bounds.Left != 0 && primaryScreenScaleFactor > screenScale /  100.0)
        {
            left = (screen.Bounds.Left / primaryScreenScaleFactor) * screenScale / 100;
        }

        double top = screen.Bounds.Top;
        if (screen.Bounds.Top != 0 && primaryScreenScaleFactor > screenScale / 100.0)
        {
            top = (screen.Bounds.Top / primaryScreenScaleFactor) * screenScale / 100;
        }

        var width = (screen.Bounds.Width / primaryScreenScaleFactor) * screenScale / 100;

        var height = (screen.Bounds.Height / primaryScreenScaleFactor) * screenScale / 100;

        result.Add(new ScreenInfo
        {
            Index = i,
            DeviceName = screen.DeviceName,
            Scale = screenScale,
            Bounds = new Rect((int)left, (int)top, (int)width, (int)height),
            Primary = false
        });
    }

    return result.ToArray();
}

/// <summary>
/// Retrieve the scale factor of the specified screen
/// </summary>
/// <param name="screen">The screen</param>
/// <returns>Return a number between 100 and 300. The value is in percent.</returns>
private static int GetMonitorScaleFactor(System.Windows.Forms.Screen screen)
{
    var point = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
    var hMonitor = MonitorFromPoint(point, 2); // MONITOR_DEFAULTTONEAREST = 2
    int screenScale;
    GetScaleFactorForMonitor(hMonitor, out screenScale);
    return screenScale;
}

//https://msdn.microsoft.com/en-us/library/windows/desktop/dd145062(v=vs.85).aspx
[DllImport("User32.dll")]
private static extern IntPtr MonitorFromPoint([In]System.Drawing.Point pt, [In]uint dwFlags);

//https://msdn.microsoft.com/en-us/library/windows/desktop/dn280510(v=vs.85).aspx
[DllImport("Shcore.dll")]
private static extern IntPtr GetScaleFactorForMonitor([In]IntPtr hmonitor, [Out]out int deviceScaleFactor);

N’hésitez pas à me faire des feedbacks. Si vous avez trouvé plus simple ou si savez pourquoi l’API de base se comporte ainsi, je suis preneur.

A bientôt ! :D