![]() |
|
Published 1999-01-01 Printer-friendly version
There are all sorts of cool Windows features that Clarion can achieve, but many of these seem more difficult than they need to be, compared with Clarion's regular, impressive ease-of-use. One of these is "Drag-and-Drop" or "D&D". That's where you drag something from one place, and drop it onto a different control (or sometimes even the same control if it's a List).
The most common example is dragging from one list of items to another. If you are hand coding all of your list boxes, then adding support for D&D is no big deal. However, most of us are using the BrowseBox template to produce our list boxes, and adding D&D can be a little tricky. Before we get too far into such a complex implementation, let's look at the basics of D&D. There are two properties and three events that are used to monitor this activity:
PROP:DragID This control attribute indicates that you will be dragging something from this control.
PROP:DropID This control attribute indicates that you will be dropping something on this control. The D&D operation is only valid if you are dragging something from another control where the other control's PROP:DragID matches this controls PROP:DropID.
Notice that I've used the generic term "control", and not "list box". This is because you can drag and drop from almost any control type to almost any other control type. The only exception that I can recall is that you can't drag from a REGION, but you can drop on something on one. This does make sense, because a region represents a "place" rather than a "thing", whereas most other controls are both "things" and "places". Additionally, list boxes are containers for things.
Something else you should recognize is that a control can have multiple PROP:DragIDs and PROP:DropIDs. This means that you could have multiple types of D&D operations. If you drag from one control to another it means one thing, and these two controls would have a matching DragID (from) and DropID (to). Meanwhile, dragging something from the first control to a different control means something else, and the first control would have an additional DragID matching the third control's DropID. For example:
?List1: DragID = A,B
?List2: DropID = A
?List3: DropID = B
You can also have multiple controls with the same DragID and DropID. This would indicate that the items could be dragged from and to multiple locations around the window. Also, your "From" and "To" controls don't have to be on the same window. This can get pretty complex, and most implementations are of the simpler form; i.e., one-way or bidirectional.
By setting these properties, Clarion will automatically do the following for you:
It is up to us to handle these events. EVENT:Dragging is used to change the mouse cursor to something other than the generic "Can't Drop Here" cursor. In most cases, you won't change this. We're more concerned with the other two events: EVENT:Drag and EVENT:Drop.
Think of EVENT:Drag as "picking something up from here", and EVENT:Drop as "dropping the thing there". With EVENT:Drag you must remember what you've picked up, and maybe remove it from the original location. With EVENT:Drop, you must add it to the destination. Remember, though, that this is done in two stages.
However, if you have only one Drag control for a given Drop, then you can cheat a little. The Drag and Drop events always come together. Therefore, you sometimes have the option of using one or the other. In our example we'll use both events when dragging in one direction, and only the Drag event for the other direction.
You must use a local variable to remember the "something". If you are dragging from one window to another within the same program, you must use a global variable (non-threaded). If you are dragging from one program to another you need to use something like a field in a data file or an INI file. For the purpose of this article, we're going to concentrate on dragging from one list box to another on the same window. Remember that the techniques can be utilized in all of these other situations too.
Let's define our test case further: We're creating a system to track employee skills. These skills come from a predefined pool of skills that are of interest to the company. On the employee update form we want to have a browse of all possible skills, and another with the skills of the employee currently being edited. In addition, if the employee has a particular skill, then we don't want to see that skill included in the list of all possible skills.
To achieve this we require three files: Employee, Skill, and EmployeeSkill. When an Employee has a Skill, the EmployeeSkill file will contain a record with a pointer to the Employee file, and another to the Skill file. (This is a standard many-to-many relationship.) I suggest that you download the sample application and look through it as you read this article. The files will look something like this:
Employee FILE,PRE(Emp),RECLAIM,CREATE
SysIDKey KEY(Emp:SysID),UNIQUE,PRIMARY
NameKey KEY(Emp:LastName,Emp:FirstName),NOCASE,OPT
Record RECORD
SysID LONG
FirstName STRING(20)
LastName STRING(30)
END
END
Skill FILE,PRE(Ski),RECLAIM,CREATE
SysIDKey KEY(Ski:SysID),UNIQUE,PRIMARY
NameKey KEY(Ski:Name),UNIQUE,NOCASE,OPT
Record RECORD
SysID LONG
Name STRING(80)
END
END
EmployeeSkill FILE,PRE(EmpSki),RECLAIM,CREATE
EmployeeSkillKey KEY(EmpSki:EmployeeSysID,EmpSki:SkillSysID),UNIQUE,PRIMARY
EmployeeKey KEY(EmpSki:EmployeeSysID)
SkillKey KEY(EmpSki:SkillSysID)
Record RECORD
EmployeeSysID LONG
SkillSysID LONG
END
END
Notice that the EmployeeSkill file seems to have a redundant key. We've got both EmployeeKey and EmployeeSkillKey. The first is used in browses to display an employee's skills. There are no field components beyond the EmployeeSysID, which makes it possible to use the "Additional Sort Fields" to display the entries by Ski:Name. The second is used to perform a "quick hit" on the file for filter purposes, as well as ensuring that there are no duplicates. I'll be expanding on these issues (i.e. overriding browse orders) in a future article. For now, we need both keys.
On the Employee update form you'll have two browses. For the Skill browse to include only those skills that the employee does not have, you must add code to the ValidateRecord method to lookup an EmployeeSkill record for the current skill and employee. Don't forget to make the Ski:SysID into a Hot Field in the Browse settings. The code would look like this:
EmpSki:EmployeeSysID = Emp:SysID EmpSki:SkillSysID = Ski:SysID IF ~Access:EmployeeSkill.TryFetch(EmpSki:EmployeeSkillKey) RETURN Record:Filtered END
Regarding our earlier discussion of the "redundant" EmpSki:EmployeeSkillKey, if we did not have it, we would have to build a VIEW structure to access the file to determine if there was a matching record. (We could manually walk through the file with a SET+NEXT, but that's old-fashioned <g>.)
The EmployeeSkill browse is easier to implement, as it uses a regular Range to display records with a File Relationship involving the Employee file.
Here's what we need to do to add D&D to the window the "hard" way:
IF DRAGID() = 'InsertSkill' SkillBrowse.UpdateViewRecord Loc:SkillSysID = Ski:SysID END!IF
IF DROPID() = 'InsertSkill'
Access:EmployeeSkill.PrimeRecord
EmpSki:EmployeeSysID = Emp:SysID
EmpSki:SkillSysID = Loc:SkillSysID
IF Access:EmployeeSkill.Insert()
Access:EmployeeSkill.CancelAutoInc
ELSE
ThisWindow.Reset(1)
END
END
IF DRAGID() = 'DeleteSkill'
EmployeeSkillBrowse.UpdateViewRecord
IF ~Relate:EmployeeSkill.Delete(0)
ThisWindow.Reset(1)
END
END
The template must do the same things that you did yourself, but it should look at the whole concept of D&D in a generic fashion. It may be that you need multiple D&D templates to handle different situations. If possible, though, we're going to make each template handle as many special cases as possible, without making them too weird to use.
Let's look at our test case. We can define the template as "implementing drag and drop between two browses on a window. One browse represents one side of the MANY:MANY relationship, while the opposite file in the relationship is 'fixed'. The second browse represents records in a 'join' file between the two other files. This template will add and delete records to that file". It needs to know the following:
The rest of the information can be derived from this. The %FromControl browse indirectly specifies the Anchor file (e.g. Skill), while the %ToControl specifies the Join file (e.g. EmployeeSkill). In our example we could have determined the %FixedFile automatically, because we're doing this in a Form procedure. However, we can make this much more flexible by asking the question. Once we know the two controls, we can determine all of the file and field names as necessary. This is what the template prompts will look like:
#EXTENSION(mhDragDropM2M,'Drag & Drop|
(MANY:MANY)'),DESCRIPTION('[MikeH] Drag & Drop between ' &|
%FromControl & ' and ' & %ToControl),MULTI,PROCEDURE
#BOXED('')
#PROMPT('From Anchor Browse:', FROM(%Control, |
%ControlTemplate='BrowseBox(ABC)')), %FromControl, REQ
#PROMPT('To Join Browse:', FROM(%Control, |
%ControlTemplate='BrowseBox(ABC)')), %ToControl, REQ
#PROMPT('Other Anchor File:', FILE), %FixedFile, REQ
#PROMPT('Drag in both directions', CHECK), %Bidirectional, |
DEFAULT(%True)
#ENDBOXED
Now let's determine the rest of the stuff that we'll need in the #ATSTART and #AT(%GatherSymbols) sections. I split this up, because the Browse extensions need to initialize their symbols before I peek at them. %GatherSymbols occurs after the Browse has initialized.
#ATSTART
#DECLARE(%FromFile)
#DECLARE(%FromObject)
#DECLARE(%ToFile)
#DECLARE(%ToObject)
#EQUATE(%InstancePrefix, 'MHDD'& %ActiveTemplateInstance &':')
#ENDAT
#!-----
#AT(%GatherSymbols)
#CALL(%DDGetBrowseSettings, %FromControl, %FromFile, %FromObject)
#CALL(%DDGetBrowseSettings, %ToControl, %ToFile, %ToObject)
#FIX(%File, %FixedFile)
#FIX(%Relation, %ToFile)
#IF(%FileRelationType <> '1:MANY')
#ERROR(%Procedure &' (Drag&Drop): '& %FixedFile &' and '& |
%ToFile &' are not related 1:MANY')
#ENDIF
#FIX(%File, %FromFile)
#FIX(%Relation, %ToFile)
#IF(%FileRelationType <> '1:MANY')
#ERROR(%Procedure &' (Drag&Drop): '& %FromFile &' and '& |
%ToFile &' are not related 1:MANY')
#ENDIF
#ENDAT
Our %DDGetBrowseSettings group is responsible for checking the settings of a Browse template to determine its primary file and class object. These values are part of an "unrelated" template. Therefore, we must use #ALIAS to grab the value of %ManagerName, and we have to change the #ActiveTemplate and %ActiveTemplateInstance to get the %Primary file. Note that the PRESERVE attribute on the #GROUP will restore the %ActiveTemplate settings after the group is finished. (We used to do this manually.)
#GROUP(%DDGetBrowseSettings,%Ctl,*%F,*%O),PRESERVE #FIX(%Control, %Ctl) #ALIAS(%M, %ManagerName, %ControlInstance) #SET(%O, %M) #FIX(%ActiveTemplate, %ControlTemplate) #FIX(%ActiveTemplateInstance, %ControlInstance) #SET(%F, %Primary)
We need to create the local variables to remember the current entry as it is being dragged across the window.
#AT(%DataSection) #FIX(%File,%FromFile) #FIX(%Relation,%ToFile) #FOR(%FileKeyField) %[20](%InstancePrefix&':'&%FileKeyField) LIKE(%FileKeyField) #ENDFOR #ENDAT
Now we must initialize the necessary control attributes after the window is opened:
#AT(%WindowManagerMethodCodeSection, 'Init', '(),BYTE'), PRIORITY(8030)
%FromControl{PROP:DragID} = '%(%InstancePrefix &'F')'
%ToControl{PROP:DropID} = '%(%InstancePrefix &'F')'
#IF(%Bidirectional)
%FromControl{PROP:DropID} = '%(%InstancePrefix &'B')'
%ToControl{PROP:DragID} = '%(%InstancePrefix &'B')'
#ENDIF
%FromControl{PROP:NoBar} = True
%ToControl{PROP:NoBar} = True
#ENDAT
Finally, we must insert the control event handling code:
#AT(%ControlEventHandling, %FromControl, 'Drag'), PRIORITY(7500)
IF DRAGID() = '%(%InstancePrefix &'F')'
%FromObject.UpdateViewRecord
#FIX(%File,%FromFile)
#FIX(%Relation,%ToFile)
#FOR(%FileKeyField)
%InstancePrefix:%FileKeyField = %FileKeyField
#ENDFOR
END
#ENDAT
#!-----
#AT(%ControlEventHandling, %ToControl, 'Drop'), PRIORITY(7500)
IF DROPID() = '%(%InstancePrefix &'F')'
Access:%ToFile.PrimeRecord
#FIX(%File,%FixedFile)
#FIX(%Relation, %ToFile)
#FOR(%FileKeyField)
%FileKeyFieldLink = %FileKeyField
#ENDFOR
#FIX(%File,%FromFile)
#FIX(%Relation, %ToFile)
#FOR(%FileKeyField)
%FileKeyFieldLink = %InstancePrefix:%FileKeyField
#ENDFOR
IF Access:%ToFile.Insert()
Access:%ToFile.CancelAutoInc
ELSE
%WindowManager.Reset(1)
END
END
#ENDAT
#!-----
#AT(%ControlEventHandling, %ToControl, 'Drag'), PRIORITY(7500), WHERE(%Bidirectional)
IF DRAGID() = '%(%InstancePrefix &'B')'
%ToObject.UpdateViewRecord
IF ~Relate:%ToFile.Delete(0)
%WindowManager.Reset(1)
END
END
#ENDAT
Here we are again, hopefully with a better understanding of Drag and Drop, as well as a template to save us from needing to understand it <grin>. Of course, this template handles only one D&D scenario, albeit one of the most common. If you encounter others, you should be able to apply the techniques presented here. As usual, you can download the template (along with the rest of my public domain templates) from www.BoxsoftDevelopment.com. Look for MHAB*.EXE.
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