Month-at-a-Glance Calendar in Xdialog

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. I’m just giving my personal take on which new features have been most helpful to me.

Calendar At A Glance Mini-Application

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:

Monthly Calendar

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:

One Appointment

Figure 2. Click on an individual appointment to edit it.

Clicking on a day, on the other hand, brings up the whole day’s list of appointments:

One Day

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:

Change Month or Year

Figure 4. Drop-down selectors help change the "pages" of the calendar.

Data Elements

The table holding the data, schedule.dbf, is a simple flat file:

Schedule structure

Figure 5. There are only 3 fields in Schedule.dbf: Date (D), Time (C,14), and Appt (C,120).

The Code

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 day’s 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:

daylist24 contents

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.

The Xdialog

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 can’t find the daylist event in the Xbasic manual—it’s 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 lines—42 to be exact—in 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 chosen—if 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 locking”—we 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 day’s 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 day’s 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!

Return to home