Ruby/Tk Primer, Part 3
Pages: 1, 2
Creating the Cron Manager GUI
|
Related Reading
Ruby in a Nutshell |
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.
# Create Proc objects for each action of the GUI
addButtonClicked = proc {addCronJob}
updateButtonClicked = proc {updateCronJob}
deleteButtonClicked = proc {deleteCronJob}
browseButtonClicked = proc {browseCommands}
saveButtonClicked = proc {saveCronJobs}
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.
# Create frames to hold the list view and detail view
listViewFrm = TkFrame.new(@root).pack(
'side'=>'left',
'padx'=>10,
'pady'=>10,
'fill'=>'both')
detailViewFrm = TkFrame.new(@root).pack(
'side'=>'left',
'padx'=>10,
'pady'=>10)
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.
# Create a scrollbar for the cron job listbox
scrollbar = TkScrollbar.new(listViewFrm).pack(
'side'=>'right',
'fill'=>'y')
# Create the listbox to hold all of the cron jobs
@cronListbox = TkListbox.new(listViewFrm) {
width 25
}.pack(
'fill'=>'y',
'expand'=>'true')
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.
# Bind the setDetailView method to the item selected event
@cronListbox.bind("<ListboxSelect>") {setDetailView()}
Next, we link the listbox to the scrollbar by calling the yscrollbar method and passing in the scrollbar object reference.
# Link the listbox to scrollbar
@cronListbox.yscrollbar(scrollbar);
Finally, we need to populate the listbox with the cron jobs that we have stored within the CronJobMgr object we created at the beginning of the method.
# Populate the cron job listbox
@cronJobMgr.each { |job|
@cronListbox.insert('end', job)
}
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.
# Create all of the labels for the GUI
labels = ['Minute', 'Hour', 'Day', 'Month', 'Weekday',
'Command'].collect { |label|
TkLabel.new(detailViewFrm, 'text'=>label)
}
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.
# Create the minute drop down list
minutes = ['*'] + (0..59).to_a
@minute = TkVariable.new()
minuteOptionMenu = TkOptionMenubutton.new(
detailViewFrm, @minute, *minutes) {
width 1
}.grid('column'=>1, 'row'=>0,
'sticky'=>'w', 'padx'=>5)
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.
# Create the command text field
@command = TkVariable.new()
@commandEntry = TkEntry.new(detailViewFrm) {
width 30
relief 'sunken'
}.grid('column'=>1,
'row'=>5, 'sticky'=>'w', 'padx'=>5)
@commandEntry.textvariable(@command)
# Create the browse for command button
browseButton = TkButton.new(detailViewFrm) {
text '...'
command browseButtonClicked
}.grid('column'=>2, 'row'=>5, 'sticky'=>'w')
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.
Return to MacDevCenter.com.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 6 of 6.
-
Great article quick question
2004-12-05 21:15:44 allClass [Reply | View]
-
Great article quick question
2005-01-17 09:07:38 Christopher Roach |
[Reply | View]
I just wanted to point out one more option for wrapping Ruby/Tk apps for shipping and installation onto other computers. That solution would be RubyGems. The following is a synopsis taken from the RubyGarden website:
RubyGems is a new approach to managing packages of Ruby code. The most convenient feature for the average user is that gems (apps and/or libs) can be downloaded and installed in one step from the command-line.
I haven't really looked into this technology that much yet, but it sounds just like what your looking for. If you want to find out more about RubyGems, you can do so at the RubyGems Wiki or you might want to look into the newest edition of the Programming Ruby book (Pickaxe II) from the Pragmatic Bookshelf where you'll find a chapter entitled "Package Management With RubyGems".
I hope that helps out, and thanks once more for the kind words. Oh, and be on the lookout for more Ruby articles in the near future.
Christopher -
Great article quick question
2004-12-27 16:04:43 Christopher Roach |
[Reply | View]
First, let me start by apologizing for such a late reply, but I've been out of the country for the past few weeks.
Next, let me just say that I really appreciate your comments on the article. The best reward for writing these articles is just knowing that someone out there read them and liked them.
Finally, I wanted to reply to your question. I have to admit that I hadn't really looked into creating an installer for my Ruby/Tk scripts. For the most part I just save the script to a USB key or email it to myself to use it another computer. In other words, I have so far only developed Ruby/Tk apps for myself.
That said, I did find a couple of articles on the O'Reilly Network that may be of assistance to you. They can be found at the following addresses:
PackageMaker Pro Tips
Creating Easy-to-Deploy Unix Applications for OS X
I haven't really found any good open source applications to wrap Ruby/Tk apps yet, but I'll keep looking. In the meantime, I hope the articles I've pointed out above help a bit, and if I find out anything else of interest, I'll post it here.
Thanks again for the comments.
-
Problems with complete script
2004-08-01 03:55:02 m_keightley [Reply | View]
I'm having some problems with the complete script as shown in parts 1 and 3. I get the follow error
Ruby# ./CronJobMgr.rb:74:in `commandName': private method `split' called for nil:NilClass (NoMethodError)
from ./CronJobMgr.rb:96:in `to_s'
from /usr/local/lib/ruby/1.8/tk.rb:295:in `_get_eval_string'
from /usr/local/lib/ruby/1.8/tk.rb:312:in `ruby2tcl'
from /usr/local/lib/ruby/1.8/tk.rb:1021:in `tk_call'
from /usr/local/lib/ruby/1.8/tk.rb:1021:in `collect!'
from /usr/local/lib/ruby/1.8/tk.rb:1021:in `tk_call'
from /usr/local/lib/ruby/1.8/tk.rb:3775:in `tk_send'
from /usr/local/lib/ruby/1.8/tk.rb:5219:in `insert'
from CronManager.rb:244:in `initialize'
from CronManager.rb:243:in `each'
from CronManager.rb:243:in `initialize'
from CronManager.rb:344:in `new'
from CronManager.rb:344
This suggests the problem lays in the to_s method, which I've proved by removing the \t\t#{commandName} bit which then enables the script to run with out error. I've copied and pasted the script, so I sure it's not my rather poor typing! Any ideas? -
Problems with complete script
2004-08-01 04:02:11 m_keightley [Reply | View]
Okay, donkey of the day award goes to me. I've now spotted what the problem was. The problem is on this line is to_s
"#{@weekday}\t\t#{commandName}"
It should read
"#{@weekday}\t\t#{@commandName}"
Note the @ before commandName, showing it to be an instance variable, according to part 1.
Hope this helps other newbies out there. -
Problems with complete script
2004-08-01 10:27:50 Christopher Roach |
[Reply | View]
Oops, that was a typo on my part. Thanks for pointing that out for the other readers.






Here's the question. Can TK/Ruby apps be wrapped and shipped as an installed app?