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).
main()
function of the programfunction 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:
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:
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 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
|
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.
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 |
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.
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
Creating a menu bar is not particularly complicated, as we can see here. In this
particular case, the definition of the functionality for handling the events when a menu
item is selected has not yet been included. This is deliberate, since it allows us to create
the menu bar before the window even exists. Later, when the window has been created, we will
call the initmenubar()
to add the handlers for the events, plus the
reference object for each event.
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.
Now that the menu bar has been created, let's look at the code to initialize it.
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 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.
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
As with the menu bar creation code, the tool bar code is also not particularly complex.
The basic approach is to create the empty tool bar, and then add the tools into the tool bar
in the order they should appear. Since the very first things that are shown are the table
and index combo boxes, and since these are not native tools for the tool bar, they are added
by creating a form to host them and then the form is inserted into the tool bar. Following
on from there, the tools used for selecting records are all added to the tool bar. Each tool
uses two images, one showing what it looks like when it is enabled, and another for when it
is disabled. All of the selection functions use the same event handling function, called
selrec()
. The final two items are disabled here, since there is only
one page in the form in this example.
The code that creates the combos is very straightforward. It uses a function to create the form and return it to the caller.
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.
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.
prepaddressbookform()
Functionfunction 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 used only 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.
ab_onnewrecord()
Functionfunction 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. |