Search code examples
c#winformsscreenshotscalingmultiple-monitors

C#/.Net code to screen capture multiple monitors with scaling


We have a large WinForm C# .Net 4.6 program which from time to time needs to do obtain screen captures for debugging purposes. We currently use this code:

private static void DoScreenCapture(string filename)
{
    // Determine the size of the "virtual screen", including all monitors.
    int screenLeft = SystemInformation.VirtualScreen.Left;
    int screenTop = SystemInformation.VirtualScreen.Top;
    int screenWidth = SystemInformation.VirtualScreen.Width;
    int screenHeight = SystemInformation.VirtualScreen.Height;

    // Create a bitmap of the appropriate size to receive the screenshot.
    using (Bitmap bmp = new Bitmap(screenWidth, screenHeight))
    {
        // Draw the screenshot into our bitmap.
        using (Graphics g = Graphics.FromImage(bmp))
        {
            g.CopyFromScreen(screenLeft, screenTop, 0, 0, bmp.Size);
        }

        // Stuff the bitmap into a file
        bmp.Save(filename, Imaging.ImageFormat.Png);
    }
}

This code does all that we want, except when the user has scaled his monitors.

I've looked at bunches of Stack Overflow articles. Most of them provide code like we already have, but that doesn't handle the monitor scaling issue. For example:

Take screenshot of multiple desktops of all visible applications and forms

Some Stack Overflow articles indicate that making our application DPI aware would solve the problem. Yes, it would, but that's more than we can tackle today. For example:

Windows screenshot with scaling

There is also code which will do a capture for all monitors one at a time, but we much prefer to have all the monitors captured in the same image.

Can someone give me a C# code snippet which will take a screenshot of multiple monitors which have varied scaling factors?

For example, if I have three identical 1920x1080 monitors and arrange them left to right with the leftmost monitor at 175%, the center monitor at 100%, and the rightmost monitor at 150%, then this would be the screenshot that I want:

Expected screenshot

But this is the screenshot that my current code produces. Note that the rightmost monitor is missing a piece on the far right.

Actual screenshot


Solution

  • We needed a solution so I did some experimenting. First of all, we needed a C# class for some Windows methods. This code is stolen, not original.

    class NativeUtilities
    {
        [Flags()]
        public enum DisplayDeviceStateFlags : int
        {
            /// <summary>The device is part of the desktop.</summary>
            AttachedToDesktop = 0x1,
            MultiDriver = 0x2,
            /// <summary>This is the primary display.</summary>
            PrimaryDevice = 0x4,
            /// <summary>Represents a pseudo device used to mirror application drawing for remoting or other purposes.</summary>
            MirroringDriver = 0x8,
            /// <summary>The device is VGA compatible.</summary>
            VGACompatible = 0x16,
            /// <summary>The device is removable; it cannot be the primary display.</summary>
            Removable = 0x20,
            /// <summary>The device has more display modes than its output devices support.</summary>
            ModesPruned = 0x8000000,
            Remote = 0x4000000,
            Disconnect = 0x2000000
        }
    
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct DisplayDevice
        {
            [MarshalAs(UnmanagedType.U4)]
            public int cb;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string DeviceName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string DeviceString;
            [MarshalAs(UnmanagedType.U4)]
            public DisplayDeviceStateFlags StateFlags;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string DeviceID;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string DeviceKey;
        }
    
        [StructLayout(LayoutKind.Sequential)]
        public struct DEVMODE
        {
            private const int CCHDEVICENAME = 0x20;
            private const int CCHFORMNAME = 0x20;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
            public string dmDeviceName;
            public short dmSpecVersion;
            public short dmDriverVersion;
            public short dmSize;
            public short dmDriverExtra;
            public int dmFields;
            public int dmPositionX;
            public int dmPositionY;
            public ScreenOrientation dmDisplayOrientation;
            public int dmDisplayFixedOutput;
            public short dmColor;
            public short dmDuplex;
            public short dmYResolution;
            public short dmTTOption;
            public short dmCollate;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
            public string dmFormName;
            public short dmLogPixels;
            public int dmBitsPerPel;
            public int dmPelsWidth;
            public int dmPelsHeight;
            public int dmDisplayFlags;
            public int dmDisplayFrequency;
            public int dmICMMethod;
            public int dmICMIntent;
            public int dmMediaType;
            public int dmDitherType;
            public int dmReserved1;
            public int dmReserved2;
            public int dmPanningWidth;
            public int dmPanningHeight;
        }
    
        [DllImport("user32.dll")]
        public static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);
    
        public const int ENUM_CURRENT_SETTINGS = -1;
        const int ENUM_REGISTRY_SETTINGS = -2;
    
        [DllImport("User32.dll")]
        public static extern int EnumDisplayDevices(string lpDevice, int iDevNum, ref DisplayDevice lpDisplayDevice, int dwFlags);
    }
    

    Then I wrote a method to call this code, using the above Windows methods, as opposed to the .Net methods we had been using:

        public static void ScreenCapture(string filename)
        {
            // Initialize the virtual screen to dummy values
            int screenLeft = int.MaxValue;
            int screenTop = int.MaxValue;
            int screenRight = int.MinValue;
            int screenBottom = int.MinValue;
    
            // Enumerate system display devices
            int deviceIndex = 0;
            while (true)
            {
                NativeUtilities.DisplayDevice deviceData = new NativeUtilities.DisplayDevice{cb = Marshal.SizeOf(typeof(NativeUtilities.DisplayDevice))};
                if (NativeUtilities.EnumDisplayDevices(null, deviceIndex, ref deviceData, 0) != 0)
                {
                    // Get the position and size of this particular display device
                    NativeUtilities.DEVMODE devMode = new NativeUtilities.DEVMODE();
                    if (NativeUtilities.EnumDisplaySettings(deviceData.DeviceName, NativeUtilities.ENUM_CURRENT_SETTINGS, ref devMode))
                    {
                        // Update the virtual screen dimensions
                        screenLeft = Math.Min(screenLeft, devMode.dmPositionX);
                        screenTop = Math.Min(screenTop, devMode.dmPositionY);
                        screenRight = Math.Max(screenRight, devMode.dmPositionX + devMode.dmPelsWidth);
                        screenBottom = Math.Max(screenBottom, devMode.dmPositionY + devMode.dmPelsHeight);
                    }
                    deviceIndex++;
                }
                else
                    break;
            }
    
            // Create a bitmap of the appropriate size to receive the screen-shot.
            using (Bitmap bmp = new Bitmap(screenRight - screenLeft, screenBottom - screenTop))
            {
                // Draw the screen-shot into our bitmap.
                using (Graphics g = Graphics.FromImage(bmp))
                    g.CopyFromScreen(screenLeft, screenTop, 0, 0, bmp.Size);
    
                // Stuff the bitmap into a file
                bmp.Save(filename, System.Drawing.Imaging.ImageFormat.Png);
            }
        }
    

    This works and has been pulled from a large application. I hope I've included all the necessary pieces.