Creating a Mac OS X Application with RubyCocoa

Credit: Alun ap Rhisiart

Problem

You want to create a native Mac OS X program with a graphical user interface.

Solution

Use the Mac OS X Cocoa library along with RubyCocoa and the Interface Builder application. RubyCocoa creates real OS X applications and provides a GUI interface for building GUIs, as opposed to other libraries, which make you define the GUI with Ruby code. RubyCocoa is a free download, and the Cocoa development tools are on the Mac OS X installation DVD.

Interface Builder is very powerful: you can create simple applications without writing any code. In fact, it takes longer to explain what to do than to do it. Heres how to create a simple application with Interface Builder:

  1. Start the Xcode application and create a new project from the File menu. Choose "Cocoa-Ruby Application" from the "New Project" list, hit the Next button, give your project a name and location on disk, and click Finish.

    XCode will create a project that looks like Figure 21-7.

    Figure 21-7. A new Cocoa-Ruby project

    The Cocoa-Ruby project template comes with two files: main.m (an Objective-C file) and rb_main.rb (a RubyCocoa file). For a simple application, this is all the code you need.

  2. Open the NIB Files group and doubleclick MainMenu.nib to open Interface Builder. You get a new application window, into which you can drag and drop GUI widgets, and a menubar labeled MainMenu.nib (English)MainMenu.

    Youll also see a palette window with a selection of GUI objects; a nib document window named MainMenu.nib (English), containing classes, instances, images and sounds; and an inspector. If the inspector is not open, select Show Inspector from the Tools menu.

The screenshot in Figure 21-8 shows what we e going to do to our new application window (seen in the upper left).

Figure 21-8. Our destination Interface Builder screenshot

  1. Select the new application window and set the applications title. Type "Tiny RubyCocoa Application" in the inspectors Window Title field (you need to select the "Attributes" tab to see this field).

  2. Add a text label to the application window. Select the Text palette in the palette window. The visible controls are all text fields, with only slight differences between them. Well use the control called System Font Text: drag this control into your application window.

  3. Double-click the new text field in the application window and type "You are a trout!"

  4. For completeness, go through the menus in the menubar and change "New Application" to "Tiny RubyCocoaApp" wherever it occurs. Save your nib.

  5. Go back to Xcode. Click the Build and Go button. Your application should now run; it will look like Figure 21-9.

Figure 21-9. You are a Mac OS X trout

A compiled, doubleclickable version of the application will be found in your project build folderusually within the project subfolder.

Discussion

This simple application doesn show much about RubyCocoa, but it gives a glimpse of the power of the Cocoa framework. The NSApplication class gives you a lot of functionality for free: spellchecking, printing, application hiding, and so on. Ruby-Cocoa creates an instance of NSApplication, which deals with the run loop, handling events from the operating system, and more. You could have created this GUI application entirely in code (it would have looked something like the Tk example), but in practice, programmers always use Interface Builder.

For a more realistic example, well need to write some code that interacts with the interface. Like Rails and many other modern frameworks, Cocoa uses a Model-View-Controller pattern.

Lets create a RubyCocoa version of the Stopwatch program seen in previous GUI recipes like Recipe 21.12. First, we need to create a new Cocoa-Ruby Application project in Xcode, and once more open the MainMenu.nib file in Interface Builder. Because RubyCocoa makes it easy, well display the time on the stopwatch two ways: as a digital readout and as an analog clock face (Figure 21-10).

Figure 21-10. The RubyCocoa stopwatch in analog mode

  1. Create a new Cocoa-Ruby application. Select the new application window and change its title in the inspector to Timer.

  2. Create the clock. From the Text palette we used before, drag a NSDatePicker (a label that displays a date and time) into the application window. In the inspector, change the style to "Graphical", date selection to "None", and time selection to "Hour, Minute, and Second". The NSDatePicker now shows up as a clock.

  3. Create the digital readout. Drag an NSTextField ("System Font Text", as in the previous example) onto the window below the clock. Now drag a date formatter (marked with a small calendar in the palette) onto the NSTextField. The Inspector changes to show a list of possible formats; select %H:%M:%S.

  4. Create the stopwatch button. Switch to the button palette and drag a normal, rounded, NSButton to the application window. In the Inspector, change the title to "Start" and make sure its type is "Push Button".

  5. Build the menu bar. Change to the menus palette and drag Submenu objects onto the "MainMenu" menubar. Double-click them to change their titles (to "Program" and "Reset"), and drag Item objects onto the menu objects to add items to the menu. As in the stopwatch examples for other GUI libraries, our "Program" menu will contain menu items for "Start" and "Stop". The "Reset" menu will have a single menu item: "Reset Stopwatch". Unlike in the other examples, the application menus will contain no menu item for "Exit". This is because Mac OS X already provides a way to exit any program from the apple menu.

  6. Now we have all our interface elements in place. We need a model object to actually do the work. Click on Classes in the MainMenu.nib window, to bring up the class browser (Figure 21-11).

Figure 21-11. The class browser

Select NSObject and then "Subclass NSObject" from the Classes menu. Change the name of the new class to Timer. This class will implement the stopwatch code.

We need to tell Interface Builder about the interface to this class. Start by specifying three methods. In the inspector, with the new class still selected in the class browser, make sure that the Attributes-Actions tab is selected and hit the Add button three times. Name the methods reset:, start:, and stop:. These are the methods that will be called from the button and menus.

The model class we are creating also needs to know about some interface elements; for instance, it needs to know about the time controls so it can change the displayed time. The model class accesses Interface Builder widgets through instance variables called outlets. Switch to the "Attributes-Outlets" tab and click Add tHRee times. Name the outlets clock, timeField, and button.

  1. With the model object declared and all the interface elements in place, we can connect everything together. Recall that Interface Builder deals with instances of objects; we have a Timer class that implements the stopwatch functionality, but as of yet we have no instance of the class. Keeping the Timer class selected in the class browser, choose "Instantiate Timer" from the Classes menu. The window switches to the Instances tab, with a new icon representing the Timer instance.

    To make a connection between two objects, we drag from the object that needs to know, to the object it needs to know about. First, lets deal with the actions.

    When we click the Start button, we want the start method on our Timer class to be called. The button needs to know about the start: method. Control drag from the Start button to the Timer instance icon. The Inspector changes to show the methods of Timer, and automatically selects the start: method for you (it matches the button label). Click the Connect button to make the connection.

    Make the same connection from the menu item "Program/Start" to the Timer, and then from "Program/Stop" to the stop: method. Connect "Reset/Reset Stopwatch" to the reset: method.

  2. The controls now know which Ruby methods they trigger. We need to tell our Timer class which interface elements are accessible from its outlets (instance variables). Now the connections are made from the Timer class to the interface controls it needs to know about. Control-drag the Timer instance to the clock control: the inspector changes to show the outlets tab for Timer. Select clock and click the Connect button.

    Connect the textField and button outlets to the digital time control and the start button. Save the nib file as Timer.rb.

Back in Xcode, we are finally ready to write the Ruby code that actually implements the stopwatch. Choose "New File…" from the File menu, and then select "Ruby-Cocoa NSObject subclass" from the list. The core model object code is very similar to the Tk recipe, with some small differences:

require osx/cocoa include OSX ZeroDate = NSDate.dateWithString(2000-01-01 00:00:00 +0000) class Timer < NSObject ib_outlets :clock, :timeField, :button def initialize @timer = NSTimer. scheduledTimerWithTimeInterval_target_selector_userInfo_repeats( 1.0, self, :tick, nil, true) end

First, we call the ib_outlets decorator to specify instance variables that are matched up with the objects specified in Interface Builder.

In the other GUI examples, we displayed a plaintext label and formatted the time as a string for display. Here, the label has its own date formatter, so we can tell it to display an NSDate object and have it figure out the formatting on its own.

NSTimer is a Cocoa class we can use to tap into the Mac OS X user-event loop and call a method at a certain interval. We can get submillisecond time intervals from NSTimer, but theres not much point because NSDate won display fractions of a second. So we set it up to call the tick method once a second.[5]

[5] If, as in the other GUI recipes, wed decided to format the time ourselves and display it as a string, we could set a shorter interval and make the fractions of a second whiz by.

Now we define the start method, triggered when the end user pushes the "Start" button:

def start(sender) @running = true @start = NSDate.date @accumulated = 0 unless @accumulated @elapsed = 0.0 @button.setTitle(Stop) @button.setAction(:stop) end

One thing to note here: NSTimer hooks into the operating systems event loop, which means it can be switched off. We define a @running variable so we know to ignore the timer when we are not running the stopwatch.

The rest of the code is similar to the other GUI examples:

def stop(sender) @running = false @accumulated += @elapsed @button.setTitle(Start) @button.setAction(:start) end def reset(sender) stop(nil) @accumulated, @elapsed = 0.0, 0.0 @clock.setDateValue(ZeroDate) @timeField.setObjectValue(ZeroDate) end def tick() if @running @elapsed = NSDate.date.timeIntervalSinceDate(@start) d = ZeroDate.addTimeInterval(@elapsed + @accumulated) @clock.setDateValue(d) @timeField.setObjectValue(d) end end end

This recipe is pretty long-winded compared to the other GUI recipes, but thats because it takes more words to explain how to use a GUI application than to explain how a block of Ruby code works. Once you e familiar with Interface Builder, you can create complex Cocoa applications very quickly.

The combination of Ruby and Cocoa can make you very productive. Cocoa is a very big class library, and the GUI part, called AppKit, is only a part of it. There are classes for speech recognition, Bluetooth, disc recording, HTML rendering (Web-Kit), database (Core Data), graphics, audio, and much more. The disadvantage is that a RubyCocoa program is tied to Mac OS X, unlike Tk or wxRuby, which will work on Windows and Linux as well.

With Apples recent change to Intel processors, youll want to create "universal binaries" for your application, so that your users can run it natively whether they have a PowerPC or an Intel Mac. The Ruby code doesn need to change, because Ruby is an interpreted language; but a RubyCocoa application also contains Objective-C code, which must be compiled separately for each architecture.

To make a universal binary, select the top-most group in the "Groups & Files" list in Xcode (the one with the name of your project). Get Info on this (Command-I), go to the "Build" tab, select "Architectures", and click the Edit button. Select both the PowerPC and Intel checkboxes, and your packaged application will include compiled code for both architectures.

See Also

Категории