Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

How to capture a Window as an Image and save it

Rate me:
Please Sign up or sign in to vote.
4.43/5 (12 votes)
15 Jun 2007CPOL3 min read 142.3K   6.4K   91   24
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.

C#
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:

C#
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.

C#
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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Decebal Mihailescu is a software engineer with interest in .Net, C# and C++.

Comments and Discussions

 
QuestionGetClientRect Pin
Member 1026859917-Oct-13 22:54
Member 1026859917-Oct-13 22:54 
Questionsamething in WPF Pin
Member 102685999-Oct-13 2:14
Member 102685999-Oct-13 2:14 
AnswerRe: samething in WPF Pin
dmihailescu10-Oct-13 16:32
dmihailescu10-Oct-13 16:32 
QuestionSupport for multiple monitor Pin
andre123454-Oct-13 3:30
andre123454-Oct-13 3:30 
AnswerRe: Support for multiple monitor Pin
dmihailescu10-Oct-13 16:33
dmihailescu10-Oct-13 16:33 
GeneralGraphics.CopyFromScreen Method Pin
sankranty23-Oct-10 2:31
sankranty23-Oct-10 2:31 
GeneralRe: Graphics.CopyFromScreen Method Pin
dmihailescu23-Oct-10 3:03
dmihailescu23-Oct-10 3:03 
GeneralIt's leaking Pin
assthestenisthemlmististic8-Apr-10 10:44
assthestenisthemlmististic8-Apr-10 10:44 
GeneralRe: It's leaking Pin
dmihailescu8-Apr-10 12:56
dmihailescu8-Apr-10 12:56 
GeneralCool, but slow Pin
aldo hexosa31-Mar-09 21:00
professionalaldo hexosa31-Mar-09 21:00 
GeneralRe: Cool, but slow Pin
dmihailescu8-Apr-10 12:58
dmihailescu8-Apr-10 12:58 
QuestionHow to capture backgournd windows? Pin
codeporukki24-Jul-07 23:44
codeporukki24-Jul-07 23:44 
AnswerRe: How to capture backgournd windows? Pin
dmihailescu26-Jul-07 17:47
dmihailescu26-Jul-07 17:47 
GeneralRe: How to capture backgournd windows? Pin
codeporukki30-Jul-07 4:53
codeporukki30-Jul-07 4:53 
Generalan update Pin
dmihailescu28-Sep-07 7:06
dmihailescu28-Sep-07 7:06 
AnswerA sample code in C# [modified] Pin
dmihailescu4-Oct-07 8:01
dmihailescu4-Oct-07 8:01 
GeneralCool Pin
HankiDesign6-Jul-07 8:23
HankiDesign6-Jul-07 8:23 
Generaloverlapping window... Pin
mosquets6-Jul-07 2:25
mosquets6-Jul-07 2:25 
GeneralRe: overlapping window... Pin
dmihailescu6-Jul-07 13:05
dmihailescu6-Jul-07 13:05 
GeneralRe: overlapping window... Pin
_Olivier_9-Jul-07 5:53
_Olivier_9-Jul-07 5:53 
GeneralRe: overlapping window... [modified] Pin
dmihailescu9-Jul-07 9:18
dmihailescu9-Jul-07 9:18 
Actually, as somebody else mentioned, I found out that you can get the picture outside the Desktop boundary, but it is quite a difficult hack[^].


-- modified at 17:55 Friday 13th July, 2007
GeneralRe: overlapping window... Pin
dmihailescu28-Sep-07 7:10
dmihailescu28-Sep-07 7:10 
GeneralNice article but... Pin
Jim Crafton15-Jun-07 5:19
Jim Crafton15-Jun-07 5:19 
GeneralRe: Nice article but... Pin
dmihailescu15-Jun-07 6:05
dmihailescu15-Jun-07 6:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.