65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (11投票s)

2007 年 6 月 15 日

CPOL

3分钟阅读

viewsIcon

147955

downloadIcon

6464

拍摄任何 UI 应用程序主窗口的快照

Screenshot - snapshot.png

引言

有时您可能需要抓取一些 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。辅助函数 IsValidUIWndEnumThreadCallback 如下所示

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 和单显示器显卡上进行了测试。

您可能会在此应用程序中找到其他一些很酷的东西,例如系统菜单弹出窗口,但这超出了当前主题的范围。 尽情享受吧!

© . All rights reserved.