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.

MFC: Controlling Notepad From C++ Applications

CApplication Class

CApplication is the base class for other application-specific classes like CNotepad. It exposes the following public methods:

BOOL CApplication::Open(LPCTSTR pszExeName, LPCTSTR pszCmdLine, LPCTSTR pszClassName);

Starts a given application, keeps in mind process information then waits until the application's process has finished its initialization.

Parameters
  • pszExeName - name of executable application file (or full path and file name), for example: "c:\\windows\\notepad.exe";
  • pszCmdLine - command line arguments, e.g. "myfile.txt";
  • pszClassName - application's main window class name, e.g. "Notepad".

Return value
Non-zero in case of success, zero (FALSE) otherwise.

BOOL CApplication::IsOpen();

Checks if application is open; actually it tries to find the main application' window.

Return value
Non-zero if the application is open, zero (FALSE) otherwise.

void CApplication::SendCommand(E_CMD cmd);

Sends a command to the application.

Parameters

  • cmd - the index of the command; can have one of the CApplication::E_CMD enum values.

CNotepad Class

CNotepad extends CApplication by adding text handling methods.

BOOL CNotepad::Open(LPCTSTR pszTxtFile = NULL);

Opens the Notepad application.

Parameters

  • pszTxtFile - (optional) the text file to be open in Notepad.

void CNotepad::SetText(LPCTSTR pszText);

Replaces the entire text in Notepad's edit window.

Parameters

  • pszText - the new text.

void CNotepad::AppendText(LPCTSTR pszText);

Adds text to the end of existing one.

Parameters

  • pszText - text to be added.

Note: the implementation details can be found in the attached source files (CN_Source_Files.zip).

Demo Application

The demo application (see attached CN_Demo_Application.zip archive), demonstrates how to use CApplication and CNotepad in MFC applications.
First, push <Open Notepad> button. Further you can:

  • set the entire text;
  • append text;
  • send a command to Notepad application.

[01_Demo_Application.jpg]

Final Notes

  • As already said, if using Microsoft Visual Studio 2005 or newer, you can include CApplication and CNotepad classes in every MFC, ATL, Win32 or Console Application project. For Win32 and console applications you have to include AtlStr.h header file.
  • CApplication and CNotepad classes may be improved in a future update by:
    • adding new methods to CNotepad; practically, all edit control functions can be done, like setting read-only and so on;
    • using safer functions EnumWindows and EnumChildWindows instead of GetWindow;
    • using a Windows hook to catch when the application's main window is closed; that way can avoid searching it each time a command is sent;
    • others; any idea is welcome.

Resources



About the Author

Ovidiu Cucu

Graduated at "Gh. Asachi" Technical University - Iasi, Romania. Programming in C++ using Microsoft technologies since 1995. Microsoft MVP awardee since 2006. Moderator and article reviewer at Codeguru.com, the number one developer site. Co-founder of Codexpert.ro, a website dedicated to Romanian C++ developers.

Related Articles

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: November 6, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds