如何将窗口捕获为图像并保存






4.43/5 (11投票s)
拍摄任何 UI 应用程序主窗口的快照
引言
有时您可能需要抓取一些 Windows 的快照以用于演示或监视任务。有一些关于如何执行此操作的文章,例如 Lim Bio Liong 的文章,但它使用旧的非托管 C++ 代码,或者当目标窗口超出桌面边界时会失效。
因此,我创建了这个 C# 应用程序,它允许捕获指定的窗口并将其保存在支持的格式文件类型中。
背景
为了捕获窗口,您需要获取它的句柄,并使用本机 win32 API 调用到位图句柄,该句柄将被托管代码使用。 FCL 中没有太多帮助,所以我不得不导入大量本机调用。网站 pinvoke.net 对于这样的任务非常有用。
获取窗口句柄
如果您知道您正在寻找的窗口的标题和/或类名,那么使用 FindWindow
本机 win32 API 获取窗口句柄非常简单。 但是知道这些信息可能需要 Spy++,即使这样,您也可能有多个具有相同参数的 Windows。
FindWindow
按钮将始终根据您键入的参数获取句柄。 另一种更方便的方法是获取在本地计算机上运行的 UI 应用程序的主框架窗口句柄。 这在 90% 的情况下有效,但仍然有一些应用程序,例如 Toad for Oracle,它们的主框架窗口被隐藏,因此我们必须寻找要捕获的“真实”窗口。 为此,我们将检查进程中每个线程中具有最大矩形的可見窗口。
internal UIApp(System.Diagnostics.Process proc)
{
_proc = proc;
_RealHWnd = IntPtr.Zero;
_windowHandles = new List<IntPtr>();
GCHandle listHandle = default(GCHandle);
try
{
if (proc.MainWindowHandle == IntPtr.Zero)
throw new ApplicationException
("Can't add a process with no MainFrame");
RECT MaxRect = default(RECT);//init with 0
if (IsValidUIWnd(proc.MainWindowHandle))
{
_RealHWnd = proc.MainWindowHandle;
return;
}
// the mainFrame is size == 0, so we look for the 'real' window
listHandle = GCHandle.Alloc(_windowHandles);
foreach (ProcessThread pt in proc.Threads)
{
Win32API.EnumThreadWindows((uint)pt.Id,
new Win32API.EnumThreadDelegate(EnumThreadCallback),
GCHandle.ToIntPtr(listHandle));
}
//get the biggest visible window in the current proc
IntPtr MaxHWnd = IntPtr.Zero;
foreach (IntPtr hWnd in _windowHandles)
{
RECT CrtWndRect;
//do we have a valid rect for this window
if (Win32API.IsWindowVisible(hWnd) &&
Win32API.GetWindowRect(hWnd, out CrtWndRect) &&
CrtWndRect.Height > MaxRect.Height &&
CrtWndRect.Width > MaxRect.Width)
{ //if the rect is outside the desktop, it's a dummy window
RECT visibleRect;
if (Win32API.IntersectRect(out visibleRect, ref _DesktopRect,
ref CrtWndRect)
&& !Win32API.IsRectEmpty(ref visibleRect))
{
MaxHWnd = hWnd;
MaxRect = CrtWndRect;
}
}
}
if (MaxHWnd != IntPtr.Zero && MaxRect.Width > 0 && MaxRect.Height > 0)
{
_RealHWnd = MaxHWnd;
}
else
_RealHWnd = proc.MainWindowHandle;
//just add something even if it's a bad window
}//try ends
finally
{
if (listHandle != default(GCHandle) && listHandle.IsAllocated)
listHandle.Free();
}
}
此应用程序启动时会创建 UI 应用程序的列表。 此外,组合框中列出的应用程序必须在屏幕上可见才能被考虑在内,因为它们的大小为 0。辅助函数 IsValidUIWnd
和 EnumThreadCallback
如下所示
internal static bool IsValidUIWnd(IntPtr hWnd)
{
bool res =false;
if (hWnd == IntPtr.Zero || !Win32API.IsWindow(hWnd)
|| !Win32API.IsWindowVisible(hWnd))
return false;
RECT CrtWndRect;
if(!Win32API.GetWindowRect(hWnd, out CrtWndRect))
return false;
if (CrtWndRect.Height > 0 && CrtWndRect.Width > 0)
{// a valid rectangle means the right window is the mainframe
//and it intersects the desktop
RECT visibleRect;
//if the rectangle is outside the desktop, it's a dummy window
if (Win32API.IntersectRect(out visibleRect,
ref _DesktopRect, ref CrtWndRect)
&& !Win32API.IsRectEmpty(ref visibleRect))
res = true;
}
return res;
}
static bool EnumThreadCallback(IntPtr hWnd, IntPtr lParam)
{
GCHandle gch = GCHandle.FromIntPtr(lParam);
List<IntPtr> list = gch.Target as List<IntPtr>;
if (list == null)
{
throw new InvalidCastException
("GCHandle Target could not be cast as List<IntPtr>");
}
list.Add(hWnd);
return true;
}
捕获窗口内容
一旦我们有了“有效”的主框架句柄,我们就可以尝试大量使用 PInvoke 来捕获它。
在捕获之前,我们再次检查窗口的有效性,因为它可能在此期间已关闭。 IsClientWnd
提供了仅捕获客户端区域以节省一些空间的选项。 如果窗口被最小化,则使用 nCmdShow
,并告诉程序将其恢复到适当的大小。 此外,如果窗口的一部分超出桌面,我们必须调整矩形。
private static Bitmap MakeSnapshot(IntPtr AppWndHandle,
bool IsClientWnd, Win32API.WindowShowStyle nCmdShow)
{
if (AppWndHandle == IntPtr.Zero || !Win32API.IsWindow(AppWndHandle) ||
!Win32API.IsWindowVisible(AppWndHandle))
return null;
if(Win32API.IsIconic(AppWndHandle))
Win32API.ShowWindow(AppWndHandle,nCmdShow);//show it
if(!Win32API.SetForegroundWindow(AppWndHandle))
return null;//can't bring it to front
System.Threading.Thread.Sleep(1000);//give it some time to redraw
RECT appRect;
bool res = IsClientWnd ? Win32API.GetClientRect
(AppWndHandle, out appRect): Win32API.GetWindowRect
(AppWndHandle, out appRect);
if (!res || appRect.Height == 0 || appRect.Width == 0)
{
return null;//some hidden window
}
// calculate the app rectangle
if(IsClientWnd)
{
Point lt = new Point(appRect.Left, appRect.Top);
Point rb = new Point(appRect.Right, appRect.Bottom);
Win32API.ClientToScreen(AppWndHandle,ref lt);
Win32API.ClientToScreen(AppWndHandle,ref rb);
appRect.Left = lt.X;
appRect.Top = lt.Y;
appRect.Right = rb.X;
appRect.Bottom = rb.Y;
}
//Intersect with the Desktop rectangle and get what's visible
IntPtr DesktopHandle = Win32API.GetDesktopWindow();
RECT desktopRect;
Win32API.GetWindowRect(DesktopHandle, out desktopRect);
RECT visibleRect;
if (!Win32API.IntersectRect
(out visibleRect, ref desktopRect, ref appRect))
{
visibleRect = appRect;
}
if(Win32API.IsRectEmpty(ref visibleRect))
return null;
int Width = visibleRect.Width;
int Height = visibleRect.Height;
IntPtr hdcTo = IntPtr.Zero;
IntPtr hdcFrom = IntPtr.Zero;
IntPtr hBitmap = IntPtr.Zero;
try
{
Bitmap clsRet = null;
// get device context of the window...
hdcFrom = IsClientWnd ? Win32API.GetDC(AppWndHandle) :
Win32API.GetWindowDC(AppWndHandle);
// create dc that we can draw to...
hdcTo = Win32API.CreateCompatibleDC(hdcFrom);
hBitmap = Win32API.CreateCompatibleBitmap(hdcFrom, Width, Height);
// validate
if (hBitmap != IntPtr.Zero)
{
// adjust and copy
int x = appRect.Left < 0 ? -appRect.Left : 0;
int y = appRect.Top < 0 ? -appRect.Top : 0;
IntPtr hLocalBitmap = Win32API.SelectObject(hdcTo, hBitmap);
Win32API.BitBlt(hdcTo, 0, 0, Width, Height,
hdcFrom, x, y, Win32API.SRCCOPY);
Win32API.SelectObject(hdcTo, hLocalBitmap);
// create bitmap for window image...
clsRet = System.Drawing.Image.FromHbitmap(hBitmap);
}
return clsRet;
}
finally
{
// release the unmanaged resources
if (hdcFrom != IntPtr.Zero)
Win32API.ReleaseDC(AppWndHandle, hdcFrom);
if(hdcTo != IntPtr.Zero)
Win32API.DeleteDC(hdcTo);
if (hBitmap != IntPtr.Zero)
Win32API.DeleteObject(hBitmap);
}
}
如果成功,返回值是 FCL 中的一个托管 Image 对象。
以指定格式保存图像
由于 FCL,以 .NET framework 支持的任何格式保存图像都非常容易。
private void btnSaveImage_Click(object sender, EventArgs e)
{ if(saveFileDialog1.ShowDialog()!=DialogResult.Cancel)
{ try
{ string ext = System.IO.Path.GetExtension
(saveFileDialog1.FileName).Substring(1).ToLower();
switch (ext)
{ case "jpg":
case "jpeg":
_pictureBox.Image.Save
(saveFileDialog1.FileName, ImageFormat.Jpeg);
break;
// .........code omitted for brevity
default:MessageBox.Show(this,
"Unknown format.Select a known one.", "Conversion error!",
MessageBoxButtons.OK, MessageBoxIcon.Error);
break;
}
}
catch (Exception ex)
{ MessageBox.Show(this, ex.Message, "Image Conversion error!",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
}
历史
这是 1.0.0.0 版本,并且已在 Windows XP 和单显示器显卡上进行了测试。
您可能会在此应用程序中找到其他一些很酷的东西,例如系统菜单弹出窗口,但这超出了当前主题的范围。 尽情享受吧!