next up previous contents
Next: 8.5 The design and Up: 8 Designing Custom Qddb Previous: 8.3 A simple example

8.4 Implementing custom features

  One of the goals of the Fx toolkit is to allow the programmer to customize as much of the interface as possible without losing the common look and feel. Any user familiar with one Fx application should be able to muddle through almost all Fx applications. Of course, there may be exceptions to this rule (for example, custom secondary screens for invoice entry).

This section describes some of the common custom features that you can implement with Fx. We cannot, of course, describe all the possible things you can do. The manual pages describe all the options available to the Fx commands.

8.4.1 Naming itcl_classes, procedures, and global variables

When you write custom Fx applications, you need to realize that Fx uses the global Tcl name space to hold many variables and procedures. For this reason, you should take care not to conflict with any of Fx's predefined names. You should avoid prefixing your names with fx in any case or case mixture. Consider these names reserved for future use by the toolkit even if the name you want isn't in use at the moment.

8.4.2 Augmenting/creating menus

Fx allows you to augment existing menus or create new ones. To augment an existing menu, you must explicitly add a menu item to the appropriate menu. Menus are named according to their label in all lowercase. For example, to add a menu item to kill the last search results, you might do this:

    .mb.view.menu add command -underline 0 -label "Kill last search" \
        -command {menubar KillLastSearch}

The -underline option specifies the character to be underlined in the menu item's label and used as an accelerator.

8.4.3 Interfacing with the search results

Fx generates a reasonably nice Search Results listbox for displaying the result of a query. Since you might want to add menus for special predefined searches, Fx provides a mechanism for you to change the contents of the Search Results. There are several Fx_Menubar methods available to aid this sort of manipulation:


tabular1068

The SetLastSearch method takes a list argument that must be the result of a call to qddb_rows select -query off; it implicitly calls KillLastSearch. GetLastSearch returns such a list or {} if there are no previous search results. DisplayLastSearch displays the contents of the last search results in the standard Search Results listbox.

Example:

We can add a menu item to the View menu to generate a search that matches all tuples:

    Fx_Menubar menubar -w .mb -schema $schema
    .mb.view.menu add separator
    .mb.view.menu add command -label "All records" -command MyApp_ViewAll

    proc MyApp_ViewAll {} {
        set s [menubar info public schema -value]
        set k [qddb_search $s regexp .*]
        set k [qddb_keylist process nullop -deldup_sameentry on $k]
        set r [qddb_rows select $k]
        qddb_keylist delete $k
        menubar SetLastSearch $r
        menubar DisplayLastSearch
    }

8.4.4 Calculated fields

You can calculate some fields in your relation calculated from other fields. For example, suppose you have a Students relation containing:

    Name 
    SS verbosename "Social Security Number" separators ""
    Course verbosename "Course Number"
    Grades (
        Description
        Weight type real
        Score type real format "%.2f"
    )*
    Total type real format "%.2f"
    FinalGrade verbosename "Final Grade"

You want to recalculate the Total every time one of the scores (or weights) changes. You might do something like this (in the global context, of course):

    #...stuff deleted...
    Fx_Entry Grades.Weight -w .grades.weight -attr Grades.Weight \
        -read_only 1 -userconfig 0 -width 5
    Fx_Entry Grades.Score -w .grades.score -attr Grades.Score \
        -read_only 1 -userconfig 0 -width 5
    proc MyApp_Recalc {} {
        global weight score gv_attr
        if {[Fx_Entry :: TupleChanged] == 0} {return} ;# nothing changed
        set t [menubar info public tuple -value]
        set v [qddb_view define $t {
            {Grades.Weight weight}
            {Grades.Score score}
        }]
        set max [qddb_instance maxnum $v Grades]
        set tot 0.00
        for {set i 1} {$i <= $max} {incr i} {
            qddb_instance switch $v Grades $i
            set tot [expr $tot + ($weight * $score)]
        }
        # setting gv_attr(Total) automatically sets tuple as changed
        set gv_attr(Total) [format "%.2f" $tot]
        update idletasks
        qddb_view delete $v
    }
    bind [Grades.Weight GetEntry] <FocusOut> MyApp_Recalc
    bind [Grades.Score GetEntry] <FocusOut> MyApp_Recalc

Now whenever the user presses the <Tab> key or selects a menu button, the Total will be recalculated if the cursor was in either the Grades.Weight or Grades.Score field and the tuple has been modified.

8.4.5 Generating unique identifiers

Suppose you have a client/invoice database containing two major components: client information and invoices. You could describe this database with a single Qddb relation:

    Client (
        Name (First Last)
        Address (Street City State Zip)
        Phones (Description Area Number)
    )
    Invoices (
        Number type integer verbosename "Invoice Number"
        Date type date format "%I:%M %p, %B %d, %Y"
        Items (
            Qty type integer
            Number verbosename "Item number" separators ""
            Description
            Price type real format "%.2f"
            Total type real format "%.2f"
        )*
        Total type real format "%.2f"
    )*

The Invoices.Number field should be a unique integer and must be generated whenever an invoice is created. One common practice is to create a Setup relation containing some of the standard information you commonly need: business name/address, next invoice number, etc. A Setup Schema might look like:

    Name verbosename "Business name"
    Address verbosename "Business address" (
        Street City State
        Zip verbosename "Zip Code"
    )
    NextInvoice verbosename "Next invoice number" type integer

Using this relation, we can define a new invoice number whenever a new invoice for a particular client is created. For example, the following code fragment explains what must be done to generate a new invoice number and to prevent the user from changing it:

    Fx_Frame Invoices -w .invoices -attr Invoices -afteradd AddInvoiceProc 
    proc AddInvoiceProc {} {
        global gv_attr next {fx:status_variable}
        set s [qddb_schema open Setup]
        set k [qddb_search $s -prunebyattr Name regexp .*]
        set k [qddb_keylist process nullop -deldup_sameentry on $k]
        set t {}
        foreach i [qddb_keylist get $k] {
            set t [qddb_tuple read $s $i]
            if {[string compare $t {}] == 0} {continue}
        }
        qddb_keylist delete $k
        if {[string compare $t {}] == 0} {
            set {fx:status_variable} {Error! Must set up Setup relation!}
            return
        }
        while {[qddb_tuple lock $t] == 0} {
            set {fx:status_variable} "Waiting for Setup screen to close."
            exec sleep 1 ; set {fx:status_variable} {} ; exec sleep 1
        }
        set v [qddb_view define $t {
            {NextInvoice next}
        }]
        set gv_attr(Invoices.Number) $next
        incr next
        qddb_tuple write $t
        qddb_schema delete $s ;# deletes/unlocks tuple, view and schema.
        set gv_attr(Invoices.Date) [exec date {+%I:%M %p, %B %d, %Y}]
    }
    Fx_Entry Invoices.Number -w .invoices.number -attr Invoices.Number \
        -read_only 1 -userconfig 0 -date_search 0 -regexp_search 0
    Fx_Entry Invoices.Date -w .invoices.date -attr Invoices.Date \
        -read_only 1 -userconfig 0 -regexp_search 0

Since the procedure AddInvoiceProc is called after creating and switching to the new instance of Invoices, we just need to set the invoice number and date. The last few lines disable regular-expression and date searching on the invoice number field.

8.4.6 Designing an application for a Setup relation

The typical Setup application manipulates a relation containing a single tuple. When you run your Setup application, you want that tuple to come up immediately and you never want to search for tuples.

Suppose we have a Setup relation with the following Schema:

    Business ( Name Address ( Street City State Zip ) )
    NextInvoice verbosename "Next Invoice Number #" type integer
After we define the Fx_Menubar, Fx_Frames and Fx_Entrys, we want to go directly into Change Mode if the record has been created, and go into Add Mode otherwise. The full Setup application might look like:

    #!/usr/local/qddb/bin/qwish -f
    lappend auto_path $qddb_library/fx
    if {[info exists blt_library]} {
        lappend auto_path $blt_library
    }
    wm title . "Setup"
    set s [qddb_schema open Setup]
    wm withdraw . ;# withdraw so the user doesn't watch the drawing.
    Fx:Init $s
    Fx_Menubar menubar -w .mb -schema $s -array gv_attr
    set search_entry [menubar SearchForEntry]
    Fx_Frame Business -w .biz -attr Business -setschema $s \
        -side top -anchor nw -relief sunken -bd 2
    Fx_Entry Business.Name -w .biz.name -attr Business.Name \
        -searchfor_entry $search_entry -side top -anchor e -side left
    Fx_Frame Business.Address -w .biz.addr -attr Business.Address \
        -side top -anchor nw -relief sunken -bd 2
    Fx_Entry Business.Address.Street -w .biz.addr.str \
        -attr Business.Address.Street -width 40 -side left
    .biz.addr.str.f_0.l configure -width 20
    Fx_Entry Business.Address.City -w .biz.addr.city \
        -attr Business.Address.City -width 40 -side left
    .biz.addr.city.f_0.l configure -width 20
    Fx_Entry Business.Address.State -w .biz.addr.state \
        -attr Business.Address.State -width 40 -side left
    .biz.addr.state.f_0.l configure -width 20
    Fx_Entry Business.Address.Zip -w .biz.addr.zip \
        -attr Business.Address.Zip -width 40 -side left
    .biz.addr.zip.f_0.l configure -width 20
    Fx_Entry NextInvoice -w .nextinv -attr NextInvoice -side top
    pack .nextinv -side top -fill x
    menubar configure -instances [Fx_Entry :: GetInstances] \
        -frames [Fx_Frame :: GetInstances]        
    menubar configure -afterpost_modes {
        .mb.modes.menu entryconfigure 0 -state disabled
        .mb.modes.menu entryconfigure 1 -state disabled
    }
    menubar configure -afterpost_edit {
        .mb.edit.menu entryconfigure 7 -state disabled
    }
    set k [Fx_QddbSearchParser :: MultiSearch $s {.*} on Business.Name]
    set mykey [qddb_keylist get $k]
    if {[llength $mykey] == 0} {
        menubar configure -aftersave {
            set k [Fx_QddbSearchParser :: MultiSearch $schema {.*} on Business.Name]
            set mykey [qddb_keylist get $k]
            menubar SetLastSearch [qddb_rows select -attrs Business.Name \
                -print Business.Name $k]
            menubar ChangeModeProc 0
        }
        menubar AddModeProc
    } else {
        menubar SetLastSearch [qddb_rows select -attrs Business.Name \
            -print Business.Name $k]
        menubar ChangeModeProc 0
    }
    qddb_keylist delete $k
    wm deiconify .

First, we set up the Fx_Menubar and all the Fx_Frame and Fx_Entry instances. Next, we link the frames and entries with the menubar, then disable some of the standard Fx features we aren't interested in. Finally, we search with '.*' to provide the single record, then depending on whether a record exists, go to Change Mode or Add Mode. After saving the record in Add Mode, we switch to Change Mode to prevent entry of further records.


next up previous contents
Next: 8.5 The design and Up: 8 Designing Custom Qddb Previous: 8.3 A simple example

Herrin Software Development, Inc.