MFC: Controlling Notepad From C++ Applications

Introduction

This article presents how to start then control a third party GUI application from our own C++ applications, particularly the Notepad text editor which is shipped with Windows operating system.
Also it shows two C++ classes designed for this purpose:

  • CApplication – a class for launching a GUI application then control it by sending standard commands;
  • CNotepad – extends CApplication by adding Notepad-specific tasks, like writing text into the editor.

If using Microsoft Visual Studio 2005 or newer, you can include CApplication and CNotepad classes in every MFC, ATL, Win32 or Console Application project.

Starting the Application

To start a third-party application then get a handle to its main window, programmers may be tempted to use an easy way as follows:

  1. call ShellExecute to start application’s process;
  2. call Sleep to wait until initialization is complete;
  3. call FindWindow or GetForegroundWindow to get a handle to the application’s main window.

   if(::ShellExecute(NULL, _T("open"), _T("notepad.exe"),
                     NULL, NULL, SW_SHOW) > (HINSTANCE)32)
   {
      ::Sleep(666); // some "magic" time to wait
      HWND hWndMain = ::FindWindow(_T("Notepad"), NULL);
      // HWND hWndMain = ::GetForegroundWindow();
      // ...
   }

That may be handy and may work many times but has several leaks:

  1. ShellExecute gives us nothing in order to further control the process;
  2. we cannot know how long time takes the initialization, so using Sleep is not a good way;
  3. neither FindWindow nor GetForegroundWindow guarantee that we’ll always get the started application’s main window.

One better approach is to do the following:

  1. call CreateProcess to start application’s process; unlike ShellExecute it gives the process and main thread handles and identifiers in a PROCESS_INFORMATION structure;
  2. call WaitForInputIdle to wait until the application’s process has finished its initialization; it takes as parameter the process handle returned by CreateProcess;
  3. use data from PROCESS_INFORMATION structure to find the application’s main window; this may be done in several ways, one is by enumerating top level windows and call GetWindowThreadProcessId to compare the process identifier and/or thread identifier with the process identifier and/or thread identifier got by CreateProcess.

Note: because an application may have more than one top-level window, comparing the window class names may be still necessary.

HWND StartApplication(LPTSTR pszExeName, LPCTSTR pszWndClass)
{
   HWND hWndMain = NULL;
   STARTUPINFO startupInfo = {0};
   PROCESS_INFORMATION processInfo = {0};

   if(::CreateProcess(NULL, pszExeName,
                      NULL, NULL, FALSE, 0, NULL, NULL,
                      &startupInfo, &processInfo))
   {
      // wait for process initialization
      ::WaitForInputIdle(processInfo.hProcess, 10000);

      // find the main window
      DWORD dwProcessId = 0;
      HWND hWnd = ::GetWindow(::GetDesktopWindow(), GW_CHILD);
      while(NULL != hWnd)
      {
         DWORD dwThreadId =
               ::GetWindowThreadProcessId(hWnd, &dwProcessId);
         if((dwThreadId == processInfo.dwThreadId) &&
            (dwProcessId == processInfo.dwProcessId))
         {
            const int nMaxCount = 256;
            TCHAR pszClassName[nMaxCount];
            ::GetClassName(hWnd, pszClassName, nMaxCount);
            if(!_tcsicmp(pszClassName, pszWndClass))
            {
              hWndMain = hWnd;
               break;
            }
         }
         hWnd = ::GetWindow(hWnd, GW_HWNDNEXT);
      }
   }
   return hWndMain;
}

Sending Commands

Once we know the main window handle we can easily send commands to the application as follows:

#define ID_FILE_SAVE 777 // hard-coded command ID
   // ...
   ::SendMessage(::hWndMain, WM_COMMAND, (WPARAM)ID_FILE_SAVE, NULL);

Let’s say, we can get the “magic” number 777 by taking a look in the application menu resource.
However, this is not very good because the command IDs may vary from one application to another,
from one version to another.
Another and better approach is to dynamically get the command IDs from accelerators.
Many Windows applications use “standard” accelerator keystrokes like Ctrl+S (Save), Ctrl+C (Copy), Ctrl+V (Paste) and so on,
then will be no more troubles because of different IDs in different applications or versions.

All we have to do is to find the matching ACCELTABLEENTRY in the application’s accelerator resources.

UINT FindAcceleratorCommandId(LPCTSTR pszExeName, char Key,
                              BOOL ctrlKey, BOOL shiftKey,
                              BOOL altKey)
{
   UINT nCommandID = 0;

   HMODULE hModule = ::LoadLibrary(pszExeName);
   if(NULL != hModule)
   {
      HRSRC hRsrc =
         FindResource(hModule, _T("MAINACC"), RT_ACCELERATOR);
      if(NULL != hRsrc)
      {
         DWORD dwSize = ::SizeofResource(hModule, hRsrc) / 8;
         HGLOBAL hGlobal = ::LoadResource(hModule, hRsrc);
         if(NULL != hGlobal)
         {
            ACCELTABLEENTRY* table =
               (ACCELTABLEENTRY*)::LockResource(hGlobal);
            for(DWORD dwIndex = 0; dwIndex < dwSize; dwIndex++)
            {
               ACCELTABLEENTRY& entry = table[dwIndex];

               WORD wLo = FVIRTKEY;
               if(ctrlKey) wLo |= FCONTROL;
               if(shiftKey) wLo |= FSHIFT;
               if(altKey) wLo |= FALT;
               DWORD dwToFind = MAKELONG(wLo, (WORD)Key);

               wLo = entry.fFlags;
               wLo &= ~FNOINVERT;
               wLo &= ~0x0080;
               DWORD dwFound = MAKELONG(wLo, entry.wAnsi);
               if(dwToFind == dwFound)
               {
                  // accelerator has been found
                  nCommandID = entry.wId;
                  break;
               }
            }
            ::FreeResource(hGlobal);
         }
      }
      ::FreeLibrary(hModule);
   }
   return nCommandID;
}

Now we can send, let's say the "Save" command (Ctrl+S):

   UINT nID = FindAcceleratorCommandId(_T("notepad.exe"),
                                       'S', TRUE, FALSE, FALSE);
   if(nID > 0)
   {
      WPARAM wParam = MAKEWPARAM(nID, 1);
      ::SendMessage(hWndMain, WM_COMMAND, wParam, NULL);
   }

Note: ACCELTABLEENTRY structure is not present in any SDK header file, so we have to declare it in our own sources.

struct ACCELTABLEENTRY
{
  WORD fFlags;
  WORD wAnsi;
  WORD wId;
  WORD padding;
};

Notepad-Specific Issues

As well known, Notepad application is a simple but often used text viewer/editor. Beside launching and sending standard commands would be useful to write text in it.
The text is written in an edit Windows common control. So first, we have to find the child of main window which has class name "Edit".

HWND FindEditControl(HWND hWndMain)
{
   HWND hWndEdit = NULL;
   HWND hWnd = ::GetWindow(hWndMain, GW_CHILD);
   while(NULL != hWnd)
   {
      const int nMaxCount = 256;
      TCHAR pszClassName[nMaxCount];
      ::GetClassName(hWnd, pszClassName, nMaxCount);
      if(!_tcsicmp(pszClassName, _T("Edit")))
      {
         hWndEdit = hWnd;
         break;
      }
      hWnd = ::GetWindow(hWnd, GW_HWNDNEXT);
   }
   return hWndEdit;
}

Once having the handle, we can do anything can be done with any other edit control: replace text, append text and so on.

In the next page, you can find a brief description of CApplication and CNotepad classes and a presentation of the demo application.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read