Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to capture a Window as an Image and save it

0.00/5 (No votes)
15 Jun 2007 1  
Take a snapshot of the main Window of any UI application

Screenshot - snapshot.png

Introduction

Sometimes you might need to take snapshots of some Windows for a presentation or for a monitoring task. There are some articles about how to do it like Lim Bio Liong's article, but it uses old unmanaged C++ code, or it comes short when the target window is falling outside the desktop boundary.

Hence I've created this C# application that allows capturing the specified Window and persisting it in a supported format file type.

Background

In order to capture a window you would need to get its handle and make use of the native win32 API calls to the bitmap handle that would be used by the managed code. There isn't much help in the FCL, so I had to import a lot of native calls. The site pinvoke.net is extremely helpful for such a task.

Getting the window handle(s)

If you knew the caption and/or the class name of the window you are looking for, then getting the window handle is trivial using the FindWindow native win32 API. But knowing this information might require Spy++, and even then you can have multiple Windows with the same parameters.

The FindWindow button would always get the handle based on the parameters you type. Another more convenient way would be to get the mainframe Window handles of the UI applications running on the local machine. That would work 90% of the time, but still there are applications like Toad for Oracle that have the mainframe window hidden, so we have to look for the 'real' window to capture. To do that, we would check for a visible window that has the largest rectangle in each thread within a process.

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();
    }
}

The list of the UI applications is created when this application starts. Also the applications listed in the combo box would have to be visible on the screen to be accounted for since they have size 0. The helper functions IsValidUIWnd and EnumThreadCallback are listed below:

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;
}

 

Capturing the window content

Once we have the 'valid' mainframe handles, we can try to capture it using PInvoke heavily.
Before we capture we check again for the validity of the Window because it might have been closed in the mean time. The IsClientWnd gives the option to capture only the client area saving some space. nCmdShow is used if the Window is minimized and tells the program to bring it back to the proper size. Also we have to adjust the rectangle if part of the Window falls outside the desktop.

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);
    }
}

In case of success the return value is a managed Image object from the FCL.

Saving the image in the specified format

Saving the image in any format supported by the .NET framework is very easy thanks to the FCL.

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);
        }
    }
}

 

History

This is version 1.0.0.0 and it has been tested on Windows XP, on a single monitor graphic card.

You might find some other cool things in this application like the system menu pop up, but that's outside the scope of the current topic. Enjoy!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here