梁某人

发展才是硬道理

导航

向非COM应用注入对象模型(Add Object Models to Non-COM Apps)

In the grand tradition of "sin in haste, repent at leisure," thousands of Windows applications have been built that don't expose an object model. So you can't instantiate instances and control their behavior from within a script file or another COM client. In most cases, you need to simulate user behavior in code while opening, modifying, or printing a document.

The ability to automate the behavior of a Windows application helps in heterogeneous environments when you must combine the results of different apps and require certain operations to start automatically at certain times or be triggered by certain events. You can do all this easily with applications designed for programmability through a COM object model or a feature-rich command line. But the older or simpler the app, the less likely it is that such an app will give you an easy way to request services from it.

So can you add an object model to non-COM apps, or to COM apps that haven't been built with COM Automation in mind? You bet—and I've included the sample code to prove it.

However, you must live with a few general limitations. Although several approaches are valid in all cases, choosing the best solution and implementation strategy for your needs is rigorously application-specific. And you're not going to be able to build an object model that mirrors all the capabilities of the application. If the application hasn't been built from the ground up for automation, some of its functions might not be completely automatable.

 

Add Object Models to Non-COM Apps (Continued)

For example, almost all Windows apps let you save files through a common dialog box. From within the dialog box, you pick up a file to override or type in a new filename, then click OK. The actual writing goes on under the hood completely out of your control. The code that writes the content of the file to disk isn't visible from the application level and you can't call it from the outside environment. An automation-aware application would make this piece of code available to external callers.

Expose Functions Selectively
Basically, an application object model is a hierarchy of COM objects whose overall functionality resembles the whole set of functionality of the application. I deliberately used the verb "resemble" because no constraint forces you to expose (through automation) all the native, interactive functions of the program. You could also decide to expose extra functionality through automation or extend the programming interface with ad-hoc collections or properties and methods that make some valuable information more easily accessible.

An object model is a collection of COM objects that can be either part of the application itself or provided by an external library. You can choose any of these approaches if you're building a new application. You can only add an extra layer of code on top of existing applications if you only need to make automatable those old applications not thought for script or COM clients.

When writing such a code layer, you must first figure out a way for it to communicate with the parent application. This is a key point, because the COM layer is there only to act as a proxy for the underlying program. Any of the functionalities you can call, say, from a script, are actually provided by the program but invoked through the COM proxy. For this to happen—and this is the key mechanism for software automation—some sort of communication channel must exist between application and proxy.

In Windows, there are only two basic techniques you can use to control an application unaware of automation. One is exploiting the switches on the application's command line, and the other is based on Windows messages, hooking, and interprocess subclassing.

If you need to automate a non-COM automatable application, normally you don't have, or can't touch, the application's sources. So the command line's switches are a sort of static resource and hardly could be extended to meet your expectations and needs. You could think of writing a wrapper that spawns the original executable while exposing an extended set of commands. In this case, however, you must figure out a way for the wrapper to communicate with the core program. And using command line switches is not as powerful as using a COM object.

Give Notepad an Object Model
Any attempt to build an automation proxy is different from the next and strictly dependent on the features of the application to virtualize. As a practical demonstration, I'll show you how to build an object model for Notepad (see Figure 1). By the end of this article, you should be able to write code like this in order to open and print a text file:

Set npad = _
   CreateObject("NotepadOM.Application")
npad.Load "readme.txt"
npad.InvokeMenu NOTEPAD_FILE_PRINT

 
Figure 1. Wrap Your App to Make It Dance.

Notepad is an extremely simple application to model, but you must deal with all the major issues you have to anyway. These obstacles include figuring out a pattern for communicating with the application, designing a hierarchy of objects that is both easy to use and effective, and remaining aligned as much as possible with the overall application's programming interface.

Notepad is a typical Win32 application with a single document interface (SDI). You need to have an instance of the application running in the background and ask it, through special channels, to accomplish all the basic operations required to carry on one of the object model's methods or properties.

All GUI-based Win32 applications have a main window, a menu, and a client area. Normally, a client area is another window and, more often than not, a collection of child windows. In Win32, windows are system elements you can talk to through messages. Messages are ultimately numbers that correspond to application's functions. As such, you can exchange numbers irrespective of the different process boundaries where Notepad (or any other application you're automating) and the COM proxy are running.

The fact that the proxy and the core application run in different address spaces poses serious problems for pointers—you cannot use a native pointer of one process in the context of another process. So sending a string to another process window within a message is not allowed and, if tried, results in unpredictable effects. There are, though, some fortunate exceptions that sometimes help considerably.

When you use Notepad, you typically load a document, edit its content, then print or save the text. In doing so, you might want to use a certain font throughout the window and toggle the wrap mode on and off.

Other typical features of a text editor aren't available in Notepad—among these, send mail, word count, and even autocorrection. A COM proxy could add these features to Notepad.

Take a look at the programming interface you'll build in order to automate Notepad. The object model is realized through an ATL component whose progID is NotepadOM.Application (see Table 1).

Initialize the Object Model
Once you create a new instance of the component, call the Load method to obtain a couple results. First, connect to a running copy of Notepad. Second, open an existing document in the Notepad window or create a blank one.

To interact with Notepad, you need to grab the handle of the main window along with the handle of the edit control that covers the whole client area. You can use the C++ FindWindow API function to retrieve the first open window that matches Notepad's window class name: "notepad." (This background information has been provided by Spy++, a Visual Studio tool that lets you snoop around Windows secrets.) Use this C++ code:

STDMETHODIMP 
CNotepadApplication::Load(BSTR bstrFile)
{
   m_hwnd = FindWindow(_T("notepad"), NULL);
   if (!IsWindow(m_hwnd))
      _StartApp(OLE2T(bstrFile));

The Load method attempts to locate a running instance of Notepad. If successful, it ignores the input filename. Otherwise, it spawns notepad.exe, passing the bstrFile argument on the command line.

This is only one possible way of doing such things. You can change the behavior of the Load method to comply with several other policies. However, notice that in Notepad, the only way you can silently load a text file in the application's user interface is through the command line. Otherwise, you must resort to the Open command from the File menu, which is all but silent and automatic.

Once you find the Notepad main window handle, use it to retrieve the handle of the child edit control using this C++ code:

m_hwndEdit = FindWindowEx(
m_hwnd, NULL, _T("edit"), NULL);

The structure of Notepad provides for a window of class "notepad" whose client area is occupied by an edit control—a window of class "edit." The FindWindowEx API function retrieves the first window of class "edit," which is the child of m_hwnd.

Next, create a property on the COM object that represents the contents of the child edit control. I call this readwrite property Text. Assign a content to Text, which will be reflected immediately in the Notepad buffer:

Set npad = CreateObject("NotepadOM.Application")
npad.Load ""
npad.Text = "Sample text"

In the previous code, a new untitled text document is created and its content is set to a certain string. Of course, you can use Text to concatenate the text with other variables:

npad.Text = "Sample text"
npad.Text = npad.Text & vbCrLf & "for the article"

Even though Notepad is an SDI application, you might want to expose the text as a distinct object called, say, Document. This obeys to a cleaner and more elegant model design, but it also adds unnecessary complexity to the schema. Why create a new ATL object only to better group some text-related functionalities?

In the implementation of the Text property, I exploited a little-known feature of the Win32 edit controls. All Win32 controls cannot work across the process boundaries. For example, you cannot ask another application's rich edit box to return its content as a string. The problem with this is that any memory address makes sense only in the memory context of the process managing it. There are a few exceptions to this rule.

All the Windows standard controls (buttons, listboxes, and edit controls, among the others) don't obey this rule. Their content can be read and set across the process boundaries without limitation. This has been done since the times of Windows 95 to preserve the backward compatibility of existing Win3x applications using interprocess subclassing. This is still true in XP and Win2K.

As a result, you can use a few messages, such as WM_GETTEXT and WM_SETTEXT, to get and set the content of a textbox irrespective of the actual processes involved. Also, when you run VBS scripts, in fact, two different processes are involved: Notepad, and wscript.exe, which hosts the VBS script. Use this implementation in C++ of the Text property:

STDMETHODIMP 
CNotepadApplication::get_Text(BSTR *pVal)
{
   USES_CONVERSION;
   int nLen = 1 + SendMessage(m_hwndEdit, WM_GETTEXTLENGTH, 0, 0);
   LPTSTR pszBuf = new TCHAR[nLen];
   SendMessage(m_hwndEdit, WM_GETTEXT, nLen, (LPARAM) pszBuf);
   *pVal = SysAllocString(T2OLE(pszBuf));
   delete [] pszBuf;
   return S_OK;
}
STDMETHODIMP 
CNotepadApplication::put_Text(BSTR newVal)
{
   USES_CONVERSION;
   SendMessage(m_hwndEdit, WM_SETTEXT, 0, (LPARAM) OLE2T(newVal));
   return S_OK;
}
 

Add Editing Functions
Having access to the handle of the edit control straightens out the way for a bunch of functions that have to do with editing—in particular, those about the selection of the text. You could easily add a method to select all the text in the buffer or limit the selection between a certain range. SelectAll and SelectText in C++ do just this:

STDMETHODIMP 
CNotepadApplication::SelectText(
int nFrom, int nTo) {
   SendMessage(m_hwndEdit, EM_SETSEL, nFrom-1, nTo-1);
   return S_OK;
}

Implementing the text selection is as easy as using the EM_SETSEL message with the edit control. In Win32, the first selectable character is in position 0, but methods manage to make it start from 1. Specify a range of -1 to 0 to select all the text.

The font face name for the edit control is determined by the content of a certain Registry value (lfFaceName) located under this key:

HKEY_CURRENT_USER
   \Software
      \Microsoft 
         \Notepad

Set it to the name of the font you want to use. Notepad reads this setting before starting up. For it to be effective, remember to set it before you call Load:

set npad = CreateObject("NotepadOM.Application")
npad.Font = "Lucida Console"
npad.Load "readme.txt"
 

When an interactive user clicks on a menu item, say File | Open, the main window is sent a WM_COMMAND message, where the WPARAM argument is given by the concatenation of two words. The low word is the ID of the command. The high word contains the notification code of the message or a value indicating the trigger—keyboard accelerator or menu. To invoke a menu command programmatically, send a WM_COMMAND message in C++ to Notepad:

SendMessage(m_hwnd, WM_COMMAND, 
MAKELONG(nCommand,0), 0);

You must figure out the right value for the nCommand argument with special tools like Spy++. In this case, I used a slightly modified version of the DLL described in my article, "Hook, Line and Sinker" [Visual C++ Developers Journal February 2001]. That sample code spawns, hooks, then subclasses Notepad. It filters all the messages the window receives and pops up a dialog box with the command IDs whenever the command code is WM_COMMAND:

if (uiMsg == WM_COMMAND) {
   // Get the value of LOWORD(wParam) 
}

You need to add only the code necessary to store or display the command code. Check out the IDs for the main Notepad's menu commands (see Table 2). Given this, invoking a menu command is easy:

const NOTEPAD_FILE_OPEN = 10
Set npad = CreateObject("NotepadOM.Application")
npad.InvokeMenu NOTEPAD_FILE_OPEN
 
 

If you want to close the running instance of Notepad programmatically, you might think that calling DestroyWindow on the Notepad's window will pay the bill. Instead, DestroyWindow can only work on windows belonging to the same process that is calling it. To unload Notepad, simply send a WM_COMMAND message in C++ with this exit code:

SendMessage(m_hwnd, WM_COMMAND, 
MAKELONG(28,0), 0);

Some functionalities you just can't obtain from a nonautomation program. For example, opening a file and saving it with another name is impossible because the application doesn't expose this code through a message or an API. You need to write the code to save it. In Notepad, for example, the code to save runs in response to the invocation of Save or Save As, but those are interactive commands that require a user to be there to press OK or type in a new filename. This is an inherent limitation of the solution.

Recently, I faced a similar problem with one of my clients. I was asked to make a few legacy Windows applications (one was just Notepad) available for batch operations in a heterogeneous environment. Basically, I had a Win32 made-to-measure application getting TCP/IP channel instructions and translating them to calls to local Windows applications. Services were requested through Win32 messages in much the same way as I did here. Wrapping this communication pattern in a COM object model is just the next step.

About the Author
Dino Esposito is Wintellect's ADO.NET expert and a trainer and consultant based in Rome. Dino is the author of Building Web Solutions With ASP.NET and ADO.NET (Microsoft Press) and the cofounder of VB-2-The-Max (www.vb2themax.com). You can reach Dino at dinoe@wintellect.com.

 
 

posted on 2005-03-03 17:37  涛仔28  阅读(1238)  评论(0编辑  收藏  举报