Writing Workplace Shell Objects Using the IBM Open Class User Interface Library

From EDM2
Jump to: navigation, search

by Alan Auerbach

IBM VisualAge C++ for OS/2 is a complete and comprehensive product for developing applications, including Workplace Shell objects, for OS/2. One of the components of VisualAge C++ is the IBM Open Class Library, which contains a rich set of C++ classes for implementing a graphical user interface.

You might be wondering how hard it is to take advantage of this class library when writing Workplace Shell objects. This article will take you, step by step, through an example object written with the class library. The object (called IcluiExample) is a completely functional, though simple, object descended from WPAbstract.

Start with the Interface Definition Language

Just as with a Workplace Shell object written to the conventional Presentation Manager (PM) APIs, we need to lay out the behavior of our object. We do this via the Interface Definition Language (IDL), although the Direct-to-SOM feature of the compiler lets you define a SOM class in a header (typically .HH) file. (But we'll save that for another article.) This object is derived from WPAbstract, a class whose persistent data is stored in the OS2.INI file. This class introduces one instance variable called aVariable, as well as methods that query and set this variable. Another method, wpRefreshViews, is added to cause all open views of our object to refresh the display of the instance data. The IDL also describes a set of methods that are overridden, including wpModifyPopupMenu, wpRestoreState, wpSaveState, wpOpen, wpMenuItemSelected, and wpAddSettingsPages.

We define a metaclass that allows us to override some class methods as well. These include wpclsInitData, wpclsQueryTitle, wpclsQueryDefaultView, wpclsQueryStyle, and wpclsCreateDefaultTemplates. Don't panic; these overrides are simple and help us to specialize and improve the behavior of our class.

// Module Header
// Module Name: ICLUIXMP
// Copyright (c) International Business Machines Corporation 1995
// Author: Alan Auerbach (auerbach@vnet.ibm.com)

#ifndef icluixmp_idl
#define icluixmp_idl

#include 'wpabs.idl'
interface M_IcluiExample;
interface IcluiExample : WPAbstract

//#
//# CLASS: IcluiExample
//#
//# CLASS HIERARCHY:
//# SOMObject
//# +-- WPObject
//# +-- WPAbstract
//# +-- IcluiExample
//#
//# DESCRIPTION:
//# This is the IcluiExample Object class.
//# It is an example of interoperability between Workplace Shell
//# and ICLUI with regard to settings pages.
//#
//#
{
ULONG wpQueryaVariable( );
VOID wpSetaVariable(in ULONG value);
VOID wpRefreshViews( );

#ifdef __SOMIDL__
implementation {

releaseorder: wpQueryaVariable,
wpSetaVariable,
wpRefreshViews;

//# Class Modifiers
externalstem = icluixmp;
local;
externalprefix = it_;
majorversion = 1;
minorversion = 2;
filestem = icluixmp;
metaclass = M_IcluiExample;
callstyle = oidl;
dllname = 'icluixmp.dll';

passthru C_xih = ''
' #define INCL_WIN'
' #define INCL_DOS'
''
'
' #include <os2.h>';

ULONG aVariable;

//# Method Modifiers
wpModifyPopupMenu: override;
wpAddSettingsPages: override;
wpRestoreState: override;
wpSaveState: override;
wpOpen: override;
wpMenuItemSelected: override;
};
#endif /* __SOMIDL__ */
};
interface M_IcluiExample : M_WPAbstract
{
#ifdef __SOMIDL__
implementation {

//# Class Modifiers
externalstem = icluixmp;
local;
externalprefix = itM_;
majorversion = 1;
minorversion = 2;
filestem = icluixmp;
callstyle = oidl;
dllname = 'icluixmp.dll';

//# Method Modifiers
wpclsInitData: override;
wpclsQueryTitle: override;
wpclsQueryDefaultView: override;
wpclsQueryStyle: override;
wpclsCreateDefaultTemplates: override;

//# Data Modifiers

};
#endif /* __SOMIDL__ */
};

#endif /* icluixmp_idl */

Sample Code 1. ICLUIXMP.IDL

The .HPP file

By convention, the .HPP extension is used for those headers that contain the C++ class declarations created by our program. Our .HPP file describes two C++ classes: MyViewWindow and SettingsPage.

/****************************************************************/
/* */
/* MODULE: ICLUIXMP.HPP */
/* */
/* Includes for ICLUI classes I use */
/* */
/****************************************************************/
#include <iframe.hpp> // Frame Window
#include <ititle.hpp> // Title Bar
#include <istattxt.hpp> // Static Text
#include <icmdhdr.hpp> // Command Handler
#include <iselhdr.hpp> // Select Handler
#include <iradiobt.hpp> // Radio Button
#include <ipushbut.hpp> // Push Button
#include <itrace.hpp> // Trace
/****************************************************************/
/* Class MyViewWindow - Describes the custom object view. */
/****************************************************************/
class MyViewWindow : public IFrameWindow
{
public:
MyViewWindow (IcluiExample *Object); // constructor
~MyViewWindow(); // destructor
void refreshView(); // member function to refresh
// display of instance variable

private:
IStaticText staVariable; // Static Text field showing value
IcluiExample *myObject; // Hold a pointer to the SOM object
ITitle tiPrimary; // Title Bar
PUSEITEM myUseItem; // Save the USEITEM pointer for this
// view.
};
/****************************************************************/
/* Class SettingsPage - Describes the custom settings page for */
/* this object. */
/****************************************************************/
class SettingsPage : public IFrameWindow ,
public ICommandHandler,
public ISelectHandler
{
public:
SettingsPage (IcluiExample *Object, IWindow *notebook);
~SettingsPage();

protected:
Boolean command(ICommandEvent& cmdevt);
Boolean selected(IControlEvent& evt);

private:
IcluiExample *myObject; // Hold a pointer to the SOM object
IRadioButton rbA; // Radio Button for 'A' value
IRadioButton rbB; // Radio Button for 'B' value
IPushButton pbUndo; // 'Undo' push button
IPushButton pbDefault; // 'Default' push button
ULONG aVariableInitial; // Initial state of instance variable
// for Undo processing
};

Sample Code 2. ICLUIXMP.HPP

MyViewWindow derives from IFrameWindow and describes the customized view our object will have. We declare one constructor and a destructor, as well as a member function called refreshView. We also declare some private variables to reference our window's title bar and static text field (used as the client area). Another instance variable holds the SOM object pointer that this view represents. Finally, there's an instance variable that holds the view's USEITEM, a Workplace Shell data structure that contains information about this view.

WPSObj-Fig-1.gif

Figure 1. Our object's custom view

Our other C++ class is SettingsPage. It's somewhat more interesting because it derives not only from IFrameWindow but from ICommandHandler and ISelectHandler as well. This class lets us to do something interesting and relevant when the user interacts with this dialog.

WPSObj-Fig-2.gif

Figure 2. Our object's custom settings page

The .CPP File

That's enough preamble - let's talk about the code. First, keep in mind that the skeleton for this file, as well as the contents of the .XH, .XIH, and .DEF files, are generated from the SOM compiler from the .IDL source. The makefile below provides details on the commands and parameters used to do this.

#
# Build the DLL. Since there's only one source file, compile and link
# in one step.
#
icluixmp.dll: icluixmp.cpp \
icluixmp.hpp \
icluixmp.xih \
icluixmp.xh \
icluixmp.def \
icluixmp.res \
icluixmp.h
icc @<<
/N30 /Gd /Gm /Ge- /Ls /Wgen /Fm /Q /B'/map:full'
icluixmp.cpp
somtk.lib
icluixmp.def
<<
rc icluixmp.res icluixmp.dll # Bind the resource file

#
# Run the SOM compiler. If it builds one, it will build all.
#
icluixmp.xih \
icluixmp.xh \
icluixmp.def \
icluixmp.cpp:
sc -mnoint -s'xih;xh;def;xc' icluixmp.idl

#
# Compile the resources.
#
icluixmp.res: icluixmp.rc icluixmp.h icluixmp.dlg
rc -r icluixmp.rc

Sample Code 3.

There's one piece of global data. The HMODULE is a handle to the .DLL, and we use this to reference the resources used by the object, because the resources are bound to the .DLL. This is set via the DosQueryModuleHandle call in our wpclsInitData override - the first function in this module to be invoked. I personally prefer using DosQueryModuleHandle to DosLoadModule or any of the IDynamicLinkLibrary constructors, because with DosQueryModuleHandle the use count of the .DLL is not incremented. This makes it less likely that the use count will be left non-zero, causing the .DLL to stay loaded (when it otherwise would be unloaded).

The MyViewWindow Constructor

This constructor is called in the wpOpen processing of the object. Because this view is very simple, it initializes IFrameWindow using the IFrameWindow default constructor. This constructor simply displays a frame window with default style, including a system menu, sizing border, and minimize and maximize buttons. When we initialize the title bar, we set its text to be the title of the object we're opening. And finally, the static text field is initialized with an ID of IC_FRAME_CLIENT_ID, and the parent and owner are the MyViewWindow instance we're constructing.

/****************************************************************/
/* The constructor for MyViewWindow */
/****************************************************************/
MyViewWindow::MyViewWindow(IcluiExample *Object)
: IFrameWindow(), // Default constructor is fine
tiPrimary(this, Object->wpQueryTitle()), // Set the object title
staVariable(IC_FRAME_CLIENT_ID, this, this)
{
PVIEWITEM pViewItem;

myObject = Object; // Tuck away the SOM object ptr

myUseItem =
(PUSEITEM) myObject->wpAllocMem( sizeof(USEITEM) + sizeof(VIEWITEM), NULL);

pViewItem = (PVIEWITEM) (myUseItem+1);
memset (myUseItem, 0, sizeof(USEITEM) + sizeof(VIEWITEM));

myUseItem->type = USAGE_OPENVIEW;
pViewItem->view = OPEN_MYVIEW;
pViewItem->handle = handle();

myObject->wpAddToObjUseList(myUseItem);

myObject->wpRegisterView(handle(), 'My View');

setAutoDeleteObject();
setFocus();
show();
refreshView();

staVariable.setAlignment(IStaticText::centerCenter);
setClient(&staVariable);
}

Sample Code 4. MyViewWindow constructor

Now, the body of the constructor is doing some interesting work. We need to tell the Workplace Shell that this is a view of our object. This allows the object's in-use emphasis to be correct and lets the Display Existing Window function work correctly. First we allocate some memory. We use wpAllocMem to tie the memory to the object, so when the object is freed, the memory is freed as well. The USEITEM structure is actually two structures allocated contiguously: the USEITEM structure itself, and the type of usage item it represents. For example, in our example object, to represent an object's view we need a USEITEM and a VIEWITEM. In the VIEWITEM we set the view field to tell the shell that it is a view called OPEN_MYVIEW.

Because the IFrameWindow for this view is already constructed, we can use the handle member function to return the HWND for our window and insert that into the VIEWITEM structure.

To add the newly allocated USEITEM to the object's use list, we call the Workplace Shell method wpAddToObjUseList. This is why we passed the pointer to the SOM object in the constructor; it lets us call Workplace Shell methods from our C++ member functions. The call to wpRegisterView sets the view title and adds our familiar behavior for the system menu.

We set the AutoDeleteObject attribute on this window so we don't have to handle the CLOSE processing for the window. That is, by setting AutoDeleteObject, we know that the destructor for this object will be called by the User Interface Library when the window is closed.

Finally, we set focus to the window and show it. The call to refreshView displays the appropriate text based on the setting of our object's instance variable.

The Infamous Settings Page Question

The rest of the code for MyViewWindow is straightforward and needs no further description. Which brings us to our settings page. The question we are asked most often by programmers getting up and running with IBM Open Class and the Workplace Shell is, 'How can I add a page to the Settings notebook, when the PAGEINFO structure requires a resource ID, module handle, and Dialog procedure address? What do I use for the Dialog procedure address?' While it is possible to wrapper an existing dialog procedure, it is much simpler to construct an IFrameWindow and use that as your settings page. The reason you can do that is that the resid, dlgid, and pfnwp fields of the PAGEINFO structure are ignored if you supply a HWND in the hwndPage field. And, as we saw, you can get an HWND by calling the handle() member function on your IFrameWindow object. The unfortunate side effect of this is that objects using the Open Class Library for settings pages cannot take advantage of the performance benefit that would be achieved by the delayed loading of dialog resources.

/****************************************************************/
/* METHOD wpAddSettingsPages */
/****************************************************************/
SOM_Scope BOOL SOMLINK it_wpAddSettingsPages(IcluiExample *somSelf,
HWND hwndNotebook)
{
PAGEINFO pageinfo;
SettingsPage *aSettingsPage;
/* IcluiExampleData *somThis = IcluiExampleGetData(somSelf);*/
IcluiExampleMethodDebug('IcluiExample','it_wpAddSettingsPages');

parent_wpAddSettingsPages(somSelf, hwndNotebook);

IWindow *notebook = new IWindow(hwndNotebook);
notebook->setAutoDeleteObject();

aSettingsPage = new SettingsPage(somSelf, notebook);
aSettingsPage->setAutoDeleteObject();

memset(&pageinfo,0,sizeof(PAGEINFO));
pageinfo.cb = sizeof(PAGEINFO);
pageinfo.usPageStyleFlags = BKA_MAJOR;
pageinfo.usPageInsertFlags = BKA_FIRST;
pageinfo.pszName = 'ICLUI';
pageinfo.pCreateParams = somSelf;
pageinfo.usSettingsFlags = SETTINGS_PAGE_NUMBERS;
pageinfo.hwndPage = aSettingsPage->handle();

somSelf->wpInsertSettingsPage( hwndNotebook, &pageinfo );
return TRUE;
}

Sample Code 5. wpAddSettingsPage method

Our Settings Page Handlers

We set up two handlers for our settings page:

  • ICommandHandler handles the Undo and Default push buttons.
  • ISelectHandler handles the selection of the radio buttons.

For the most part, the settings page doesn't need to know when it is initially displayed or destroyed. The overriding theme of the Workplace Shell is the concept of 'perfect save,' which means that an object's instance variables are updated whenever a setting is changed. Thus, whenever a setting is changed via a radio button selection, the instance data is updated, the object is placed on a queue to write its data persistently, and any open views are updated to reflect the new instance data.

The one complication in all this is the Undo action. The convention is that the Undo button restores the object to the state it had before the settings page was displayed. In order to accomplish that, we need to save the state of the object at the time of the request to display the Settings notebook. We do this in the constructor for SettingsPage, which is called in wpAddSettingsPages.

The wpRefreshViews Method and refreshView Member Function

There are a couple of items worth noting about the way this object causes its views to be updated. Any call to wpSetaVariable (typically made by a user updating the Settings notebook) will cause the wpRefreshViews method to be invoked. This method uses the wpFindViewItem method to iterate through the open views of the model (Workplace Shell) object. Now the VIEWITEM structure contains an HWND that represents the view object, but we want to be able to call a member function of that view object. How do we obtain a C++ object pointer from the HWND? The User Interface Library provides a very handy static member function on IWindow called windowWithHandle, which returns a pointer to the IWindow object that has the given handle. This only works on windows that have been constructed with a User Interface Library constructor, but we know that any instance of OPEN_MYVIEW was constructed with a MyViewWindow constructor, and therefore we have a very convenient way to get a pointer to our MyViewWindow instance without having to store it distinctly.

/****************************************************************/
/* METHOD wpRefreshViews */
/****************************************************************/
SOM_Scope void SOMLINK it_wpRefreshViews(IcluiExample *somSelf)
{
IcluiExampleData *somThis = IcluiExampleGetData(somSelf);
IcluiExampleMethodDebug('IcluiExample','it_wpRefreshViews');

PVIEWITEM pViewItem;
MyViewWindow *aview;

pViewItem = somSelf->wpFindViewItem(VIEW_ANY, NULL);
while (pViewItem)
{
if (pViewItem->view == OPEN_MYVIEW)
{
aview = (MyViewWindow*)
IWindow::windowWithHandle(pViewItem->handle);
if (aview)
{
aview->refreshView();
}
}
pViewItem = somSelf->wpFindViewItem(VIEW_ANY, pViewItem);
}
}

Sample Code 6. wpRefreshViews method

/****************************************************************/
/* MyViewWindow::refreshView */
/****************************************************************/
void MyViewWindow::refreshView()
{
if (myObject->wpQueryaVariable() == VARIABLE_VALUEA)
{
staVariable.setText('my variable is A');
}
else
{
staVariable.setText('my variable is B');
}
}

Sample Code 7. MyViewWindow

The example in this article is admittedly simple - it doesn't include help, multithreading, exception handling, or the use of any enhanced classes that the User Interface Library provides, such as canvasses or viewports. It does, however, show a simple working example of a Workplace Shell object that uses the Open Class Library both to present a custom view and to insert an operational settings page.

Tracing

The IBM Open Class Library provides extensive tracing facilities. However, to invoke the trace facility for your Workplace Shell object, remember that the ICLUI_TRACE and ICLUI_TRACETO environment variables must be set in the Workplace Shell's context. To do this, you should do one of the following:

  • Include these SET statements in your CONFIG.SYS file.
  • Modify your CONFIG.SYS file to set either PROTSHELL= or SET RUNWORKPLACE= to point to \OS2\CMD.EXE, then issue the SET statements, and then bring up the Workplace Shell by entering PMSHELL.

A Note About Peaceful Coexistence

The Workplace Shell is a single-process environment. Objects from different independent software vendors (ISVs) and developers all run in the same process context. In addition to the normal rules of good PM behavior (you should take no longer than 0.1 seconds to process a message or method call on a user interface thread), you should also remember not to do anything destructive to the environment. For example, you should never call ICurrentApplication::exit or ICurrentThread::exit on a process or a thread that you did not create. You should also try to catch exceptions that would cause the User Interface Library to terminate the current thread.

In particular, you might be tempted to call ICurrentApplication::setUserResourceLibrary. Remember, though, if you are dynamically linking to a run-time .DLL version of the class library, you could be interfering with others who are doing the same; therefore, you should explicitly pass your resource identifier on any call that requires a resource. This is more of a concern for corporate developers than for ISVs, because ISVs cannot ship the run-time libraries as .DLLs with their applications without renaming them.

Note
The complete source files are in \SOURCE\DEVNEWS\VOL9\WPDTS on disc 1 of your DevCon CD-ROMs.

Reprint Courtesy of International Business Machines Corporation, © International Business Machines Corporation