11

I'm working with a system that has 4 outputs (monitors) with e.g. 1280x1024 pixels for each output. I need a screenshot of the whole desktop and all open applications on it.

I tried GetDesktopWindow() (MSDN) but it doesn't work properly. Some forms don't shown on the captured picture.

Thomas Weller
  • 55,411
  • 20
  • 125
  • 222
mat
  • 143
  • 1
  • 1
  • 11

2 Answers2

40

i tried GetDesktopWindow() function but it doesn't work properly.

Of course not.

The GetDesktopWindow function returns a handle to the desktop window. It doesn't have anything to do with capturing an image of that window.

Besides, the desktop window is not the same thing as "the entire screen". It refers specifically to the desktop window. See this article for more information and what can go wrong when you abuse the handle returned by this function.

i'm working with a system that have 4 outputs (monitors) with 1280x1024(e.g) for each output. i need a screenshot from whole desktop and all open applications on it.

This is relatively simple to do in the .NET Framework using the Graphics.CopyFromScreen method. You don't even need to do any P/Invoke!

The only trick in this case is making sure that you pass the appropriate dimensions. Since you have 4 monitors, passing only the dimensions of the primary screen won't work. You need to pass the dimensions of the entire virtual screen, which contains all of your monitors. Retrieve this by querying the SystemInformation.VirtualScreen property, which returns the bounds of the virtual screen. As the documentation indicates, this is the bounds of the entire desktop on a multiple monitor system.

Sample code:

// Determine the size of the "virtual screen", which includes 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);
    }

    // Do something with the Bitmap here, like save it to a file:
    bmp.Save(savePath, ImageFormat.Jpeg);
}

Edit:

please check your solution with a wpf application in a thread that is not your main thread. i tried it. it doesn't work!

Hmm, I didn't see a WPF tag on the question or mentioned anywhere in the body.

No matter, though. The code I posted works just fine in a WPF application, as long as you add the appropriate references and using declarations. You will need System.Windows.Forms and System.Drawing. There might be a more WPF-esque way of doing this that doesn't require a dependency on these WinForms assemblies, but I wouldn't know what it is.

It even works on another thread. There is nothing here that would require the UI thread.

Yes, I tested it. Here is my full test code:

using System.Windows;
using System.Windows.Forms;   // also requires a reference to this assembly
using System.Drawing;         // also requires a reference to this assembly
using System.Drawing.Imaging;
using System.Threading;

public partial class MainWindow : Window
{
   public MainWindow()
   {
      InitializeComponent();
   }

   private void button1_Click(object sender, RoutedEventArgs e)
   {
      // Create a new thread for demonstration purposes.
      Thread thread = new Thread(() =>
      {
         // Determine the size of the "virtual screen", which includes 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);
            }

            // Do something with the Bitmap here, like save it to a file:
            bmp.Save("G:\\TestImage.jpg", ImageFormat.Jpeg);
         }
      });
      thread.SetApartmentState(ApartmentState.STA);
      thread.Start();
   }
}
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
  • i tried this function and so many solutions like this that i have found in searches. but all of them doesn't take picture of my form and only take other forms on desktop!! it may causes for my wpf app and running this funtion in a thread that is not my main thread. – mat Apr 07 '13 at 13:33
  • please check your solution with a wpf application in a thread that is not your main thread. i tried it. it doesn't work! – mat Apr 07 '13 at 13:34
  • @user2251498 I did, I can't make it break. I get a picture saved that contains all 4 of my monitors. (Yes, I just happen to also have 4 on my dev machine.) I've posted the sample code I used. Does this work for you? – Cody Gray - on strike Apr 07 '13 at 14:06
  • of course its working, point is using thread.SetApartmentState(ApartmentState.STA), but when i add this line to my program, i don't know why my code throw an exception. i think that if you don't use that line, it will not work properly. – mat Apr 07 '13 at 17:41
  • It works fine in a multi-threaded apartment, too. I also tested that. But the real question is why your code breaks when you use a single-threaded apartment. That's probably what you *should* be using, given that you have a UI. Sounds like another question to me. – Cody Gray - on strike Apr 08 '13 at 20:25
  • i didn't get what you mean? – mat Apr 09 '13 at 05:23
  • 5
    I would suggest for completeness you use: g.CopyFromScreen(SystemInformation.VirtualScreen.Left, SystemInformation.VirtualScreen.Top, 0, 0, bmp.Size); This way if your monitors are stacked the top left isn't always (0,0) it could be something like (-768,0). This should account for that. – TaRDy Jul 22 '13 at 18:03
  • @TaRDy Good idea. I didn't think of that when I wrote the code. I've updated my answer for the benefit of others. Personally, I would have been OK with you editing to make that change yourself, but I know not everyone likes people messing with their code. So thanks for the heads up! – Cody Gray - on strike Jul 23 '13 at 07:19
  • This is my favorite solution so far for whole-screenshots instead of the zilliions of primary-screen only answers. – kayleeFrye_onDeck May 18 '18 at 02:10
  • Quick way to make it just use one screen? (selected by name?) – Worthy7 Jun 19 '18 at 14:00
4

I have created a tiny helper because I needed this case today and tried many different functions. Independently of the number of monitors, you can save it as a file on the disk or store it in a binary field in db with the following code blocks.

ScreenShotHelper.cs

using System.ComponentModel;//This namespace is required for only Win32Exception. You can remove it if you are catching exceptions from another layer.
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

namespace Company.Core.Helpers.Win32 {

    public static class ScreenShotHelper {

        private static Bitmap CopyFromScreen(Rectangle bounds) {
            try {
                var image = new Bitmap(bounds.Width, bounds.Height);
                using var graphics = Graphics.FromImage(image);
                graphics.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);
                return image;
            }
            catch(Win32Exception) {//When screen saver is active
                return null;
            }
        }

        public static Image Take(Rectangle bounds) {
            return CopyFromScreen(bounds);

        }

        public static byte[] TakeAsByteArray(Rectangle bounds) {
            using var image = CopyFromScreen(bounds);
            using var ms = new MemoryStream();
            image.Save(ms, ImageFormat.Png);
            return ms.ToArray();
        }   

        public static void TakeAndSave(string path, Rectangle bounds, ImageFormat imageFormat) {
            using var image = CopyFromScreen(bounds);
            image.Save(path, imageFormat);
        }
    }
}

Usage - Binary Field

var bounds = new Rectangle();
bounds = Screen.AllScreens.Aggregate(bounds, (current, screen) 
                           => Rectangle.Union(current, screen.Bounds));
_card.ScreenShot = Convert.ToBase64String(ScreenShotHelper.TakeAsByteArray(bounds));

Usage - Disk file

var bounds = new Rectangle();
bounds = Screen.AllScreens.Aggregate(bounds, (current, screen) 
                           => Rectangle.Union(current, screen.Bounds));
ScreenShotHelper.TakeAndSave(@"d:\screenshot.png", bounds, ImageFormat.Png);           
gurkan
  • 884
  • 4
  • 16
  • 25
  • Your code didn't work properly for me because I have 3 screens and I set the middle as the primary one, so I fixed it by specifying Left & Top for the CopyFromScreen call, like the following : graphics.CopyFromScreen(bounds.Left, bounds.Top, 0, 0, bounds.Size); – Adel Khayata Jan 12 '22 at 09:54
  • @AdelKhayata I had tested it with 3 screens before, but honestly, the possibility that the middle one would be the primary screen did not come to mind. Thanks for your suggestion. – gurkan Jan 13 '22 at 08:29