The Program Code
The good news is that there isn't actually much program code to write. In fact, this example is provided in the Projects\tutorial\addressbook
directory, as are the other samples from this book if they are not found elsewhere. The other good news is that if you were building a different application, you would still have very little coding to do, since the pieces that make up the addressbook project can be used as the basis of any database-based GUI project.
In this chapter, we won't go into all the program code, instead we will work with the main pieces that are affected. For the full story, there is no substitute for opening the actual project and reading through the source and the comments there.
The main() Function
The main()
function of the program is where the code execution begins. When it exits this function the program should normally end (unless there are still separate threads running that have not yet exited).
Example 6.1. The main() function of the program
function main()
addressbookapplication app
appwindow appw
string cd, formfilename, dirsep
integer e
wxmenubar mb
wxtoolbar tb
wxstatusbar sb
point lt, br
syscolors colors
dirsep = getdirectorysepchar()
cd = getcurrentdirectory()
formfilename = cd + dirsep + "addressform.sxf"
colors =@ syscolors.new()
e = 0
mb =@ mainmenu()
if mb !@= .nul
sb =@ wxstatusbar.new(error=e)
if sb !@= .nul
tb =@ buildiconbar(colors)
if tb !@= .nul
app =@ addressbookapplication.new(mb, tb, sb, "", "")
if app !@= .nul
appw =@ app.windows.getfirst()
if not fileexists(formfilename)
wxmessagedialog(appw.w, "Form file 'addressform.sxf' \
not found", sAPPMSGTITLE, "ok", \
"error")
else
appw.openformdirect(formfilename, sAPPMSGTITLE)
if appw.form !@= .nul
prepaddressbookform(appw)
appw.resizewindowtoform()
lt =@ point.new(0, 0)
br =@ point.new(0, 0)
getcenteredwindowrect(appw.outerwidth, \
appw.outerheight, lt, br, \
error=e)
if e == 0
appw.setposition(lt.x, lt.y)
end if
appw.setcurrentpath(cd)
selectrecord(appw, "selectcurrent", silent=.true)
appw.w.setstate(visible=.true)
app.run()
end if
end if
app.exit()
end if
end if
end if
end if
end function
Starting from the top, we declare a variable of type addressbookapplication
(more on that later), plus a variable of type appwindow
. The appwindow type is provided by the appframework.sml
library. The other variables should be relatively self-explanatory: menu bar, tool bar, and status bar. The point is used for centering the window on the display.
After initializing the path name for the form file, the program attempts to create the menu bar, the staTUS bar, and the tool bar. If all of those are created successfully (and they should be), the app variable is assigned the newly created addressbookapplication
object. Assuming it was created successfully, we retrieve the first (and currently only) appwindow
object and open the form file we created earlier. Assuming that worked correctly, we prepare the form (by assigning certain event handlers), then we resize the window to fit the form, center the window on the display, and reselect the current record (which helps if there are form-based calculations that need to be recalculated now that the event handlers might be in place.) Finally, we enter the run()
method of the addressbookapplication
object.
The addressbookapplication Type
In the design of this program, a key component is the addressbookapplication
type. So let's look at it:
Example 6.2. The addressbookapplication type
type addressbookapplication (application)
reference
application __app resolve
type(db1table) address
end type
That doesn't really explain much, but that is because this is an enhancement to the application type that is supplied by the appframework.sml library. Let's have a look at that one now too:
Example 6.3. The application type
type application (application) export
embed
string title
dring windows
dring datasources
boolean running
string inifilename
integer ostype
event onexitrequest
reference
type(*) _
type(*) __ resolve
wxbitmap windowicon
ppcstype1 ppcs
sysinfo systeminfo
localeinfoold SBLlocale
localeinfo locale
tdisplayformats displayformats
function run
function exit
function adddatasource
function closedatasource
function datasourceunused
function finddatasrc
function opendatasource
end type
There, that's a little more meaty. On closer examination we can discover the run()
method listed in the type. That is the main loop for the application framework. The program sits in that function all the time waiting for events. The exit()
method is not used. The rest are used to open, find, and close data sources. As long as the running property is set to.true
, the program will remain in the main loop in the run()
method.
Note
So why did we bother to create our own type, why not just use the application type as it is? In the current example it wasn't absolutely necessary. However, it turns out that it is useful. When we created the table we also set up one field as a unique index, and we will need a way of creating that value (SIMPOL does not currently do that for us). During the function that will handle the onnewrecord
event, easy access to the address table will make the code easier to write.
Therefore, we created our own type, placed an application property into it and made it reference - so that we have to initialize it - and resolve (so that its properties resolve as properties of the addressbookapplication
object). Since we declared the application type as resolve, we can also declare the addressbookapplication
type to have a type tag of application. This allows variables to be declared like this: type(application)
, which means they can contain any variable that is tagged with the application tag. Good design dictates that we should then make sure that anything tagged this way can be used as if it were the application type.
Now we should look at the most significant function here, the new()
method of the addressbookapplication
type. That is where the majority of the initialization takes place.
Example 6.4. The Code to Create a New addressbookapplication
function addressbookapplication.new(addressbookapplication me, wxmenubar mb, wxtoolbar tb, \
wxstatusbar sb, string iconname, string iconimagetype)
appwindow appw
datasourceinfo src
type(db1table) t
integer e
boolean ok
ok = .false
e = 0
me.__app =@ application.new(appiconfile=makenotnull(iconname), iconimagetype= \
makenotnull(iconimagetype), inifilename="", apptitle=sAPPTITLE)
me.__app.__ =@ me
me.onexitrequest.function =@ exit
me.running = .true
appw =@ appwindow.new(me, visible=.false, mb=mb, tb=tb, sb=sb)
if appw =@= .nul
wxmessagedialog(message="Error creating window", captiontext=sAPPMSGTITLE, style="ok", icon="error")
else
initmainmenu(appw.mb, me)
appw.onmanagemenu.function =@ managemenu
inittoolbar(appw.tb, appw)
appw.onmanagetoolbar.function =@ managetoolbar
src =@ me.opendatasource("sbme1", "address.sbm", appw, error=e)
if src =@= .nul
wxmessagedialog(appw.w, "Error opening the address.sbm file", sAPPMSGTITLE, "ok", "error")
else
t =@ appw.opendatatable(src, "Address", error=e)
if t =@= .nul
wxmessagedialog(appw.w, "Error opening the 'Address' table", sAPPMSGTITLE, "ok", "error")
else
me.address =@ t
ok = .true
end if
end if
end if
if not ok
me =@ .nul
end if
end function me
Starting from the top, the first thing the code does is create a new application object and assign the reference to that object to the me.__app
property. That ensures that all of the properties and methods of the application object are also available as part of the addressbookapplication
type. The next rather arcane looking bit is the assignment of a reference to the me variable to the __ (double underscore) property of the application object that we just created. This somewhat circular reference is quite important, since it means that all of the properties of the wrapper addressbookapplication
object are also available to the application object.
That is a bit convoluted, but in practice it is fairly easy and powerful. To understand it, it helps to understand the problem it solves. When an event occurs that is associated with the application object, only the application object is passed to the event handling function. If the function needs access to the wrapper object, it needs a way to get to that. Although it would be possible to pass the wrapper object as the optional reference parameter, that may be needed for something else. By assigning a reference to the wrapper object to the underscore or double-underscore property, the function can have full access to the capabilities of the wrapper object.
Tip
The single and double underscore properties are part of most SIMPOL complex data types. They were added to allow the user to add their own information to an existing type. Both properties are reference properties (they refer to an object), but the double underscore property is also marked as resolve, which means that whatever object is assigned here will take part in the resolution of the dot operator. What that means in practice is that a variable called app that refers to the application object portion of the addressbookapplication
object, will still be able to reach the address property of the addressbookapplication
. Please note that the IDE will not be able to show this, since it happens at run time.
Returning to our initialization code, we assign a function to handle the onexitrequest
event, which will be called if there are no more visible windows (this is part of the application object). The running property is set to .true
(setting this to .false
will cause the program to initiate shutdown), and then the initial window of the program is created. To that we pass the menu bar, tool bar, and status bar objects that we created earlier in the program code. We are creating the window invisibly, since we won't show it until later once the form has been loaded.
Once we have successfully created the initial window, we then initialize the menu and tool bars, and assign a function to handle the onmanagemenu
and onmanagetoolbar
events of the appwindow
object. These are called whenever something has been done that might warrant a change to the menu or tool bar state, such as opening a form, creating a new record, closing a table, etc.
Finally we open the data source (our address.sbm
file) and the data table (Address). The first is opened via a method of the application object, since data sources are managed at the application level, and the table is opened by the appwindow object, since tables are managed at the window level (the framework is designed to allow each window to open its own table objects). Finally we assign the table to the property that we defined for it in our wrapper type; the remainder of the function is self-explanatory.
The Remaining Initialization Code
The rest of the program code is mainly the definition of the menu and tool bars, plus the code to handle the events that have been defined. We will look briefly at the code that creates and initializes the menu and tool bars.
Example 6.5. The Code for the Menu Bar
function mainmenu()
wxmenubar mb
mb =@ wxmenubar.new()
// This section creates the File menu.
wxmenu filemenu
filemenu =@ wxmenu.new()
filemenu.insert("","E&xit", name="exit")
// This section creates the Data menu.
wxmenu datamenu
datamenu =@ wxmenu.new()
datamenu.insert("","&Add{9}Ctrl+N", name="add")
datamenu.insert("","&Save{9}Ctrl+S", name="save")
datamenu.insert("","&Delete{9}Ctrl+Del", name="delete")
// This section creates the Help menu.
wxmenu helpmenu
helpmenu =@ wxmenu.new()
helpmenu.insert("","&About " + sAPPTITLE + "...", name="about")
mb.insert(filemenu, "&File", name="file")
mb.insert(datamenu, "&Data", name="data")
mb.insert(helpmenu, "&Help", name="help")
end function mb
The code here should be fairly clear. We create an empty wxmenubar
object. Then we create the top level wxmenu
objects and proceed to fill these with entries. Once all the top-level menus have been created, they are added to the menu bar. Finally, the function returns the newly-created menu bar object as its return value.
Example 6.6. The Code for the Menu Bar
function initmainmenu(wxmenubar mb, addressbookapplication app)
mb!file.menu!exit.onselect.function =@ exitviamenu
mb!file.menu!exit.onselect.reference =@ app
mb!data.menu!add.onselect.function =@ newrecord
mb!data.menu!add.onselect.reference =@ app
mb!data.menu!save.onselect.function =@ saverecord
mb!data.menu!save.onselect.reference =@ app
mb!data.menu!delete.onselect.function =@ deleterecord
mb!data.menu!delete.onselect.reference =@ app
mb!help.menu!about.onselect.function =@ helpabout
mb!help.menu!about.onselect.reference =@ app
end function
In this function the Data menu events are all directed at standard functions from the appframework.sml
library. The exitviamenu()
function simply calls the exit()
function, and the helpabout()
function merely displays a wxmessagedialog()
call. For full details look at the source code.
Now let's have a look at the tool bar creation code. Like with the menu bar code, the references are added afterwards in the inittoolbar()
function, but unlike the menu bar, the functions are assigned during the creation of the tool bar.
Example 6.7. The Code for the Tool Bar
function buildiconbar(syscolors systemcolors)
wxbitmap bmp, disbmp
integer e
wxtoolbar tb
wxform f
e = 0
tb =@ wxtoolbar.new(16, 16, error=e)
if tb !@= .nul
f =@ combos(systemcolors)
if f !@= .nul
tb.insertform(f, name="fileindexcombos")
end if
bmp =@ wxbitmap.new("16x16_selfirst.png", "png")
disbmp =@ wxbitmap.new("16x16_selfirst_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select first \
record", name="tSelFirst")
tb!tSelFirst.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_selrwnd.png", "png")
disbmp =@ wxbitmap.new("16x16_selrwnd_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select \
rewind", name="tSelRwnd")
tb!tSelRwnd.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_selprev.png", "png")
disbmp =@ wxbitmap.new("16x16_selprev_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select \
previous record", name="tSelPrev")
tb!tSelPrev.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_selcur.png", "png")
disbmp =@ wxbitmap.new("16x16_selcur_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select \
current record", name="tSelCurr")
tb!tSelCurr.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_selnext.png", "png")
disbmp =@ wxbitmap.new("16x16_selnext_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select next \
record", name="tSelNext")
tb!tSelNext.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_selffwrd.png", "png")
disbmp =@ wxbitmap.new("16x16_selffwrd_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select fast \
forward", name="tSelFfwd")
tb!tSelFfwd.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_sellast.png", "png")
disbmp =@ wxbitmap.new("16x16_sellast_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select last \
record", name="tSelLast")
tb!tSelLast.onclick.function =@ selrec
bmp =@ wxbitmap.new("16x16_selkey.png", "png")
disbmp =@ wxbitmap.new("16x16_selkey_disabled.png", "png")
tb.insert(bmp, disbmp, enabled=.false, tooltip="Select a \
record by value", name="tSelKey")
tb!tSelKey.onclick.function =@ selrec
// Enable these if the form has multiple pages; the
// changepage() function is already provided
// bmp =@ wxbitmap.new("16x16_pageprev.png", "png")
// disbmp =@ wxbitmap.new("16x16_pageprev_disabled.png", "png")
// tb.insert(bmp, disbmp, enabled=.false, tooltip="Show \
// previous page", name="tPagePrev")
// tb!tPagePrev.onclick.function =@ changepage
//
// bmp =@ wxbitmap.new("16x16_pagenext.png", "png")
// disbmp =@ wxbitmap.new("16x16_pagenext_disabled.png", "png")
// tb.insert(bmp, disbmp, enabled=.false, tooltip="Show next \
// page", name="tPageNext")
// tb!tPageNext.onclick.function =@ changepage
end if
end function tb
The code that creates the combos is very straightforward. It uses a function to create the form and return it to the caller.
Example 6.8. The Code for the Tool Bar Combo Boxes
function combos(syscolors systemcolors)
wxform f
wxfont font1
type(wxformcontrol) fc
sysrgb btnface, comboback, combotext
integer e
e = 0
font1 =@ wxfont.new("MS Sans Serif", 9, error=e)
btnface =@ systemcolors.colors[COLOR_BTNFACE]
comboback =@ systemcolors.colors[COLOR_WINDOW]
combotext =@ systemcolors.colors[COLOR_WINDOWTEXT]
f =@ wxform.new(width=311, height=24)
f.setbackgroundrgb(btnface.value)
fc =@ f.addcontrol(wxformcombo, 1, 1, 150, 19, \
edittype="droplist", name="cbFiles")
fc.onselectionchange.function =@ toolbarcomboevents
fc.setbackgroundrgb(comboback.value)
fc.settextrgb(combotext.value)
fc.setfont(font1)
fc.setenabled(.false)
fc.settooltip("Select the table to view")
fc =@ f.addcontrol(wxformcombo, 156, 1, 150, 19, \
edittype="droplist", name="cbIndexes")
fc.onselectionchange.function =@ toolbarcomboevents
fc.setbackgroundrgb(comboback.value)
fc.settextrgb(combotext.value)
fc.setfont(font1)
fc.setenabled(.false)
fc.settooltip("Select the current index for the current table, \
or none for sequential access")
end function f
As can be seen here, the form controls and the form use the system colors to ensure that they blend in with the system colors as much as possible. They also set tool tip values, like the tools in the tool bar code earlier.
The last piece of this initialization code is the function that initializes the tool bar. It is quite similar to that used to initialize the menu bar, except for the fact that it passes in the appwindow object instead of the application object as a reference. This is primarily because in the case of the tool bar the events more often need fast access to the components of the appwindow
object, whereas in the more complex menu routines the application object can be more useful.
Example 6.9. The Code for the Tool Bar Initialization
function inittoolbar(wxtoolbar tb, appwindow appw)
tb!tSelFirst.onclick.reference =@ appw
tb!tSelRwnd.onclick.reference =@ appw
tb!tSelPrev.onclick.reference =@ appw
tb!tSelCurr.onclick.reference =@ appw
tb!tSelNext.onclick.reference =@ appw
tb!tSelFfwd.onclick.reference =@ appw
tb!tSelLast.onclick.reference =@ appw
tb!tSelKey.onclick.reference =@ appw
// Only uncomment these if the objects have also been created above
// tb!tPagePrev.onclick.reference =@ appw
// tb!tPageNext.onclick.reference =@ appw
tb!fileindexcombos!cbFiles.onselectionchange.reference =@ appw
tb!fileindexcombos!cbIndexes.onselectionchange.reference =@ appw
end function
Since in the code that creates the tool bar the functions were already assigned, in this function there are only a set of statements assigning the appwindow object as the reference for each event handler. As was the case with the definition of the tool bar earlier, there are some lines commented out for working with changing pages. These also need to be uncommented if the form has multiple pages.
Preparing the Form
One of the last things to be done, after initializing the program and opening the form, is to prepare the form for one very important task. There is still a need when creating records to create the unique key, and this will be done in the onnewrecord event of the dataform1
object. This section has two main parts, the code that prepares the form, and the code that creates the unique key value.
Example 6.10. The prepaddressbookform()
Function
function prepaddressbookform(appwindow appw)
dataform1 form
form =@ appw.form
form.onnewrecord.function =@ ab_onnewrecord
form.onnewrecord.reference =@ appw
end function
This function is very short, it is only used to assign the function and reference to the event. The only reason for separating it into a function is that later there may be other event handlers, as the application grows in complexity, and this way there is already a place for them without overcrowding the main()
function. The more important part is the function that handles this event. Let's look at it now.
Example 6.11. The ab_onnewrecord() Function
function ab_onnewrecord(dataform1 me, appwindow appw)
type(db1record) r
integer i, e
sbme1table address
e = 0
address =@ appw.app.address
r =@ address!AddressID.index.select(lastrecord=.true, error=e)
if r =@= .nul
i = address.recordcount()
i = i + 1
else
i = r!AddressID + 1
end if
me.masterrecord.record!AddressID = i
me.refresh()
me!tbFirstnames.setfocus()
end function
This function is not particularly clever, and it shouldn't be used for a networked application, but in single-user programs it will work just fine. What it does is fairly obvious, it retrieves the last record according to the AddressID
field's index, and increments that value by one. It then refreshes the form and sets focus to the control that is at the start of the tab order.
Tip
Creating a really powerful function for generating almost perfectly sequential numbers is a fairly non-trivial exercise. Especially if the user can discard the record after creating it. Most approaches use a database table to hold the serial numbers. Typically one record for each table. This allows the standard locking mechanisms to be used to prevent multiple users getting the same number. One approach is to only retrieve the value at the end, while saving, but this can be problematic, especially if there are dependent records that need to have a matching key value inserted. Another approach requires two tables, one for the serial numbers and one for the numbers that have been discarded. It also requires code to handle the discard of a record, so that the number can be placed in the discards table. The discards are then always used first in preference to the main serial number table. In a busy system, there still might be holes in the end of the sequence at any given point, however.