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); if (IsValidUIWnd(proc.MainWindowHandle))
{
_RealHWnd = proc.MainWindowHandle;
return;
}
listHandle = GCHandle.Alloc(_windowHandles);
foreach (ProcessThread pt in proc.Threads)
{
Win32API.EnumThreadWindows((uint)pt.Id,
new Win32API.EnumThreadDelegate(EnumThreadCallback),
GCHandle.ToIntPtr(listHandle));
}
IntPtr MaxHWnd = IntPtr.Zero;
foreach (IntPtr hWnd in _windowHandles)
{
RECT CrtWndRect;
if (Win32API.IsWindowVisible(hWnd) &&
Win32API.GetWindowRect(hWnd, out CrtWndRect) &&
CrtWndRect.Height > MaxRect.Height &&
CrtWndRect.Width > MaxRect.Width)
{ 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;
} 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)
{ RECT visibleRect;
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); if(!Win32API.SetForegroundWindow(AppWndHandle))
return null; System.Threading.Thread.Sleep(1000); 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; }
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;
}
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;
hdcFrom = IsClientWnd ? Win32API.GetDC(AppWndHandle) :
Win32API.GetWindowDC(AppWndHandle);
hdcTo = Win32API.CreateCompatibleDC(hdcFrom);
hBitmap = Win32API.CreateCompatibleBitmap(hdcFrom, Width, Height);
if (hBitmap != IntPtr.Zero)
{
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);
clsRet = System.Drawing.Image.FromHbitmap(hBitmap);
}
return clsRet;
}
finally
{
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;
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!