0
votes

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

2
And what is your question exactly?Gert Kommer
@GertKommer Sure ... I added a question to the end.bicope

2 Answers

0
votes

The simplest way is to create a wide image that the resolution of it was built using screen count * screen width, by this way you have wide image that contains all monitors screenshot and don't care about scaling.

The problem of this scenario is some blank spaces because the image height and width is based on largest screen, so some area is blank for small resolution and rendered as black.

You can see this problem in below image :

enter image description here

The problem could be fixed by a trick, change the image transparent color to black, so we remove the black color by this trick and the final image is :

enter image description here

        int screenCount = Screen.AllScreens.Length;

        int screenTop = SystemInformation.VirtualScreen.Top;
        int screenLeft = SystemInformation.VirtualScreen.Left;
        int screenWidth = Screen.AllScreens.Max(m => m.Bounds.Width);
        int screenHeight = Screen.AllScreens.Max(m => m.Bounds.Height);

        bool isVertical = (SystemInformation.VirtualScreen.Height < SystemInformation.VirtualScreen.Width);

        if (isVertical)
            screenWidth *= screenCount;
        else
            screenHeight *= screenCount;

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

            // Make black color transparent
            bmp.MakeTransparent(Color.Black);

            bmp.Save("TestImage.png", ImageFormat.Png);
        }
  • The code can be improved to reduce the final image size by resizing the image.
  • You can get each monitor resolution and scale using native API and build the final image based on this size.
0
votes

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.