C++Builder 5 Developers Guide
Right now our wizard is pretty limited. In fact, you might be thinking, " It's really not a wizard. It's just an application that gets launched off the help menu." Well, from a user 's perspective, you're right. We need to add some more capability to really integrate it with the applications that get built and launched from the C++Builder IDE. Specifically, we want our Wizard to be able to measure the memory degradation of an application from launch to exit. More precisely, we need the Wizard to be able to build and execute the application associated to the active project. Before it executes a project, we need to have it trigger the first memory check. Finally, the wizard needs to be informed when the execution is complete to perform the final memory check. This will result in the display of the memory usage.
This gets us to the heart of what the Tools API can do through the services it offers. The service interfaces offered by the Tools API enables our wizard to access, obtain, and even manipulate elements of the IDE at various modes of operation such as edit mode and debug mode.
Selecting a Service Interface
We need to determine the type of service interface for the enhanced version of our MemStat Wizard. The ToolsAPI.hpp file reveals thirteen basic types of service interfaces, as listed in Table 23.4.
Table 23.4. Tools API Service Interfaces
Service Interface | Description |
---|---|
IOTAActionServices | Used to perform file actions, such as opening a file in the IDE, and closing and saving files. |
IOTACodeInsightServices | Used to query Code Insight managers, and to add and install a custom Code Insight manager. |
IOTADebuggerServices | Used to access the IDE's debugger. |
IOTAEditorServices | Used to access the source editor and its internal buffers. |
IOTAKeyBindingServices | Used to define individual key bindings for the IDE. An example key binding is Shift-Ctrl-I, which tabs any selected text uniformly to the right. |
IOTAKeyboardServices | Used to define and query for individual key bindings, and recording and playing keyboard macros. |
IOTAMessageServices | Used to access the IDE's message view window. You can add or clear messages, or add, remove, or show a tab display. |
IOTAModuleServices | Used to retrieve a list of modules open in the IDE, creating new files, saving or closing active files, or even registering a virtual file system. |
IOTAPackageServices | Used to retrieve a list of installed packages and their associated components . |
IOTAToDoServices | Used to query To-Do Items, or register a custom To-Do list manager. |
IOTAWizardServices | Used to register or unregister an external wizard. RegisterPackageWizard , on the other hand, does not provide support unregistering a wizard, nor can it be used for a wizard contained within a DLL. |
IOTAServices | Used to provide general services not supported by any other service interface.register or unregister an external wizard. RegisterPackageWizard , on the other hand, does not provide support unregistering a wizard nor can it be used for a wizard contained within a DLL. |
INTAServices | Used to provide access to the native components of the IDE. Modify, add, or remove items menu bar, tool bars, image list, and/or action list using this native tool service. |
For our MemStat Wizard example, we need to be able to detect the active project. A project is a form of module, so we need to iterate through all the modules to find the active project. We will need to use the IOTAModuleServices interface to perform this action. We also should provide some feedback to the user through the IDE's Message View. This is the same window that provides compilation warnings and errors and search results. The service interface we need for this capability is the IOTAMessageServices . Finally, we need a way to launch the active project. The easiest way to perform this operation is to automate the key press of the Run/Run menu item, so we need access to the IDE's main menu. This is achieved using the INTAServices .
In the next section, we will take a look at how we can put it all together. Keep in mind, we'll be making no changes to the original MemStatus Wizard form.
Accessing a Service
The key element to accessing services is the BorlandIDEServices variable, which provides the source for all service interfaces. Actually, to obtain any of the specialized services, we need to use either the QueryInterface() or Supports() method provided by the BorlandIDEServices . For example, the following code grabs the IOTAModuleServices interface by using the BorlandIDEServices variable.
_di_IOTAModuleServices ModServices; BorlandIDEServices->Supports(ModServices);
It's really that simple. The _di_IOTAModuleServices identifies the Delphi Interface wrapper to the IOTAModuleServices . ModServices is the interface object. The call to the Supports() method assigns an object instance of the interface to the ModServices object, allowing us to access the interface's properties and methods (just like a class object).
NOTE
The BorlandIDEServices , which provides the source for querying all service interfaces, is a globally available variable within a Package hosting an IDE extension (such as a wizard). For a DLL, however, the access to this service interface is only provided through the first parameter passed by the DLL Entry Point function. Therefore, a BorlandIDEServices variable needs to be defined for DLLs, as shown in the following code excerpt.
#ifdef DLL #define BorlandIDEServices LocalIDEServices extern _di_IBorlandIDEServices LocalIDEServices; #endif
We'll discuss more on DLLs a little later in the chapter.
Utilizing a Service
Now that we know what service interfaces we need, and we know how to create an instance of an interface, let's see how these services can be utilized within the Execute() method of our revamped MemStat Wizard. This code is shown in Listing 23.5, which can be found in the wizard_memstatus.cpp source file in the wizard_part2_services folder for this chapter on the companion CD-ROM.
Listing 23.5 MemStatusWizard Class ” Execute() Method
void __fastcall MemStatusWizard::Execute() { Application->ProcessMessages(); // let menu processing complete bool launch = true; // unless instruected otherwise, run the application TFormMemStat* FormMemStat = NULL; SetupMessageViewAccess(); _di_IOTAModuleServices ModServices; BorlandIDEServices->Supports(ModServices); // get access to Modules) _di_IOTAProject project = FindCurrentProject(ModServices); if (project) { _di_IOTAProjectBuilder projectbuilder = project->ProjectBuilder; if (projectbuilder->ShouldBuild) { MessageServices->AddTitleMessage(AnsiString(GetName() + " - Project needs to be built first"), MessageGroup); AnsiString filename = ExtractFileName(project->FileName); AnsiString message = "MemStatus Wizard detected that the " + filename + " project needs to be built first.\n" "Do you wish to continue?"; int result = MessageBox(NULL,message.c_str(), "MemStat Wizard - Build Project?", MB_YESNO); if (result == IDYES) { projectbuilder->BuildProject(cmOTAMake,true,true); // build proj } else launch = false; // user doesn't want to run now } } else // could not find project { AnsiString message = GetName() + " - Project not loaded. Unable to run."; MessageServices->AddTitleMessage(message,MessageGroup); MessageBox(NULL,message.c_str(),"MemStat Wizard - Build Project?", MB_OK); launch = false; } if (launch) // if launch is still a go { MessageServices->AddTitleMessage(AnsiString(GetName() + " - Project is built and ready to run"),Message Group); _di_INTAServices NativeServices; BorlandIDEServices->Supports(NativeServices); // get access to IDE (menu bar) // let's find the Run top menu item... TMenuItem* MenuItem = FindMenuItemCaption(NativeServices->MainMenu->Items,"Run"); if (MenuItem) { // now let's find the Run (F9) menu item TMenuItem* temp = FindMenuItemCaption(MenuItem,"Run"); MenuItem = temp; if (MenuItem) { if (MenuItem->Enabled) { int result = MessageBox(NULL, "MemStat Wizard is ready to launch active project and "\ "measure memory performance. \n\n" \ "It's recommended that you close all other "\ "applications with the exception of C++Builder "\ "before running the memory test. \n\n" \ "Do you wish to continue?", "MemStat Wizard - Ready to Run",MB_YESNO); if (result == IDYES) { FormMemStat = new TFormMemStat(0);// instantiate MemStat form FormMemStat->Show(); // show the wizard Application->ProcessMessages(); //let FormMemStat complete FormMemStat->SpeedButtonStartClick(0); // measure memory first MessageServices->AddTitleMessage(AnsiString(GetName() + " - " + FormMemStat->GetMemoryStartFree()),MessageGroup); MenuItem->OnClick(0); // run the app } else { MessageServices->AddTitleMessage(AnsiString(GetName() + " - User aborted run"),MessageGroup); } } } } if (!MenuItem) { MessageServices->AddTitleMessage(AnsiString(GetName() + " - Unable to run application. " \ "Project not loaded."),MessageGroup); } } // little loop processing here - but don't tie up system (check and get out) if (FormMemStat) { while (FormMemStat->Visible) // check { Application->ProcessMessages(); //get out (process current messages) } delete FormMemStat; } MessageServices->AddTitleMessage(GetName() + " - Completed",MessageGroup); }
In addition to a totally revamped Execute() method, we've also added a few new methods to our custom wizard class to support some of the service processing we need to perform. These methods include SetupMessageViewAccess() , FindMenuItemCaption() , and FindCurrentProject() . In addition to these methods, there are a few properties added to our wizard class within the wizard_memstatus.h file as shown in Listing 23.6:
Listing 23.6 Excerpt of the MemStatusWizard Class Definition
private: _di_IOTAMessageServices MessageServices; _di_IOTAMessageGroup MessageGroup; void __fastcall SetupMessageViewAccess(); TMenuItem* FindMenuItemCaption(TMenuItem* topmenu, AnsiString Caption); _di_IOTAProject FindCurrentProject(_di_IOTAModuleServices ModServices);
In the following sections, we will walk through the Execute() method code as provided in Listing 23.5, and look at some of these new support methods we've added. This examination will enable us to fully understand how to use these particular Tool API services.
Look Before You Leap with ProcessMessages()
Notice the first thing we do in the Execute() method is call Application ->ProcessMessages() . When we're dealing with a myriad of messages being thrown around the IDE and, as a wizard, we're a part of that IDE, it's a good idea to make sure we've given a chance for other messages to be processed first before we get going. This enables our wizard to utilize the service interfaces properly. For example, because our Wizard is launched from the Help Menu Item, the menu processing is still occurring when our Wizard launches. If we don't allow these messages to be processed , we might not get clean access to the IDE's menu items, which, in our example, we need later to Run the active project within C++Builder.
Providing Feedback Through the IDE Message View
The next critical thing we do in the Execute() method, is call a custom method that's been added to wizard_memstatus.cpp called SetupMessageViewAccess() . The implementation for this custom method is provided in Listing 23.7.
Listing 23.7 MemStatusWizard Class ” SetupMessageViewAccess() Method
void __fastcall MemStatusWizard::SetupMessageViewAccess() { BorlandIDEServices->Supports(MessageServices); // get access to Message View MessageGroup = MessageServices->AddMessageGroup(GetName()); MessageServices->ClearMessageGroup(MessageGroup); // need a clean canvas MessageServices->ShowMessageView(MessageGroup); // make it visible MessageServices->AddTitleMessage(GetName() + " - Activated",MessageGroup); }
In this method, we grab the IOTAMessageServices interface, which allows access to the IDE's Message View. The AddMessageGroup() function sets a new message tab if it does not exist. In this case, the GetName() method is passed as a parameter which provides the name of our wizard. ClearMessageGroup() is used to clean the message view canvas for our message group. ShowMessageView() ensures that the message view is visible. Finally, the one method we use repetitively is AddTitleMessage() to write out text to our message group.
Locating and Building the Active Project
In the wizard's Execute() method we grab a local copy of the IOTAModuleServices , which will enable us to find the active project for which the Wizard will collect memory information.
_di_IOTAModuleServices ModServices; BorlandIDEServices->Supports(ModServices); // get access to Modules) _di_IOTAProject project = FindCurrentProject(ModServices);
FindCurrentProject() is another custom method we've provided for our wizard that has been added to the wizard_memstatus.cpp source file. This method is shown in Listing 23.8.
Listing 23.8 MemStatusWizard Class ” FindCurrentProject Method
[View full width]
_di_IOTAProject MemStatusWizard::FindCurrentProject(_di_IOTAModuleServices
This method iterates through all the active modules, which might include files, forms, resource files, projects, and the project group. What we're looking for is the lone project group (there can only be one project group loaded by the IDE at one time). To hunt down the lone project group, we cast each iterated module into a project group, as follows :
projectgroup = (_di_IOTAProjectGroup)module;
If projectgroup is not NULL , it has been found! From here, we can locate the active project by using the project group's ActiveProject() method, and return it to the Execute() method.
If a valid active project is returned by FindCurrentProject() , we can examine the project build information by accessing the project's ProjectBuilder() property as shown in the following code snippet.
_di_IOTAProjectBuilder projectbuilder = project->ProjectBuilder; if (projectbuilder->ShouldBuild) { if (result == IDYES) { launch = projectbuilder->BuildProject(cmOTAMake,true,true); } else launch = false; // user doesn't want to run now }
The ShouldBuild() property provided by the _di_IOTAProjectBuilder interface enables us to determine if the project needs to be built. A true condition means a modification to the code represented in that project has occurred and these changes are not yet reflected in the executable. For our MemStatus Wizard, we want to make sure the project is built before we initiate any memory measurements. If it's not built, the Tools API allows our wizard to create the executable by using the BuildProject() method, which is provided by the _di_IOTAProjectBuilder interface. Notice that there are three parameters for the BuildProject() call. Let's look at its declaration.
bool __fastcall BuildProject(TOTACompileMode CompileMode, bool Wait, bool ClearMessages) = 0;
The first parameter of BuildProject() , CompileMode , identifies how the IDE (or our Wizard from a user's perspective) should compile the project. We can compile only modified files, all files, check the syntax, or compile the current module only. In this example, cmOTAMake was used to compile only modified files. The second parameter, Wait , identifies whether the dialog box should be appear displaying to the user the compiler progress and completion status, or if control should return immediately to the wizard after compilation is complete. A value of true , as used in our example, will require the user to click the OK button of the dialog box for control to be returned to our wizard. The third parameter, ClearMessages , identifies if the Build message view should be cleared before compiling. A true value, fortunately, will not clear our custom Wizard message view that we created, but will clean the Build message view. Finally, BuildProject() will return true, if the compilation was successful.
Hacking into the IDE Responsibly
After we've located an active project, and we know it's been built, we're ready to launch it and measure memory performance. To execute the project, however, is not entirely straightforward.
One common way is to access the integrated debugger using the IOTADebuggerServices interface, and use it's CreateProcess() method. This method starts the application representing the project loaded in the IDE. However, the CreateProcess() method creates the process initially stopped at the first line of execution code. To use IOTADebuggerServices properly, we would need to use a thread notifier to detect when the process stopped and restart the process automatically. There's a lag in this execution that is less than desired for the purpose of our example.
Another way is to hack into the IDE as a virtual user and emulate clicking the Run menu item by using the INTANativeServices interface. If the project is built, it will run without the stop/start condition associated to the CreateProcess() method. The trick, however, is to locate the Run menu item. We start by grabbing a copy of the INTANativeServices interface to the IDE:
_di_INTAServices NativeServices; BorlandIDEServices->Supports(NativeServices); // get access to IDE (menu bar)
This will enable us to access the IDE's MainMenu. This interface also allows access to other IDE elements besides the MainMenu, such as ActionList, ImageList, and ToolBar. In our example, we just want access to the MainMenu, so we can find the "Run" menu item.
// let's find the Run top menu item... TMenuItem* MenuItem = FindMenuItemCaption(NativeServices->MainMenu->Items,"Run");
A custom method called FindMenuItemCaption() has been added to the wizard_ memstatus.cpp source file to iterate through the subitems of a menu item until a text match is found. This method is provided in Listing 23.9.
Listing 23.9 MemStatusWizard Class ” FindMenuItemCaption() Method
TMenuItem * MemStatusWizard::FindMenuItemCaption(TMenuItem* topmenu, AnsiString Caption) { TMenuItem *menuitem = NULL; bool done = false; if (!topmenu) return menuitem; // get out, no menu to search int index = 0; while (!done) { if (index < topmenu->Count) { menuitem = topmenu->Items[index]; if (menuitem->Caption.AnsiPos(Caption)) { done = true; } index ++; } else { done = true; menuitem = NULL; } } // while return menuitem; }
The FindMenuItemCaption() locates the menu item that matches the Caption . If it exists, a new TMenuItem is returned to the caller. In this example, the caller is the wizard's Execute() method. Within the Execute() method, we then look for the next "Run" menu item sub to the menu item that was just returned. This is done by placing another call to FindMenuItemCaption() . After we finally locate the "Run" menu item, we can trigger the OnClick() event for the specific menu item as depicted in the following code snippet from the wizard_memstatus.cpp source file.
FormMemStat = new TFormMemStat(0);// instantiate MemStat form FormMemStat->Show(); // show the wizard Application->ProcessMessages(); //let FormMemStat complete FormMemStat->SpeedButtonStartClick(0); // measure memory first MenuItem->OnClick(0); // run the app
This code also reveals what we do to ready our memory measurement analysis. First, we create an instance of the form that we're going to display. We then Show() it. Previously, we used the ShowModal() call, which forced our program to wait synchronously until it was closed (see Listing 23.4). With the Show() method, control is returned immediately to our Execute() method. Notice the use of the ProcessMessages() method again. We use it in this instance to ensure that the form we just displayed through the Show() method will be properly processed before we activate any memory analysis.
In the previous MemStat Wizard example, the user was required to do things manually. This included initiating the memory analysis by clicking the start button off the MemStat Wizard form, and then launching the application to evaluate after the MemStat Wizard memory analysis had started. Now, we're automating these actions by calling the form's SpeedButtonStartClick() method after the form is displayed enabling us to get an accurate read on the memory prior to launching the application under test. We launch that application, again, by triggering the OnClick() event for the IDE's Run menu item. It behaves as if the user has made these selections manually.
Additional Processing with Services
In the first example provided in Listing 23.4, we used the ShowModal() method to open our form, and when we were done with the form we knew when control was returned. In this example, we have no idea precisely when the form is closed because we are using the Show() method. The reason we are using the Show() method is because we need to do some additional processing within our Execute() code (using Services while the form is up). Specifically, we need to automate the Run using the native services provided by the IDE after our memory form was shown. This example is not unique. You may also find a case for your custom wizards where additional processing needs to occur within your wizard while a form is already active. The key is knowing how to gracefully detect when the form has closed. One way to do this without making any modifications is to use a processing loop as shown in the following code snippet found in the wizard_memstatus.cpp source file:
// little loop processing here - but don't tie up system (check and get out) if (FormMemStat) { while (FormMemStat->Visible) // check { // if you need to check on anything else, do it now Application->HandleMessage(); //get out (process current messages) } delete FormMemStat; //form no longer visible, we created it, let's delete it } MessageServices->AddTitleMessage(GetName() + " - Completed",MessageGroup);
Notice we only do the looping if FormMemStat exists and if it is visible. We don't want to tie up the system, so we just check for when the form is visible, and then, within our loop process, we relinquish control to the kernel using the HandleMessage() method, thereby allowing other messages in the system queue to be processed. Certainly, other ways to check for form closure exist, including threading and windows messaging, but this approach works very effectively.
You'll notice the last thing we do in our Execute() method is call MessageServices ->AddTitleMessage() to write out the final text within the Message View. This is depicted in Figure 23.5.
Figure 23.5. Messages for MemStat Wizard.
|
Top |