![]() |
|
Published 1998-11-01 Printer-friendly version
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 doesnt 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.

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. Its 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 lets look at the essential design criteria for our system:
Were obviously talking about creating a bunch of similar listboxes here, so lets also examine how what we are trying to do differs from adding just one browse box to the window at design time.
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 dont 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.

First of all, lets 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, lets 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 (well 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 doesnt 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 students 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 dont 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 users 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.]
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224