This article is a continuation of the series I began in the July-August issue of the Washington Apple Pi Journal about simple programming in REALbasic (RB). I began by showing the would-be programmer how to use RB to construct a simple check register. In later installments I have transformed that simple check register into a full-featured application by adding more features as the series has developed. The application through Part 4 can be downloaded here:
http://www.wap.org/journal/realbasic/
This article will demonstrate the addition of the following functions:
All Mac users are familiar with using preferences to establish the desired behavior of applications. For the check register I'll use one simple preference to demonstrate how preferences can be saved. As you may know, application preferences are saved in a Preferences folder located in the Users Library, commonly denoted thus: ~Library/Preferences.
To demonstrate a preference I'll offer a choice of how CheckWindow operates. If the user wants to enter more than one check during a single session, it is more convenient if the window remains open rather than closing after every check entry. To allow that window response, I'll make that choice a preference.
To begin, I'll add two boolean properties, manyCheck and prefsHaveChanged, to globalFinancial. Next I'll add a new preferences window where the user can make a selection. I've designated the preferences window as a Sheet window, a form now familiar to Mac users that seems to slide down out of the title bar of the parent window (see Figure 1). Within the preferences window I have added a few new controls. The simplest control is the Separator (the line between the title and the text). The others are the familiar Group Box and the Radio Buttons. The radio buttons are named rbMultipleChecks and rbSingleCheck. RB takes care of managing the radio button logic so that only one can be selected. I have added the following to the preferences window Open event handler to set the buttons to whatever value is held by the variable manyCheck:
if manyCheck = True Then
rbMultipleChecks.value = True
else
rbSingleCheck.Value = True
end if
The window's OK button gets the following code:
if rbMultipleChecks.value = true then
manyCheck = true
elseif rbSingleCheck.value = true then
manyCheck = false
end if
prefsHaveChanged = True
self.close
Note that the if-then test looks at which radio button is selected, sets manyCheck accordingly and sets the change flag.
Figure 1
The boolean variable prefsHaveChanged has been added to the list of properties in globalFinancial. The variable prefsHaveChanged will be tested when the application is closed to execute writePrefs if this variable is true. I do this by adding the following to the transWindow method saveAll:
if prefsHaveChanged then writePrefs
The Cancel button only closes the window, so it receives the simple code:
self.Close
The writePrefs method will write a preference file to the location described above and is placed in globalFinancial as follows:
dim f As folderItem
dim o As TextOutputStream
f = preferencesFolder.child("Bank Checking prefs")
o = f.createTextFile
// manyCheck status
if manyCheck = true then
o.writeline "manyCheck" + chr(9) + "true"
else
o.writeline "manyCheck" + chr(9) + "false"
end if
o.Close
RB makes a simple task of locating the preference file. It is simply a "child" of "preferencesFolder." The code writes the legend "manyCheck," then a tab and then the word "true" or "false." I'll also need a companion method (readPrefs) to read the preference file when the application opens:
dim f As folderItem
dim instream As TextInputStream
dim s As string
manyCheck = false
// get manyCheck flag
f = preferencesFolder.child("Bank Checking prefs")
if f.exists then
inStream = f.openastextFile
s = inStream.readLine
if NthField(s, chr(9), 2) = "true" then manyCheck
= true
instream.Close
Else
prefsHaveChanged = True // force writePrefs to make a file
End
The call to readPrefs is placed in the transWindow initialize
method so the manyCheck flag is set upon application start. Note
that if a preference file is not found, the prefsHaveChanged flag
will force writePrefs to write a file when the application closes.
The only remaining task is to get the preferences window to open. As you know the Preferences menu item always appears in the Apple menu but RB doesn't have a way to place any menu item in the Apple menu. Instead, RB places the Preferences menu item in the Edit menu, and in the Property/Value table its Super is set to PrefsMenuItem (See Figure 2). This tells RB that when the application is compiled, the Preferences menu item must appear in the Apple menu.
Figure 2
The Preference Menu Handler is added to transWindow:
Preference.Show
Now that I've established a preference for how CheckWindow should operate, I have to make some changes to that window to reflect that preference. If the preference manyCheck is set false, CheckWindow will not change at all, but if manyCheck is true the window must remain open until I'm done entering checks. To accomplish this, the Cancel button will be labeled "Done" and the OK button will function as before, but won't cause the window to close. Therefore, the Cancel button Open event handler will be coded as follows:
if manyCheck then
me.caption = "Done"
Else
me.caption = "Cancel"
End
The OK button Action event handler, which ended before with a call to close the window, now ends with the following:
// Reset Check Window if necessary
if Not manyCheck then
self.Close
Return
Else
BalText.text = "Balance: $"+ TransWindow.transList.Cell(ntransactions
- 1, 6)
CkNrText.text="Check Number : " + str(nextcknr)
ckdate.setfocus
CkDate.SelStart = 0
CkDate.SelLength = Len(CkDate.text)
ckamount.text = ""
pay.text = ""
PayeeList.scrollposition = 0
PayeeList.listindex = - 1
End
This added code obviously closes the window if manyCheck is false. If it is true, this code reconfigures CheckWindow to its original condition so another check can be entered.
Occasionally I may enter a new transaction out of sequence. Obviously, I'd like the Register to remain in date and check-number order. I accomplish this by adding a new testEntryDate method to globalFinancial that will compare every transaction with the last entry and call for a sort if the transactions are out of order.
I add a call to the testEntryDate method to OK button action events of the three types of transactions I have programmed: CheckWindow, DepositWindow and atmWindow. This will insure that any out-of-sequence transactions are handled properly. A pointer to the last transaction (lineNr) is sent to the testEntryDate method and the comparison is made to the transaction before lineNr.
// Tests last entry to see if it's earlier than previous
entry
if TransWindow.translist.cell(lineNr - 1, 0) < TransWindow.translist.cell(lineNr
- 2, 0) then
Transwindow.DateSort()
else
FindBalance()
end if
This method simply looks at the date of the last line in the check register and compares it with the date of the previous line. If the date on the last line is earlier, then the method calls for the register to be sorted by the DateSort method, also newly added to transWindow:
transList.SortedColumn = 2 //sort by check
number
transList.Sort
transList.SortedColumn = 0 //then by date
transList.Sort
FindBalance()
TransWindow.translist.scrollposition = 1000 //set slider to bottom
Those of you who have persevered with these articles from the beginning may
recall that transWindow's transList has a hidden column
0 in which I stored the transaction date in totalSeconds, RB's master property
for storing date/time variables. This allows me to do a date sort using column
0.
If you need to void a check that has already been written and recorded, you can do it by:
I add the new menu item in the same manner as other menu items and name it EditVoidCheck. The menu handler is only one line that opens the new window:
voidCheck.show
VoidCheck is shown in Figure 3. It is merely an edit box (ckNr) and two familiar buttons. Its Open event handler simply places the cursor in the edit box:
ckNr.setfocus
Figure 3
The Cancel button does the familiar task of closing the window with no further action. The OK button action handler follows:
dim i, j as integer
dim s, vDate As string
for i = 1 to ntransactions - 1
if TransWindow.transList.Cell(i, 2) = ckNr.text then
j = i
exit
end
next
// Is entry outside the check number range?
if j = 0 and val(ckNr.text) <> nextcknr then
MsgBox "That is not in the range of Check Numbers for
this year"
self.close
return
end
if ckNr.text <> str(nextcknr) then
// Build a replacement line; first get original date
vDate = TransWindow.transList.cell(j, 1)
s = buildVoidLine(vDate, ckNr.text)
isEdit = true
TransWindow.addTransRow(s, j)
else
// Entry is the next check number
s = buildVoidLine(today.shortDate, ckNr.text)
TransWindow.addTransRow(s, ntransactions)
end
FindBalance //recompute Balance
ListHasChanged = True
self.close
First every record in the register is searched to find the check number that
matches the entry in the edit box. If no match is found the message box so
advises. If a match is found a replacement record must be built with the
word "Void" in the Payee/Payer column, a check in the "cleared" box,
and "0.00" in the debit column. First the date of the original
transaction is determined. Then, that date and the check number are sent
to the new method, buildVoidLine:
dim s As string
s = vDate + chr(9)
s = s + ckNr + chr(9) // check nr
s = s + "x" + chr(9) // check considered cleared
s = s + "Void" + chr(9)
s = s + "0" // debit
return s
This method constructs a record in the same format as a record that is read from the data file so the same transWindow method, addTransRow can be used to insert the revised record into the Check Register. However, there is one nuance in this transaction that must be managed. The method addTransRow begins by adding a new line to the list box, transList, but I don't want to add a new line, I want to modify an existing line. To do this I've added a new globalFinancial property, isEdit, as Boolean, and I've changed the beginning line in addTransRow to read:
if not isEdit then transList.addRow""
This insures that the new record is simply inserted in place of the old record.
From time to time, the Depositor and Payee name files will may to be edited. A company name could change, or there might be a new name or one you no longer use. To accomplish this edit, I'll build one new window that will be used to edit both the Depositor and Payee name files. This window, EdList, which is shown in Figure 4, is equipped with a listBox, an editField, three radio buttons and two push buttons. It will contain the following new properties: EditIndex as integer, incomplete & replace as boolean and listMod as string. It will be called by two new menu items in the Edit Menu, Edit Depositor List... & Edit Payee List..., that I will construct just as I did the Void Check... menu items above.
When this application was built, the Depositors List and the Payees List were only opened by the DepositWindow and the CheckWindow respectively. Now these lists must also be opened by the new EdList, so I need to add four new properties to globalFinancial to accommodate the two lists: depList(0) and payList(0) as string arrays, and nDepositors & nPayees as integers. In addition I'll add three new properties to globalFinancial to accommodate the editing: depositorsChanged and payeesChanged as boolean, and newName as string.
The application will now read the two list files upon opening by adding code to the initialize method. From here on I will describe only the code associated with the Payee functions. The Deposit functions are the same, but of course use a different file.
redim payList(0)
npayees = 0
payeeFile = dataFileFolder.child("Payee Names") //
location of Payee List
if payeeFile.exists then
inStream = payeeFile.openastextFile
while not instream.eof
currentLine = instream.readLine //read
Payee list
payList.append currentLine
npayees = npayees + 1
wend
else
msgBox "The Payee Names file is missing."
return
end if
instream.close
This is essentially identical to the method in the CheckWindow that read the file into the window's listBox but now reads into the array payList. When the CheckWindow opens it merely reads this array into the listBox.
dim i as integer
for i = 1 to npayees
me.addrow payList(i)
next
me.headingindex = 0
me.sort
The editing menu handlers will call the EdList init method using the input parameter list, which will define what type of edit (deposit or payee) is called. The Payee Edit menu handler is:
EdList.init ("payee")
Return True
The Depositor Edit handler will contain the list parameter, "deposit" instead of "payee."
The EdList init method follows:
listMod = list
select case list
case "deposit"
self.Title = "Edit Depositors List"
loadList
case "payee"
self.Title = "Edit Payees List"
loadList
end Select
self.Show
The new function Select Case will execute its commands depending on the content of the parameter list, and once that is complete, the window opens. Note that the variable listMod is set to the input parameter list. In the case of "payee," the window's title is defined and load list is called to fill the ListBox. In this method listMod controls the case selection:
dim i as integer
select Case listMod
case "deposit"
for i = 1 to ndepositors
editBox.addrow depList(i)
next
editBox.headingIndex = 0
editBox.sort
case "payee"
for i= 1 to npayees
editBox.addrow payList(i)
next
editBox.headingIndex = 0
editBox.sort
end Select
Again, Select Case defines which set of commands are executed. In the payee case, the names from the array payList are loaded and sorted.
Now, if the user clicks on any name in the listBox named EditBox the CellClick handler does this:
ModField.text = me.cell(row, 0)
EditIndex = row
ModField.setfocus
Replace = true
First the name in the clicked cell is placed in the editBox, ModField. Then EditIndex is set to the row number clicked. The focus is placed in ModField and Replace is set True. When the OK button is pressed, here's the result:
If ModField.text = "" then
msgBox "You must have an entry in the edit box"
return
end if
NewName = ModField.text
incomplete = False
Select case listMod
case "deposit"
modDep
case "payee"
modPay
end select
if incomplete then Return //one of the "mod" handlers
had an error
self.Close
After an error check the Select Case again determines what method is called. Here's the modPay method:
dim i As Integer
If ReplaceButton.value then
if Replace = false then
msgBox "You must select a name to replace."
incomplete = True
return
end if
editBox.cell(editIndex, 0) = newName
ElseIf NewButton.value = true then
editBox.addrow newName
npayees = npayees + 1
payList.append " "
Else //delete Button
editBox.removeRow editIndex
npayees = npayees - 1
end if
editBox.sort
for i = 1 to npayees
payList(i) = editBox.cell(i - 1, 0)
next
payeesChanged = True
Depending on which radio button is selected, the name is replaced, added or deleted and then payList is rebuilt. The boolean property, payeesChanged has been established in globalFinancial to trigger the new method writePayeeList if it is True. I've also added a test for payeesChanged and depositorsChanged in the promptSave and EnableMenuItems methods so this writePayeeList is called:
dim o As textoutputStream
dim i As integer
//write Payee Name List
o = payeeFile.CreateTextFile
for i = 1 to nPayees
o.Writeline payList(i)
next
o.close
payeesChanged = false
This concludes this article on REALbasic. Future articles will cover the following:
The compiled application to-date along with the data files and the source code can be downloaded here:
http://www.wap.org/journal/realbasic/