Jump to content

Drawing your own listboxes: Difference between revisions

From EDM2
No edit summary
mNo edit summary
Line 2: Line 2:


==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.


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.
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.


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.
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.


==The Journal Record Hook==
This re-use of code is supported by the PM API in a variety of ways.


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.
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.


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.
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.


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.
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.


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.
The advantages 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.


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.
==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 main application uses a simple fixed-font list box to display the received journal messages.
The basic method is to create a list box with the LS_OWNERDRAW style. PM will then send you two message values:


==Notes on the code==
    o a WM_MEASUREITEM message to obtain the height (and optionally the
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.
      width) of each item


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).
    o 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.


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 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.
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 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').
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.


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.
==Programming notes==


==Notes about the code in the DLL==
I am using Microsoft C6.00 and OS/2 1.20 & 1.30.  The compilation command I am using is:


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!)
        cl /G2s drawlist.c -link /pm:pm


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 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!


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.
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.


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 WM_MEASUREITEM message returns the height of each item in the box.
I used the height of the icon plus some spacing for readability.


Notes about the data in the DLL, with the IBM compiler
The WM_DRAWITEM message processing is the bulk of the interest.


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.
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 IBM C Set ++ compiler provides a number of choices for this. You may refer to the programming guide for a full list and description.
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!)


In my opinion there are two straightforward ways to go from the plethora of available options:
The item handle is set using the LM_SETITEMHANDLE message once the item has been added to the list box.


(a) Don't use the C runtime at all in your DLL, and compile with the /Rn option.
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 rectange strcture is filled in relative to the TOP of the passed rectangle.


(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.
The second point to note is that the rectangle may need blanking first, and this is done by the WinFillrect() call.


This example is written to use (b) as illustration.
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 three things required for this method to work are:
  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.


(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
  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.


(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.
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.


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.
==Program source==


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".
  #define INCL_PM
  #include <os2.h>


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
  typedef struct _listitem          /* structure of list box handle        */
    {
    char *text;                    /* test string for item                */
    SHORT icon;                    /* icon index                          */
    } LISTITEM, FAR *PLISTITEM;


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.
  LISTITEM list[] = {
    { "Application Icon", SPTR_APPICON },
    { "Information Icon", SPTR_ICONINFORMATION },
    { "Question Mark",    SPTR_ICONQUESTION },
    { "Error Icon",      SPTR_ICONERROR },
    { "Warning Icon",    SPTR_ICONWARNING }
    };


==Code==
   HWND        hwndList;             /* list box window handle              */
----------------------------- Makefile -------------------------
target : Journal.dll Showchar.exe
Journal.obj : Journal.c Journal.h
        icc /Ge- /Gd+m+ /Wall+ppt- /Gs /c /Fo$@ $*.c >$*.err
        type $*.err
Journal.dll : Journal.obj Journal.def
        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    */
static HMODULE hJournal = 0;      /* our own module handle                  */
static HWND HookClient = 0;      /* Window currently getting data          */
static HMTX hMutexSem = 0;        /* To ensure multiple threads work OK      */
#pragma data_seg( )
/* Ensure externally callable functions use correct exception handler */
#pragma handler( JournalHookProc )
#pragma handler( JrnAbort )
#pragma handler( JrnStart )
#pragma handler( JrnStop )
/*****************************************************************************/
/* _DLL_InitTerm is the function that gets called by the operating system    */
/* loader when it loads and frees this DLL for each process using it        */
/*****************************************************************************/
ULONG APIENTRY _DLL_InitTerm( HMODULE hModule, ULONG ulFlag )
    {
    ULONG lRet = 1;                /* default non-zero implies success */
    if ( ulFlag == 0 ) /* If ulFlag is zero then the DLL is being loaded */
       {
      hJournal = hModule;
      if ( _CRT_init() == -1 )    /* First initialise the C runtime          */
          lRet = 0;
      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;
    }
/*****************************************************************************/
/* JournalHookProc: this function is called for each input message.  We      */
/* are only interested in WM_CHAR and ignore the others                      */
/*****************************************************************************/
static void EXPENTRY JournalHookProc(
    HAB habInstall,
    PQMSG pQmsg )
    {
    habInstall = habInstall; /* Keep compiler happy */
    if ( ( pQmsg->msg == WM_CHAR ) && ( HookClient ) )
      WinPostMsg( HookClient, UWM_JRN_MSG, pQmsg->mp1, pQmsg->mp2 );
    return;
    }
/*****************************************************************************/
/* JrnAbort: cope with abnormal process termination                          */
/*****************************************************************************/  


static VOID APIENTRY JrnAbort( ULONG ulTerm )
  #define        Y_PAD  1        /* Y spacing - for readability          */
    {
  #define        X_PAD  5        /* X spacing                            */
    ulTerm = ulTerm; /* Keep compiler happy! */
    /* 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 )
          rc = ERROR_SHARING_VIOLATION;
      else
          {
          HookClient = hwndClient;
          if ( ! WinSetHook( WinQueryAnchorBlock( hwndClient ), NULLHANDLE,
                    HK_JOURNALRECORD, (PFN) JournalHookProc, hJournal ) )
            rc = ERROR_ACCESS_DENIED;
          else
            {
            rc = DosExitList( EXLST_ADD, JrnAbort );
            if ( rc != 0 )
                {
                JrnStop( HookClient );
                }
            }
          }
      DosReleaseMutexSem( hMutexSem );
      }
    return rc;
    }


   
  USHORT cxborder = 0;            /* width of listbox border              */
/*****************************************************************************/
  USHORT cyborder = 0;            /* height of listbox border            */
  /* JrnStop: stop recording keystrokes                                        */
  USHORT charHeight = 0;          /* height of character in listbox      */
  /*****************************************************************************/  
   USHORT cyList = 0;               /* height of list box item              */
APIRET APIENTRY JrnStop( HWND hwndClient )
    {
    APIRET rc = 0;
    
   
    rc = DosRequestMutexSem( hMutexSem, 1000 );
    if ( rc == 0 )
      {
      if ( HookClient != hwndClient )
          rc = ERROR_INVALID_HANDLE;
      else
          {
          WinReleaseHook( WinQueryAnchorBlock( hwndClient ), NULLHANDLE,
                      HK_JOURNALRECORD, (PFN) JournalHookProc, hJournal );
          DosExitList( EXLST_REMOVE, JrnAbort );
          HookClient = 0;
          }
      DosReleaseMutexSem( hMutexSem );
      }
    return 0;
    }


----------------------------- Journal.def -------------------------
LIBRARY Journal INITINSTANCE
DATA    NONSHARED
SEGMENTS
    global_data  CLASS 'DATA' SHARED
EXPORTS
JrnStart
JrnStop
----------------------------- ShowChar.c -------------------------
#define INCL_BASE
#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 )
          ptr[(i*3)+2] = 'X';
      }
    WinSendDlgItemMsg( hwnd, ID_LIST, LM_INSERTITEM,
          MPFROMSHORT( 1 ), szBuff );
    }
/*****************************************************************************/
/* 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.
  /**************************************************************************/
  /* drawitem: function to draw a single owner-draw list box item          */
  /**************************************************************************/


The IBM compiler provides a large number of ways of writing and packaging dynamic link libraries - probably more than you want!
  MRESULT drawitem (POWNERITEM po)
    {
    PLISTITEM listp;
    RECTL rectl;


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]]
    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);
    }
 
 
 
==Postscript - CDD.SYS==
 
I have had several queries passed on to me following the Technical Tip in the September/October issue.  These queries are still arriving so I thought it worth describing the three commonest problems to save any other people wasting time unnecessarily.
 
Firstly, the compilation command was in error.  The correct command should be:
    cl /G2s /W3 cdd.c cdd.def /Fecdd.sys os2.lib
 
The original command gave link errors L2025 and L2029 caused by the attempt to resolve references to stack checking.
 
Secondly, a misprint of pkt> instead of pkt-> (for example pkt>PktCmd not pkt->PktCmd) confused some programmers less familiar with C.
 
Thirdly, on some versions of OS/2, if OS/2 tracing is turned on the keyboard subsystem hangs when the call to KbdPeek() is made.
 
I am sorry for the wasted time these three problems have caused some people, and hope those who persevered found it was worth while.

Revision as of 22:45, 1 January 2012

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 advantages 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:

   o a WM_MEASUREITEM message to obtain the height (and optionally the
     width) of each item
   o 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 C6.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 rectange strcture 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);
    }


Postscript - CDD.SYS

I have had several queries passed on to me following the Technical Tip in the September/October issue. These queries are still arriving so I thought it worth describing the three commonest problems to save any other people wasting time unnecessarily.

Firstly, the compilation command was in error. The correct command should be:

    cl /G2s /W3 cdd.c cdd.def /Fecdd.sys os2.lib

The original command gave link errors L2025 and L2029 caused by the attempt to resolve references to stack checking.

Secondly, a misprint of pkt> instead of pkt-> (for example pkt>PktCmd not pkt->PktCmd) confused some programmers less familiar with C.

Thirdly, on some versions of OS/2, if OS/2 tracing is turned on the keyboard subsystem hangs when the call to KbdPeek() is made.

I am sorry for the wasted time these three problems have caused some people, and hope those who persevered found it was worth while.