9781449380373
_application_kit.html

Chapter 5. Application Kit

The Application Kit, also known as the AppKit framework, contains all the classes needed to build OS X GUI applications. This is the framework we used in our “Hello World!” example in the section called “Code Example”.

AppKit is a very large framework and this book will not cover all the classes provided. I’ll introduce the concepts you need to get started and illustrate a few of the classes that every programmer uses for graphical interfaces.

Cocoa Key Principles

You need to understand a few key concepts before digging further into Cocoa. So far, we have looked closely only at the Foundation classes, which map fairly intuitively to concepts and library calls in other programming languages and programming environments. With Cocoa, we jump into unique concepts that are less intuitive.

Cocoa is a well-thought-out, well-designed, and very consistent API. To assure this consistency, a few key concepts underlie most of the API classes. By following the designed conventions, you will avoid spending a lot of time rewriting your code over and over.

Note

These Cocoa concepts and conventions are unfortunately not enforced in all the available frameworks. Some C-based frameworks were incorporated into Cocoa simply by adding an Objective-C wrapper, and these usually don’t follow the conventions as nicely as frameworks written directly in Objective-C.

Model-View-Controller Design Pattern

Ruby developers who have done any web development should be quite familiar with the model-view-controller (MVC) design pattern. It’s one of the key conventions enforced by the Ruby on Rails framework, and it goes back to the early 1980s when the Smalltalk community defined it, so it can also be found in plenty of other environments: GTK, Tcl Tk, Qt, Swing, WPF, and many others, including Cocoa. Basically, your code ends up divided in three parts: the model, which represents the data upon which the application operates, the view, which renders the model and potentially makes it interactive, and finally the controller, which is the glue between the model and the view and runs what many people call the “business logic” unique to your application.

When developing a Cocoa app, you will usually define your views in Interface Builder, Cocoa’s UI tool, while your models and controllers will be written in your editor/IDE (usually Xcode). We will go through these tools later in this chapter.

Protocols

As you have probably guessed, a Cocoa protocol has nothing to do with the international agreement aimed at ending child labor in the production of cocoa, known as the Cocoa Protocol and signed in September 2001.

Protocols are well-defined APIs that any class may choose to implement. In other words, protocols are a way to declare a convention via an API. Two classes implementing the same protocol can easily communicate with each other without having to be subclasses of the same superclass.

Protocols are basically just a set of well-defined methods adopted by different classes. Some APIs expect to receive certain objects to implement certain protocols. So if you are developing your own class, you will need to make sure to implement the expected API. Once you do, we say that your class conforms to the protocol.

Key-Value Coding

Often abbreviated KVC, the NSKeyValueCoding protocol defines the mechanism to set and retrieve an object’s attributes/properties by name instead of by invoking a method. This is a fundamental value concept in Cocoa programming, and it essentially allows you to access instances as if they were hash tables/dictionaries. There are many advantages of such an approach. The most obvious are that you don’t need to explicitly write getters and setters and that you can implement UI bindings and serialization much more easily, since knowing the name of a property is enough to get and set it.

Note

KVC is often mentioned with KVO (key value observing), which is a notification system using the observer pattern and based on KVC.

Here is a simple example in which we will pretend that Player is a KVC-compliant class:

matt = Player.new

matt.setPoints(42)
matt.valueForKey('points') # => 42
matt.setValue(442, forKey: 'points')
matt.valueForKey('points') # => 442

The Player class doesn’t have to implement a setPoints accessor method. Because the class implements the NSKeyValueCoding protocol, it intercepts your invocation of setPoints and sets the points instance variable in your instance to the value you supply. The other three methods shown here are also interpreted according to the protocol.

The code you can write using this protocol is quite similar to Ruby’s accessor syntax:

matt = Player.new

matt.points = 42
matt.points # => 42
matt.points = 442
matt.points # => 442

You can simply implement it like this:

class Player
  attr_accessor :points
end

It turns out that MacRuby generates the KVC code for you when you add a Ruby accessor. This way, your classes can easily conform to the KVC protocol:

framework 'foundation'
class Player
  attr_accessor :points
end

matt = Player.new
matt.points = 100 # Ruby syntax
matt.valueForKey('points') # => 100 (KVC protocol)
matt.setPoints(42) # KVC protocol
matt.points # => 42
matt.setValue(442, forKey: 'points') # KVC protocol
matt.points # => 442

Here is another example using a class that’s KVC-compliant by default: Hash and a KVC method named valueForKeyPath that we haven’t seen yet:

framework "Cocoa"
class Me < Hash
  def initialize
    super
    self["siblings"] = {"brothers" => ["Arnaud"], "sister" => ["Marjorie"]}
  end
end

moi = Me.new
p moi.valueForKey('siblings')
# => {"brothers"=>["Arnaud"], "sister"=>["Marjorie"]}
p moi.valueForKeyPath("siblings.brothers")
# => ["Arnaud"]

In this example, we create a new class called Me that inherits from Hash and therefore is KVC-compliant by default. We define the initialize method to set some default value. The method starts by calling super so the code defined by the Hash constructor can be triggered, then sets the default values. Once my code is written, we create an instance of Me called moi and use the previously discussed valueForKey method, which works as expected. Then we invoke another KVC protocol method, called valueForKeyPath, which takes a path to the object we are interested in. Notice that valueForKeyPath requires a scalar as its argument; we can’t pass it an array index. For instance, passing siblings.brothers[0] will result in a nil value.

Bindings

When developing a Cocoa application, you can bind an attribute of one object to a property of another. For instance, imagine we are developing a music player. On the one hand, we have a player object defining a volume attribute, and on the other, we have a slider UI controller to allow the user to control the output volume. We also want to display the setting of the volume. To implement all this, we are going to load our player instance in the controller and bind its volume attribute to our slider and text field values.

Let’s start by defining our model. Create a new Xcode MacRuby project (see the intro to Xcode to learn more about Xcode) and add a new Ruby file called player.rb. Our model code is very simple—just a KVC conform class with a volume attribute, as shown here:

class Player
  attr_accessor :volume
end

Our AppDelegate class is not much more complicated, we just need to add a new accessor for the player and initialize an instance of the object when the class instance is initialized:

class AppDelegate
  attr_accessor :window
  attr_accessor :player

  def applicationDidFinishLaunching(a_notification)
    # Insert code here to initialize your application
  end

  def initialize
    @player = Player.new
  end

end

Now edit your view file (MainMenu.xib) by clicking on it in Xcode.

The template already has an NSObject instance called App Delegate, which represents an instance of the AppDelegate class (Figure 5.1, “Details of the controller object viewed in the Inspector”). Look at it via the Identity Inspector and notice that the AppDelegate class is set as the object’s class.

Figure 5.1. Details of the controller object viewed in the Inspector

Details of the controller object viewed in the Inspector

Now open the Object Library, look for the vertical slider, drop it on the UI inside the window’s view (Figure 5.2, “Vertical slider viewed from the Object Library”), then look for the text field icon and drop it inside the view, as shown in Figure 5.3, “Object hierarchy and UI”.

Figure 5.2. Vertical slider viewed from the Object Library

Vertical slider viewed from the Object Library

Figure 5.3. Object hierarchy and UI

Object hierarchy and UI

Once the UI objects are in place, you need to set the bindings for the slider. To do so, select the slider and open it in the Inspector. Then select the bindings tab in the Utility area and set the bindings settings as shown in Figure 5.4, “Slider’s bindings viewed in the Inspector”.

Figure 5.4. Slider’s bindings viewed in the Inspector

Slider’s bindings viewed in the Inspector

Set the bind to App Delegate and set the model key path to player.volume. Do the same for the text field. The slider will set the value while the text field will read the value. Save your project and run the app from Xcode by clicking the Run button or by using the Command + R shortcut. The end result should look something like Figure 5.5, “View of the slider in the compiled app”.

Figure 5.5. View of the slider in the compiled app

View of the slider in the compiled app

When you move the slider, the text value changes, and vice versa. But, what’s even more interesting is that the model’s attribute value also changes at the same time. As you can see, bindings can save you a lot of development time.

Delegation

We used and discussed delegation when we worked on our “Hello World!” example in Chapter 1, Introduction. In the delegate class we created, we implemented two delegate methods. One was an NSApplication delegate named applicationDidFinishLaunching and the other was an NSWindow delegate named windowWillClose.

Delegation is a common pattern in Cocoa. It often offers an elegant way to hook into events. For instance, when an application is started, two delegates are triggered if they are available. The applicationWillFinishLaunching delegate is called as soon as the run loop is ready, but before the application is started. applicationDidFinishLaunching is called after the application is launched.

An NSWindow instance has a lot of delegates available for implementation (see http://developer.apple.com/mac/library/documentation/cocoa/reference/NSWindowDelegate_Protocol/Reference/Reference.html). A few of the simpler delegates include:

  • windowShouldClose

  • windowWillClose

  • windowWillMove

  • windowDidMove

Look at the documentation to learn more about the delegates available for the classes you are using.

User Interface

Most of the Ul-related classes are provided by the Cocoa framework. Instead of going down the long list of UI classes, let’s talk about the main concepts and elements critical to MacRuby developers.

Windows, Views, and Cells

Windows, views, and cells are the visible units the user deals with. For instance, a button is usually a view, as is a text entry box.

Windows and panels

Windows are conceptually easy to understand and don’t need much explanation. Technically, a Cocoa window is an instance of the NSWindow class in which a view (NSView instance) displays its content. To group windows, an application can use panels via the NSPanel class, which is itself a subclass of NSWindow.

Views

Per Cocoa’s definition, “A view instance is responsible for drawing and responding to user actions in a rectangular region of a window.” In other words, views render some of the app content and handle user-initiated events.

Whenever you need to display content in a window, you will need a view. The view also handles keyboard and mouse events. Views often act as containers and contain one or more nested views.

The primary view class is called NSView, which inherits directly from NSResponder. This lets you easily implement keyboard/mouse event handling by overriding NSView methods. This class implements the fundamental view behavior, making it a perfect candidate for creating a custom view.

Application Kit offers a set of great NSView subclasses to help with common tasks. Here are a few examples:

  • Controls such as buttons via NSButton

  • Text fields and display via NSTextField and NSTextView

  • PDF display via PDFView

  • Movie display via QTMovieView

  • Image display via NSImageView

  • Open GL contexts via NSOpenGLView

Cells

For reasons of performance and reusability, most views delegate drawing to another class called the cell (generally an NSCell subclass). For instance, buttons are instances of the NSButton class (itself a subclass of NSControl), but use different NSButtonCell instances for drawing.

Positioning

The position of an object is called its coordinates, and this refers to the x and y pixel positions of its bottom, leftmost corner. When using Cocoa views, it’s important to keep in mind that the coordinates of each object are always relative to the immediately enclosing view. Thus, each of the views you create will define and maintain its own coordinate systems. All the objects in each view will be relative to the view’s coordinate system.

By default, as shown in Figure 5.6, “Representation of the drawing origin”, the origin is at (0.0, 0.0), which is located in the lower left corner of the parent object.

Figure 5.6. Representation of the drawing origin

Representation of the drawing origin

Let’s take an example. Imagine that we want to display a web page in a window. The web page’s view should take up only a quarter of the window and we want it to be at the top left corner (Figure 5.7, “Window with a web preview on top left corner”).

Figure 5.7. Window with a web preview on top left corner

Window with a web preview on top left corner

You could just use Interface Builder to create such a simple UI, but for the sake of explaining how views work, I will show how to programmatically create a window and content view, and to place a web page view on the top left corner of the window. Here is the code:

framework 'Cocoa'
framework 'WebKit'

application = NSApplication.sharedApplication

# create the window
width  = 800.0
height = 600.0
frame  = [0.0, 0.0, width, height]
mask = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask
window = NSWindow.alloc.initWithContentRect(frame,
          styleMask:mask,
          backing:NSBackingStoreBuffered,
          defer:false)

# assign a content view instance
content_view = NSView.alloc.initWithFrame(frame)
window.contentView = content_view

# create a web view positioned in the top left quarter of the super view
web_view_frame = [0.0, height/2, width/2, height/2]
web_view = WebView.alloc.initWithFrame(web_view_frame, frameName: "Web Frame", 
groupName: nil)
request = NSURLRequest.requestWithURL(NSURL.URLWithString("http://macruby.org"))
web_view.mainFrame.loadRequest(request)
content_view.addSubview(web_view)

# center the window
window.center

# show the window
window.display
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless

application.run

You can copy this code to a file and run it with the MacRuby executable. I will skip the part about requiring the frameworks and creating an application.

We start the code by creating a frame for our window:

width  = 800.0
height = 600.0
frame  = [0.0, 0.0, width, height]

Technically, a frame is just an array with four floating-point values representing the x-axis coordinate, y-axis coordinate, width, and height. An Objective-C code example would have the following syntax:

NSRect frame = NSMakeRect(0.0, 0.0, 800.0, 600.0);

Both code snippets just shown are equivalent, except that one is written in Ruby and the other in Objective-C.

A frame defines the size of its content and its position inside the containing views. A frame in Cocoa can be considered like a picture frame in real life: you position it on the wall and the frame dimensions define the maximum area available for the canvas inside.

Our example starts by creating a window that will hold our UI. We define two variables, width and height, that we use for the frame. We will reuse these variables later on to position our web page view relative to the window.

We also need to create a content view that will hold our UI items. Remember that we want our web page view to take only one-fourth of the space. For that to happen, we need a placeholder. That’s what our NSView instance offers:

content_view = NSView.alloc.initWithFrame(frame)
window.contentView = content_view

Note

If you start a new Interface Builder Application project, a window and its content view instance will already be created for you.

Now we need to create a frame for our web view. This frame is positioned relative to the content view, which in turn is positioned relative to our window. Since we stored the window’s width and height, we can easily define our web view frame to be displayed only at the top left corner of our GUI:

web_view_frame = [0.0, height/2, width/2, height/2]

Our frame defines the web view x-axis position as 0.0, which is the graphics environment axis origin and located all the way to the left. Then we do some math to define the y-axis position, width, and height. We take the superview (content_view) height and divide it in two to find the middle point to position our web view. Because we want our web view to only take a fourth of the UI, we set the width and the height to be half the corresponding width and the height of the containing view.

Note

Remember that we are starting to draw from the bottom left and not from the top, as you do when designing a web page.

We then create the WebView instance, set the URL to load, and finally add it to the content view subviews:

content_view.addSubview(web_view)

At this point, we are finished placing our content. However, why not center our window on the user’s screen while we are at it?

window.center

Calling the center instance method on a window places it exactly in the center horizontally and somewhat above center vertically. Another way to center the window is to manually locate its frame:

screen_frame = NSScreen.mainScreen.frame
window.frameOrigin = [(screen_frame.size.width - width)/2.0,
                      ((screen_frame.size.height - height)/2.0)]

This code starts by retrieving the screen frame. Then we position our window right in the middle of the screen by taking the width and height of the screen, subtracting our UI width and height, and dividing the sum by 2.

On my laptop, my resolution is set to 1280x800. So, to find the x coordinate, our code takes the screen width (1280), subtracts our content view’s width (800), and divides the result by 2, locating the x coordinate at 240.0. We do the same with the height and get a y coordinate of 100.0.

Once we have the coordinates figured out, we move the window frame origin and display the window.

Great, our content displays properly—but what if we want to track keyboard and/or mouse events?

Events and the Responder Chain

When an event message is dispatched, the message is sent down the window. The window itself has a responder chain, which is a linked series of responder objects. If the first object in the chain doesn’t handle the message, the message is passed to the next responder in the chain and so forth, until a responder handles the message or the responder chain has been entirely traversed (in which case the window’s responder noResponderFor method is dispatched).

Note

If you have multiple windows, only the window in which the associated user event occurred will forward the event message to its responder chain.

The default responder chain is constructed by the Application Kit framework, but can be modified by developers. The default responder chain for a key event begins with the first responder in the window where the focus is when the key is pressed. In contrast, the default responder chain for a mouse event begins with the view on which the event occurred. If not handled, the mouse event message is sent to the window’s first responder, which is usually the selected view object. The next responder in the chain is the UI object’s containing view (superview) and so on up to the NSWindow object.

You can insert responders in the responder chain by using the setNextResponder method or the nextResponder= alias on the view object.

Let’s experiment with the responder chain. Open Xcode and create a new project using the MacRuby application template. Now create a new Ruby file and call it event_view.rb. Our new class will be a View subclass that will handle our events:

class EventView < NSView

  def acceptsFirstResponder
    true
  end

  def keyDown(event)
    puts "key down"
    characters = event.characters
    if characters.length == 1 && !event.isARepeat
      character = characters.characterAtIndex(0)
       case character
       when NSLeftArrowFunctionKey
         puts "you pressed the left arrow"
        when NSRightArrowFunctionKey
         puts "you pressed the right arrow"
       end
    end
  end

end

The subclass implements a keyDown method that takes an event argument. This method is part of the responder chain. You can see from the example that we can get the characters pressed by calling characters on the event. We inspect the characters pressed to log an appropriate message. We then check that the event isn’t a repeated event, in the sense that if the user leaves his or her finger on a key, we want to get only one event. We extract the characters (NSString instance) from the event (NSEvent). We then take only the first character using characterAtindex and try to match it to some keyboard constants.

Now click on the MainMenu.xib file. The contents will be displayed in a format like that shown in Figure 5.8, “MainMenu.xib with the window expanded”.

Figure 5.8. MainMenu.xib with the window expanded

MainMenu.xib with the window expanded

Select the View in the Identity Inspector and, in the Custom Class section, change the class value to EventView (see Figure 5.9, “View with a custom class handling keystrokes”). Xcode automatically detects the new class we created.

Figure 5.9. View with a custom class handling keystrokes

View with a custom class handling keystrokes

Save the project and click Run. When the application is running, type on your keyboard and see the output in the Debug area (shown in Figure 5.10, “Output shown in the Debug area”). Try pressing the left or right arrow keys and notice the different output.

Figure 5.10. Output shown in the Debug area

Output shown in the Debug area

To see which key events are available, read the NSResponder class documentation (http://developer.apple.com/mac/library/documentation/cocoa/Reference/ApplicationKit/Classes/NSResponder_Class/Reference/Reference.html). What we did was to set our custom view as first responder and check the key pressed to see whether it matched one of the NSResponder’s constants.

Drawing

Cocoa’s drawing environment is available via the Application Kit framework, which is itself based on Quartz, the Macintosh’s basic 2D graphics rendering software. The environment automatically takes advantage of graphics hardware wherever it can, making the drawing process as fast as possible.

If you come from a browser-oriented background, you might not think about how UI elements are rendered. After all, when using HTML, you just use UI tags and let the browser deal with rendering. In Cocoa, however, understanding how UI elements are drawn and how to interact with the drawing processing can be very beneficial. If you use only standard system controls, all the drawing is automatically handled by Cocoa. But as soon as you start using custom views and controls, managing the customized appearance of your elements become your responsibility. Also, you might want to use the drawing features to display images, PDFs, vector-based drawings, or videos, to render openGL code, or to custom print.

But before getting too deep into custom drawing, let’s talk more about how a normal control element is drawn.

A control element such as a button is usually an instance of a custom NSView class. NSView instances are responsible for their own drawing, but in most views the drawing is delegated to another class called the cell (generally a subclass of NSCell).

In the case of NSButton, the drawing is delegated to an NSButtonCell instance. To customize the drawing of a button, you can subclass the cell and override some of its properties or methods. The advantage of having the drawing done in the cell instead of the view is that one cell instance can be shared by multiple controls of the same kind. Also, a cell can be given to a more specialized view to customize only a part of it. A good example of this is customizing a table view column using a different cell.

If you look at the Interface Builder library and search for NSButton, you will find a lot of different buttons (Figure 5.11, “Choice of NSButton types offered by the Object Library”). That’s because each button uses a different NSButtonCell under the covers to implement its own UI. The NSButtonCell associated with the button will set the background color, style, images, states, sounds, and events. It will also take care of the button’s content drawing.

Figure 5.11. Choice of NSButton types offered by the Object Library

Choice of NSButton types offered by the Object Library

When the button is loaded, Cocoa draws it in its initial state. When a mouse event occurs on the button or a triggering key is pressed, the NSButtonCell state is changed and the object is marked as needing to be redrawn. You can also force the redrawing of an element by calling setNeedsDisplay or setNeedsDisplayInRect on it. When the run loop restarts its cycle, all the elements that need redrawing are redrawn.

Note

For more information about buttons, refer to Apple’s documentation, "Introduction to Buttons" at http://developer.apple.com/mac/library/documentation/cocoa/Conceptual/Button/Button.html.

You can also totally customize the drawing of your views by reimplementing the drawRect method on your NSView class. drawRect is called on a view when something about your view has changed or can be called manually.

Let’s cover some other important Cocoa drawing concepts.

Graphics Context

In Cocoa, as in many GUIs, you don’t apply visual traits directly to objects; that is, you don’t say, “Draw this rectangle with a blue border” or “Write this text in italic.” Instead, you define the traits in general for the whole window, using an abstraction called the graphics context. A typical sequence is:

  1. Set the color in the graphics context to red.

  2. Draw the background.

  3. Set the color in the graphics context to black.

  4. Draw a rectangle.

  5. Etc..

The procedure may seem awkward, but it makes the best use of the hardware capabilities. A graphics context is thus a representation of the area onto which you’re drawing. The context has a huge range of attributes, each of which takes on one value at a time, such as red or black. Its job is to set and hold all the information needed so a view can be drawn properly. It even saves its state before rendering so changes can be undone.

Most of the time, the graphics context you will interact with represents one of your application’s windows, but the drawing destination can also refer to an image, an output device (i.e., a printer), a file, or an OpenGL surface. Cocoa maintains a separate graphics context per window and per thread. So, an application can have multiple contexts.

In a Cocoa application, graphics contexts for most types of canvas are represented by the NSGraphicsContext class. To access the current thread’s graphics context, you can simply call NSGraphicsContext.currentContext. This is usually something you do when you implement your own drawRect method. The problem is that if you modify the current context, all other items drawn in the same thread will be affected by this change. For instance, if you are changing the fill color to red, all objects redrawn after the change will be redrawn with a red fill color. Here is an illustration of the problem:

framework 'Cocoa'
class CustomView < NSView

  def drawRect(rect)
    # draw a red background
    NSColor.redColor.set
    NSBezierPath.fillRect(rect)

    # draw a rectangle
    path = NSBezierPath.bezierPath
    path.lineWidth = 2
    # starting point
    path.moveToPoint [100, 50]
    # draw a rectangle
    path.lineToPoint [100, 100]
    path.lineToPoint [200, 100]
    path.lineToPoint [200, 50]
    # close the rectangle automatically
    path.closePath
    path.stroke
  end

end

application = NSApplication.sharedApplication

# create the window
frame  = [0.0, 0.0, 300, 200]
mask = NSTitledWindowMask | NSClosableWindowMask
window = NSWindow.alloc.initWithContentRect(frame,
          styleMask:mask,
          backing:NSBackingStoreBuffered,
          defer:false)

# assign a content view instance
content_view = CustomView.alloc.initWithFrame(frame)
window.contentView = content_view

# show the window
window.display
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless

application.run

The result is shown in Figure 5.12, “Display with improperly drawn rectangle”. The problem is that the rectangle we drew isn’t visible.

Figure 5.12. Display with improperly drawn rectangle

Display with improperly drawn rectangle

The source of the problem is quite easy to spot: we have switched the context color from black to red by using NSColor.redColor.set, but when we then draw the rectangle, the stroke is still red and we can’t distinguish the rectangle.

An easy fix would obviously be to reset the color to black before drawing the stroke. While that would work for this simple example, imagine if you were changing more than two context attributes and you were drawing several items in the view. Having to manually reset the context every time would become quite challenging. A more robust approach is to save the current context, set a new context, draw what you want using the new context, and restore the old context. This is why Cocoa offers methods to save the context and then restore it:

framework 'Cocoa'
class CustomView < NSView

  def drawRect(rect)
    context = NSGraphicsContext.currentContext
    context.saveGraphicsState
    # draw a red background
    NSColor.redColor.set
    NSBezierPath.fillRect(rect)
    context.restoreGraphicsState

    # draw a rectangle
    path = NSBezierPath.bezierPath
    path.lineWidth = 2
    # starting point
    path.moveToPoint [100, 50]
    # draw a rectangle
    path.lineToPoint [100, 100]
    path.lineToPoint [200, 100]
    path.lineToPoint [200, 50]
    # close the rectangle automatically
    path.closePath
    path.stroke
  end

end

application = NSApplication.sharedApplication

# create the window
frame  = [0.0, 0.0, 300, 200]
mask = NSTitledWindowMask | NSClosableWindowMask
window = NSWindow.alloc.initWithContentRect(frame,
          styleMask:mask,
          backing:NSBackingStoreBuffered,
          defer:false)

# assign a content view instance
content_view = CustomView.alloc.initWithFrame(frame)
window.contentView = content_view

# show the window
window.display
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless

application.run

The result is shown in Figure 5.13, “Display with all objects visible”. This time, the rectangle is visible.

Figure 5.13. Display with all objects visible

Display with all objects visible

And here is a more complete example, where we extract the graphics context switch into a method that can then be invoked for each object we draw:

framework 'Cocoa'
class CustomView < NSView

  # Using a method yielding a block, we can keep
  # our code cleaner
  def temp_context(&block)
    context = NSGraphicsContext.currentContext
    context.saveGraphicsState
    yield
    context.restoreGraphicsState
  end

  def drawRect(rect)
    # draw a red background
    temp_context do
      NSColor.redColor.set
      NSBezierPath.fillRect(rect)
    end

    path = NSBezierPath.bezierPath
    path.lineWidth = 2
    # starting point
    path.moveToPoint [100, 50]
    # draw a rectangle
    path.lineToPoint [100, 100]
    path.lineToPoint [200, 100]
    path.lineToPoint [200, 50]
    # close the path automatically
    path.closePath

    # fill the content of the path in transparent white
    temp_context do
      NSColor.colorWithCalibratedWhite(0.9, alpha: 0.5).set
      NSBezierPath.fillRect([100, 50, 100, 50])
    end

    # draw the rectangle stroke after the content was filled
    path.stroke

    # draw some text, because we are changing the context shadow
    # we are doing that in a temp context
    temp_context do
      shadow = NSShadow.alloc.init
      shadow.shadowOffset = [4, -4]
      shadow.set
      font = NSFont.fontWithName("Helvetica", size:24)
      attributes = {NSFontAttributeName => font, 
                        NSForegroundColorAttributeName => NSColor.whiteColor}
      "MacRuby Rocks".drawAtPoint([60, 120], withAttributes: attributes)
    end
  end

end

application = NSApplication.sharedApplication

# create the window
frame  = [0.0, 0.0, 300, 200]
mask = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask
window = NSWindow.alloc.initWithContentRect(frame,
          styleMask:mask,
          backing:NSBackingStoreBuffered,
          defer:false)

# assign a content view instance
content_view = CustomView.alloc.initWithFrame(frame)
window.contentView = content_view

# show the window
window.display
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless

application.run

The result is shown in Figure 5.14, “Result of the sample using an extracted context switch”.

Figure 5.14. Result of the sample using an extracted context switch

Result of the sample using an extracted context switch

The temp_context method takes, as its argument, a block of code that is executed after the current context is saved and before it is reset. The end result allows for a more expressive syntax and the code ends up being easier to maintain.

As a side note, as shown in the previous example, drawing text in a view is very simple. Probably the simplest way is to use drawAtPoint, as follows:

position = [60, 120]
"MacRuby Rocks".drawAtPoint(position, withAttributes: nil)

Other ways to draw text are to use NSAttributedString or NSTextView. I leave it up to you to explore further.

Images

To manipulate images, the Application Kit offers the NSImage class. With this class, you can load existing images from disk, draw image data into your views, create new images, scale and resize images, or convert images to any of several different formats.

A typical method that draws a simple image follows:

def drawRect(rect)
  NSColor.whiteColor.set
  NSBezierPath.fillRect(rect)
  img_url = NSURL.URLWithString('http://bit.ly/apple_logo_png')
  img = NSImage.alloc.initWithContentsOfURL(img_url)
  img.drawAtPoint([0,0],
  fromRect: NSZeroRect,
  operation: NSCompositeSourceOver,
  fraction: 1)
end

We start by setting the color to white and filling the background, then we specify the URL of the image to load. With the instance of NSURL ready, we just need to pass it to our NSImage constructor and we get an instance representing our image. Finally, we use an API similar to the one that drew text in the previous section.

In the call to drawAtPoint, the first parameter is the coordinate where we want to start drawing the picture (bottom left). Then we pass an empty rectangle represented by the NSZeroRect constant (passing [0,0,0,0] would have done the same thing). The operation parameter refers to the composing operation: in other words, the way the image should be composed. A list of operation constants is available in the NSImage class reference (http://developer.apple.com/mac/library/documentation/cocoa/Reference/ApplicationKit/Classes/NSImage_Class/Reference/Reference.html#//apple_ref/c/tdef/NSCompositingOperation). Finally, the last parameter refers to the opacity. This ranges from 0 (totally transparent) to 1, totally opaque.

The code just shown appears near the top of the following example, which is a complete application using the image:

framework 'Cocoa'
class CustomView < NSView

  def drawRect(rect)
    NSColor.whiteColor.set
    NSBezierPath.fillRect(rect)
    img_url = NSURL.URLWithString('http://bit.ly/apple_logo_png')
    img = NSImage.alloc.initWithContentsOfURL(img_url)
    img.drawAtPoint([0,0], fromRect: NSZeroRect, operation: NSCompositeSourceOver,
    fraction: 1)
  end

end

application = NSApplication.sharedApplication

# create the window
frame  = [100, 100, 152, 186]
mask = NSTitledWindowMask | NSClosableWindowMask
window = NSWindow.alloc.initWithContentRect(frame,
          styleMask:mask,
          backing:NSBackingStoreBuffered,
          defer:false)

# assign a content view instance
content_view = CustomView.alloc.initWithFrame(frame)
window.contentView = content_view

# show the window
window.display
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless

application.run

Another way to display images is to use Interface Builder. Start by adding an image to your Xcode project. Then open your .xib file in Interface Builder and drag an instance of NSImageView (referred to as Image Well) from the Library to your UI. Select the new UI element and open the Inspector. In the Image View Attributes, use the scroll-down menu to pick your image. Voilà, you’re done!

Note

NSImageView also optionally allows users to drag an image to your UI and display it.

Site last updated on: November 9, 2011 at 10:00:57 AM PST
Cover for MacRuby: The Definitive Guide