Creating the Cron Manager GUI

First thing you'll notice is that we have included our file from our first
tutorial. So, to get your application to work, you'll have to copy the first
lesson's file into the same directory as this file; change the path of the
CronJobMgr file in the requires clause to the true location of the file;
or copy the contents of the CronJobMgr.rb file into the CronManager.rb
file, and just remove the requires clause. Whichever you prefer will be fine.

I chose to separate the files just in case I ever wanted to reuse the backend with another interface. However, it may be easier to transfer between machines if you have everything in one file. So, if you plan on sharing this file with anyone else, or if you plan on using it on several different machines, you may want to put everything into one file.

Next, you'll notice that we have defined all of the callback methods we described earlier as well as one helper method called resetDetailView. Skip these for the time being and move down to the initialize method.

The initialize method is where we set up everything. In this method we create all of the widgets in our GUI, determine their location in our application, and set the callback methods associated with each of their events.

Before we create our GUI, however, we need to start by creating an instance
of our CronJobMgr class and reading in the list of cron jobs we have
already scheduled on our system. I use a local file called cronjobs that
I place in the same directory as the Cron Manager application's files. But if
you want to, and you have the correct permissions, you could alter the cron
jobs for the entire system by just changing filename variable's value.

After we have created an instance of our CronJobMgr class, we will need to load the cron jobs into the newly created object. Easy enough, we just call the loadCronJobs method from our class. There is one catch, however, and that is that our method could throw an error since it must open a file on our system, or create a new one if one doesn't already exist, and we may not have the necessary permissions.

If this happens, we don't want our application to crash; rather we want it to handle the error gracefully. We do this by using Ruby's error-handling mechanism, which you can see in the code below.

# Try to open the cronjobs file and load the jobs
begin
@cronJobMgr.loadCronJobs(@filename)
rescue
TkWarning.new("There was a problem loading the jobs " +
"from the cronjobs file. Make sure that " +
"you have permission to read/write files.")
end

Ruby handles exceptions by surrounding the code we wish to monitor for errors with a begin/end block. Then we add rescue clauses for all of the exceptions we wish to catch. This is very similar to the try/catch method that many of us are familiar with from our Java or C++ experience. You can see that we have placed our loadCronJobs method within our begin/end block and we are using a general (default) rescue clause to catch any errors that may be returned from the method. We notify the user of this error by creating an instance of the TkWarning
dialog which is simply an error dialog that contains our string we passed to the
new method and a single 'Ok' button.

The next step in programming our GUI is to create Proc objects for
all of the callback methods in our application. The reason for this is that
the command option, which we use to set our callback methods for each
of the widgets, takes a Proc object as its argument. The code below is
taken from our CronManager class and shows how to create Proc
objects for all of our callback methods.

A Proc object is an objectified block of code. The nice thing about a Proc object is that it retains local variable values whenever it is called unlike a normal method whose variables are created anew with every call. We could have also created our Proc object by calling the Proc class's new method and passing in a block of code like so:

AddButtonClicked = Proc.new {addCronJob}

Finally, we can create the actual GUI portion of our program. Remember to start each of your Tk-based applications with a TkRoot object. We do exactly that when we start creating the widgets for our cron manager GUI. The following line of code creates the TkRoot object for our application and sets it title to "Cron Manager."

@root = TkRoot.new() {title 'Cron Manager'}

Next, we'll create the two frame objects for our application. The first is called listViewFrm and it holds the listbox and scrollbar objects that will be used to display all of the cron jobs on our system. The second frame is the detailViewFrame and it holds the rest of the widgets in the application that will be used to add, edit, and delete each cron job. The following is the code for creating each of the TkFrame objects.

Notice that when we call the new method of each TkFrame object,
we pass in a reference to the root object we created earlier. That's because
the new method takes an object representing the parent of the widget
(i.e., the widget in which the new widget will reside). Directly after the call
to the new method is another method call. This one is a call to the pack
method. This alerts the packer geometry manager to the presence of the two frames
and sets the size and position of the frames in the application. Notice that
what we did here is tell the root window to pack itself around the frames with
nothing but a 10-pixel pad around each of the frames. We also set it so that
the frames align themselves to the left side of the root frame.

Now we create the components that will go into the listViewFrm. You need to create a scrollbar and a listbox and link the two together. To begin, create each of the components and pass in the listViewFrm object as their parent.

Use the pack command again to place the scrollbar in the frame. In this case we place the scrollbar on the right side of the listbox, since this is the normal place for a scrollbar. And we tell it to fill the frame with respect to the y-axis. Next create a listbox object and call the pack method on it once again.

You'll notice that we specified a width for the listbox object. The geometry manager will adjust the size of the frame in which it resides to fit the widget. Once again, we set the fill option to "y" in order to make sure that the listbox is the same height as the scrollbar. This time, however, we set a new option -- the expand option. The expand option just tells the system to increase the size of the widget with respect to the size of the frame. Thus, if we resize the window, the listbox should also grow to consume the extra space.

In the final portion of the code we call the bind method to associate the setDetailView callback method with the ListboxSelect event. What this does is set all of the dropdown lists and entry widgets to the data associated with the cron job currently selected in the listbox. Thus, when a user clicks on a cron job in the list, he/she should see the details of that job in the widgets to the right of the listbox.

So now that we are done with the list view, we need to create the widgets that will make up our detailed view. The next portion of the code does exactly this starting with the labels for each of the GUI components. We do this by using a couple of Ruby shortcuts to create an array of TkLabel objects. Take a look at the following code to see just how we go about employing some cool Ruby techniques to quickly create the six TkLabel objects in our application.

First, we create an array of strings representing each of the labels. By simply placing a comma-separated list of strings within brackets we have created an array (e.g., ['This', 'is', 'an', 'array']). If you remember from our last lesson, everything in Ruby is an object, which means that we can call methods on our new array without having a variable represent it. So, we call the collect command, which is an iterator that invokes a block of code for each item in the array and replaces that item with what was returned from the code block. In our block of code we create a new TkLabel object for each of the strings in the array and what is returned is a new array containing the resultant label objects.

Now, that we have created our label objects the only thing left to do is place them within the application. The next portion of code shows how we do this using the grid geometry manager.

# Add each of the labels to the dialog
labels.each_index { |i|
labels[i].grid('column'=>0,
'row'=>i, 'sticky'=>'w')
}

This time we are going to switch geometry managers from the packer to the grid. When using the grid geometry manager we basically specify the row and column in which we would like our widget to reside. In the case of our label objects, we place one label on each row in the first column of our table. We also set an option called sticky that tells the geometry manager on which side (north, south, east, or west), or combination of sides, our widget will be anchored.

After we create the labels, we need to create the widgets they identify. For the most part, this is pretty straightforward. There are a few things of interest, however. We'll start off by explaining the nuances associated with the option menu buttons. Take a look at the code for the first TkOptionMenubutton -- the minutesOptionMenu.

First, observe that we have created a TkVariable object for each of the TkOptionMenubutton widgets. The TkVariable object is passed into the widgets new method as the second parameter. The TkVariable object allows us to get and set the data in the widget. For example, we can choose which item in the drop-down list is selected by setting our TkVariable object like so:

@minute = "45"

The next thing you'll notice that is different from the previous widgets is that we pass in an array as the third parameter. This array serves a two-fold purpose. It sets the items in the option menu button and it also sets the default item selected. The asterisk in front of the array is very important (just take it out to see why). The asterisk basically expands the array, turning each of the items in the array into individual parameters. If you leave out the asterisk, you end up with one very long item in your TkOptionMenubutton object.

The next thing we need to add to our application is a text field (TkEntry) to hold the name of the command we wish to schedule. This is done with two widgets - a TkEntry and a TkButton to open a file browser dialog. The code creating both of these objects is shown below.

The TkEntry object is created in relatively the same way as the other widgets we've already created, with the exception of the relief option. This option basically determines the look of the object (whether it is raised, lowered, flat, etc.). In this case, we have chosen the sunken attribute to make it look like a traditional text field.

The TkButton object is also fairly similar, however, this is our first
widget to have a callback method associated with it, and thus, our first widget
for which we have set the command option. The command option takes
a Proc object as its argument and sets it to be the callback method for
the widget. We'll be using this option for the remaining buttons in our application.

The final portion of our initialize method creates the Add, Update, Delete, and Save buttons for our application. Each of these are basically created the same way as the browse button so we will skip that portion of the code in this tutorial. The only thing you'll notice different from the previous widgets is that we have created another frame for the layout of the buttons.

Final Thoughts

Well, that's it. We now have our GUI complete, and the only thing left to
do is to write the callback methods for each of the widgets and the helper methods.
The helper methods basically set all of our detail view widgets to either a
default position or to the data associated with the currently selected cron
job in our TkListbox object. The other callback methods basically update
our CronJobMgr object and the graphical interface elements to reflect
the changes chosen by the user. I leave it up to you to take the knowledge you
have garnered from this and the last two articles to figure out how each method
goes about doing its job.

Christopher Roach
recently graduated with a master's in computer science and currently works in Florida as a software engineer at a government communications corporation.