Introduction
Not many people have dual or multiple monitors and thus a large chunk of screen capture/grabbers don't offer any support for second monitor.
I myself never needed a software like this until I was working on extended dual monitor display; and it was a pain to move the window to first display to be able to capture a screenshot.
So I thought of making a program myself that would be small, to the point and support multiple monitors.
Background
In simple sense, to understand how Windows 'display' system works on multiple monitors, see the picture below.
Monitors are stacked one after the another without any gap in either direction.
Monitors may have different resolution. Total display width and height in pixels can be derived from the last monitors (who has the largest Bound value for X and Y. (Thanks Philippe Mori for the correction).
Screen[] screens;
screens = Screen.AllScreens;
int noofscreens = screens.Length, maxwidth = 0, maxheight = 0;
for (int i = 0; i < noofscreens; i++)
{
if (maxwidth < (screens[i].Bounds.X + screens[i].Bounds.Width)) maxwidth = screens[i].Bounds.X + screens[i].Bounds.Width;
if (maxheight < (screens[i].Bounds.Y + screens[i].Bounds.Height)) maxheight = screens[i].Bounds.Y + screens[i].Bounds.Height;
}
capture_class.CaptureScreentoClipboard(0,0, maxwidth, maxheight);
Windows provide a Device Context to its (total) display system. This Device context is used to read the entire display data into a bitmap.
This bitmap can then be saved into clipboard or a file.
Using the code
Code is written in C#. Screen_Capture
is the main class that provide functionality to capture the Full display of the windows and save it to clipboard. It does this work in these steps:
- Create Device Context for display device.
- Convert it to compatible Device Context (has to be done).
The main method takes input the left-top XY coordinates and the width/height of the user selected area.
public static void CaptureScreentoClipboard(int x,int y, int wid, int hei)
{
int hdcSrc = CreateDC("DISPLAY", null, null, IntPtr.Zero);
int hdcDest = CreateCompatibleDC(hdcSrc);
int hBitmap = CreateCompatibleBitmap(hdcSrc, wid, hei);
SelectObject(hdcDest, hBitmap);
Bitmap bmp = new Bitmap(wid, hei);
Image ii = (Image)bmp;
Graphics gf = Graphics.FromImage(ii);
IntPtr hdc = gf.GetHdc();
BitBlt(hdcDest, 0, 0, wid, hei, (int)hdc, 0, 0, 0x00FF0062);
gf.Dispose();
ii.Dispose();
bmp.Dispose();
Screen[] screendata = Screen.AllScreens;
int X, X1, Y, Y1;
for (int i = 0; i < screendata.Length; i++)
{
if (screendata[i].Bounds.X > (x + wid) || (screendata[i].Bounds.X +
screendata[i].Bounds.Width) < x || screendata[i].Bounds.Y > (y + hei) ||
(screendata[i].Bounds.Y + screendata[i].Bounds.Height) < y )
{ }
else {
if (x < screendata[i].Bounds.X) X = screendata[i].Bounds.X; else X = x;
if ((x + wid) > (screendata[i].Bounds.X + screendata[i].Bounds.Width))
X1 = screendata[i].Bounds.X + screendata[i].Bounds.Width; else X1 = x + wid;
if (y < screendata[i].Bounds.Y) Y = screendata[i].Bounds.Y; else Y = y;
if ((y + hei) > (screendata[i].Bounds.Y + screendata[i].Bounds.Height))
Y1 = screendata[i].Bounds.Y + screendata[i].Bounds.Height; else Y1 = y + hei;
BitBlt(hdcDest, X-x, Y-y , X1-X, Y1-Y, hdcSrc, X, Y,
0x40000000 | 0x00CC0020); }
}
Image imf = Image.FromHbitmap(new IntPtr(hBitmap));
Clipboard.SetImage(imf);
DeleteDC(hdcSrc);
DeleteDC(hdcDest);
DeleteObject(hBitmap);
imf.Dispose();
}
Various APIs above are called from GDI32/USER32.dll as they are not provided by .NET.
[DllImport("GDI32.dll")]
public static extern bool BitBlt(int hdcDest,int nXDest,int nYDest,
int nWidth,int nHeight,int hdcSrc,int nXSrc,int nYSrc,int dwRop);
[DllImport("GDI32.dll")]
public static extern int CreateCompatibleBitmap(int hdc,int nWidth, int nHeight);[DllImport("GDI32.dll")]
public static extern int CreateCompatibleDC(int hdc);
[DllImport("GDI32.dll")]
public static extern bool DeleteDC(int hdc);
[DllImport("GDI32.dll")]
public static extern bool DeleteObject(int hObject);
[DllImport("gdi32.dll")]
static extern int CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData);
[DllImport("GDI32.dll")]
public static extern int GetDeviceCaps(int hdc,int nIndex);
[DllImport("GDI32.dll")]
public static extern int SelectObject(int hdc,int hgdiobj);
[DllImport("User32.dll")]
public static extern int GetDesktopWindow();
[DllImport("User32.dll")]
public static extern int GetWindowDC(int hWnd);
[DllImport("User32.dll")]
public static extern int ReleaseDC(int hWnd,int hDC);
One point of interest above is that we are painting the original compatible DC white before making the screen data transfer. This is because if the user selects display area spanning different screens of different resolutions, the 'hidden' area will be made black by the transfer function. We are thus painting it white with
BitBlt
function using the WHITENESS
option of the API.
Program, on execution, makes a Winform to get input from user to choose if he wish to select a particular screen, whole display or part of the display.
Now there can be many ways to detect the screen number on which our mouse is when the user makes a click to select it. We are following the method described below:
- We begin by getting the total number of monitors attached to the system.
Screen[] screens;
screens = Screen.AllScreens;
int noofscreens = screens.Length;
- We create a full size Winform on each monitor. This winform should have the following properties:
this.StartPosition = System.Windows.Forms.FormStartPosition.Manual;
this.Opacity = 0.3;
this.ControlBox = false;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Location = new Point(xstart, ystart);
this.WindowState = FormWindowState.Maximized;
Making Startposition
as Manual
, we can explicitly place the window in any monitor. By making its form style none, we get the entire screen as client area. Opacity of 0.3 makes the background desktop visible almost clearly.
Startlocation
is read from the screens.Bound.X
and
Y
property for each screen.
We create mouse event to detect clicks on each WinForm which then triggers the closure of all these child
WinForms and call the screencapturetoclipboard
function with area information.
Using the mousemove
event on each window, we also draw a rectangle which the user can use to see the area being selected.
Points of Interest
There are two most important things in the code:
- Setting the children WinForms
Size
property to Manual
. Without this, we can't place the windows on each monitor.
- During desktop copy using
Bitblt
API, we should use the
CAPTUREBLT
flag to copy the layered windows.
- There can be many ways in which the user can be made to select the desktop. Primary function is getting the bitmap of the entire screen.
- If the user selects area from multiple screens, the 'hidden' area is colored black. To avoid this, we paint the initial target bitmap white. When making the image transfer from display to our bitmap, we scan check for the area selected by user from each screen and transfer only that area. This way we avoid showing black area for 'hidden' sections of the display.
History
I would like to thank some websites from where I got help in making these utility, namely: tech.pro, c-sharpcorner.com, and codeproject.com.