by Dr. Peter Wayne
presentation at the April 2003 Alpha Five Conference
Xbasic 2003
From its inception, Xbasic was a language that handled table operations
well. The advances in the newest incarnation of Xbasic have to do with:
1) vastly expanded string functions--indeed, Xbasic has evolved into a
high-level string processing language--
and
2) xdialog--a new user interface language.
The most useful new methods and concepts in my own programming have been table.external_record_content_get() replaces a table open and close, a query and a while..end while loop with a single line, and ui_dlg_box() and its sister, ui_modeless_dlg_box(), namespaces and evaluate_template().
Some of you, no doubt, are fans of collections or of the *for_each() construct. Im just giving my personal take on which new features have been most helpful to me.
For this meeting I decided to build an entire mini-app, a month-at-a-glance scheduler. Here is a view of the completed scheduler:
Figure 1. View of scheduler
The schedule presents, in easy-to-visualize form, the appointments for the month. Clicking on an appointment brings up a pop-up dialog to edit the appointment:

Figure 2. Click on an individual appointment to edit it.
Clicking on a day, on the other hand, brings up the whole days list of appointments:

Figure 3. Clicking on a day brings up a pop-up of the day's appointments.
From here it is possible to enter a new appointment or to edit or delete one of the ones that is listed.
There is also a drop-down selector at the top that enables one to change months or years:

Figure 4. Drop-down selectors help change the "pages" of the calendar.
The table holding the data, schedule.dbf, is a simple flat file:

Figure 5. There are only 3 fields in Schedule.dbf: Date (D), Time (C,14), and Appt (C,120).
The calendar script starts by creating variables needed to display the months and years:
dim calendar as p with calendar ' note use of namespace dim monthname as c dim active as c dim active_line as c dim new_date as d monthlist=<<%lst% January February March April May June July August September October November December %lst% yearlist="" for i=2000 to 2020 yearlist=yearlist+i+crlf() next end with
Note the use of with calendar..end with. I have created a namespace of calendar. All the variables defined in this script are part of the calendar namespace, and as such will not conflict with other variables in other scripts, even if the script is called with script_play_local(). I could have written this segment as
dim calendar.monthname as c dim calendar.active as c dim calendar.active_line as c dim calendar.new_date as d calendar.monthlist=<<%lst% January February March April May June July August September October November December %lst% calendar.yearlist="" for calender.i=2000 to 2020 calendar.yearlist=calendar.yearlist+i+crlf() next
Clearly, the with..end with is simpler.
The introduction is followed by a definition of a simple function that reads one days appointments into a carriage return-linefeed delimited string:
function get_day_list as c(day as c)
get_day_list=table.external_record_content_get\
("schedule","time+left(strtran(strtran(appt,crlf(),
'-'),'^',''),30)+'|White,Blue$'+cdate(date)+'^'+recno()",\
"toseconds(time)","date={"+day+"}")
end function
Notice that the background color formatting, the appointment date, and the record number of the record are also part of the string. Putting these in the function makes it easier to format and edit entries later on.
I could have made this a global function, but I thought it would execute faster in the main script. Since it is called repeatedly during the filling up of the different cells of the calendar, here is one area where optimization of code may pay off.
This is the output of the function when given an input of 03/05/2003:
9:00 am meeting with Sharon |White,Blue$20030305^3 10:30 am go to bank to withdraw 1 or 2 |White,Blue$20030305^9
There are 2 lines, containing the time, the first 30 characters of the appointment details, the pipe character followed by background color formatting, a $ sign, the date in YYYYMMDD style, and the record number after the caret (^) character.
The heavy lifting of the script is done by the next function, make_month(), which creates the header lines over each cell (identifying the date) and fills in all the cells with appointment information:
function make_month as v(vars as p, start_day as d)
with vars
monthname=cmonth(start_day)+" "+cyear(start_day)
mnth=month(start_day)
curr_month=cmonth(start_day)
curr_year=cyear(start_day)
dim dd as d
if day(start_day)>1 then
start_day=start_day-day(start_day)+1
end if
if dow(start_day)>1 then
start_day=start_day-dow(start_day)+1
end if
for week=1 to 6 ' rows of calendar
for j=0 to 6 ' columns (days of week)
dd=start_day+j+((week-1)*7)
if month(dd)=mnth then
dayhdr_tmp=""+day(dd)+"|"+"Blue\White,Blue$"+cdate(dd)+"^"+crlf()
else
dayhdr_tmp=""+day(dd)+"|"+"Dirty White,Blue$"+cdate(dd)+"^"+crlf()
end if
daylist_tmp="daylist"+week+dow(dd)+\
"=dayhdr_tmp+get_day_list("+quote(dtoc(dd))+")"
evaluate_template(daylist_tmp)
next
next
end with
end function
The script continues with
make_month(calendar,date())
After this script runs, there are 42 strings, daylist11 through daylist67, that contain the day numbers, formatting directions and appointments for each date. As an example, here is the value of daylist24 after make_month() runs:

Figure 6. Contents of daylist24 after a call to make_month().
The first portion of each line contains the day of the month, then the pipe character, followed by the color scheme employed for the header and the CDATE form of the full date. I elected to signify the dates belonging to the current month by showing them in blue white, while dates for the preceding and following month are shown in dirty white.
Next comes the calendar proper, which is displayed as a modeless Xdialog box. Here is the dialog part of the code, with comments:
with calendar
ui_modeless_dlg_box("Calendar At A Glance",<<%dlg%
{comment This next line tells what happens when the X is pressed in the
upper right hand corner
of the dialog box--without it, the dialog won't close!}
{can_exit=on_exit}
{comment This first region contains the month and year and a
"Print" button}
{region}
{font=Arial,14,B}
{text=24,2:monthname}
{font=Arial,8}
[curr_month^=monthlist!startchange]|[curr_year^=yearlist!startchange]|
{sp=5}
<%B=T;T=Print this month;O={J=C}{I:'$a5_print'} Print
%!Print_button_click >
{endregion};
;
{comment Writing 'region=a' means that all identically designated regions
will share the same
horizontal tab dimensions.}
{comment This region contains the day of week header}
{region=a}
{font=Arial,8,B}
Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday
{endregion};
{comment Each block defines 7 'cells', or one week, containing the dates on
the
first line and appointments on the subsequent lines of the cells.}
{region=a}
{font=Arial,8}
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL11^#daylist11!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL12^#daylist12!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL13^#daylist13!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL14^#daylist14!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL15^#daylist15!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL16^#daylist16!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL17^#daylist17!daylist];
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL21^#daylist21!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL22^#daylist22!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL23^#daylist23!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL24^#daylist24!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL25^#daylist25!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL26^#daylist26!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL27^#daylist27!daylist];
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL31^#daylist31!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL32^#daylist32!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL33^#daylist33!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL34^#daylist34!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL35^#daylist35!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL36^#daylist36!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL37^#daylist37!daylist];
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL41^#daylist41!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL42^#daylist42!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL43^#daylist43!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL44^#daylist44!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL45^#daylist45!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL46^#daylist46!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL47^#daylist47!daylist];
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL51^#daylist51!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL52^#daylist52!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL53^#daylist53!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL54^#daylist54!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL55^#daylist55!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL56^#daylist56!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL57^#daylist57!daylist];
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL61^#daylist61!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL62^#daylist62!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL63^#daylist63!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL64^#daylist64!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL65^#daylist65!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL66^#daylist66!daylist]|
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL67^#daylist67!daylist]
{endregion};
%dlg%,
Each cell has an owner draw string that reads something like this one:
[%{O={J=L}{B=@$(|)+1,$($)-1}{@1,$(|)-1}%.18,5DL11^#daylist11!daylist]
Translation: Display the string in daylist11. For each cr-lf delimited line in daylist11, format the background color using the directions embedded in the line, starting with the first character following the | and leading up to the character preceding the $. Display the characters from the first position through to the one preceding the pipe. Make the whole cell 18 characters wide and 5 lines deep. When the user clicks on the cell, put the result in the variable DL11 and trigger an event, daylist.
You cant find the daylist event in the Xbasic manualits an event that Xdialog allows me to name, and it fires off every time the value in daylist changes. You may have noticed that there are a whole bunch of lines42 to be exactin which DLxx can be changed and daylist can be triggered. One of the powers of Xdialog is that it allows multiple actions to trigger the same event. This means that selection of any cell anywhere on the form will trigger the same daylist event.
The daylist events, along with a few other events, is handled in the code section of the xdialog:
<<%code%
if a_dlg_button="on_exit" then 'when user presses the X in the upper right corner
ui_modeless_dlg_close("Calendar At A Glance")
end if
if a_dlg_button="startchange" then 'user changed month or year
new_date=ctod(curr_month+"/1/"+curr_year)
make_month(calendar,new_date)
end if
if a_dlg_button="daylist" then ' user clicked on a cell
active_cell_string=alltrim(ui_dlg_ctl_current("Calendar At A Glance"))
if show_everyone=.t. then
ui_msg_box("active_cell_string",active_cell_string)
end if
' cell name comes after .18,5 in cell formatting directive, so look for
' a "5" in the active_cell string
active_cell=substr(active_cell_string,at("5",active_cell_string)+1,4)
active_line=eval(active_cell)
if show_everyone=.t. then
ui_msg_box("now active_line is",active_line)
end if
if word(active_line,2,"^")="" then
' it's a day in the cell header
day_chosen=word(word(active_line,2,"$"),1,"^")
calendar.edit_day(day_chosen) ' edit the whole day
make_month(calendar,ctod(substr(day_chosen,5,2)+"/"+right(day_chosen,2)+"/"+left(day_chosen,4)))
else
' it's an appointment in the cell body
active_record=word(active_line,2,"^")
edited_date=calendar.edit_entry(calendar,active_record)
make_month(calendar,edited_date)
end if
evaluate_template(active_cell+"=0")
end if
if a_dlg_button = "Print_button_click" then
calendar.calendar_report(wordat(curr_month,monthlist,crlf()),val(curr_year))
end if
%code%)
end with
Selection of an individual cell line fires up the daylist event, whicleft(strtran(appt,crlf(),'-'),50) first figures out who was calling it by polling ui_dlg_ctl_current(). It then figures out what line of the cell was chosenif a record number is present (detected by looking to see if something follows the ^), then another function, edit_entry(), is called to edit the appointment:
'Date Created: 02-Mar-2003 01:49:20 PM
'Last Updated: 02-Mar-2003 07:28:10 PM
'Created By : Peter Wayne
'Updated By : Peter Wayne
function edit_entry as d(vars as p,rec as c)
query.filter="recno()="+rec
query.order=""
query.options=""
t=table.open("schedule",file_ro_shared)
ix=t.query_create()
initial_text=alltext(t.name_get())
date=t.date
time=t.time
appt=trim(t.appt)
t.close()
result=ui_dlg_box("Edit Appointment",<<%Dlg%
{region}
Date: |[%DATE;P=popup.calendar(dtoc(Date));I=popup.calendar%.14Date];
Time: |[.14time];
Details: |[%MW%120.30,4appt]
{endregion};
{line=1,0}{lf};
<*15OK><15Delete><15Cancel>;
%Dlg%)
if result="OK" then
t=table.open("schedule",file_rw_shared)
ix=t.query_create() ' same query filter and options as before
ix.drop()
if alltext(t.name_get())<>initial_text then
ui_msg_box("Error","Another user has changed this appointment!")
else
t.change_begin()
t.date=date
t.time=time
t.appt=appt
t.change_end()
end if
t.close()
end if
if result="Delete" then
t=table.open("schedule",file_rw_shared)
ix=t.query_create() 'same query filter and options as before
if alltext(t.name_get())<>initial_text then
ui_msg_box("Error","Another user has changed this appointment!")
else
t.change_begin()
t.delete()
t.change_end(.t.)
end if
t.close()
end if
edit_entry=date
end function
I could have used table.external_record_content_get() here as well, but I knew that eventually I would have to open the schedule table, so there was no reason not to use the somewhat longer query_create() method. Notice that I also read the initial value of the record into a variable called initial_text, and then before updating the record I check that record has not been altered by another user. This is known as optimistic lockingwe assume that no other user is going to update the record at the same time we are. If someone does, then we just abandon our changes and start over again. It may seem inefficient but in a large multiuser system it is actually more efficient than pessimistic locking, in which users lock records as soon as they initiate changes.
There are 3 other global functions used by this portion of the calendar. The first is the edit_day() function, which displays all of one days appointments, and is called if the daylist event code decides that the header of the cell was selected:
'Date Created: 02-Mar-2003 01:40:00 PM
'Last Updated: 02-Mar-2003 01:58:27 PM
'Created By : Peter Wayne
'Updated By : Peter Wayne
FUNCTION edit_day AS V (chardate AS c )
dim dt as d
dt=ctod(substr(chardate,5,2)+"/"+right(chardate,2)+"/"+left(chardate,4))
Dim format as p
Format.tab_stops="1,6"
Format.odd_row_color="White"
Format.even_row_color="Blue White"
Format.odd_selected_color="Dark Blue"
Format.even_selected_color="Dark Blue"
Format.font="Arial,8"
Format.font_color_unselected="Black"
Format.font_color_selected="White"
Format.group_size=1
Format.number_rows=.f.
Format.alternating_bands=.t.
get_recs=<<%str%
daylist=table.external_record_content_get\
("schedule","time+'|'+left(strtran(appt,crlf(),'-'),50)+'|'+recno()","toseconds(time)","date={"+dtoc(dt)+"}")
daylist_disp=a5_owner_draw_list_fmt(daylist,Format)
%str%
evaluate_template(get_recs)
title="Calendar for "+cdow(dt)+", "+dtoc(dt)
DIM one_record as n
DIM result as C
ok_button_label="&OK"
cancel_button_label="&Cancel"
result=ui_dlg_box(title,<<%Dlg%
{region}
Choose an entry to edit:;
[%d;O={@@}%.60,5one_record^#daylist_disp!chosen];
{endregion};
{line=1,0};
{region}
<*15OK> <15New Entry!New_Entry> <15CANCEL>
{endregion};
%Dlg%,<<%code%
if a_dlg_button="chosen" then
a_dlg_button=""
original_entry=word(daylist,one_record,crlf())
entry_record_number=word(original_entry,3,"|")
edit_one(val(entry_record_number))
evaluate_template(get_recs)
end if
if a_dlg_button="New_Entry" then
a_dlg_button=""
new_entry(dt)
evaluate_template(get_recs)
end if
%code%)
END FUNCTION
You may notice that instead of creating a function to pull up the days records, as we did in the original calendar script with get_day_list(), here I just define a string with commands and use evaluate_template() to execute the commands:
get_recs=<<%str%
daylist=table.external_record_content_get("schedule","time+'|'+left(appt,50)+'|'+recno()","toseconds(time)","date={"+dtoc(dt)+"}")
daylist_disp=a5_owner_draw_list_fmt(daylist,Format)
%str%
evaluate_template(get_recs)
This could just as easily have been written as
Function get_recs as v(vars as p)
With vars
daylist=table.external_record_content_get("schedule","time+'|'+left(appt,50)+'|'+recno()","toseconds(time)","date={"+dtoc(dt)+"}")
daylist_disp=a5_owner_draw_list_fmt(daylist,Format)
end with
end function
get_recs(local_variables())
The next global function is called by edit_day() and is called edit_one():
'Date Created: 02-Mar-2003 01:31:11 PM
'Last Updated: 02-Mar-2003 07:28:26 PM
'Created By : Peter Wayne
'Updated By : Peter Wayne
FUNCTION edit_one AS V (rec AS N )
query.filter="recno()="+rec
query.order=""
query.options=""
t=table.open("schedule",file_ro_shared)
ix=t.query_create()
initial_text=alltext(t.name_get())
date=t.date
time=t.time
appt=trim(t.appt)
t.close()
result=ui_dlg_box("Edit Appointment",<<%Dlg%
{region}
Date: |[%DATE;P=popup.calendar(dtoc(Date));I=popup.calendar%.14Date];
Time: |[.14time];
Details: |[%MW%120.30,4appt]
{endregion};
{line=1,0}{lf};
<*15OK><15Delete><15Cancel>;
%Dlg%)
if result="OK" then
t=table.open("schedule",file_rw_shared)
ix=t.query_create() ' same query filter and options as before
ix.drop()
if alltext(t.name_get())<>initial_text then
ui_msg_box("Error","Another user has changed this appointment!")
else
t.change_begin()
t.date=date
t.time=time
t.appt=appt
t.change_end()
end if
t.close()
end if
if result="Delete" then
t=table.open("schedule",file_rw_shared)
ix=t.query_create() ' same query filter and options as before
ix.drop()
if alltext(t.name_get())<>initial_text then
ui_msg_box("Error","Another user has changed this appointment!")
else
t.change_begin()
t.delete()
t.change_end()
end if
t.close()
end if
END FUNCTION
It is very similar to edit_entry(). The final function in this group is new_entry():
'Date Created: 02-Mar-2003 01:57:11 PM
'Last Updated: 02-Mar-2003 01:57:11 PM
'Created By : Peter Wayne
'Updated By : Peter Wayne
FUNCTION new_entry AS V (dt AS D )
'Create an XDialog dialog box to prompt for parameters.
DIM Time as C
DIM Appt as C
DIM varC_result as C
ok_button_label="&OK"
cancel_button_label="&Cancel"
varC_result=ui_dlg_box("New Entry for "+dtoc(dt),<<%Dlg%
{region}
Time:| [.14Time];
Details:| [%mw%.120,5Appt];
{endregion};
{line=1,0};
{region}
<*15=ok_button_label!OK> <15=cancel_button_label!CANCEL>
{endregion};
%Dlg%)
if varC_result="OK" then
t=table.open("schedule")
t.enter_begin()
t.date=dt
t.time=time
t.appt=appt
t.enter_end(.t.)
t.close()
end if
END FUNCTION
If this last function looks suspiciously like something the Xbasic genie created, you better believe it, Roscoe! By this time I was delighted to let the computer do the coding. Enough is enough!
Click here to download the xdialog script and functions described here.
4/6/2003
Don't forget, we need your feedback to make this site better!