Jump to content

Drawing your own listboxes: Difference between revisions

From EDM2
No edit summary
Ak120 (talk | contribs)
 
(9 intermediate revisions by 2 users not shown)
Line 1: Line 1:
By [[Roger Orr]]
''By [[Roger Orr]]''


==Introduction==
==Introduction==
Presentation manager provides a range of ways to obtain information about messages in the system. One of these ways is to use the 'hook' facility; which means a user provided function to be called by PM during its own processing.
One of the strengths of PM is that it encourages re-use of code with the attendant benefits this brings. This is true of both pure application code and also of the system's own window classes - in particular the standard controls.


A number of such hook points are provided, and the system can be asked to install a hook function for a specific message queue or for all message queues. In order for the latter case to work, you must provide the hook function in a Dynamic Link Library (which can be loaded into the memory space of each process being hooked) to allow the function to be called in the context of the appropriate application.
For those whose PM is a bit rusty, a PM control is a window for user command/control input. A push button and a menu are both examples of controls. They provide the consistent look and feel of PM across all applications.


I thought that a simple example of one such hook might be instructive, both for itself and also to provide a few points of note about the writing of DLLs.
This re-use of code is supported by the PM API in a variety of ways.


==The Journal Record Hook==
For example, a previous article in this magazine by Adrian Thompson described writing a DLL to add line numbers to the standard multi-line edit (MLE) control by re-registering a system defined window class.


The hooks supported by OS/2 are passed a range of parameters; and some also provide a return code. In general any structures pointed to can be altered, and the return code can specify whether or not to continue with the default processing of the message.
Another common way is to use WinSubClassWindow() to replace the usual window procedure for a specific control window with your own, and call the usual window procedure after your own application-specific actions.


For simplicity I decided to install a hook for what is known as the 'journal recording' hook. This hook is designed for the recording of user input (keyboard and mouse) and is often used in conjunction with the 'journal playback' hook for such things as automated testing. This is one of the simplest hooks - the function called returns VOID and no change to the behaviour of PM is supported - but the principles are generally valid for all hooks.
However it appears to be less well known that some of the system controls have standard methods to allow you to replace some or all of the DRAWING of the control, while leaving the other behaviour of the control, such as the focussing and mouse behaviour, unchanged.


All this sample hook function does is to post on any WM_CHAR messages received to a client (using the UWM_JRN_MSG message number) which in this case is responsible for logging them to a window.
The advantage of this method is that since the behaviour of the control is unchanged the PM user will have an intuitive understanding of how it will behave. In addition you do not have to have exhaustive knowledge of all the various actions the control takes for the possible input events such a mouse movement, etc. Finally, if enhancements are made to the way the control operates in subsequent versions of OS/2 you inherit them for nothing.


This simple application therefore provides a method of displaying in a window all the keystrokes received by PM. Note that we are NOT handling all the messages passed to the hook, which also receives mouse messages, WM_JOURNALNOTIFY messages and WM_VIOCHAR messages (for keystrokes in the OS/2 command prompt windows). This hook also gives us the keystrokes for the PM hotkey sequences, such as Ctrl+Esc, which are processed by PM and not passed on directly to an actual window procedure.
==Overview of the example==
I have chosen to describe a simple example of using this technique in a list box. The example program draws a window containing a list of five of the system icons and their name, demonstrating how you can mix text, colour and graphics in a list box.


Our application's interface to the hook function is provided by two the functions JrnStart and JrnStop. Both take one parameter - the client window handle. To improve the resiliency of the DLL we use an exit list internally to ensure the client is tidied up if the application exits without first calling JrnStop.
The basic method is to create a list box with the LS_OWNERDRAW style. PM will then send you two message values:
* a WM_MEASUREITEM message to obtain the height (and optionally the width) of each item
* a WM_DRAWITEM message when a specific item in the list box is to be drawn. The message contains a structure describing the item to draw.
A very similar method is provided for the menu control, whereby the menu item attribute MIS_OWNERDRAW can be specified for some or all of the items in a menu.


The main application uses a simple fixed-font list box to display the received journal messages.
The push button control provides a similar feature by providing a style of BS_USERBUTTON. A WM_CONTROL message of type BN_PAINT is sent when the button needs to be drawn.


==Notes on the code==
==Programming notes==
The WinSetHook() API is used to set all the PM hooks. For global hooks it requires the module handle, and one clean way to provide that is to make the DLL responsible for providing it. It is helpfully provided by the initialisation call which is made by the operating system when the DLL is loaded and unloaded - see below for more about this.
I am using Microsoft C 6.00 and OS/2 1.20 & 1.30. The compilation command I am using is:
cl /G2s drawlist.c -link /pm:pm
The program is pared to the minimum for this article. Note in particular that there is no processing of the LN_SELECT or LN_ENTER commands from the list box control so the example doesn't actually DO anything!


JrnStop is called to explicitly remove the hook using WinReleaseHook, and to reset the internal data so another client can be accepted. (This code only supports one client at a time to keep it simple).
The list box is created in the client area of a size such that the border of the list box does not appear, and the WM_SIZE message is processed to keep the window sizes in step.


Whenever a WM_CHAR message is received (basically one for each key press and key release made) the data is posted to the client window. This window formats the data based on which fields in the input are valid and adds it to a list box. The client window is not visible - it is just used to handle messages and it to keep the size of its child window (the list box) in step with its own size.
The WM_MEASUREITEM message returns the height of each item in the box.
I used the height of the icon plus some spacing for readability.


The SWP_MOVE and SWP_SIZE flags are BOTH required on the WinSetWindowPos of the list box in the client window procedure (unless CS_SIZEREDRAW is specified) since PM will by default move the child window when the client is resized.
The WM_DRAWITEM message processing is the bulk of the interest.


The fields in the character message I have decoded are the character and virtual key code (if valid), the flags word and a 2 letter mnemonic for each bit in the flag word - eg "KU" for KC_KEYUP. Most keys generate either a character (eg 'A') or a virtual key (eg 'PgUp') - some do not generate either (eg 'Ctrl') and some generate BOTH (eg 'Enter').
PM passes a OWNERITEM pointer containing (among other things) a window handle, a presentation space to use for drawing, a rectangle in which to draw, some flags and the application defined item handle.


I have picked WinPostMsg to pass the character message to the application to try and remove the possiblility of hanging the PM input processed by some sort of deadly embrace. If I were to use WinSendMsg the input of the keystroke would be held until the client window processed the message sent to it. Alternatively I could pass data using some linked list or array in the DLL itself. For this example I haven't address queue overflow, or what happens if the client window is destroyed without ending the owning application.
The drawitem() procedure uses the item handle as the address of the data structure which contains a system icon number and a string. (I used system icons because everyone would have them on their machine!)


==Notes about the code in the DLL==
The item handle is set using the LM_SETITEMHANDLE message once the item has been added to the list box.


The DLL must be written to handle potentially multiple threads all trying to call JrnStart and JrnStop simultaneously. To ensure that only one at a time can access the global data we use a Mutex semaphore. This can be initialised during the function called by the operating system loader at library load and unload, which for the IBM C Set ++ compiler is called "_DLL_InitTerm", and will be called once for every process loading the DLL if we specify 'INITINSTANCE' in the linker file "Journal.DEF". If the handle is zero we create the semaphore, and if not we open it (to ensure that it is available to that process). (We are making use of the shared memory for hMutexSem to pass the semaphore handle between the two processes!)
The first point to note is that the rectangle may contain only PART of the list box item being drawn, in particular when the item being drawn is the last visible item and is not complete. For this reason the local rectangle structure is filled in relative to the TOP of the passed rectangle.


I do not like using indefinite waits for semaphores since it is too easy to cause deadlock this way. So I have restricted the timeout to 1 second in JrnStop and JrnStart, and the API will pass this semaphore timeout to the caller if it occurs. The program could log an error and then exit gracefully since in this case a timeout indicates a problem.
The second point to note is that the rectangle may need blanking first, and this is done by the WinFillrect() call.


If the input window's anchor block is destroyed before JrnStop is called to explicitly remove the hook and reset the internal data then PM will remove the hook as part of WinTerminate processing. However the journal DLL will not have cleared its internal data properly and this might stop the application being restarted successfully. To enable the DLL to sort itself out even in this case we add a tidy up function (JrnAbort) to the exit list for the application. This will give us a change to tidy up when the application exits. When the JrnStop API is called we can remove this exit function since the tidy up is no longer required.
Then the icon and text are drawn using the presentation space provided. I have picked different colours for the text to differentiate selected and unselected, note these are not the standard colours!


An alternative inside a DLL is to make further use of the _DLL_InitTerm function - which is called when the DLL is unloaded as well as loaded. However, depending upon compiler options, etc., the C runtime may not be available during the termination function; exit list processing is done BEFORE this occurs and is therefore safer.
The usual system colours for a list box are SYSCLR_WINDOWTEXT on SYSCLR_WINDOW for unselected items and SYSCLR_HILITEFOREGROUND on SYSCLR_HILITEBACKGROUND for selected items. (Under OS/2 1.10 the selected item was shown with the colours inverted.) I recommend using these for some or all of the list box item unless there are strong reasons for another colour choice or highlight method.


Notes about the data in the DLL, with the IBM compiler
The final point to note is that the fsState AND fsStateOld flags in the passed structure are cleared. These flags are used by the list box control to allow you do the drawing but to leave the highlighting to the list box. Unfortunately this is rarely possible since the default highlighting is not usually compatable with the non-default drawing! In this example the highlighting is done by changing the colours used for the text based on the state of the fsState flag.
Setting the flags to 0 before returning tells the list box control that all the drawing and highlighting have been performed.


We want all the global data in the module to be shared. This is the default for DLLs, but there are complications depending upon the particular choices made about compiler options and C runtime used.
;Note:I have noticed problems with list boxes in the client area when WM_ERASEBACKGROUND message processing is non-default. The scroll bar sometimes disappears or is only partly drawn.


The IBM C Set ++ compiler provides a number of choices for this. You may refer to the programming guide for a full list and description.
==Program source==
 
<code>
In my opinion there are two straightforward ways to go from the plethora of available options:
  #define INCL_PM
 
  #include <os2.h>
(a) Don't use the C runtime at all in your DLL, and compile with the /Rn option.
 
(b) Use the C runtime packaged as a DLL in its own right. The only problem with this one is that if you want to ship a DLL, or to run different levels of the compiler, you have to use DLLRNAME to rename the C runtime library.
 
This example is written to use (b) as illustration.
 
The three things required for this method to work are:
 
(i) default data must be non-shared
 
(ii) _CRT_init() must be called in the DLL initialisation function if you provide your own - the default one does this for you - and the DLL must be INITINSTANCE
 
(iii) compile with /Gd+m+ to get the multithreaded DLL C library
 
There are additional things you may need to do to get DLLs using C++ to work - see the IBM documentation.
 
So, firstly, all the global data in this module is to be shared, and so I use the IBM C Set ++ compiler pragma 'data_seg' to explicitly name the segment for three items to be shared. I then use the DATA NONSHARED directive in the linker definition file to make default data items non shared, ie. one copy for each instance of the DLL loaded; and finally use the SEGMENTS directive to make the data in the 'global_data' segment shared. There are other ways to achieve the same result, but this way has the benefit that it is obvious from the code which data items are shared, with all other items non shared.
 
Secondly, as I am providing my own _DLL_InitTerm function I ensure it calls _CRT_init before anything else. In actual fact the C runtime DLL will call this anyway as part of its own initialisation but it is safer to put in an explicit call since we cannot force the order in which OS/2 will initialise DLLs.
 
Thirdly I use the right options in the makefile - and lastly check it all worked by running EXEHDR on the output DLL and looking for one data segment "READABLE, WRITEABLE, SHARED" and one only "READABLE, WRITEABLE".
 
If you look at the code in the Journal.c ytou will notice that it in fact doesn't call any C runtime functions - the only use of the C runtime is for the exception handling (see the next paragraph). So we could convert this particular DLL to method (a) as follows: - remove all the #pragma lines - remove the call to _CRT_init from _DLL_InitTerm - remove the DATA NONSHARED and SEGMENTS directive from the DEF file since all our data is to be shared - change /Gd+m+ to /Rn for the compile and link lines for Journal
 
The last detail I have added is an exception handler for the DLL to allow the C runtime used by the DLL to catch and process any traps or problems occuring during processing. In the IBM C Set ++ environment this is done when the compiler pragma 'handler' is invoked with the name of the function for which to set up an exception handler. A more advanced step would be write our own exception handler to attempt to tidy up - for example releasing the mutex semaphore.
 
==Code==
  ----------------------------- Makefile -------------------------
  target : Journal.dll Showchar.exe
   
   
  Journal.obj : Journal.c Journal.h
  typedef struct _listitem         /* structure of list box handle        */
         icc /Ge- /Gd+m+ /Wall+ppt- /Gs /c /Fo$@ $*.c >$*.err
    {
        type $*.err
    char *text;                    /* test string for item                */
    SHORT icon;                   /* icon index                          */
Journal.dll : Journal.obj Journal.def
     } LISTITEM, FAR *PLISTITEM;
        icc /Ge- /Gd+m+ $** /B/NOE
Journal.lib : Journal.def
        implib $@ $**
ShowChar.obj : ShowChar.c Journal.h
        icc /c /Wall+ppt- $*.c >$*.err
        type $*.err
ShowChar.exe : ShowChar.obj ShowChar.def Journal.lib
        icc $**
----------------------------- Journal.h -------------------------
APIRET APIENTRY JrnStart( HWND hwndClient );
APIRET APIENTRY JrnStop( HWND hwndClient );
#define UWM_JRN_MSG    WM_USER
----------------------------- Journal.c -------------------------
#define INCL_BASE
#define INCL_WIN
#include <os2.h>
#include "Journal.h"              /* Interface definition                    */
extern int _CRT_init( void );     /* C runtime initialisation function      */
   
   
  #pragma data_seg( global_data )  /* All global data must be made shared     */
  LISTITEM list[] = {
     { "Application Icon", SPTR_APPICON },
    { "Information Icon", SPTR_ICONINFORMATION },
    { "Question Mark",    SPTR_ICONQUESTION },
    { "Error Icon",      SPTR_ICONERROR },
    { "Warning Icon",    SPTR_ICONWARNING }
    };
   
   
  static HMODULE hJournal = 0;     /* our own module handle                   */
  HWND        hwndList;             /* list box window handle               */
static HWND HookClient = 0;      /* Window currently getting data          */
static HMTX hMutexSem = 0;        /* To ensure multiple threads work OK      */
   
   
  #pragma data_seg( )
  #define        Y_PAD  1        /* Y spacing - for readability          */
#define        X_PAD  5        /* X spacing                            */
   
   
  /* Ensure externally callable functions use correct exception handler */
  USHORT  cxborder = 0;            /* width of listbox border              */
  #pragma handler( JournalHookProc )
  USHORT  cyborder = 0;            /* height of listbox border            */
  #pragma handler( JrnAbort )
  USHORT  charHeight = 0;          /* height of character in listbox      */
  #pragma handler( JrnStart )
  USHORT cyList = 0;              /* height of list box item              */
  #pragma handler( JrnStop )
   
   
  /*****************************************************************************/
  /**************************************************************************/
  /* _DLL_InitTerm is the function that gets called by the operating system    */
  /* drawitem: function to draw a single owner-draw list box item          */
/* loader when it loads and frees this DLL for each process using it        */
  /**************************************************************************/
  /*****************************************************************************/
   
   
  ULONG APIENTRY _DLL_InitTerm( HMODULE hModule, ULONG ulFlag )
  MRESULT drawitem (POWNERITEM po)
     {
     {
     ULONG lRet = 1;               /* default non-zero implies success */
     PLISTITEM listp;
     RECTL rectl;
     if ( ulFlag == 0 ) /* If ulFlag is zero then the DLL is being loaded */
      {
      hJournal = hModule;
   
   
      if ( _CRT_init() == -1 )    /* First initialise the C runtime          */
    rectl = po->rclItem;
          lRet = 0;
    listp = (PLISTITEM) po->hItem;
      else if ( hMutexSem == 0 )
          {
          if ( DosCreateMutexSem( NULL, &hMutexSem, DC_SEM_SHARED, 0 ) != 0 )
            lRet = 0;
          }
      else if ( DosOpenMutexSem( NULL, &hMutexSem ) != 0 )
          lRet = 0;
      }
   
   
     return lRet;
     /* create correct size rectangle for text display */
     }
    rectl.xLeft += WinQuerySysValue (HWND_DESKTOP, SV_CXICON) + (2 * X_PAD);
    rectl.yBottom = rectl.yTop - ((cyList + charHeight) / 2);
     rectl.yTop = rectl.yBottom + charHeight;
   
   
/*****************************************************************************/
    /* Start with a clean drawing area */
/* JournalHookProc: this function is called for each input message. We      */
    WinFillRect (po->hps, &po->rclItem, SYSCLR_WINDOW);
/* are only interested in WM_CHAR and ignore the others                      */
 
/*****************************************************************************/
    /* Draw the icon ... */
    WinDrawPointer (po->hps,
                    (SHORT)po->rclItem.xLeft + X_PAD,
                    (SHORT)po->rclItem.yTop + Y_PAD - cyList,
                    WinQuerySysPointer (HWND_DESKTOP, listp->icon, FALSE),
                    DP_NORMAL );
   
   
static void EXPENTRY JournalHookProc(
    /* ... and add the text */
     HAB habInstall,
     WinDrawText (po->hps, 0xffff, listp->text, &rectl,
    PQMSG pQmsg )
                po->fsState ? CLR_WHITE : CLR_BLACK,
    {
                po->fsState ? CLR_BLUE : CLR_BACKGROUND,
    habInstall = habInstall; /* Keep compiler happy */
                DT_LEFT | DT_VCENTER | DT_ERASERECT);
   
   
     if ( ( pQmsg->msg == WM_CHAR ) && ( HookClient ) )
     /* tell the system we did ALL the drawing */
      WinPostMsg( HookClient, UWM_JRN_MSG, pQmsg->mp1, pQmsg->mp2 );
    po->fsState = po->fsStateOld = 0;
   
   
     return;
     return (MRESULT) TRUE;
     }
     }
 
/**************************************************************************/
/* Client window procedure                                                */
/**************************************************************************/
   
   
  /*****************************************************************************/
  MRESULT APIENTRY _export _loadds WndProc (HWND hwnd, USHORT msg,
  /* JrnAbort: cope with abnormal process termination                          */
                                          MPARAM mp1, MPARAM mp2 )
/*****************************************************************************/
 
static VOID APIENTRY JrnAbort( ULONG ulTerm )
     {
     {
     ulTerm = ulTerm; /* Keep compiler happy! */
     switch (msg)
    /* PM will decently remove the hook when the process/window dies,
      but we must remove the data to free up for another client */
    HookClient = NULLHANDLE;
    }
/*****************************************************************************/
/* JrnStart: start journal recording for keystrokes - data will be posted    */
/* to the specified window.                                                  */
/*****************************************************************************/
APIRET APIENTRY JrnStart( HWND hwndClient )
    {
    APIRET rc = 0;
    rc = DosRequestMutexSem( hMutexSem, 1000 );
    if ( rc == 0 )
       {
       {
       if ( HookClient != NULLHANDLE )
       case WM_MEASUREITEM:
           rc = ERROR_SHARING_VIOLATION;
          return (MRESULT) cyList;
       else
      case WM_DRAWITEM:
          {
           return drawitem (PVOIDFROMMP(mp2));
           HookClient = hwndClient;
       case WM_SIZE:
           if ( ! WinSetHook( WinQueryAnchorBlock( hwndClient ), NULLHANDLE,
           /* resize list box to fit */
                    HK_JOURNALRECORD, (PFN) JournalHookProc, hJournal ) )
           WinSetWindowPos (hwndList, HWND_TOP, -cxborder, -cyborder,
            rc = ERROR_ACCESS_DENIED;
                          SHORT1FROMMP(mp2) + 2 * cxborder,
          else
                          SHORT2FROMMP(mp2) + cyborder,
            {
                          SWP_MOVE | SWP_SIZE);
            rc = DosExitList( EXLST_ADD, JrnAbort );
           return 0;
            if ( rc != 0 )
                {
                JrnStop( HookClient );
                }
            }
           }
      DosReleaseMutexSem( hMutexSem );
       }
       }
     return WinDefWindowProc (hwnd, msg, mp1, mp2);
     return rc;
     }
     }
   
   
  /*****************************************************************************/
  /**************************************************************************/
  /* JrnStop: stop recording keystrokes                                        */
  /* main procedure                                                        */
  /*****************************************************************************/  
  /**************************************************************************/
   
   
  APIRET APIENTRY JrnStop( HWND hwndClient )
  void cdecl main (void)
     {
     {
     APIRET rc = 0;  
     ULONG  flCreateFlags = FCF_STANDARD & ~FCF_ACCELTABLE &
 
                          ~FCF_MENU    & ~FCF_ICON;
    QMSG    qmsg;
    SWP    swp;
    USHORT  iItem;
    HMQ    hmq;
    HWND    hwndClient, hwndFrame;
    HPS    hPS;
    FONTMETRICS fm;
   
   
     rc = DosRequestMutexSem( hMutexSem, 1000 );
     hmq = WinCreateMsgQueue (WinInitialize(0), 0);
    if ( rc == 0 )
      {
      if ( HookClient != hwndClient )
          rc = ERROR_INVALID_HANDLE;
      else
          {
          WinReleaseHook( WinQueryAnchorBlock( hwndClient ), NULLHANDLE,
                      HK_JOURNALRECORD, (PFN) JournalHookProc, hJournal );  
   
   
          DosExitList( EXLST_REMOVE, JrnAbort );  
    WinRegisterClass (0, "DrawList", (PFNWP)WndProc, 0, 0);
   
   
          HookClient = 0;
    cxborder = (USHORT)WinQuerySysValue (HWND_DESKTOP, SV_CXBORDER);
          }
    cyborder = (USHORT)WinQuerySysValue (HWND_DESKTOP, SV_CYBORDER);
   
   
      DosReleaseMutexSem( hMutexSem );
    hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE,
      }
                                    &flCreateFlags,
                                    "DrawList", "", WS_VISIBLE,
                                    (HMODULE) NULL, 0, &hwndClient);
   
   
     return 0;
     /* set up item height */
     }
     cyList = (USHORT) WinQuerySysValue (HWND_DESKTOP, SV_CYICON) +
 
            (2 * Y_PAD);
----------------------------- Journal.def -------------------------
LIBRARY Journal INITINSTANCE
   
   
DATA    NONSHARED
    /* create list window for the data, ensuring border is invisible */
    WinQueryWindowPos (hwndClient, &swp);
   
   
SEGMENTS
    hwndList = WinCreateWindow (hwndClient, WC_LISTBOX, "",
    global_data  CLASS 'DATA' SHARED
                                LS_NOADJUSTPOS | LS_OWNERDRAW,
                                -cxborder, -cyborder,
                                swp.cx + 2 * cxborder,
                                swp.cy + cyborder,
                                hwndClient, HWND_TOP, 0, NULL, NULL);
   
   
EXPORTS
    /* obtain character height */
JrnStart
    hPS = WinGetPS (hwndList);
JrnStop
    GpiQueryFontMetrics (hPS, sizeof(fm), &fm);
    charHeight = (USHORT) fm.lMaxBaselineExt;
    WinReleasePS (hPS);
   
   
----------------------------- ShowChar.c -------------------------
    /* populate the list box */
#define INCL_BASE
    for (iItem = 0; iItem < sizeof(list) / sizeof(list[0]); iItem++)
#define INCL_PM
#include <os2.h>
#include <stdio.h>
#include <string.h>
#include "Journal.h"
#define ID_LIST 100
/* space + 2 chars for each bit in the WM_CHAR 'fs' field - see PMWIN.H */
#define SZFLAGS " CH VK SC SH CT AL KU PD LK DK CM IN TO IC D1 D2"
/*****************************************************************************/
/* Process WM_CREATE by creating a list box for the output messages          */
/*****************************************************************************/
static void DoCreate( HWND hwnd )
    {
    char szFont[] = "10.Courier";
    HWND hwndList = WinCreateWindow( hwnd, WC_LISTBOX, NULL,
              WS_VISIBLE | LS_NOADJUSTPOS,
              0, 0, 0, 0, hwnd,
              HWND_TOP, ID_LIST, NULL, NULL );
    WinSetPresParam( hwndList, PP_FONTNAMESIZE,
                  sizeof( szFont ), szFont );
    WinSendMsg( hwndList, LM_INSERTITEM, 0, "Chr Vkey Bits" SZFLAGS );
    }
/*****************************************************************************/
/* DoChar: add unpacked WM_CHAR fields into the list box at the top          */
/*****************************************************************************/
static void DoChar( HWND hwnd, PCHRMSG pChar )
    {
    char szBuff[ 120 ] = "";
    char *ptr = szBuff;
    int i = 0;
    if ( ( pChar->fs & KC_CHAR ) /* && ( isprint( pChar->chr ) ) */ )
      ptr += sprintf( ptr, "'%c'", pChar->chr );
    else
      ptr += sprintf( ptr, "  " );
    if ( pChar->fs & KC_VIRTUALKEY )
      ptr += sprintf( ptr, " %4u", pChar->vkey );
    else
      ptr += sprintf( ptr, "    " );
    ptr += sprintf( ptr, " %4.4x",  pChar->fs );
    memset( ptr, ' ', 3*16 );
    ptr[3*16] = '\0';
    for ( i = 0; i < 16; i++ )
       {
       {
       if ( (USHORT)( 1 << i ) & pChar->fs )
       WinSendMsg (hwndList, LM_INSERTITEM, (MPARAM) LIT_END,
          ptr[(i*3)+2] = 'X';
                  list[iItem].text);
      WinSendMsg (hwndList, LM_SETITEMHANDLE, (MPARAM) iItem,
                  &list[iItem]);
       }
       }
   
   
     WinSendDlgItemMsg( hwnd, ID_LIST, LM_INSERTITEM,
     /* finally display the list box */
          MPFROMSHORT( 1 ), szBuff );
    WinShowWindow (hwndList, TRUE);
    WinSetFocus(HWND_DESKTOP, hwndList);
    /* Process all of the messages */
    while (WinGetMsg (NULL, (PQMSG) &qmsg, NULL, 0, 0))
      WinDispatchMsg (NULL, (PQMSG) &qmsg);
     }
     }
</code>
/*****************************************************************************/
/* Window procedure                                                          */
/*****************************************************************************/
MRESULT EXPENTRY ClientWndProc ( HWND hwnd, ULONG msg, MPARAM mp1, MPARAM mp2 )
    {
    MRESULT mr = 0;
  switch (msg)
      {
      case WM_CREATE:
        DoCreate( hwnd );
        break;
      case WM_SIZE:    /* keep list box same size as client */
        WinSetWindowPos( WinWindowFromID( hwnd, ID_LIST ),
                0, 0, 0, SHORT1FROMMP( mp2 ), SHORT2FROMMP( mp2 ), SWP_SIZE );
        break;
      case WM_USER:
        DoChar( hwnd, CHARMSG( &msg ) );
        break;
      default:
        mr = WinDefWindowProc (hwnd, msg, mp1, mp2) ;
        break;
      }
  return mr;
  }
/*****************************************************************************/
/* M A I N  P R O C E D U R E                                              */
/*****************************************************************************/
int main ( void )
  {
  static CHAR  szClientClass [] = "ShowChar" ;
  static ULONG flFrameFlags = FCF_TITLEBAR | FCF_SYSMENU | FCF_SIZEBORDER |
                              FCF_MINMAX  | FCF_SHELLPOSITION | FCF_TASKLIST;
  HWND        hwndFrame = NULLHANDLE, hwndClient = NULLHANDLE;
  QMSG        qmsg = { 0 };
  HAB hab = WinInitialize( 0 );
  HMQ hmq = WinCreateMsgQueue( hab, 0 );
 
  WinRegisterClass( hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0 );
  hwndFrame = WinCreateStdWindow ( HWND_DESKTOP, WS_VISIBLE, &flFrameFlags,
                  szClientClass, "Show Characters", 0L, 0, 0, &hwndClient );
  if ( hwndFrame )
      {
      JrnStart( hwndClient );
      while( WinGetMsg( hab, &qmsg, NULLHANDLE, 0, 0 ) )
          WinDispatchMsg( hab, &qmsg );
      JrnStop( hwndClient );
      WinDestroyWindow( hwndFrame );
      }
  WinDestroyMsgQueue( hmq );
  WinTerminate( hab );
  return 0 ;
  }
----------------------------- ShowChar.def -------------------------
NAME          WINDOWAPI
DESCRIPTION    'Show characters as typed'
----------------------------------------------------------------
==Conclusion==
 
Presentation Manager gives you a flexible and relatively simple way to add function to the various places where hooks are provided. This makes debugging, understanding and testing applications easier since the input data and messages can be captured and examined; and also make it possible to actually modify the behaviour of PM - for example using an 'input' hook.
 
The IBM compiler provides a large number of ways of writing and packaging dynamic link libraries - probably more than you want!
 
Once you have an example of an application and DLL demonstrating one such hook I hope you will be encouraged to experiment further.


[[Category: PM Articles]]
[[Category:PM Articles]]

Latest revision as of 03:44, 7 October 2022

By Roger Orr

Introduction

One of the strengths of PM is that it encourages re-use of code with the attendant benefits this brings. This is true of both pure application code and also of the system's own window classes - in particular the standard controls.

For those whose PM is a bit rusty, a PM control is a window for user command/control input. A push button and a menu are both examples of controls. They provide the consistent look and feel of PM across all applications.

This re-use of code is supported by the PM API in a variety of ways.

For example, a previous article in this magazine by Adrian Thompson described writing a DLL to add line numbers to the standard multi-line edit (MLE) control by re-registering a system defined window class.

Another common way is to use WinSubClassWindow() to replace the usual window procedure for a specific control window with your own, and call the usual window procedure after your own application-specific actions.

However it appears to be less well known that some of the system controls have standard methods to allow you to replace some or all of the DRAWING of the control, while leaving the other behaviour of the control, such as the focussing and mouse behaviour, unchanged.

The advantage of this method is that since the behaviour of the control is unchanged the PM user will have an intuitive understanding of how it will behave. In addition you do not have to have exhaustive knowledge of all the various actions the control takes for the possible input events such a mouse movement, etc. Finally, if enhancements are made to the way the control operates in subsequent versions of OS/2 you inherit them for nothing.

Overview of the example

I have chosen to describe a simple example of using this technique in a list box. The example program draws a window containing a list of five of the system icons and their name, demonstrating how you can mix text, colour and graphics in a list box.

The basic method is to create a list box with the LS_OWNERDRAW style. PM will then send you two message values:

  • a WM_MEASUREITEM message to obtain the height (and optionally the width) of each item
  • a WM_DRAWITEM message when a specific item in the list box is to be drawn. The message contains a structure describing the item to draw.

A very similar method is provided for the menu control, whereby the menu item attribute MIS_OWNERDRAW can be specified for some or all of the items in a menu.

The push button control provides a similar feature by providing a style of BS_USERBUTTON. A WM_CONTROL message of type BN_PAINT is sent when the button needs to be drawn.

Programming notes

I am using Microsoft C 6.00 and OS/2 1.20 & 1.30. The compilation command I am using is:

cl /G2s drawlist.c -link /pm:pm

The program is pared to the minimum for this article. Note in particular that there is no processing of the LN_SELECT or LN_ENTER commands from the list box control so the example doesn't actually DO anything!

The list box is created in the client area of a size such that the border of the list box does not appear, and the WM_SIZE message is processed to keep the window sizes in step.

The WM_MEASUREITEM message returns the height of each item in the box. I used the height of the icon plus some spacing for readability.

The WM_DRAWITEM message processing is the bulk of the interest.

PM passes a OWNERITEM pointer containing (among other things) a window handle, a presentation space to use for drawing, a rectangle in which to draw, some flags and the application defined item handle.

The drawitem() procedure uses the item handle as the address of the data structure which contains a system icon number and a string. (I used system icons because everyone would have them on their machine!)

The item handle is set using the LM_SETITEMHANDLE message once the item has been added to the list box.

The first point to note is that the rectangle may contain only PART of the list box item being drawn, in particular when the item being drawn is the last visible item and is not complete. For this reason the local rectangle structure is filled in relative to the TOP of the passed rectangle.

The second point to note is that the rectangle may need blanking first, and this is done by the WinFillrect() call.

Then the icon and text are drawn using the presentation space provided. I have picked different colours for the text to differentiate selected and unselected, note these are not the standard colours!

The usual system colours for a list box are SYSCLR_WINDOWTEXT on SYSCLR_WINDOW for unselected items and SYSCLR_HILITEFOREGROUND on SYSCLR_HILITEBACKGROUND for selected items. (Under OS/2 1.10 the selected item was shown with the colours inverted.) I recommend using these for some or all of the list box item unless there are strong reasons for another colour choice or highlight method.

The final point to note is that the fsState AND fsStateOld flags in the passed structure are cleared. These flags are used by the list box control to allow you do the drawing but to leave the highlighting to the list box. Unfortunately this is rarely possible since the default highlighting is not usually compatable with the non-default drawing! In this example the highlighting is done by changing the colours used for the text based on the state of the fsState flag. Setting the flags to 0 before returning tells the list box control that all the drawing and highlighting have been performed.

Note
I have noticed problems with list boxes in the client area when WM_ERASEBACKGROUND message processing is non-default. The scroll bar sometimes disappears or is only partly drawn.

Program source

#define INCL_PM
#include <os2.h>

typedef struct _listitem          /* structure of list box handle         */
   {
   char *text;                    /* test string for item                 */
   SHORT icon;                    /* icon index                           */
   } LISTITEM, FAR *PLISTITEM;

LISTITEM list[] = {
   { "Application Icon", SPTR_APPICON },
   { "Information Icon", SPTR_ICONINFORMATION },
   { "Question Mark",    SPTR_ICONQUESTION },
   { "Error Icon",       SPTR_ICONERROR },
   { "Warning Icon",     SPTR_ICONWARNING }
   };

HWND        hwndList;             /* list box window handle               */

#define         Y_PAD   1         /* Y spacing - for readability          */
#define         X_PAD   5         /* X spacing                            */

USHORT  cxborder = 0;             /* width of listbox border              */
USHORT  cyborder = 0;             /* height of listbox border             */
USHORT  charHeight = 0;           /* height of character in listbox       */
USHORT  cyList = 0;               /* height of list box item              */

/**************************************************************************/
/* drawitem: function to draw a single owner-draw list box item           */
/**************************************************************************/

MRESULT drawitem (POWNERITEM po)
   {
   PLISTITEM listp;
   RECTL rectl;

   rectl = po->rclItem;
   listp = (PLISTITEM) po->hItem;

   /* create correct size rectangle for text display */
   rectl.xLeft += WinQuerySysValue (HWND_DESKTOP, SV_CXICON) + (2 * X_PAD);
   rectl.yBottom = rectl.yTop - ((cyList + charHeight) / 2);
   rectl.yTop = rectl.yBottom + charHeight;

   /* Start with a clean drawing area */
   WinFillRect (po->hps, &po->rclItem, SYSCLR_WINDOW);
 
   /* Draw the icon ... */
   WinDrawPointer (po->hps,
                   (SHORT)po->rclItem.xLeft + X_PAD,
                   (SHORT)po->rclItem.yTop + Y_PAD - cyList,
                   WinQuerySysPointer (HWND_DESKTOP, listp->icon, FALSE),
                   DP_NORMAL );

   /* ... and add the text */
   WinDrawText (po->hps, 0xffff, listp->text, &rectl,
                po->fsState ? CLR_WHITE : CLR_BLACK,
                po->fsState ? CLR_BLUE : CLR_BACKGROUND,
                DT_LEFT | DT_VCENTER | DT_ERASERECT);

   /* tell the system we did ALL the drawing */
   po->fsState = po->fsStateOld = 0;

   return (MRESULT) TRUE;
   }
 
/**************************************************************************/
/* Client window procedure                                                */
/**************************************************************************/

MRESULT APIENTRY _export _loadds WndProc (HWND hwnd,  USHORT msg,
                                          MPARAM mp1, MPARAM mp2 )
   {
   switch (msg)
      {
      case WM_MEASUREITEM:
         return (MRESULT) cyList;
      case WM_DRAWITEM:
         return drawitem (PVOIDFROMMP(mp2));
      case WM_SIZE:
         /* resize list box to fit */
         WinSetWindowPos (hwndList, HWND_TOP, -cxborder, -cyborder,
                          SHORT1FROMMP(mp2) + 2 * cxborder,
                          SHORT2FROMMP(mp2) + cyborder,
                          SWP_MOVE | SWP_SIZE);
         return 0;
      }
   return WinDefWindowProc (hwnd, msg, mp1, mp2);
   }

/**************************************************************************/
/* main procedure                                                         */
/**************************************************************************/

void cdecl main (void)
   {
   ULONG   flCreateFlags = FCF_STANDARD & ~FCF_ACCELTABLE &
                          ~FCF_MENU     & ~FCF_ICON;
   QMSG    qmsg;
   SWP     swp;
   USHORT  iItem;
   HMQ     hmq;
   HWND    hwndClient, hwndFrame;
   HPS     hPS;
   FONTMETRICS fm;

   hmq = WinCreateMsgQueue (WinInitialize(0), 0);

   WinRegisterClass (0, "DrawList", (PFNWP)WndProc, 0, 0);

   cxborder = (USHORT)WinQuerySysValue (HWND_DESKTOP, SV_CXBORDER);
   cyborder = (USHORT)WinQuerySysValue (HWND_DESKTOP, SV_CYBORDER);

   hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE,
                                   &flCreateFlags,
                                   "DrawList", "", WS_VISIBLE,
                                   (HMODULE) NULL, 0, &hwndClient);

   /* set up item height */
   cyList = (USHORT) WinQuerySysValue (HWND_DESKTOP, SV_CYICON) +
            (2 * Y_PAD);

   /* create list window for the data, ensuring border is invisible */
   WinQueryWindowPos (hwndClient, &swp);

   hwndList = WinCreateWindow (hwndClient, WC_LISTBOX, "",
                               LS_NOADJUSTPOS | LS_OWNERDRAW,
                               -cxborder, -cyborder,
                               swp.cx + 2 * cxborder,
                               swp.cy + cyborder,
                               hwndClient, HWND_TOP, 0, NULL, NULL);

   /* obtain character height */
   hPS = WinGetPS (hwndList);
   GpiQueryFontMetrics (hPS, sizeof(fm), &fm);
   charHeight = (USHORT) fm.lMaxBaselineExt;
   WinReleasePS (hPS);

   /* populate the list box */
   for (iItem = 0; iItem < sizeof(list) / sizeof(list[0]); iItem++)
      {
      WinSendMsg (hwndList, LM_INSERTITEM, (MPARAM) LIT_END,
                  list[iItem].text);
      WinSendMsg (hwndList, LM_SETITEMHANDLE, (MPARAM) iItem,
                  &list[iItem]);
      }

   /* finally display the list box */
   WinShowWindow (hwndList, TRUE);
   WinSetFocus(HWND_DESKTOP, hwndList);

   /* Process all of the messages */
   while (WinGetMsg (NULL, (PQMSG) &qmsg, NULL, 0, 0))
      WinDispatchMsg (NULL, (PQMSG) &qmsg);
   }