多显示器屏幕截图






4.50/5 (6投票s)
实用程序,支持多屏幕,用于截取全屏或部分屏幕。
介绍
很少有人使用双显示器或多显示器,因此大量的屏幕截图/抓取工具不支持第二个显示器。直到我在使用扩展双显示器显示器时,我才真正需要这样的软件;将窗口移动到第一个显示器以截取屏幕截图是一件很痛苦的事情。所以我想到自己制作一个程序,它既小巧又实用,并支持多个显示器。
背景
简单来说,要了解 Windows 的“显示”系统在多显示器上的工作原理,请参见下图。
显示器在没有任何间隙的情况下一个接一个地堆叠在一起。显示器可能具有不同的分辨率。总显示宽度和高度(以像素为单位)可以从最后一个显示器派生(该显示器具有最大的 X 和 Y 的 Bound 值。(感谢 Philippe Mori 的更正)。
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 为其(总)显示系统提供了一个设备上下文 (Device Context)。此设备上下文用于将整个显示数据读取到位图中。然后可以将此位图保存到剪贴板或文件中。
使用代码
代码是用 C# 编写的。 Screen_Capture
是主类,它提供捕获 Windows 的全显示并将它保存到剪贴板的功能。它通过以下步骤完成这项工作
- 为显示设备创建设备上下文。
- 将其转换为兼容的设备上下文(必须执行)。
主方法将左上角 XY 坐标和用户选择区域的宽度/高度作为输入。
//function to capture screen section
public static void CaptureScreentoClipboard(int x,int y, int wid, int hei)
{
//create DC for the entire virtual screen
int hdcSrc = CreateDC("DISPLAY", null, null, IntPtr.Zero);
int hdcDest = CreateCompatibleDC(hdcSrc);
int hBitmap = CreateCompatibleBitmap(hdcSrc, wid, hei);
SelectObject(hdcDest, hBitmap);
// set the destination area White - a little complicated
Bitmap bmp = new Bitmap(wid, hei);
Image ii = (Image)bmp;
Graphics gf = Graphics.FromImage(ii);
IntPtr hdc = gf.GetHdc();
//use whiteness flag to make destination screen white
BitBlt(hdcDest, 0, 0, wid, hei, (int)hdc, 0, 0, 0x00FF0062);
gf.Dispose();
ii.Dispose();
bmp.Dispose();
//Now copy the areas from each screen on the destination hbitmap
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 )
{// no common area
}
else {
// something common
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;
// Main API that does memory data transfer
BitBlt(hdcDest, X-x, Y-y , X1-X, Y1-Y, hdcSrc, X, Y,
0x40000000 | 0x00CC0020); //SRCCOPY AND CAPTUREBLT
}
}
// send image to clipboard
Image imf = Image.FromHbitmap(new IntPtr(hBitmap));
Clipboard.SetImage(imf);
DeleteDC(hdcSrc);
DeleteDC(hdcDest);
DeleteObject(hBitmap);
imf.Dispose();
}
上面的各种 API 从 GDI32/USER32.dll 中调用,因为 .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);
上面一个值得关注的点是,我们在进行屏幕数据传输之前,将原始兼容的 DC 绘制成白色。这是因为如果用户选择的显示区域跨越具有不同分辨率的不同屏幕,则“隐藏”区域将被传输函数变成黑色。因此,我们使用 API 的 WHITENESS
选项,使用 BitBlt
函数将其绘制成白色。
程序在执行时,会创建一个 Winform,以获取用户输入,选择他是否希望选择特定的屏幕、整个显示器或部分显示器。
现在,当用户单击选择时,可以通过多种方式检测鼠标所在的屏幕编号。我们采用以下描述的方法
- 我们首先获取连接到系统的显示器的总数。
- 我们在每个显示器上创建一个全尺寸的 Winform。这个 winform 应该具有以下属性:
Screen[] screens;
screens = Screen.AllScreens;
int noofscreens = screens.Length;
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;
将 Startposition
设置为 Manual
,我们可以显式地将窗口放置在任何显示器上。通过将其窗体样式设置为 none,我们得到整个屏幕作为客户端区域。透明度为 0.3 使得背景桌面几乎清晰可见。Startlocation
从每个屏幕的 screens.Bound.X
和 Y
属性中读取。
我们创建鼠标事件来检测对每个 WinForm 的点击,然后触发所有这些子 WinForms 的关闭,并使用区域信息调用 screencapturetoclipboard
函数。
使用每个窗口上的 mousemove
事件,我们还绘制一个矩形,用户可以使用它来查看正在选择的区域。
关注点
代码中有两个最重要的事情
- 将子 WinForms 的
Size
属性设置为Manual
。没有这个,我们无法将窗口放置在每个显示器上。 - 在使用
Bitblt
API 进行桌面复制期间,我们应该使用CAPTUREBLT
标志来复制分层窗口。 - 用户可以选择桌面的方式有很多种。主要功能是获取整个屏幕的位图。
- 如果用户从多个屏幕中选择区域,则“隐藏”区域将被涂成黑色。为了避免这种情况,我们将初始目标位图涂成白色。从显示器向我们的位图进行图像传输时,我们扫描检查用户从每个屏幕中选择的区域,并且只传输该区域。这样我们就避免了显示器“隐藏”部分的黑色区域。
历史
我想感谢一些网站,从这些网站我获得了制作这些实用程序的帮助,例如:tech.pro、c-sharpcorner.com 和 codeproject.com。