Comics Db Application Usage | Comics Db Application Home | Source Code Zip File | Contact Me |
As has been stated several times here, I don't like the NS Basic development environment. So I put together the toolchain seen above, In addition, I wanted to isolate the creation and placement of the various widgets on the screen. In NS Basic, each widget is a window and is an object. So I created an array of windows to contain all the widgets on the display. I wrote a subroutine for each widget that set up its object. The object was stored in the window array and then the show command would cause all the widgets to be displayed. The code below creates the wins array and calls the subroutines.
5 rem Comics Data Entry Application
15 rem Copyright 2005 Kent Archie
25 rem free for non-commercial use
35 rem
45 rem HOLDS THE WINDOW REFERENCES GENERATED
55 rem IN THE SUBROUTINES BELOW
65 dim wins[17]
75 rem THESE ARE THE LETTER GROUPS USED TO ORGANIZE THE PUBLISHERS
85 rem AND TITLES INTO 9 GROUPS
I needed the letter groups a couple of times so I made an array of the strings containing the ranges. I created a couple of global variables to track the users choices.
95 groups := ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]
105 pubrange = nil // WHICH OF THE 9 GROUPS IS THIS PUBLISHER IN
115 pubchoice = nil // WHICH PUBLISHER (NUMBER) WAS CHOSEN
125 curpubname = nil // NAME OF THE SELECTED PUBLISHER
135 rem
Now we have to create the main application window. This just shows the overall application outline and contains all the other windows.
145 rem THIS SETS UP THE MAIN WINDOW OF THE APPLICATION
155 mainspec := {goto: 'appdone, title:"Comics DB"}
165 window mainwin,mainspec,"app"
175 SHOW mainwin
185 rem
We call each subroutine to create a widget and put it into the window array , wins. After all the calls we execute the show command to display all the windows.
195 rem NOW CALL THE SUBROUTINES THAT CREATE THE OTHER WINDOWS
205 rem AND WIDGETS ON THE SCREEN. EACH PUTS A WINDOW REFERENCE IN THE
215 rem WINS ARRAY. THEY ARE ALL DISPLAYED AT ONCE AT THE END.
225 gosub makepublist // CREATE PUBLISHER CHOICE
235 gosub makelabels // CREATE BOX AND PRICE PICKERS
245 gosub makebuttons // CREATE NEW BUTTONS AND SAVE
255 gosub makenumber // CREATE FROM AND TO PICKERS
265 gosub maketitles // CREATE VARIOUS TITLES
275 gosub makedatalabels // CREATE LABELS TO HOLD CHOICES
285 show wins // DISPLAY THE WINDOWS AND WIDGETS
The data from the soups must be loaded into the widgets and indices created. Because I had several places where I wanted to give the user a message, I wrote a subroutine called popup that took whatever was in the string variable MESG and displayed it in a small windows that would disappear after a while. The wait -1 statement cause the application to go into a kind of event loop waiting for the user to take action.
295 gosub makefiles // CREATE OR OPEN DATA SOUPS AND LOAD DATA
305 rem THE POPUP SUBROUTINE DISPLAYS A BRIEF MESSAGE ON THE SCREEN
315 rem IT SHOWS WHATEVER IS IN THE VARIABLE MESG.
325 mesg="READY"
335 gosub popup
345 wait -1 // START THE EVENT LOOP (OF SORTS)
I'm not going to detail all the widget subroutines. They are pretty similar. I'll show a simple one, the price picker and a complicated one, the title lists.
This function sets up a couple of widgets at the same time. To create the price picker, we create a data object by using a special NS Basic syntax. The := operator does this. Then in the braces, we specify the values of some of the fields of the object.
955 pricespec := {text:"Price",minvalue:-1,maxvalue:10000,value:0}This specifies the name to be displayed on the picker, the minimum and maximum allowed values and the initial value. Since prices were to be entered in cents, (because this made using this widget much easier) we had to allow a pretty big range. We also don't want leading zeros so we set that using NS Basics object syntax of object.property.
965 pricespec.showleadingzeros=nilFinally, we have to specify the position on the display where the widget will be shown. The viewbounds property is set to contain an object created by the setbounds() function. The arguments to setbounds() are the x and y values for the upper left and lower right corners of the widget. These values are within the boundaries of the application.
This is one of the main reasons I didn't want to edit the code generated by the NS Basic forms editor. The viewbounds data was embedded in the object and I would have to retype all the object data just to move the widget a little. Now you might say, that the form editor would take care of that for you. Probably so, but doing it this way gave me more control over the details of the widget. But to save some effort, I used the form editor to initially create each widget and then copied and pasted the result into this program.
While we have set up the data for the widget as an object, we haven't created the window nor have we told the system what kind of widget this is. That happens here.
985 window wins[0],pricespec,"numberpicker"The window command takes three parameters. The first is a variable to hold the window object. Here we use on of the slots of the wins array. The index on the array used for a given widget is arbitrary and is sort of related to when I wrote the code. The second parameter is the data object that specifies the properties of the widget. The last is a string with the name of the widget to be created. This is not a user choice, but the kind of widget we want. In this case, it is a number picker. The rest of the code creates a list picker for the choice of which box the comic is in. This is done this way because some boxes have names like "Kate's Comics" or "Sell" rather than a simple number.
915 rem CREATE A NUMBERPICKER FOR CHOOSING PRICES AND WHICH BOX THE COMICS
925 rem ARE IN. ORIGINALLY, THE PRICES WERE A SOUP BASED LIST, BUT AFTER
935 rem TRYING IT, A NUMBERPICKER SEEMED EASIER.
945 makelabels: rem CREATE LABEL PICKERS
955 pricespec := {text:"Price",minvalue:-1,maxvalue:10000,value:0}
965 pricespec.showleadingzeros=nil
975 pricespec.viewbounds = setbounds(52,260,200,290)
985 window wins[0],pricespec,"numberpicker"
995 rem
1005 boxspec := {text:"Box",gosub:'boxchosen}
1015 boxspec.viewbounds = setbounds(221,296,299,316)
1025 boxspec.labelcommands = [""] // ADD LIST OF BOXES LATER
1035 window wins[1],boxspec,"labelpicker"
1045 return // END OF makelabels
This control is a collection of listpicker widgets, nine of them. Each one will contain the list of title that start with the 3 (2 for the last one) letters of the alphabet that are assigned to that picker. It has this in common with the publisher control. So most of this discussion will apply to the publisher control as well. In fact the base specification frames for the title lists and the publisher list are made at the same time.
The widget specification frames (objects) contain information about the layout and appearance of the widget. We saw these being created for the numberpicker above. We are going to create 9 list pickers for the titles control. While there may be a simpler way to handle some of this, I didn't want to invest the time.
The first problem ( although I didn't discover it until later), is that if I want to change the contents of a widget, say if the user picks a different publisher so I need to set up a different list of titles, or the user adds a title or publisher, I can't just change the contents of the list. In order to redraw the display, I have to create new specifications for the lists. What seemed the simplest, and I hope, fastest way to do this was to create an array of base specification frames for the publisher lists and the title lists. Then, when I need to update the lists, I replace the old specifications with the base ones and update the list items.
So, the first thing in the specification frame is the name of a subroutine to call if this list is chosen. Since the code is pretty much the same for all of them, I'll just talk about one.
First, we create the frame for the specification and add a gosub slot. This is the name of a subroutine that will be called when this list is chosen.
755 if i = 0 then basetitlespecs[i] := {gosub: 'titletab0}
The code for titletab is as follows:
6305 titletab0: rem GET TITLE CHOICE DATA FOR LETTER GROUP 0
6315 titlerange = 0
6325 gosub settitles
6335 return
I'm reasonably sure that if I had spent the time, I could have made this work using a single subroutine that took the range number as a parameter. But cutting and pasting was pretty easy. The titlerange variable is used later in the code as an internal key for the titles.
Before we can discuss the settitles subroutine, some data structures have to be explained.
I'm not going to do a tutorial on soups, mostly because this is long enough, and there are other real tutorials somewhere and I only know what I needed to know. For my purposes, they are a collection of frames. Frames are a collection of name-value pairs. They persist after the program has ended. In NSBasic, they are treated as files. This application uses several of them. The first and smallest is the counters soup. It contains the count of different kinds of data objects and is defined like this:
1675 counters = {recid:"0",pubcount:0,titlecount:0,boxcount:0,reccount:0}
Pubcount is the number of publisher records, titlecount is the number of titles, boxcount is the number of boxes and reccount is the number of records created by the program.
This code opens the soup and tries to load the counters data. There is only one record in the soup. It handles the case where the soup doesn't exist yet and creates one with 0 for the counts.
1635 rem SET UP THE SYSTEM DATA SOUP CONTAINS SIZES OF OTHER SOUPS
1645 open cnts,"cdb-counters",recid
1655 if fstat = 1 then // NOT THERE, CREATE IT
1665 create cnts,"cdb-counters",recid
1675 counters = {recid:"0",pubcount:0,titlecount:0,boxcount:0,reccount:0}
1685 put cnts,counters
1695 else // SOUP FOUND
1705 get cnts,counters,"0"
1715 if fstat <> 0 then
1725 notify("Loading Data","Can't get counter data")
1735 stop
Line
1645 opens the soup, the recid
is the name of the field in each frame to use as a key value.
In
t his soup, there is only one record but we need the key value anyway.
In other soups, this will be used to fetch individual records from a
soup. In line 1705, we have found that the soup already exists and we
want to read record 0, as indicated by the third parameter. The first
is
the file reference we got from open and the second is the name of a
variable to contain the record.
Notify() is a
function that
pops up a little window with a message in it.
The next data structure we have to look at is only slightly more complicated. The publisher soup, named cdb-pubsoup, contains a collection of frames that have this structure.
{ recid:sequential record id,pubname:publisher name}
There is a program called loadpubs.txt that creates the initial publisher soup from some data I had from a previous inventory. There are similar programs to load the initial title data and prices data. I ended up not using a soup for the prices data, however.
The only data structure I had access to in NSBasic, besides the frame, was an array. NSBasic does support nested arrays, so I could build somewhat more complex containers for the data. They look like C arrays in that each element in an array can be another array. We will look at two of them here.
The first is the publisher array, pubarr. This is a 2 dimensional array. The first level has 9 slots, one for each group in the publisher list. The groups in the list are number from 0 ('abc') to 8 ('yz'). This number is stored in the variable pubrange after the user selects one of the lists in the publisher group.
Each slot in this first array is an array itself that contains the names of publishers in that alphabetical range. In the example in the usage section, the user selected the range 'def'. This sets pubrange to 1. pubarr[1] contains an array of frames. Each frame has 2 fields, pid, which is the publisher number. This is assigned when the soup was initialized by loadpubs.txt or when a new publisher is added. The second field is called name, which is just the publisher name. For reasons that made sense at the time, the pid field in the pubarr frames is called recid in the original soup. Thus, pubarr[1][1] is the frame {16,"dc"}.
The structure that contains the title data is similar but has 3 levels. The first level is an array with one slot for each publisher. Each of these slots contains an array of 9 slots, one for each alphabetical range. Each of these slots contains an array of frames. In this case, the frame contains 3 fields, the tid is just a sequence number for the title, the pid is the publisher id and lastly, the title is the string containing the title. So tdata[16] contains the data for DC titles.
The titles array tdata is organized this way to save time. When the user selects a publisher, we have to create all the 9 lists that make up the title section. Rather than search a soup for all the titles of that publisher and then organize them into the groups at runtime, we do this at startup time. This makes the program take a bit longer to startup, but this is done once. Doing all this work each time the user changes publishers would have made the program seem very slow. The user will be less annoyed having to wait once rather than having to wait frequently while using the program.
When a new publisher is added, a new slot is created in the publisher array. This is done like this:
4245 addarrayslot(pubarr[refres],rec1)
In this code, refres is the letter group number for the new publisher, between 0 and 8. The frame for this publisher is in rec1. So if there are currently 10 titles in the letter group 'def', then this adds a new slot, number 10. If the frame rec1 is {99,"new pub"}, then this is stored in pubarr[1][10]. We then sort the list of frames in the letter group on the publisher name with this code:
4275 sort(pubarr[refres],'|str<|,'name)
The first parameter is the array to be sorted, the second is the sorting order. In this case, '|str<| means to sort the array in ascending order and the third parameter is the name of the field in the frame to sort on. Note that the single quotes are significant.
When a new publisher is added, a set of empty arrays for the letter groups are created in the tdata collection.
The last soup I will describe here contains the data entered by the user. This contains the id numbers for the publisher, title and box, the to and from issue number values and the price. It looks like this:
5465 r := {recid:rid,pid:pid,tid:tid,box:box,price:price,first:fromissue,last:toissue}
There are some other details about data handling, but they should be described pretty well in the code.
Now back to settitles:
6675 settitles: rem COMMON TITLE TAB HANDLING
6685 titlechoice = titlespecs[titlerange].viewvalue
6695 p=floor(stringtonumber(pubarr[pubrange][pubchoice].pid))
6705 curtitlename = tdata[p][titlerange][titlechoice].title
6715 setvalue(titlelabelspec,'text, clone(curtitlename))
6725 print curtitlename
6735 return
First we look at line 6685. The titlespecs array contains the specification frames for the listpickers that make up the title widget. titlerange is set to 0 through 8, depending on which alpha group the user selected. In that specification frame is a value called viewvalue. This contains the index of the title the user selected from the widget. In the screenshot on the usage page, the user had chosen a publisher of Eclipse. They then chose the 'mno' range, which sets titlerange to 4. After choosing the title 'Miracleman', the viewvalue would be 3.
In line 6695, we are trying to get the publisher id. As described above, pubarr is an array of 9 slots. pubrange is the number of the alpha group the user selected. In this case, it is 1. The variable pubchoice is the id in the list of publishers for the alpha range. In this case, we chose Eclipse, which is the 6th entry on the list. In the frame stored for this publisher at pubarr[1][6], we get the pid. The data in the frame is stored as strings, so we have to convert the pid to a number before we can use it as an index into an array. If I recall correctly, stringtonumber() returns a floating point number so I used the floor() function to convert it to an integer.
The last step is to get the title name so we can display it. After the user has made a choice of title and publisher, it can be hard to see what got chosen in the lists. So I added a couple of text boxes to store them in. To get the title, we have to use the publisher id, just acquired with great difficulty and use it to the the set of titles for that publisher from tdata. As described above, tdata[p] is a 9 element array, one for each alpha group. We have the group number in titlerange. This gets us tdata[p][titlerange] which is the list of title frames for that alpha group. We know that titlechoice is the index of the title the user selected. So tdata[p][titlerange][titlechoice] is the frame containing the data about the title the user chose. And the title field is the string containing the title. All that is left is to call the setvalue() function to put that string in the textbox reserved for it.
The last part I will describe here concerns redisplaying the titles if the user changes publisher or if a new title is added. Maybe there is a simpler way to do this, but it appears that just changing the data in the listpickers doesn't work. Nor could I find any kind of redraw function on the listpickers. So I recreated them each time. To do this, I had to make new specification frames. Rather than duplicate the code in the initialization code, I saved the specification frames made there in an array called basetitlespecs. In the dotitles() subroutine, I copied these to a new array called titlespecs. I couldn't just copy them, for reasons I can no longer recall, I had to clone them. Here is the line that does that:
7125 titlespecs[i] = clone(basetitlespecs[i])
The next bit is to see if there are any titles stored for the current publisher in the alpha group i. We can tell this using the classof() function. In this example, we check if the alpha group i for the publisher pubchoiceid has any titles in it.
7155 if classof(tdata[pubchoiceid][i]) = 'Array then
If this is true, then there is an array of titles in this group. We have to copy the title names into a new array and then set this into the specification. Along the way, we have to check that there are frames in the array and we use the classof() method again at line 7225. If there are no titles for the alpha group, we make a one element array as a place holder so the user can see that there are no titles. Otherwise, when the selected the alpha group, nothing would happen. It would be hard for the user to tell if this was an error or if there was nothing to show.
7285 titlespecs[i].labelcommands = ["EMPTY"]
I think that concludes my explanation of the tricky bits. The source code is pretty heavily annotated as I knew I would forget all this shortly after I stopped working on the code. Feel free to contact me if there are other part you want to figure out but can't or if there are mistakes in the code or the explanation.