All the Little Boxes... - Where do they all come from?

by Jon Waterhouse

Published 1998-11-01    Printer-friendly version

Download the code here

Several years ago, when I was first asked to write a program to help with movie production scheduling, I had a paper model of the process to work from — all the reports that were originally prepared manually. A major step close to the beginning of the process was to group together sets of scenes that would all be shot on one day. The paper version worked like this: you took the full list of scenes, ran it through a waxer, cut the sheets up with one scene per strip, lay thirty or forty blank sheets of paper around the room (matching the planned number of shoot days), and then went through the scene strips one by one, sticking them on the appropriate sheet of paper. At any time scenes could be taken off a day they had been placed on and moved somewhere more appropriate.

People adopt computer programs faster when it doesn’t change their habitual way of working too much, and I have always wanted to be able to provide the electronic equivalent of the sticky scenes and the days to stick them on. This article explains how to accomplish this scheme.

My previous version of the film scheduling program, which looks like the screen below, provided most of the functionality: a day browse allows you to select an existing day or add a day; a browse box (the one at the bottom of the screen) shows the scenes on the selected day, and another browse box shows all the scenes as yet unallocated. Scenes can be dragged from the unallocated list to any particular day (dropping on a particular item on the day list), and moved from the currently active day to any other day. The only real functionality missing was the ability to look at the list of scenes on several days at the same time. In addition, a small but important point, the system did not closely resemble the paper version it was supposed to replace.

Film.gif

In addition to the movie production scenario, this set of drag and drop lists can be used anywhere that things have to be divided into groups. For example, it can be used to divide students into activity groups. Each student takes several activities, and each activity has several students. It’s a classic many-to-many situation and can be handled by creating a members file that contains just the student ID and the ID of the activity. This is the type of application that the rest of the article will look at.

So let’s look at the essential design criteria for our system:

  • It should allow the user to create an arbitrary number of activity groups on which to drop students. These activity groups should be mobile on the screen to allow easy viewing of related groups.
  • The user should be able to drag and drop students between each of the groups and to and from a list of unallocated students.
  • The list of students in each of the groups should be visible when desired.

We’re obviously talking about creating a bunch of similar listboxes here, so let’s also examine how what we are trying to do differs from adding just one browse box to the window at design time.

  • The list box will not be included in the screen structure — it will be created using CREATE(). All of the list box properties (like size, DropID, FORMAT etc.) will have to be explicitly declared after creation of the new control.
  • The regular browse box (and drop list) declares a VIEW and a QUEUE. We can reuse the VIEW to populate the queues in each of our listboxes, but we will have to have a separate queue for each one. The regular queue should therefore be created as a TYPE, and each new listbox will have to create (and destroy) a listbox of this type.
  • When a regular listbox is created its field equate number is known, and code can be added into the CASE FIELD() statement to manage events for the field. Since the field equate labels are not known in advance, this is not an option. As new lists are added to the window, we will maintain a queue of the FEQs for the lists and where the events must be passed for processing.
  • Listboxes are not set up for being moved around by the user, although the innate capability (changing PROP:Xpos and PROP:Ypos) exists. We will have to add something to each listbox to allow them to be moved around.
  • Each of our listboxes will be range-limited. Each one will show the students in a particular activity (which is specified when the box is created). Each box should remember the range limit that it was set up with (and the associated name activity).

My choice to meet the design issues above was to create a class derived from the DropListClass. I guess I could just as well have chosen the BrowseClass, but the browse class is a little bit heavy duty for what we require — we don’t need to worry about step locators or edit-in-place. And we can create use the DropList class to manage a listbox that looks like an ordinary listbox; we just CREATE() a List control instead of a Droplist control.

In order to use the DropListClass I had to remove the PRIVATE property from the ListControl property. Since the control is created it must also be DESTROYed, and this requires that the Kill procedure in the derived class can get the ListControl property so it knows what to delete.

Our listboxes are going to be set up to display the students in a particular activity. The primary file will be the MEMBERS file; they will grab the matching information for each STUDENT, and they will be limited to a single activity. The screen will look something like the screen below, where all of the six listboxes at the bottom of the screen have been added by the user.

Orff.gif

First of all, let’s look at the new properties and methods that we need to add to the class:

MultiDropClass CLASS(FileDropClass),|
               MODULE('ABMULTI.CLW'),|
               TYPE,|
               LINK('ABMULTI.CLW',|
                    _ABCLinkMode_),|
               DLL(_ABCDllMode_)
MbrActLimit      LONG
ActLabel         STRING(25)
LabelFEQ         SIGNED
RegionFEQ        SIGNED
MoveRegion       SIGNED
BRQ              &Mbr:Queue:Browse
Init             PROCEDURE(SHORT ActLimit,|
                           WindowManager WM,|
                           Signed MoveRegion)
Kill             PROCEDURE(),VIRTUAL
SetQueueRecord   PROCEDURE(),VIRTUAL
ListEvent        PROCEDURE(Signed eventnum),|
                   BYTE,VIRTUAL
RegionEvent      PROCEDURE(Signed eventnum),|
                   BYTE,VIRTUAL
               END

MbrActLimit is the activity ID that provides the range limit for the box.

ActLabel is the name of the activity (which will be used to provide a title for the box)

LabelFEQ is the field equate for the title that goes above the box

RegionFEQ is the field equate for the region that will be used to allow box movement

MoveRegion is the label of a region on the window that defines the area where the box can be placed

BRQ is a reference to a queue (of the type that the listbox will use)

We also derive a few methods for the class. Init has to be replaced for many reasons. Kill must get rid of the controls and the queues that are created by Init. SetQueueRecord is derived just because we want to create a full name from FirstName and LastName.

ListEvent and RegionEvent are the event handlers for the list and region parts of the controls. Unlike the other methods they do not call parent methods of the same name.

Now, let’s look at the code for the derived class.

Up at the top of the class we have the queue declaration exactly as it would be for a single listbox, but declared as a TYPE, rather than the declaration of the actual clue. The BRQ reference in the class refers to this type of queue.

Following the queue we have the VIEW declaration, again exactly as we would for a single listbox. Then follow each of the methods. Each one is preceded by a description of what it does.

  MEMBER('multibox.clw')
  INCLUDE('ABDROPS.INC')
  INCLUDE('ABWINDOW.INC')
  MAP.
Mbr:Queue:Browse   QUEUE,TYPE
fullname             STRING(25)
STU:BOOK             LIKE(STU:BOOK)
STU:PIECE            LIKE(STU:PIECE)
STU:AGE              LIKE(STU:AGE)
STU:FNAME            LIKE(STU:FNAME)
STU:LNAME            LIKE(STU:LNAME)
Mbr:PARTICIPANT      LIKE(Mbr:PARTICIPANT)
Mbr:ACTIVITY         LIKE(Mbr:ACTIVITY)
STU:ID               LIKE(STU:ID)
ViewPosition         STRING(1024)
                   END
MbrView            VIEW(MEMBERS)
                     PROJECT(Mbr:PARTICIPANT)
                     PROJECT(Mbr:ACTIVITY)
                     JOIN(STU:BY_ID,Mbr:PARTICIPANT)
                       PROJECT(STU:BOOK)
                       PROJECT(STU:PIECE)
                       PROJECT(STU:AGE)
                       PROJECT(STU:FNAME)
                       PROJECT(STU:LNAME)
                       PROJECT(STU:ID)
                     END
                   END

Init is the most complicated of the methods that have to be derived. A lot of the code in the Init procedure for the class is code that would normally be found in the Init procedure for the window. In addition, a lot of the code is necessary because the properties of the created controls have to be set in the Init method rather than being declared in the screen structure. In addition to the code belonging to the list box itself, the Init method also creates two new controls. One is a string control that contains the name of the activity for which the box contains members. The second is a region that we will use to allow the user to move the control around the screen.

The CREATE() statement adds a control to the screen. The control is initially hidden, and this allows us to set all of the properties that we want before we UNHIDE() the control. You will see that the UNHIDE() statements are found at the end of the Init method. According to the documentation, the first parameter can be omitted; however, this does not seem to work in practise, so we start by explicitly determining what the FEQ of the new control should be.

The way that we want to move the box about is to initiate a move, move the mouse to where we want to position the box, and place the box on a left mouse click (we’ll allow a double-click, too). To do this we check for action using the KEYCODE() statement. Surprisingly (maybe), you will only get a keycode returned if the click occurs on a control; anywhere else and nothing happens. To deal with this we therefore need to set up a region on the window that is activated at the beginning of the move, and will accept the mouse click. Once the move is complete we disable the control again so that it doesn’t interfere with anything else. We also use this region when placing the control originally.

MultiBoxClass.Init  PROCEDURE(SHORT ActLimit,|
                              WindowManager WM,|
                              SIGNED MoveRegion)
MBRFEQ              SIGNED
  CODE
  !The range limit for the box
  SELF.MbrActLimit = ActLimit
  !The region in which the control is allowed to move
  SELF.MoveRegion = MoveRegion
  !Get an FEQ for the new List
  MbrFEQ = LASTFIELD() + 1
  !Create the list control
  CREATE(MbrFEQ,CREATE:List)
  !Create a new queue
  SELF.BRQ &= NEW Mbr:Queue:Browse
  PARENT.Init(MBRFEQ,SELF.BRQ.ViewPosition,|
              MBRView,SELF.BRQ,Relate:Members,|
              WM)
  !use the key to do the range limit
  SELF.AddSortOrder(Mbr:BY_ACTIVITY)
  !And sort by student name (view manager)
  SELF.AppendOrder(+STU:Lname,+STU:Fname)
  !Set up the range limit
  SELF.AddRange(Mbr:ACTIVITY,SELF.MbrActLimit)
  ! The view fields are linked with the
  ! matching fields in the queue
  SELF.AddField(Mbr:PARTICIPANT,|
                SELF.BRQ.Mbr:PARTICIPANT)
  SELF.AddField(Mbr:ACTIVITY,SELF.BRQ.Mbr:ACTIVITY)
  SELF.AddField(STU:BOOK,SELF.BRQ.STU:BOOK)
  SELF.AddField(STU:PIECE,SELF.BRQ.STU:PIECE)
  SELF.AddField(STU:AGE,SELF.BRQ.STU:AGE)
  SELF.AddField(STU:FNAME,SELF.BRQ.STU:FNAME)
  SELF.AddField(STU:LNAME,SELF.BRQ.STU:LNAME )
  SELF.AddField(Mbr:PARTICIPANT,SELF.BRQ.Mbr:PARTICIPANT)
  SELF.AddField(Mbr:ACTIVITY ,SELF.BRQ.Mbr:ACTIVITY)
  SELF.DefaultFill = 0
  !We get the current position of the mouse for position
  MbrFEQ{PROP:Xpos} = MouseX()
  MbrFEQ{PROP:Ypos} = MouseY()+15
  MbrFEQ{PROP:Width} = 180
  MbrFEQ{PROP:Height} = 40
  MbrFEQ{PROP:Format}='100R(2)|M~Name~L(0)@s25@12R(1)'|
                     &'|M~Bk~C(0)@n2@15R(1)|M~Pc~C(0)'|
                     &'@n2@24R(1) |M~AGE~C(0)@n2@'
  !We designate our new queue as the FROM queue for the list
  MbrFEQ{PROP:From}=SELF.BRQ
  !We set up the ability to drag and drop between boxes
  MbrFEQ{PROP:DropID} = 'MoveMembers'
  MbrFEQ{PROP:DragID} = 'MoveMembers'
  !We get the activity record so we can get the name
  Act:ID = SELF.MbrActLimit
  Access:Activity.Fetch(Act:By_ID)
  SELF.ActLabel = ACT:Name
  !Add the label above the list box
  SELF:LabelFEQ = MbrFEQ + 1
  CREATE(SELF.LabeLFEQ,CREATE:String)
  IF SELF.LabelFEQ > 0
    SELF.LabelFEQ{PROP:Xpos} = MOUSEX()
    SELF.LabelFEQ{PROP:Ypos} = MOUSEY()
    SELF.LabelFEQ{PROP:Width} = 180
    SELF.LabelFEQ{PROP:Height} = 15
    SELF.LabelFEQ{PROP:Text} = ACT:Name
    SELF.LabelFEQ{PROP:FontSize} = 14
  END
  !And we set up a region over the same area
  SELF.RegionFEQ=MbrFEQ+2
  CREATE(SELF.RegionFEQ,CREATE:Region)
  IF SELF.RegionFEQ > 0
    SELF.RegionFEQ{PROP:Xpos} = MouseX()
    SELF.RegionFEQ{PROP:Ypos} = MouseY()
    SELF.RegionFEQ{PROP:Width} = 180
    SELF.RegionFEQ{PROP:Height} = 15
    !Makes a yellow border
    SELF.RegionFEQ{PROP:FillColor} = COLOR:Yellow
  END
  !Unhide all three controls
  UNHIDE(MbrFEQ)
  UNHIDE(SELF.LabelFEQ)
  UNHIDE(SELF.RegionFEQ)

In the kill method we have to make sure that the three controls that we made using CREATE() and the queue that we created with NEW() are disposed of. We also need to make sure that the Kill method is called when the window is killed, and this requires that we make a call to WindowManager.AddItem for the new instance of the class when it is created.

MultiBoxClass.Kill PROCEDURE
  CODE
  DISPOSE(SELF.BRQ)
  DESTROY(SELF.ListControl)
  DESTROY(SELF.LabelFEQ)
  DESTROY(SELF.RegionFEQ)
  PARENT.Kill

Nothing special here; just reformatting the student’s name.

MultiBoxClass.SetQueueRecord PROCEDURE
  CODE
  SELF.BRQ.fullname=clip(STU:fname) & ' ' & STU:lname
  PARENT.SetQueueRecord

I created two methods to deal with events; one for events on the list box itself, specifically drag and drop events, the other to handle clicks on the title region, which are used to move the box around. In the list box, on a drag we want to make sure that we know which student is being dragged.  Fortunately the TakeNewSelection method for the DropList class regets the record corresponding to the highlighted item in the queue. We return a value of 1 to the Window on a drag. This will mark this particular listbox as requiring a ResetQueue. The reset of the queue will be carried out when a value of 2 is returned to the window (which you can see that we return after a drop). The reason for this is that we don’t want to waste energy updating all of the list boxes on the window. When we do a drag and drop from one box to another, we need to refresh only those two boxes. We can refresh the box on which the drop happened immediately (and you can see that we do). However, we also need to remove the dropped student from the box that he/she came from. When a student is dropped we therefore loop through the boxes that have been added to the window, and refresh the one that we have marked when we started the drag.

MultiBoxClass.ListEvent PROCEDURE(SIGNED eventnum)
  CODE
  CASE eventnum
  OF EVENT:Drag
    SELF.TakeNewSelection(SELF.ListControl)
    Return(1)
  OF EVENT:Drop
    !Update the activity that this student is in
    Mbr:Activity=SELF.MbrActLimit
    Relate:Members.Update()     !And replace the record
    SELF.ResetQueue(1)  !Refresh the display on this box
    RETURN(2)
  ELSE
    !MESSAGE('List Got passed an event with |
    !number ' & eventnum & ' dragid ' & PROP:Dragid)
  END
  RETURN(LEVEL:Benign)

The region that we have set up above the list box generates an EVENT:Accepted when we click on it. When this happens we change the cursor to a cross shape and start a little accept loop that is terminated when the user presses the left mouse button within the acceptable region (which we have enabled to accept input). At this point we reposition the three controls at the new mouse position and disable the MoveRegion control.

MultiBoxClass.RegionEvent PROCEDURE(SIGNED eventnum)
  CODE
  CASE eventnum
  OF EVENT:Accepted
    SETCURSOR(CURSOR:Cross)
    ENABLE(SELF.MoveRegion)
    ACCEPT
      CASE KEYCODE()
      OF MouseLeft
      OROF MouseLeft2
        SELF.LabelFEQ{PROP:XPos}=MOUSEX()
        SELF.LabelFEQ{PROP:YPos}=MOUSEY()
        SELF.RegionFEQ{PROP:XPos}=MOUSEX()
        SELF.RegionFEQ{PROP:YPos}=MOUSEY()
        SELF.ListControl{PROP:XPos}=MOUSEX()
        SELF.ListControl{PROP:YPos}=MOUSEY()+15
        SETCURSOR(CURSOR:Arrow)
        DISABLE(SELF.MoveRegion)
        BREAK
      END
    END
  END
  RETURN(LEVEL:Benign)

The final thing to look at is what has to be added to the window itself. The only complicated part is the handling of the events on the boxes created by the user, and the code to create the boxes in the first place. These are described here; the other stuff you can look at in the attached example. My original example used a browse box for the unassigned people (the browse box at the top of the screen that is added normally). For some reason this caused all of the other boxes to malfunction: the TakeNewSelection method would not return the highlighted record. I suspect there is a bug there somewhere; to get around it I used a drop down listbox for the unallocated people instead.

The essential is that events on the new controls on the window can be passed to the instance of the MultiBox class that has to deal with it. To manage this we set up a queue that is ordered by control number, and contains a reference to the class instance and a flag that denotes that the box should be refreshed on a drop event (because it contributed the record being dragged).

The queue is declared as:

BoxQueue    QUEUE,PRE()
CP            &MultiBoxClass !Class pointer
ControlNumber SIGNED !Control # on window
NeedsUpdate   BYTE !Mark for refresh
            END

The boxes are created by the following code (which is attached to a list box):

SETCURSOR(CURSOR:Cross) !Change cursor
ENABLE(?region1) !enable drop area
ACCEPT !wait for mouseleft
  CASE KEYCODE()
  OF MouseLeft
  OROF MouseLeft2
    !Create new class instance
    BoxQueue.CP &= NEW(MultiBoxClass)
    !Call the init method
    BoxQueue.CP.Init(Loc:Activity,ThisWindow,?region1)
    !Fill the queue
    BoxQueue.CP.ResetQueue(1)
    !Put control # in the queue
    BoxQueue.ControlNumber = BoxQueue.CP.ListControl
    !And add to the queue of boxes
    ADD(BoxQueue,BoxQueue.ControlNumber)
    !Add item to the window (for kill)
    ThisWindow.Additem(BoxQueue.CP)
    SETCURSOR(CURSOR:Arrow) !Back to regular cursor
    DISABLE(?Region1) !disable the drop area
    BREAK
  END
END

The box placement part is similar to the method for moving boxes. The class is first created (using NEW), then initialised. We keep track of the boxes we have created in the boxqueue. We use WindowManager.Additem to make sure that the Kill method is called for the class instance when the window is killed.

In the TakeFieldEvent method for the window, we add the code to deal with drags and drops among the new boxes.

IF FIELD() > LastRegularField
  !Dont deal with window accept events
  IF NOT Window{PROP:AcceptAll}
    !Try list events first
    BoxQueue.ControlNumber = FIELD()
    !Get the matching box
    GET(BoxQueue,BoxQueue.ControlNumber)
    !If is a list event
    IF NOT ERROR()
      !Check the return value: 1 = Drag; 2=Drop
      CASE BoxQueue.CP.ListEvent(EVENT())
      OF 1
        !Mark for later update on drop
        BoxQueue.NeedsUpdate=True
        !And update the queue
        PUT(BoxQueue)
      OF 2
        !Do the marked updates
        ThisWindow.UpdateBoxes()
        !Refresh unassigned list if necessary
        IF RefreshStudents
          FDB1.ResetQueue(1)
        END
      END
      RETURN(Level:Benign)
    ELSE
      !Must be a region event.
      !Subtract 2 to find matching box
      BoxQueue.ControlNumber|
         = FIELD()-2
      GET(BoxQueue,BoxQueue.ControlNumber)
      !Ought to be found
      IF NOT ERROR()
        !Will do the move if called for
        RETURN |
          BoxQueue.CP.RegionEvent(EVENT())
        ELSE
          !Shouldnt happen
          MESSAGE('Could not find '|
                 &'control to pass to '|
                 & field())
          RETURN(Level:Benign)
        END
      END
    END
  END

The ability to allow the user to add lists to the screen as part of a process of grouping items together is a powerful addition to the regular browse/form paradigm. What I have presented here is only the beginning. I am sure you have already thought of things you would add: the ability to add all of the lists on startup or to restore the user’s previous box positions on re-entry; enhancing the control that the user has over the list boxes (horizontal and vertical resize, removal, etc.). In addition, though the implementation presented here is all hand code, the scheme can obviously easily be converted into a template; only minor modifications to the DropList template would be required. This would also make it less difficult to incorporate fancy stuff into the list boxes (like cell colouring).

The files that accompany this article are an example dct and app, plus three little data files, and the ABMULTI.CLW and ABMULTI.INC files that contain the code that has been described above.

[Just a bit of background on the example screen. It comes from a program used for scheduling music camps. When placing students in groups, the musical skill of the student and his/her age are both important. The Book and Piece variables provide the information on skill level.]

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links