Click here to Skip to main content
Click here to Skip to main content

WindowSnapshot - System Tray Utility To Surgically Capture Bitmaps of Windows/Controls On The Screen

, 13 Jan 2002
Rate this:
Please Sign up or sign in to vote.
A System Tray Utility to precisely capture screenshots of Windows and Controls anywhere on the screen.
<!-- Article image -->

<!-- Add the rest of your HTML here -->

Introduction

This article demonstrates yet another effective use of the WindowFinder utility that I presented in my CodeProject article early in January this year

It also incorporates the screen capture routine presented by Joseph M. Newcomer in his CodeProject article

The demo app is a Sys Tray utility that activates a context menu when doubled-clicked. Using the demo app, you are able to capture any window or control anywhere on the screen and save the screen-captured bitmap into the clipboard. After that, you'll be able to paste the bitmap from the clipboard onto any bitmap editing tool of your choice.

Although my main intention is to give a demonstration of one possible use of the Window Finder utility, such a screen capture utility is pretty useful all on its own. Although you can always use the "PrintScreen" button or the "Alt+PrintScreen" buttons, this utility allows you to precisely select windows and controls. Thus saving you the trouble of having to perform further bitmap area selection and cutting and pasting.

Usage

  • Start the demo app and the Window Snapshot icon will appear in the System Tray area :

  • Right-click on the icon and a context menu will appear. There will be 3 menu items :
    • Take A Snapshot - this option will invoke the "Search Window" dialog box which will allow you to select a window or control to take a screenshot of.
    • About - displays an about message box.
    • Shutdown - shuts down the entire Sys Tray app. [Note that if the Search Window Dialog Box is already running, right-mouse-clicking on the icon will not bring up the context menu.]
  • You can also double-click on the icon to directly invoke the Search Window Dialog box. If the dialog box is already running, it is brought to the foreground.
  • Once a window is selected, its information will be displayed in the Search Window Dialog Box and once you click on the "OK" button, a screenshot of the selected window or control will be taken and saved into the system clipboard.
  • Thereafter, use your favourite bitmap editing tool to further edit or save the bitmap.

Summary Of How It Works

Please read through my "MS Spy++ Window Finder" article to understand how the window finder part of this demo app works. I have made extensive re-use of the source codes of the WindowFinder app. The following is a summary of the enhancements and transformations that were made to the original WindowFinder source codes :
  • From Standard Window App to System Tray App. This requires :
    • Adding a Sys Tray icon.
    • Hiding the main window of the app.
    • Defining a custom Window message for processing Sys Tray activities.
    • Defining a context menu to be activated when the user right-clicks on the Sys Tray icon.
    • Defining handlers for the context menu items.
  • Screen Capturing the selected Window/Control.
    • After a window has been selected and the user selects the "OK" button of the Search Window Dialog Box, we proceed to take a snapshot of the window or control.
    • A slightly modified version of Joseph Newcomer's toClipboard() routine is used to perform the screen capture.
    • I made the modifications to Joseph's toClipboard() routine to handle some possible resource leakage detected by Numega's Bounds Checker while I was doing debugging.
  • I have informed Joseph of the possible leakage and Joseph has kindly informed me he will look into this.

Detailed Explanation Of How It Works

In order to make a System Tray app, I have removed the WS_VISIBLE style from the main window of the application and explicitly hidden it after creation :

  // Enhancement to WindowFinder - main window will not have 
  // WS_VISIBLE style.
  dwStyle = WS_OVERLAPPEDWINDOW;  

  // Create the main window.
  g_hwndMainWnd = CreateWindow
  (
    szMainWindowClassName,     // name of window class 
    WINDOW_SNAPSHOT_MAIN_WINDOW_TITLE,  // title 
    dwStyle, // window style - normal 
    CW_USEDEFAULT, // X coordinate - let Windows decide 
    CW_USEDEFAULT, // Y coordinate - let Windows decide 
    CW_USEDEFAULT, // width - let Windows decide 
    CW_USEDEFAULT, // height - let Windows decide
    NULL,          // no parent window 
    NULL,          // no override of class menu 
    hInstance,     // handle for this instance
    NULL           // no additional arguments 
  );

  ...
  ...
  ...

  // Display the window. 
  ShowWindow(g_hwndMainWnd, SW_HIDE);  
    // Enhancement to WindowFinder - hide the main window.

  UpdateWindow(g_hwndMainWnd);

The main window will still be required to process messages for the Sys Tray icon.

I have also defined a new function InitialiseShellModules() which is called within WinMain(). This function will perform initializations for the Sys Tray icon. This function is listed below :

BOOL InitialiseShellModules()
{
  NOTIFYICONDATA nid;
  BOOL bRetTemp = FALSE;
  BOOL bRet = TRUE;

  g_hIconSysTray = (HICON)LoadImage
  (
    (HINSTANCE)g_hInst, 
      // handle of the instance that contains the image 
    (LPCTSTR)MAKEINTRESOURCE(IDI_ICON_SYS_TRAY), 
      // name or identifier of image 
    (UINT)IMAGE_ICON, // type of image 
    (int)16, // desired width 
    (int)16, // desired height 
    (UINT)0 // load flags 
  ); 

  memset (&nid, 0, sizeof(NOTIFYICONDATA));
  nid.cbSize = sizeof(NOTIFYICONDATA);
  nid.hWnd = g_hwndMainWnd;
  nid.uID = ICON_ID;
  nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
  nid.hIcon = g_hIconSysTray;
  nid.uCallbackMessage = WM_SYS_TRAY_MESSAGE;
  strcpy (nid.szTip, WINDOW_SNAPSHOT_TOOL_TIP);
  
  bRetTemp = Shell_NotifyIcon
  (
    (DWORD)NIM_ADD, // message identifier 
    (PNOTIFYICONDATA)&nid // pointer to structure 
  ); 

  g_dwLastError = GetLastError ();

  return bRet;
}

The function contains basic sys tray initialization and startup code. The counterpart of InitialiseShellModules() is UninitialiseShellModules() which will basically unregister our icon from the System Tray.

As can be seen in InitialiseShellModules(), we have indicated that the main application window g_hwndMainWnd is to receive the WM_SYS_TRAY_MESSAGE message meant for our icon. Hence, we provide a WM_SYS_TRAY_MESSAGE message handler in MainWndProc() :

  case WM_SYS_TRAY_MESSAGE:
  {
    lRet = 0;
    bCallDefWndProc = TRUE;

    if (wParam == ICON_ID)
    {
      // Enhancement - Bio/Andrew Peace. Invoke context menu 
      // only when RIGHT-mouse click.
      if (lParam == WM_RBUTTONDOWN)
      {
        // Enhancement to Window Finder - display context menu only 
        // if we currently allow it. 
        if (g_bAllowContextMenu)
        {
          DisplayContextMenu (hwnd);
        }
      }

      // Enhancement - Bio/Andrew Peace. Invoke Search Window Dialog 
      // directly when icon is double-clicked.
      if (lParam == WM_LBUTTONDBLCLK)
      {
        // Enhancement to Window Finder - display the Search Window 
        // Dialog box immediately. If the Search Window Dialog is 
        // already alive, put it in the foreground. 
        if (g_hwndSearchDialog)
        {
          SetForegroundWindow (g_hwndSearchDialog);
        }
        else
        {
          PostMessage (hwnd, WM_START_SEARCH_WINDOW, 0, 0);
        }
      }
    }
    break;
  }

When the user right-clicks on the icon, we first check the BOOL flag g_bAllowContextMenu to see if we allow the display of the context menu. This is important because we do not want to allow the user the possibility of having two Search Window Dialogs running at the same time. The g_bAllowContextMenu flag is set to TRUE when the Search Window Dialog Box is displayed and will be set to FALSE when it is closed (see the WM_INITDIALOG and the WM_COMMAND (with wID == IDOK) message handlers in SearchWindowDialogProc() for more details).

We may, of course, opt to disable the "Take A Snapshot" menu item. This is a good idea, especially if there are any other options in the context menu not related directly to window selection. This will certainly complicate the code further but it can be done. When the user double-clicks on the sys tray icon, to directly start up the Search Window Dialog Box, we first check the global variable g_hwndSearchDialog to see if it contains any value. This g_hwndSearchDialog global variable is set to the HWND of the Search Window Dialog box when the dialog box is started up (see WM_INITDIALOG handler in SearchWindowDialogProc()) and is set to NULL when the dialog is closed (see WM_COMMAND, wID == IDOK, in SearchWindowDialogProc()).

If the Search Window Dialog Box is not running at the moment (null value in g_hwndSearchDialog), we start up the dialog by posting the WM_START_SEARCH_WINDOW message to the Main Window. If the Search Window dialog box is already running at the moment (non-null value in g_hwndSearchDialog), we bring the Search Window to the foreground immediately. This provides greater convenience for the user especially when the Search Window is hidden by another window. By bringing the Search Window dialog to the foreground when the user double-clicks the Sys Tray icon, the user can avoid minimizing windows in order to get to it.

The various handlers for the context menu items are listed below. They are part of the WM_COMMAND handler of the MainWndProc() :

  case WM_COMMAND:
  {
    WORD wNotifyCode = HIWORD(wParam); // notification code 
    WORD wID = LOWORD(wParam);         
      // item, control, or accelerator identifier 
    HWND hwndCtl = (HWND)lParam;      // handle of control 

    if (wNotifyCode == 0)
    {
      // Message is from a menu.
      // Enhancement to Window Finder - perform 
      // a window snapshot operation.
      if (wID == IDM_CONTEXT_TAKE_SNAPSHOT)
      {
        PostMessage (hwnd, WM_START_SEARCH_WINDOW, 0, 0);
      }

      if (wID == IDM_CONTEXT_ABOUT)
      {
        MessageBox (NULL, ABOUT_WINDOW_FLOATER, 
             WINDOW_SNAPSHOT_MAIN_WINDOW_TITLE, MB_OK);
      }

      if (wID == IDM_CONTEXT_SHUTDOWN)
      {
        PostMessage (hwnd, WM_CLOSE, 0, 0);
      }

      lRet = 0;
      bCallDefWndProc = FALSE;
    }
    else
    {
      bCallDefWndProc = TRUE;
    }
 
    break;
  }

Let us examine the handler for IDM_CONTEXT_TAKE_SNAPSHOT. It will post the WM_START_SEARCH_WINDOW message back to the main window which will eventually call on StartSearchWindowDialog() to start the Search Window dialog box :

  case WM_START_SEARCH_WINDOW :
  {
    lRet = 0;
    bCallDefWndProc = FALSE;

    StartSearchWindowDialog (hwnd);

    break;
  }

The Window Search Dialog Box is managed by the SearchWindowDialogProc() dialog box procedure. This has been covered in my "MS Spy++ Style Window Finder" article. Please refer to the article for more details.

The important modification to the original SearchWindowDialogProc() is the handler for the IDOK button :

  if ((wID == IDOK) && (g_hwndFoundWindow))
  {
    // Action.  
    // Important to hide the dialog and make sure it really disappears 
    // before we take the snapshot.
    ShowWindow (hwndDlg, SW_HIDE);
    Sleep(500);  // That's why we must wait for a while.
    CaptureWindowToClipboard (g_hwndFoundWindow);
  }

Here, when the user clicks on the "OK" button, and a window or control has been selected, we perform 3 important steps to capture the screenshot of the selected window/control :

  • We hide the Search Window dialog box.
  • We halt our application for about half a second.
  • We proceed to perform the screen-capture which is encapsulated by the CaptureWindowToClipboard() function.

The reasons for taking the 3 steps are explained next. In general, before one captures the screen shot of a window or control, that window or control must be entirely visible. It must not be obscured partially or entirely by another window. Please refer to the discussion thread in Joseph Newcomer's "Screen Capture to the Clipboard" article in which this is discussed.

As such, please ensure that the selected window/control is totally visible when you click on the "OK" button of the Search Window dialog box. Note that this is most evident when you are performing debugging because you will be brought to the Visual Studio IDE and the selected window may be obscured. However, anticipating that the Search Window Dialog Box itself could be obscuring the selected window/control, I have made sure that I first hide the dialog box before performing the screen capture.

Furthermore, while I was doing testing, I noticed that sometimes, even after I have commanded the dialog to hide itself, I still get an image of the dialog box on top of the selected window in the screenshot. The speed of the hiding of any window really depends on the OS itself. To get round this problem, I have added in the Sleep() API to make our application halt for just half-a-second (enough time to make sure the dialog box is really hidden) before taking the screenshot. If you find that you need more time for this, simply increase the parameter for Sleep() to a greater value.

I would also like to touch a little on the CaptureWindowToClipboard() function and the modifications that I made to Joseph's toClipboard() function.

BOOL CaptureWindowToClipboard (HWND hwndToCapture)
{
  BOOL bRet = FALSE;

  // Enhancement - Bio/Ahmed. Check first that "hwndToCapture" 
  // is a valid window.
  if((hwndToCapture) && (::IsWindow (hwndToCapture)))
  {
    bRet = TRUE;
    toClipboard_Bio((CWnd *)CWnd::FromHandle (hwndToCapture), TRUE);
  }

  return bRet;
}

I basically used the CWnd::FromHandle() method to dynamically convert the selected window to a CWnd pointer.

void toClipboard_Bio(CWnd * wnd, BOOL FullWnd)
{
  CDC *dc;
  if(FullWnd)
  { /* full window */
    dc = new CWindowDC(wnd);
    //HDC hdc = ::GetWindowDC(wnd->m_hWnd);
    //dc -> Attach(hdc);
  } /* full window */
  else
  { /* client area only */
    dc = new CClientDC(wnd);
    //HDC hdc = ::GetDC(wnd->m_hWnd);
    //dc -> Attach(hdc);
  } /* client area only */

I have created a new function toClipboard_Bio() which is essentially a copy of Joseph's toClipboard() routine albeit I changed the "dc" variable from a CDC object to a pointer to a CDC. Via polymorphism, I have later created a pointer to a CWindowDC or a CClientDC based on the FullWnd BOOL parameter. All references to "dc" have been modified in the light of the fact that "dc" is now a pointer. I have also made sure that "dc" is deleted at the end of the routine. The problem discovered with Joseph's code lies in the way that CDC objects are destroyed. Please refer to my discussion thread in Joseph's article for more details.

In Conclusion

  • I would like to thank Joseph Newcomer for his Screen Capture code. I hope readers of Joseph's article will find this utility useful as well.
  • I would also like to thank the many readers who have read this article and have given me very supportive comments and suggestions. Through their suggestions and bug reports, WindowSnapshot has been upgraded and made more robust. This spirit is truly what makes CodeProject a great web site for developers.
  • Note that I have also submitted another article last week that demonstrates another effective use of the WindowFinder :
  • My best wishes to all.

Updates, Enhancements And Bug Fixes

  • Monday January 14th 2002. Usability Enhancement.
    • An enhancement has been added to WindowSnapshot in response to Andrew Peace's suggestion (please refer to the discussion thread below) to directly invoke the Search Window Dialog box when the user double-clicks on the Sys Tray icon.
  • I also noticed that while trying to capture a few screens, the context menu generally got in the way of fast deployment.
  • The code changes are in main.cpp and WindowFinder.cpp. These are explained below.
    • Changes in main.cpp :
        case WM_SYS_TRAY_MESSAGE:
        {
          lRet = 0;
          bCallDefWndProc = TRUE;
      
          if (wParam == ICON_ID)
          {
            // Enhancement - Bio/Andrew Peace. Invoke context 
            // menu only when RIGHT-mouse click.
            if (lParam == WM_RBUTTONDOWN)
            {
              // Enhancement to Window Finder - 
              // display context menu only if we 
              // currently allow it. 
              if (g_bAllowContextMenu)
              {
                DisplayContextMenu (hwnd);
              }
            }
      
            // Enhancement - Bio/Andrew Peace. Invoke 
            // Search Window Dialog directly when icon 
            // is double-clicked.
            if (lParam == WM_LBUTTONDBLCLK)
            {
              // Enhancement to Window Finder - 
              // display the Search Window 
              // Dialog box immediately. If the Search Window 
              // Dialog is 
              // already alive, put it in the foreground. 
              if (g_hwndSearchDialog)
              {
                SetForegroundWindow (g_hwndSearchDialog);
              }
              else
              {
                PostMessage (hwnd, WM_START_SEARCH_WINDOW, 0, 0);
              }
            }
          }
          break;
        }
      

      A handler for WM_RBUTTONDOWN is added to process right-mouse-clicks and the context menu is displayed this way. Note that usual check is made to see if a context menu is allowed to be displayed in the first place. This depends solely on whether the Search Window Dialog is currently already running. If so, the context menu is not allowed to be invoked. In the WM_LBUTTONDBLCLK message handler, no context menu is displayed. Instead, the Search Window is either directly started up, or, if it is already running (most likely obscured behind other windows), it is set to be the foreground window.

    • Changes to WindowFinder.cpp :
        case WM_INITDIALOG :
        {
          g_hwndSearchDialog = hwndDlg; 
              // Enhancement to Window Finder - keep 
              // HWND of dialog in the global var 
              // "g_hwndSearchDialog".
          g_hwndFoundWindow = NULL;  
             // Enhancement to Window Finder - initialise 
             // g_hwndFoundWindow to NULL.
          g_bAllowContextMenu = FALSE;  
            // Enhancement to Window Finder - now disallow 
            // activation of context menu.
          SetForegroundWindow (hwndDlg); 
            // Enhancement - Bio/Andrew Peace. 
            // Because Search Window 
            //can now be invoked directly, we must set this dialog 
            //to foreground when it is first displayed.
          bRet = TRUE;
          break;
        }
      

      Because Search Window can now be invoked directly, we must set this dialog to be the foreground whenever it is first displayed.

  • Thanks, Andrew, for your suggestion Smile | :)
  • Monday January 14th 2002. Bug Fix.
  • A bug has been spotted by Ahmed Ismaiel Zakaria (please refer to discussion thread below) in the toClipboard_Bio() function which causes a crash when a user selects a top-level (main frame) window or any window which does not have any parent window (e.g. a parent-less dialog box).
  • This is a good catch by Ahmed. Ahmed also gave some suggestions for resolving the bug and I've modified them a little. These are explained next.
    • Changes to function toClipboard_Bio() :
        // Enhancement - Bio/Ahmed. Call OpenClipboard() 
        // directly instead of getting parent to do it.
        // wnd->GetParent()->OpenClipboard();
        wnd->OpenClipboard();
      

      Instead of calling GetParent() from "wnd" and then using the parent window's CWnd pointer to call OpenClipboard() (which is the cause of the crash if "wnd" does not have any parent), I've decided to call OpenClipboard() from "wnd" directly.

    • Changes to function CaptureWindowToClipboard() :
      BOOL CaptureWindowToClipboard (HWND hwndToCapture)
      {
        BOOL bRet = FALSE;
      
        // Enhancement - Bio/Ahmed. Check first that 
        // "hwndToCapture" is a valid window.
        if((hwndToCapture) && (::IsWindow (hwndToCapture)))
        {
          bRet = TRUE;
          toClipboard_Bio((CWnd *)CWnd::FromHandle (hwndToCapture), 
            TRUE);
        }
      
        return bRet;
      }
      

      I've made more stringent checks on the parameter hwndToCapture so that only valid hwnds are passed to the toClipboard_Bio() function.

  • Thanks, Ahmed, for spotting this for us Smile | :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Lim Bio Liong
Web Developer
Singapore Singapore
Lim Bio Liong is a Specialist at a leading Software House in Singapore.
 
Bio has been in software development for over 10 years. He specialises in C/C++ programming and Windows software development.
 
Bio has also done device-driver development and enjoys low-level programming. Bio has recently picked up C# programming and has been researching in this area.

Comments and Discussions

 
QuestionHow to capture CMediaplayer image? PinmemberJesper S10-Apr-02 9:15 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140827.1 | Last Updated 14 Jan 2002
Article Copyright 2002 by Lim Bio Liong
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid