WPS Programming the Easy Way - Part 2

From EDM2
Revision as of 07:36, 27 January 2018 by Ak120 (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search
WPS Programming the Easy Way
Part 1 Part 2

Written by Frank Matthijs

Source code: edm0209s.zip

Introduction

This is the second and last article of WPS Programming the Easy Way. This mini series is intended to be an introduction to WPS programming. Still more than the previous article, you are encouraged to follow the article with your compiler ready. This way, you can see for yourself how to make and extend a typical WPS class.

I'll presume you already know a little about WPS programming. If this is not the case, I suggest you read my previous article. I explained things like classes, metaclasses, inheritance, overriding, SOM (briefly) and the essence of WPS programming. You should at least know these terms and what they mean in order to be able to follow this article.

Overview

At the end of the previous article, I already showed how to make a primitive, but fully functional WPS class. The instances of that class have a distinct type, which makes it very easy to recognize them, e.g. when they are dropped over your application program. This article will further expand the class, adding an icon and a settings page, and improving the WPS integration by allowing folders to show details of our objects (apart from the normal details like file creation time, etc.). These features will be added step by step, allowing you to study each improvement separately.

The examples are chosen to show some methods you'll override very often (as in the previous article), and as an addition to the Car sample from the IBM toolkit. The latter does a lot of things right, but there are some things it does not do properly (like showing details data). These things will be explained here.

As in the previous article, you'll encounter useful tips from time to time. These tips give more information or show problem areas of WPS programming.

A tip looks like this:

This is a tip.

First Things First

Before I start the article proper, I have some additions to my previous article.

The locked .DLL problem

OK, you know the problem: when you deregister a class DLL, it is still locked, so you cannot simply copy your new DLL over it and register the class again. The DLL gets unlocked when you delete the template, as I told you in the previous article. Since deleting the template can be tedious, there is a simpler method, which was pointed out to me by Martin Thierer (thanks, Martin!): instead of always having to delete the template, why not prevent a template from being generated in the first place? You can do this by overriding the wpclsQueryStyle method and setting the CLSSTYLE_NEVERTEMPLATE flag. See the PM Reference for more information. You can do this during development, and add the template for your final class release.

Unfortunately, both methods don't always work. When you are working with classes that affect other classes (such as the details display I'll cover later on), the WPS seems to become unstable when you replace a DLL as outlined above. The way I solve this problem is by killing the PMSHELL.EXE process. The WPS has a built-in recovery process, so the shell will restart automatically. This achieves the same effect as completely rebooting, but takes considerably less time. Moreover, all your windows (and the command history) stay intact. Only make sure the shell doesn't try to restart all your processes. As always, if you have a better way of solving this problem, please let me know.

I have included a REXX script (RESTART.CMD) that restarts the WPS as outlined above. It kills the second instance of PMSHELL.EXE and uses KILL.EXE for that purpose (this program can kill any process of which the process ID is known). Make sure you have this KILL program and PSTAT somewhere in your path for this to work.

Metaclasses

Some of you have asked me to explain the concept of metaclasses in more detail. Since this is something not all OO-ish languages support, I'll do that right away. Note however, that this can actually make things less clear (it may get confusing if you're not familiar with this).

In SOM (and thus in WPS), you not only have objects as instances of a class, but the classes themselves are also instances of some class. This class is called their metaclass. This means that a class is itself an object, called a class object to distinguish it from normal objects. This class object has methods of its own, defined by the metaclass (just as a normal object responds to methods defined by the class). To make the distinction clear, the methods performed by class objects (and defined by their metaclass) are called class methods, while the methods performed by normal objects (and defined by their class) are called instance methods.

It is important to make the distinction between parent classes and metaclasses. A parent of a class is a class from which instance methods and instance variables are inherited: instances of the subclass (the child class) can also perform instance methods defined by the parent class. The class Animal might define methods for eating, so all classes derived from Animal inherit the methods for eating. Instances of such derived classes can thus perform these methods.

A metaclass is a class that defines the class methods (the methods that the class objects can perform). So the metaclass of Animal might be AnimalMClass, which defines the methods that can be invoked on class Animal (such as methods for creating objects of the class Animal). Note that there are methods that the class object can perform, and there are method the class defines for its instances.

To summarize:
the parent of a class provides inherited methods that the class's instances can perform, whereas the metaclass of a class provides class methods that the class itself can perform.

So what's the relevance with regard to WPS programming? Well, the only thing you should always keep in mind is that there are such things as metaclasses, so in WPS programming you really work with two classes: the normal class where you define the functionality of the instances of your class (instance methods), and its metaclass, where you define the functionality of the class itself (class methods).

Since these are completely different classes, class variables are not available in the implementation of instance methods and vice versa. That's why for example I use a file scope variable to store the module handle of the class DLL file, instead of a class variable: I'll need this handle later on in an instance method.

Class methods are really used quite often in WPS. They all contain cls as part of their name, and define the behaviour and properties of the class itself, like the name of the class (wpclsQueryTitle), its icon (wpclsQueryIconData) and so on. The other sort of methods are instance methods, and they define the behaviour and properties of each individual instance of the class (like the contents of a folder (wpQueryContent), the real name of a file system object (wpQueryRealName) and so on.

I'll explain some class and instance methods in what follows. And what follows will start right away.

Step 1: Adding an Icon

Until now, our data file has been rather dull looking. The remedy is to give our class its own icon. This can be done by overriding the wpclsQueryIconData method. So we add the following line to our class definition file (datadel.csc):

override wpclsQueryIconData, class;

The way we will implement this is by adding an icon to our DLL file and reading this icon when necessary. Adding the icon to the DLL file is simple. Since we already have a resource file, we only need to add one line to it:

POINTER ID_ICON "datadel.ico"

We already have the necessary tools for the implementation of wpclsQueryIconData: we can use clsQueryModuleHandle to get the handle of our DLL where the icon is stored. Listing 1 shows what else we need to do to add an icon.

SOM_Scope ULONG   SOMLINK dfdM_wpclsQueryIconData(M_MyDataFile *somSelf,
PICONINFO pIconInfo)

{
    M_MyDataFileData *somThis = M_MyDataFileGetData(somSelf);
    M_MyDataFileMethodDebug("M_MyDataFile","dfdM_wpclsQueryIconData");

    if(pIconInfo) {
       pIconInfo->fFormat = ICON_RESOURCE;
       pIconInfo->hmod = _clsQueryModuleHandle(somSelf);
       pIconInfo->resid = ID_ICON;
       }
    return (sizeof(ICONINFO));
}

Listing 1

The ICONINFO structure can be used to identify the icon in several different ways. We use the module handle and resource ID here. The use of this method is explained properly in the PM Reference, except for the field names of the ICONINFO structure.

If you compile the source files, you'll notice the template for our class has indeed a new icon, and if you create a few instances of our class, you'll see the new icon there too. (This icon does not look very exciting either, but you can always substitute your own flashy icon.)

This is actually more a reminder than a tip. When you want to compile the source files discussed in this article, you should first install the sample programs. To do this, unzip the files for this article and type the following at any OS/2 prompt (in the directory where you unzipped the files):

WPS INSTALL

When the program is running, you can specify a directory to install the samples in. This will be your working directory for compiling the sample classes. I suggest you assign a separate directory for this purpose. You can also use the same directory you used for the samples of the previous article. In that case, be sure to rename the old WPS.CMD program to something different (e.g. WPS1.CMD) if you still need it, because otherwise it gets overwritten (no other files are overwritten during the installation).

After successfully installing the files, you can use the WPS program in your working directory to ready the correct files for each step: simply invoke the WPS program with the number of the step as a parameter, for example

WPS 1

This will ready the source files for this step (step 1). Note that the WPS program will only install the files that are different from the previous step, so don't skip any steps.

Step 2: Filling the File

Remember: when you create an instance of our MyDataFile class, you create a real file at the same time. This file had zero size in all previous examples. There are situations, however, where a file of zero size is not really a valid file of the intended type. For example, an empty bitmap is not a bitmap of zero size. Instead, it has a header identifying it as a bitmap, even if it doesn't contain picture data. Many data files will in fact use a header, so it is inconvenient and even incorrect if we only generate zero size instances (after all, this doesn't generate valid data files, because they lack the necessary header).

Fortunately, it is not very difficult to write a header (or whatever data is needed) to the file at creation (instantiation) time. As always, the trick is to find a proper method we can override. In this case, wpSetup is a good candidate. It is called when an object is created, and allows the object to initialize itself. Since writing a header is part of the initialization, this is the place to do it. This is the first instance method we override in the examples. It is not a class method, because every object can be set up differently, so the method acts on instances of the class, not on the class itself.

Listing 2 shows how to do it. This is a nice example of delegating some work to the parent: wpSetup is also used to pass a setup string to an object, such as "OPEN=DEFAULT". You can define your own setup strings and interpret them here, but in our case, we simply use the existing setup strings. We don't do anything with the setup strings in our code. Instead, we let the parent process them.

SOM_Scope BOOL   SOMLINK dfd_wpSetup(MyDataFile *somSelf,PSZ
pszSetupString){

    VALUES val;
    /* MyDataFileData *somThis = MyDataFileGetData(somSelf); */
    MyDataFileMethodDebug("MyDataFile","dfd_wpSetup");

    DosBeep(2000,20);
    val.ulField1 = DEFAULT_FIELD1;
    val.ulField2 = DEFAULT_FIELD2;
    return (WriteToDisk(somSelf, &val)
              && parent_wpSetup(somSelf,pszSetupString));
}

Listing 2

The code requires some explanation. VALUES is a structure with two fields. This will be the data that is written to file. The code fills the fields with default values, and then writes the structure to file. Everything related to this data is contained in the files values.c and values.h. For example, the code to write the data to file is contained in values.c and can be found in listing 3.

BOOL WriteToDisk(MyDataFile* somSelf, VALUES* val) {
    CHAR szObjectFileName[CCHMAXPATH];
    ULONG cb = sizeof(szObjectFileName);
    HFILE hf;
    ULONG ulAction;
    ULONG cbBytesWritten;

    if (!_wpQueryRealName(somSelf, szObjectFileName, &cb, TRUE)) return
FALSE;

    if (DosOpen(szObjectFileName, &hf, &ulAction,
                0, FILE_NORMAL, FILE_OPEN | FILE_CREATE,
                OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYNONE,
                NULL)) return FALSE;
    DosWrite(hf, val, sizeof(VALUES), &cbBytesWritten);
    DosClose(hf);
    return (cbBytesWritten == sizeof(VALUES));
}

Listing 3

The interesting functions are indicated in red. First of all, we need a way to determine the name of the data file. After all, we have been working with object pointers (MyDataFile *), not with file names. The method wpQueryRealName will give us a fully qualified file name, just what we need. After that, we can use the normal Dos API functions to manipulate the file.

If anything should go wrong, we return FALSE. When wpSetup returns FALSE, the creation of the object is terminated.

Of course, the call to DosBeep in listing 2 is not really required. I've added it here, so that you can hear when this method gets called. You'll find it is called at class registration time. At that time, the template is generated. Since the template is a data file just like any other instance of our class, wpSetup is called for the template when it is created.

I've included a REXX script to create an instance of class MyDataFile (it is called INSTANCE.CMD). When you run it, it will create a new data file on the desktop, and you'll hear a beep to tell you wpSetup has been called. Now try to drag the template to create another instance. You won't hear any beeps here, because when you drag a template of a data file, the WPS will simply copy the contents of the file, without calling wpSetup (the method has been called already, at the time the template was created). But the final result is the same: all instances of MyDataFile have the right contents (you can check the file size in the settings notebook).

There is something you should know about the way WPS handles templates. When you register a class, the system will check if there already is a template for it. When there is, the template will not be updated. This is important if you change something in your source code that affects the setup of the instances (for example, if you define a different header). If you don't delete the old template (from a previous compile, for example), you won't get the new one.

Step 3: Using Details Data

The previous steps (in this and the previous article) were quite straightforward and showed some standard methods you will override very often. This step is a little bit more involved. We will inform the WPS that objects of class MyDataFile have some extra information that can be displayed in every WPS folder's details view. This extra information can also be used in other parts of the WPS, as we'll see later. We'll use the contents of the file header for this extra data.

There are two things we should do to make this work:

  1. Inform the WPS that our class really has this data. This is done by overriding the class method wpclsQueryDetailsInfo. In this method, we provide all the details the WPS needs, like for example the kind of data (ULONG in our example), the length of each data item (4 bytes here), etc.
  2. Allow each instance to provide the actual values of the data. Since these values can differ from instance to instance, we do this by overriding an instance method, namely wpQueryDetailsData.

The wpclsQueryDetailsInfo method is called quite often. Therefore, we will precalculate the necessary data in the wpclsInitData method. Listing 4 shows what we need to do (the new items are in red).

SOM_Scope void   SOMLINK dfdM_wpclsInitData(M_MyDataFile *somSelf){
    ULONG i;
    PCLASSFIELDINFO pCFI;
    M_MyDataFileData *somThis = M_MyDataFileGetData(somSelf);
    M_MyDataFileMethodDebug("M_MyDataFile","dfdM_wpclsInitData");

    if (!WinLoadString(WinQueryAnchorBlock(HWND_DESKTOP), _clsQueryModuleHandle(somSelf),
          ID_TITLE, sizeof(_szTitle), _szTitle))
      strcpy(_szTitle, parent_wpclsQueryTitle(somSelf));

    parent_wpclsInitData(somSelf);

    for (i = 0, pCFI = fieldinfo; i < DETAILS_COLUMNS; i++, pCFI++) {
        memset((PCH) pCFI, 0, sizeof(CLASSFIELDINFO));
        pCFI->cb                = sizeof(CLASSFIELDINFO);
        pCFI->flData            = CFA_RIGHT | CFA_SEPARATOR | CFA_FIREADONLY;
        pCFI->flTitle           = CFA_CENTER | CFA_SEPARATOR | CFA_HORZSEPARATOR |
                                  CFA_STRING | CFA_FITITLEREADONLY;
        pCFI->pNextFieldInfo    = pCFI + 1;
        pCFI->pTitleData        = pszTitles[i];
        pCFI->flCompare         = COMPARE_SUPPORTED | SORTBY_SUPPORTED;
        pCFI->ulLenCompareValue = sizeof(ULONG);

        switch (i) {
            case INDEX_FIELD1:
                pCFI->>flData         |= CFA_ULONG;
                pCFI->offFieldData     = (ULONG)(FIELDOFFSET(VALUES, ulField1));
                pCFI->ulLenFieldData   = sizeof(ULONG);
                pCFI->pMaxCompareValue = &ulMaxField1;
                break;
            case INDEX_FIELD2:
                pCFI->flData          |= CFA_ULONG;
                pCFI->offFieldData     = (ULONG)(FIELDOFFSET(VALUES, ulField2));
                pCFI->ulLenFieldData   = sizeof(ULONG);
                pCFI->pMaxCompareValue = &ulMaxField2;
                break;
            }
        }
    fieldinfo[DETAILS_COLUMNS - 1].pNextFieldInfo = NULL;
}

Listing 4

For each data item, we must fill in a CLASSFIELDINFO structure. This is an extension of the FIELDINFO structure (used by container classes), and is defined in the file wpobject.h. All structures should form a linked list. We start by filling the complete structure with zeroes. This way, we don't need to fill in everything explicitly (the structure has many fields). The switch statement is used to fill in the data that can differ for each data item.

The following are the most important fields of CLASSFIELDINFO:

flData
This defines some properties of the data item. The most important property is the type of the data. In our case, we will display ULONG values, so we add CFA_ULONG to the flags. See the PM Reference under FIELDINFO for more information on these flags.
ulLenFieldData
This is the length of the data in bytes.
pNextFieldInfo
This is a pointer to the next CLASSFIELDINFO structure. The structures form a linked list.
pTitleData
Here we define the column heading data. In our example, we use strings to describe the data.

Now that we have the necessary information, we can provide it when the WPS calls wpclsQueryDetailsInfo. This method gets called to query the size of all the details data, as well as to get the details data itself. We first call the parent, and add our own data to the parent's data. Listing 5 shows how it is done.

SOM_Scope ULONG   SOMLINK dfdM_wpclsQueryDetailsInfo(M_MyDataFile *somSelf,
PCLASSFIELDINFO *ppClassFieldInfo, PULONG pSize)

{
    ULONG cParentColumns;
    PCLASSFIELDINFO pCFI;

    M_MyDataFileData *somThis = M_MyDataFileGetData(somSelf);
    M_MyDataFileMethodDebug("M_MyDataFile","dfdM_wpclsQueryDetailsInfo");

    cParentColumns = parent_wpclsQueryDetailsInfo(somSelf,ppClassFieldInfo,pSize);

    if (pSize) *pSize += sizeof(VALUES);
    if (ppClassFieldInfo) {
        pCFI = *ppClassFieldInfo;
        if (pCFI) {
            while (pCFI->pNextFieldInfo) pCFI = pCFI->pNextFieldInfo;
            pCFI->pNextFieldInfo = fieldinfo;
            }
        else *ppClassFieldInfo = fieldinfo;
        }
    return (ULONG)cParentColumns + (ULONG)DETAILS_COLUMNS;
}

Listing 5

All this code does is add the size of our data to the size of our ancestors' data, and add the list of CLASSFIELDINFO structures to the ancestors' list. This completes the first part of our job.

Now we need to provide the actual data in the wpQueryDetailsData method. Listing 6 shows the implementation of the method.

SOM_Scope ULONG   SOMLINK dfd_wpQueryDetailsData(MyDataFile *somSelf, PVOID
*ppDetailsData, PULONG pcp)

{
    /* MyDataFileData *somThis = MyDataFileGetData(somSelf); */
    MyDataFileMethodDebug("MyDataFile","dfd_wpQueryDetailsData");

    parent_wpQueryDetailsData(somSelf,ppDetailsData, pcp);

    if (ppDetailsData) {
        PVALUES pdd = (PVALUES) *ppDetailsData;
        ReadFromDisk(somSelf, pdd);
        *ppDetailsData= pdd + 1;
    }
    else  *pcp += sizeof(VALUES);
    return TRUE;
}

Listing 6

The code reads the complete VALUES structure from the file directly into the details data. ReadFromDisk is responsible for this, and is very similar to WriteToDisk (it is also contained in the values.c file).
Figure 1

After the code is compiled and the class registered, you won't notice anything different about the objects. In fact, our code affects WPFolder objects and objects of derived classes. You can see this by opening the settings notebook of a folder. When you select the last page under the "View" tab, you can see something similar to figure 1 (select the correct object class under "Object type"). Under "Details to display", you can see the two fields we have added.

When you open that folder in details view, it will show the contents of the header of every instance of MyDataFile it contains.
Figure 2
The "Include" tab also shows something new. There now is a completely new settings page for every folder on your desktop (see figure 2). Here you can add criteria the WPS uses to decide which objects to display. When you select the "Add" button, you get a dialog box similar to the one in figure 3.
Figure 3

Figure 3 shows how you would make the WPS only show Datafile Deluxe objects with a Field1 values less that 40.

These features are available for each data item of which the flCompare field of the CLASSFIELDINFO structure contains the flag COMPARE_SUPPORTED. If this field contains the flag SORTBY_SUPPORTED, the contents of the folder can also be sorted based on that item (see the "Sort" tab).

The pMaxCompareValue field contains a pointer to the maximum value of the data item. You can use this to constrain the possible values, and the entry field for adding criteria ("Comparison value" in figure 3) will not allow larger values.

Step 4: Adding a Settings Page

The last thing we'll add to our class is a page in the settings notebook. This is in fact not very difficult. We define an instance method (AddAboutPage) for adding our page and override wpAddSettingsPages so we can really add the page to the settings notebook.

Since a notebook page is in fact a dialog, we also need to provide the functionality of the dialog. I have put the relevant code in the files dialog.c and dialog.h.

Listing 7 shows how we add a page. We only need to fill in a PAGEINFO structure. The interesting fields are in red.

SOM_Scope ULONG   SOMLINK dfd_AddAboutPage(MyDataFile *somSelf,HWND
hwndNotebook)

{
    PAGEINFO pageinfo;

    /* MyDataFileData *somThis = MyDataFileGetData(somSelf); */
    MyDataFileMethodDebug("MyDataFile","dfd_AddAboutPage");

    memset((PCH)&pageinfo, 0, sizeof(PAGEINFO));
    pageinfo.cb = sizeof(PAGEINFO);
    pageinfo.hwndPage = NULLHANDLE;
    pageinfo.usPageStyleFlags = BKA_MAJOR;
    pageinfo.usPageInsertFlags = BKA_FIRST;
    pageinfo.pfnwp = AboutDlgProc;
    pageinfo.resid = hmod;
    pageinfo.dlgid = ID_ABOUT;
    pageinfo.pszName = "~About";
    pageinfo.pCreateParams = somSelf;
    return _wpInsertSettingsPage(somSelf, hwndNotebook, &pageinfo);
}

Listing 7

The most relevant items are discussed below:

pfnwp
This is the dialog procedure to be used with the dialog.
resid
This identifies where the resources are to be found. We use the module handle of our class DLL here. Note that we use the file scope variable hmod without a call to clsQueryModuleHandle. This is because we can't access class methods here (they are methods of a completely different class). The method has been called at the point hmod is needed here, so it indeed contains the correct value.
dlgid
This is the ID of our dialog template.
pszName
This is the text for the page tab.
pCreateParams
This should always be somSelf.

The implementation of wpAddSettingsPages is also very simple, as shown in listing 8.

SOM_Scope BOOL   SOMLINK dfd_wpAddSettingsPages(MyDataFile *somSelf,HWND
hwndNotebook)
{
	/* MyDataFileData *somThis = MyDataFileGetData(somSelf); */
	MyDataFileMethodDebug("MyDataFile","dfd_wpAddSettingsPages");

	return (parent_wpAddSettingsPages(somSelf,hwndNotebook)
		&& _AddAboutPage(somSelf, hwndNotebook));
}

Listing 8

We let the parent add its pages, and then we add our own. The pages are filled from bottom to top, so if we want to add our page to the bottom, we should do so before calling the parent's method. Here we add our page after calling the parent. This adds our page on top of the others.

Listing 9 shows the dialog procedure. In this example, it is kept very simple (and not even completely functional).

MRESULT EXPENTRY AboutDlgProc(HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2)
{

switch(msg) {
        case WM_INITDLG :

            WinSetWindowText(WinWindowFromID(hwnd, DID_ENTRY), "This is a test");

            WinSetFocus(HWND_DESKTOP, WinWindowFromID(hwnd, DID_ENTRY));
            return (MRESULT) TRUE;
        case WM_COMMAND :
            switch (SHORT1FROMMP(mp1)) {
              /* Process commands here */
              }
            return (MRESULT) TRUE;
            break;
    }
    return WinDefDlgProc(hwnd, msg, mp1, mp2);
}

Listing 9

It is important that one of the entry fields have the input focus initially, because otherwise, the settings notebook will lose the input focus in some cases and will even allow another window to get on top of it when the extra page is selected (try this with the Car example). In our example, we explicitly call WinSetFocus to set the focus to one of the fields.

When handling WM_COMMAND, don't pass control to WinDefDlgProc, because if you do, your dialog box will disappear from the notebook when a button is pressed.

You should define the dialog page without titlebar, system menu and border.

Compile and register the class, and you will see the settings page added as the first page in the settings notebook of every instance of our MyDataFile class.

Conclusion

I think you now have a fairly good idea of what WPS programming is and how you can make WPS classes. There are some important things I haven't explained here (such as modifying the popup menu of an object, and opening a view of an object), but I think you can determine how to do these things by examining sample WPS source code with the knowledge you now have (the Car class in the Toolkit, for example, but there are also a few samples available on various electronic systems, such as the one you obtained this EDM from).

I hope these two articles will inspire you to start making your own WPS classes. WPS programming is a bit different from "normal" programming, but it can be very rewarding (and fun). And if you don't want to spend too much time on this, you can always use the source code from the articles as a starting point for your own data files.

One last remark: The current versions of OS/2 come with SOM 1.0, whereas future versions will have SOM 2.0 built-in. SOM 2.0 uses a different syntax to describe classes (Interface Definition Language) but is backwards compatible with previous versions of SOM. I'm only telling you so that you know you'll see some changes in the near future, but you can continue using the syntax used in this and the previous article.

If you have any comments or questions about this or the previous article, feel free to contact me. See elsewhere in this issue for information on how to reach me.