![]() |
![]() |
|
![]() |
Multiple Selection List Boxes © 2003 Kim A. Howarter Download the sample files used in this article. Preface Paradox lists and combo boxes only allow one item to be selected at a time. This article shows how to get around this limitation by using a dialog box with two lists to make multiple selections. One list shows the list to select from and the other list shows the selections. This is very useful for selection queries or other actions that need to use one or more items from a list or table. In addition, the dual list dialog box is table driven so that you can use this one dialog box across your entire application. Introduction The table driven dual list dialog box (shown below) is one way to provide this feature in Paradox. The dialog box, table, library, and setup form are the only items needed to use the dialog box in your application. They will unzip to a directory named SelectionList. They can be put in any directory you choose, but it needs to be the working directory for them to work. This article describes how to add an alias for ease of use. The dialog box is opened by passing a global variable through a library. Then, the dialog box opens a TCursor and locates this global variable in the SelectBX.db table. The fields in the record set up and operate the dialog box. There are only four library methods (three on the dialog box and one on the calling form) used. The sample files include all of the files to use the dialog box along with a button on the SelBxSU.fsl form to call the dialog box. This easily integrates with an application by using the existing library, or moving the four library methods to your library, or changing the library calls to use existing methods. The reason I suggest this is that most applications will already use some variation of these methods, so you may want to use your own methods to more fully integrate this into your coding style. The changes to accomplish this should be minimal. ![]() The selection list is shown on the left and the selected items are displayed on the right. Each list displays up to 15 selections and Paradox automatically adds scroll bars if the list is larger. The four buttons in the center box between the two lists control the addition and removal of items from the selected list. This allows the addition or removal of one item at a time or all at once. If a button is pressed and there is nothing to add/remove, a beep is heard and nothing else happens. Also, the return key, space bar, or a mouse double click will add/remove the active highlighted item. For details, see the page's keyPhysical method of the dialog box. The SelectBX.db Table The table contains the field values used to setup and operate the various parts of the dialog box and is keyed on the CriteriaName field. This ensures that there are no duplicate criteria names in the table (some of you might want to see a long integer key field, but I chose not to. If you like, add the integer key field, but include validation for duplicate entries in the CriteriaName field.). All of the fields that are associated with the selection choices list (left side list box on the form) start with "List", and the fields associated with the selected list (right side list box on the form) start with "Item". Here are the table fields and their descriptions:
The SelBxSU.fsl Form The SelBxSU.fsl form makes it easy to set up the dialog box in the SelectBX.db table described above. ![]() The List Table and Output Table field entries set up most of the drop down fields with list data. List Table Setup:
On the bottom of the SelBxSU.fsl form, there are "Run" and "Close" buttons in a box that makes them self contained. I left room for an additional button for your use, such as a report button to document the setup information for the dialog box. The icons are put on as described in Ken Loomis's article How To Put An Icon On A Paradox Button Object. The SelectBX.fsl form is set up the same way with "OK" and "Cancel" buttons.. I like the looks of these buttons with icons, so I use them. The Calling Form Button on the SelBxSU.fsl Form I have included a "Run" button on the "SelBxSU.fsl" form to call the dialog box. It uses the criteria from the active record. After making your selections, click "OK" on the dialog box, a table window shows the items that you selected. This is just for test purposes so you can see your selections. However, in actual practice, a query or some other action would be taken. This dialog box can be called from a button, library method or a script depending upon the application's needs. I have made the button self-contained so it can be copied to your forms and your code built around it. In actual practice if you move the four library methods to your library, then this will need to change. The "Run" button code is shown below. The comments in the code explain its operation. Const ;// This sets the table name and library name so you can easily change it in one place. stSelectBX = "selectBX.fsl" stLibSystem = "System.lsl" endConst Var libSystem Library endVar method open(var eventInfo Event) ;// Open the System Library. if not libSystem.open(stLibSystem,GlobalToDesktop) then errorShow("Cannot open the System Library, please notify the System Administrator.") close() endIf endMethod Uses ObjectPal ;//System Library System.lsl cmSetGlobalVariable(const stVarName String, const atVarValue AnyType) Logical endUses method pushButton(var eventInfo Event) Var foSelectBX Form loResult Logical stErrorCode,stErrorMsg String tvSelected TableView endVar ;// Unlock the record before proceeding so that any changes will take affect. if isEdit() then if active'locked then if not action(DataUnlockRecord) then eventInfo.SetErrorCode(UserError) Return endIf endIf endIf ;// Make sure that the error Environment String is empty. writeEnvironmentString("SelectBXErrorCode","") ;// Set the Global Variable to the current record's CriteriaName libSystem.cmSetGlobalVariable("stCriteriaName",CriteriaName.value) ;// Open the dialog box and handle the error if it doesn't open. if not foSelectBX.open(stSelectBX) then if readEnvironmentString("SelectBXErrorCode") <> "" then stErrorCode = readEnvironmentString("SelectBXErrorCode") stErrorMsg = readEnvironmentString("SelectBXErrorMsg") msgStop(stErrorCode,stErrorMSg) else errorShow() endIf endIf ;// Bring the dialog box to the top. foSelectBX.BringToTop() ;// Wait for user to make their selection. loResult = foSelectBX.wait() ;// Handle the dialog box return value. if not loResult = True then Try foSelectBX.close() ;// User clicked the <Cancel> button. return OnFail errorClear() EndTry else Try foSelectBX.close() ;// User Clicked <OK> button. OnFail errorClear() EndTry endif ;// View your selections. tvSelected.open(ItemOutputTable) tvSelected.wait() tvSelected.close() endMethodThe SelBxSU.fsl form makes it easy to input the setup information for each different use of the dialog box to the SelectBX.db table. Since the "CriteriaName" field is used as the key field, the only requirements to call the dialog box are to use the "CriteriaName" to identify the record in the SelectBX.db table for each different use. If the requirements are the same from multiple calling locations, one "CriteriaName" and record for all of the locations should be used. An example would be two buttons that produce different results, but use a criteria table selected from the same list or two buttons on different forms that do the same thing. This allows reuse of the list source table to load the list for as many different uses as desired. For different uses of the dialog box, just enter a new record in the SelectBX.db table. One note on the SelBxSU.fsl form. There is no alias used with the table, so if you move them to different directories it will require opening the form with a new table and resaving it. The SelectBX.fsl Form Now, the dialog form, where it all happens. The form starts out by opening the library and reading the global variable loaded from the calling form's button. If it fails to open the library, it loads an error message into two environment variables and sends them back to the calling form to display the error. This prevents two errors from displaying, one from the dialog box and one from the calling button. Then a TCursor is opened on the SelectBX.db table, which locates the record identified by the global variable stCriteriaName. Finally the title is assigned and the text box below the two lists is filled. All of these actions are in the "init" method and are performed based upon information from the SelectBX.db table. If desired, additional fields could be added to include changing the titles of the lists and the titles under the lists, which show the number of items in the lists. Just add the field(s) to the table, add the TCursor calls to the "init" method in the dialog box, and assign the variables to the feature in the dialog box. The table and library names are assigned in the Const section of the form. This makes it easy to change to the alias and names that you might want to use. The selection list is populated on the Page Open method by setting both list.count properties to zero to first empty both lists. Next, the DataSource Property is used to load the Selection list. This requires less coding than using a ForNext loop and incrementing the list.count. Dynamic TCursors are used to open the tables and then they are closed in the Close method of the dialog box. Then a UIObject variable is attached to the Available Selections list to use in the sorting process and finally a call to the cmSortList() method. See the code and comments below. method open(var eventInfo Event) ;// Set the background color to match the current windows setting. ;// Then finish executing the built-in code before populating the list. self.color = cl3dFace doDefault ;// Empty the two list box objects. AvailableSelectionsList.list.count = 0 CurrentlySelectedList.list.count = 0 ;// Re-populate the selection list using the DataSource property ;// Get the table path and name from the ListTable field. ;// Get the field name from the ListField field. uiListTo.attach(AvailableSelections.First) uiListTo.dataSource = dyTC["tcSelectBX"]."ListTable".value + "." + dyTC["tcSelectBX"]."ListField" ;// Attach to and sort the contents of the selection list box. uiListTo.attach(AvailableSelectionsList) bxActions.cmSortList() endmethodThe page Arrive method recalculates the number of items in each list, clears any messages, and sets the timer for 1/2 second to delay setting the highlight on the selection list until the dialog box is fully opened. In the keyPhysical method, shortcut keys have been setup to process selections, and activate the "OK" and "Cancel" buttons. A passEvent is used to send the keyPhysical events on the list, listField, and box containers to the page for handling. This eliminates the need for the keyPhysical event code to be duplicated in several places. Review the code on the page to see how the shortcuts are set up and adjust them to meet your needs. By double-clicking on an item in a list, it is immediately transferred to the other list. This is accomplished by making a call to the appropriate add or remove button from the mouseDouble method of each list. cmSortList() custom method The custom method "cmSortList" is used to sort both lists and it is located on the "bxActions" box that contains the four buttons, which is used for both lists. It is called from the page Open method as well as the four Add and Remove buttons. These are the only times the list changes, so too the only times it needs sorting. There are three sort options (Alpha, Index, and Number) in this method. They are to sort the list alphabetically, use a second field in the ListTable to sort the list by numbers in any order you desire, or to use a different table index. By looking at the sample list with the numbers spelled out it becomes very apparent that being able to sort the list numerically is important. Another reason might be to sort a list of names that you want the most frequently used names at the top of the list. By allowing the use of a secondary index, it opens up many other possibilities. A Switch statement is used to select between the three choices.
method cmSortList() var dyList DynArray[]AnyType liListSortBy LongInt siCounter SmallInt stListSortBy,stListSortFrom String endVar ;// Case = Alpha: Read the list into the dynArray using the value as the ;// key to the dynArray. This sorts the list alphabetically. ;// Case = Number: Read the list into the dynArray using the value from the ;// ListSortFrom as the index to create the number sort order. ;// Case = Index: Read the list into the dynArray using the value from the ;// ListSortFrom with a switchIndex to create the sort order. Switch case dyTC["tcSelectBX"]."ListSortBy" = "Alpha" : ;// Copy the list into the dynArray. for siCounter from 1 to uiListTo.list.count uiListTo.list.selection = siCounter dyList[uiListTo.list.value] = uiListTo.list.value endFor case dyTC["tcSelectBX"]."ListSortBy" = "Number" : ;// Open the List Table. if not dyTC["tcPopulateList"].open(dyTC["tcSelectBX"]."ListTable") then msgStop("System Error","The cmSortList method failed to open the "+ (dyTC["tcSelectBX"]."ListTable") + "table\n"+ "Please notify the System Administrator.") Return endIf ;// Get the ListSortFrom Field value and put it in a string variable stListSortFrom = dyTC["tcSelectBX"]."ListSortFrom".value ;// This loop loads the list into a dynArray using the index to sort by. for siCounter from 1 to uiListTo.list.count ;// Load the list.selection into a smallInt variable. uiListTo.list.selection = siCounter ;// Locate the list.value in the table that populated the list. if not dyTC["tcPopulateList"].locate(dyTC["tcSelectBX"]."ListField",uiListTo.list.value) then msgStop("System Error","The cmSortList method failed to locate the list value.\n"+ "Please notify the System Administrator.") Return endIf ;// Get the value from the index field of the List table. stListSortBy = dyTC["tcPopulateList"].(stListSortFrom) ;// This makes the list index 10 characters long so it will sort by the number. stListSortBy = String(format("W10,EZC", numVal(stListSortBy))) ;// Load the values and index into the dynArray. dyList[stListSortBy] = uiListTo.list.value endFor case dyTC["tcSelectBX"]."ListSortBy" = "Index" : ;// Open the List Table. if not dyTC["tcPopulateList"].open(dyTC["tcSelectBX"]."ListTable") then msgStop("System Error","The cmSortList method failed to open the "+ (dyTC["tcSelectBX"]."ListTable") + " table\n"+ "Please notify the System Administrator.") Return endIf ;// Switch to the index from the selectBX.db ListSortFrom Field. dyTC["tcPopulateList"].switchIndex(dyTC["tcSelectBX"]."ListSortFrom") for siCounter from 1 to uiListTo.list.count ;// Load the list.selection into a smallInt variable. uiListTo.list.selection = siCounter ;// Locate the list.value in the table that populated the list. if not dyTC["tcPopulateList"].locate(dyTC["tcSelectBX"]."ListField",uiListTo.list.value) then msgStop("System Error","The cmSortList method failed to locate the list value.\n"+ "Please notify the System Administrator.") Return endIf ;// Put the current records Record Number in a variable. liListSortBy = dyTC["tcPopulateList"].RecNo() ;// This makes the list index 10 characters long so it will sort by the number. stListSortBy = String(format("W10,EZC", liListSortBy)) ;// Load the value and index into the dynArray. dyList[stListSortBy] = uiListTo.list.value endFor endSwitch ;// Copy the dynArray back to the list. siCounter = 1 ForEach element in dyList uiListTo.list.selection = siCounter uiListTo.list.value = dyList[element] siCounter = siCounter + 1 endForEach ;// Allow the screen to refresh. delayScreenUpdates(No) endmethod Adding And Removing Items From The Lists The four buttons that Add and Remove items from the lists are powered by two custom methods attached to the "bxActions" box. Both custom methods receive an "Add" or "Remove" string, which is used with a Switch statement to attach the source and destination lists to UIObject variables. By controlling how the UIObjects are assigned it allows us to use one method to either add or remove items from the lists. Each custom method is described and the code listed below. The cmAddRemoveAll() custom method first uses a switch statement to assign the two UIObjects based upon either "Add" or "Remove" being sent to the method. Next it checks to see if the source list is empty and if so, it beeps and returns because there is nothing to copy. Then it checks to see if the destination list is empty. If not empty, it copies the items back to the source list. Next it steps backward through the source list and copies it to the destination list. Then the source list count is set to zero to empty the list. Finally, move to the destination list, recalculate the totals at the bottom of the lists, and sort the list with the cmSortList() method. Below is the cmAddRemoveAll custom method code: method cmAddRemoveAll(stAction String) var siCounterRemove,siCounterAdd SmallInt endVar ;// Attach to the "To" or "From" lists based upon the calling button. Switch case stAction = "Add" : uiListFrom.attach(AvailableSelectionsList) uiListTo.attach(CurrentlySelectedList) case stAction = "Remove" : uiListFrom.attach(CurrentlySelectedList) uiListTo.attach(AvailableSelectionsList) otherwise: Return endSwitch ;// If the list is empty, then beep because there is nothing to copy. if uiListFrom.list.count = 0 then beep() message("The List is Empty.") return endif ;// Delay any screen updates. DelayScreenUpdates(Yes) ;// Check to make sure the destination list is empty before we take action. ;// If not empty, then put the values back into the source list so they won't ;// get lost when the copy process is performed. if uiListTo.list.count > 0 then for siCounterRemove from uiListTo.list.count to 1 step -1 uiListTo.list.selection = siCounterRemove uiListFrom.list.count = uiListFrom.list.count + 1 uiListFrom.list.selection = uiListFrom.list.count uiListFrom.list.value = uiListTo.list.value uiListTo.list.value = "" endFor endif ;// Step through the list from the last item to the first ;// in order to add each item to the "other" list object. for siCounterAdd from uiListFrom.list.count to 1 step -1 uiListTo.list.selection = siCounterAdd uiListFrom.list.selection = siCounterAdd uiListTo.list.value = uiListFrom.list.value uiListFrom.list.value = "" endFor ;// Empty the source list by setting its count to 0. uiListFrom.list.count = 0 ;// Refresh the counters under the lists. AvailableSelectionsCount.action(DataRecalc) CurrentlySelectedCount.action(DataRecalc) ;// Sort the list and then allow screen updates. cmSortList() DelayScreenUpdates(No) ;// Move the Highlight to the copied list. uiListTo.list.selection = 1 uiListTo.moveTo() endMethodThe cmAddRemoveOneItem() custom method first uses a switch statement to assign the two UIObjects based upon either "Add" or "Remove" being sent to the method. Next another switch checks to see if the source list is empty or nothing is selected. Next the selected item is added to the destination list and assigned to a variable, and removed from the source list. Then the counters are recalculated, the highlight is repositioned on the item just added to the list, and if the source list is empty the active highlight is moved to the destination list. Below is the cmAddRemoveOneItem() custom method code: method cmAddRemoveOneItem(stAction String) var siCounter SmallInt stFindValue String endVar ;// Attach to the "To" or "From" lists based upon the calling button. Switch case stAction = "Add" : uiListFrom.attach(AvailableSelectionsList) uiListTo.attach(CurrentlySelectedList) case stAction = "Remove" : uiListFrom.attach(CurrentlySelectedList) uiListTo.attach(AvailableSelectionsList) otherwise: Return endSwitch ;// If the list is empty or nothing selected don't do anything. Switch case uiListFrom.list.count = 0 : beep() message("The List is Empty") return case uiListFrom.list.selection = 0 : beep() message("Nothing Selected") return endSwitch ;// Delay Screen updates. DelayScreenUpdates(Yes) ;// Add the selected (i.e., highlighted) item to the other list. uiListTo.list.selection = uiListTo.list.count + 1 uiListTo.list.value = uiListFrom.list.value ;// Store the selected item that was just added so that we can reposition ;// the highlight on that item after it has been added to the list. stFindValue = uiListFrom.list.value ;// Go back and remove the item from the source list. uiListFrom.list.value = "" ;// Refresh the counters under the lists. AvailableSelectionsCount.action(DataRecalc) CurrentlySelectedCount.action(DataRecalc) ;// Sort the contents of the list. cmSortList() ;// Reposition the highlight on the item just added to the list. for siCounter from 1 to (uiListTo.list.count) uiListTo.list.selection = siCounter if (stFindValue = uiListTo.list.value) then quitLoop endif endfor ;// Move the active highlight to the other list if the active list is empty. Switch case uiListFrom.list.count <> 0 : uiListFrom.moveTo() case uiListTo.list.count <> 0 : uiListTo.moveTo() endSwitch ;// Allow the screen to refresh. DelayScreenUpdates(No) endmethod Summary So there you have it. The table controlled dialog list provides many choices when using list selection boxes in your applications. This Selection List Dialog box is self-contained so it can be plugged in anywhere in your application. The library, the setup form, and a button to run the code from are included. I have done the same thing with drop down edit boxes on a dialog box to further reduce the number of these little dialog boxes and it ensures that they all look the same. They take much less code than this. By using this as a pattern, you should be able to create your own. I will leave it up to you to make use of these techniques. Editor's note: Code was developed with Paradox 9 SP4. Discussion of this article |
![]() Feedback | Paradox Day | Who Uses Paradox | I Use Paradox | Downloads ![]() |
|
![]() The information provided on this Web site is not in any way sponsored or endorsed by Corel Corporation. Paradox is a registered trademark of Corel Corporation. ![]() |
|
![]() Modified: 03 Feb 2004 Terms of Use / Legal Disclaimer ![]() |
![]() Copyright © 2001- 2004 Paradox Community. All rights reserved. Company and product names are trademarks or registered trademarks of their respective companies. Authors hold the copyrights to their own works. Please contact the author of any article for details. ![]() |
![]() |
|