Drawing your own listboxes

From EDM2
Jump to: navigation, search

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:

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