SIMPOL Documentation

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]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]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
        

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.

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
        

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.

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 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.

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]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.