

7. The view concept

This chapter discusses the concepts surrounding views and goes into some detail what can be done with them. As a result, most of this chapter is concerned with the creation of custom views, which is not necessary for general application development. If you want to create your own view classes or are interested in how GNUstep manages views, then this chapter should be useful.


7.1 Introduction

In GNUstep applications, we introduce the idea of a view. A view is a graphical element on the window in your interface. It is much like the idea of a window in the Microsoft Windows C API, except more powerful. Note that views are a generalisation of a control, that is, a control is a special type of view.

A view is a subclass of the AppKit NSView class. You should not instantiate this class directly, but instead use a class that is derived from it. A custom view can be created by inheriting from it.


7.2 The view hierachy

A view may contain any number of subviews, which are views that are displayed within it. Those views may also have subviews, and as a result, you can setup a hierachy of views. This can be a powerful model for your interface designs (especially where you create your views programatically instead of just in Gorm.app).

Each window has a primary view, known as the content view, which acts as a top-level view (or superview) to all the views you place on your window. It sits at the top of the view hierachy. Most applications will only have one level of views below the content view, and for most applications, this is all you need.


7.3 Frames and Bounds

As views can be placed within other views, GNUstep needs to setup some rules to determine how this will work. Due to this, each view has two important properties defining how it is positioned and displayed on the screen. These are it's frame and its bounds.

GNUstep uses cartesian coordinate systems for defining positions and sizes. It has the origin of any coordinate system placed at the bottom-left corner and has an x-axis and a y-axis. Like a normal cartesian coordinate system, the x-axis runs from left to right, and the y-axis runs from the bottom to the top. A view is defined within two coordinate systems, not just the coordinate system of the entire screen or window.

The frame and the bounds describe the view in terms of a rectanglei placed in a coordinate system. The rectangle has an origin (located at the bottom-left corner of the rectangle) and a width and a height. Programmatically, the concept of a rectangle is tied up in an NSRect structure, which in turn contains an NSPoint structure (for the origin) and an NSSize structure.

The contents of your view is not dissimilar to a canvas. You can draw anywhere on this canvas, but only a certain portion of it is displayed. Where it is displayed and what part of it you choose to display is defined in the frame and the bounds rectangle of the view.

The frame is the location and size of your view, as defined in its superview. The content view has it's frame defined with it's origin at the bottom-left corner of a window, and it's width and height equal to that of the window it is placed in (ignoring the window decoration). If you change the origin of your frame rectangle, you effectively move your view within it's superview. By changing the frame rectangle's width or height, you resize your view with regards to the coordinate system of it's superview.

The bounds rectangle defines what part of your view's internal coordinate system will be displayed. It is therefore defined in the coordinate system of your view. By default, it is set to be a rectangle located at the origin of your view's internal coordinate system, with it's size set to be the same size as your frame rectangle. However, it can be programatically streched, rotated, moved and skewed so that various parts of your view's internal coordinate system are displayed in it's frame rectangle.

In essence, the frame is defined by the coordinate system of your view's superview, and the bounds is defined by the coordinate system of your view. These concepts can be difficult to grasp, so we recommend you read over this bit, as well as play around with the various methods in GNUstep that let you modify the bounds and frame rectangles of a view.

It is the internal coordinate system where your view does it's drawing and which defines the location and size of any subviews. It is the coordinate system of your view's superview that defines where and how big your view is displayed.


7.4 Manipulating the coordinate system

As mentioned earlier, the bounds and frame of a view can be stretched, shrunken, shifted and even rotated. Firstly we will show you how to manipulate these directly, and then briefly describe the mathematics behind coordinate transformations.

NSView provides some simple methods for manipulating the coordinates and coordinate systems of the frame and bounds rectangles. Note that after calling any of these methods, you need to get the view to redisplay itself manually. You can do this by calling the -setNeedsDisplay: and -display: on the view o bject.

We can change where a view is placed within it's superview (most likely the window's content view) by manipulating it's frame origin. This is done using the -setFrameOrigin: method. For convenience, you can use the NSMakePoint() function to easily construct a point for the new location.

The size of a view's frame can also be adjusted using the -setFrameSize: method. Similiarly, the NSMakeSize() method can be used to construct an NSSize parameter that is needed. Changing this will cutoff whatever is internal to the view, although some classes behave differently. Check the documentation for the class with regards to it's reaction to a change in it's frame size.

Where necessary, these can be adjusted as a rectangle, making use of the -setFrame: method and the NSMakeRect() function.

Methods used for manipulating the bounds have subtly different meanings. Like the frame rectangle, the bounds rectangle can be manipulated as well.

The bounds origin and size can be manipulated using the -setBoundsOrigin: and -setBoundsSize: methods respectively. Changing the bounds origin effectively sets the new origin to be displayed at the origin of your frame rectangle. Changing the bounds size can be used to skew the coordinate system of the bounds, as it is displayed within the frame rectangle.

Another method for skewing the internal coordinate system of a view is to use the -scaleUnitSquareToSize: method. It's useful when you need to express your transformation as a percentage or fraction, where a size of 1.0 is considered to be 100%. Note that this method is cumulative, so that when you set this, it is effectively the first transformation multiplied by the second. For example, setting it to 0.5 and the 0.75 is the same as setting it to 0.5 x 0.75 = 0.375.

To rotate the frame or bounds rectangle counterclockwise, call the -setFrameRotation: or -setBoundsRotation: methods respectively. These methods take an angle in degrees. You can specify clockwise rotation with a negetive angle.

Alternatively, you can rotate the bounds rectangle by using the -rotateByAngle: method. This method rotates the bounds on top of what it has already been rotated.


7.5 Subclassing NSView

Sometimes the need arises to create a custom view. This is achieved by subclassing NSView. From here, you can override default event handlers and drawing methods to customise your view's representation.

Note that in some cases, the NSControl class may prove to be a better model for your custom view, especially if it behaves more like a control instead of an entire document representation. You should read the chapter on controls and weigh up the options for creating a view vs creating a control. This section is still useful though to understand the drawing code aspect, which is relevant to the display of control's as well.


7.5.1 Drawing code

One of the first things you will do is write your own drawing code. All custom drawing code is placed in the NSView method drawRect:. NSView's implementation is blank by default.

In this section, we will describe the various facilities at your disposal for drawing in a view.


7.5.2 NSBezierPath

This class is an abstract representation of a bezier path. A bezier path contains a series of straight and curved lines representations which come together to form a number of shapes that describe the object you wish to draw. This "path" can then be "filled", "stroked" or used as a clipping path within the view you are working on. It also contains the pen width, pen dash information and the current point.

A bezier path represents a series of graphical primitives operations. You call methods corresponding to these operations on an NSBezierPath object, and when it is drawn, these operations are executed in the order that you called them on the object.

It also has a concept of a current point. After every graphics operation, a new, internal point is set that will be used as the start point for the next operation. It is the destination point of the previous operation. For example, if you want a bezier path to move to the point origin, then draw a line from the origin to point (10,20), then a line from (10,20) to (20,20) you only require three operations, i.e. (in psuedocode):
move to (0,0)
draw a line to (10,20)
draw a line to (20,20)

In this case, the bezier path first sets the current point to (0,0). Then, when the line operation is called, you only pass in the destination point, (10,20), which causes it to draw a line from (0,0) to (10,20). After the line operation, the current point is set to the destination of the line operation, i.e. (10,20). Then, the next line operation draws a line from (10,20) to (20,20). In this way, we only need specify the destination point for line and move operations, as the start point is determined by the destination point of the previous operation. There is no need to specify the start point for each drawing operation, as it is implied by the destination point of the previous operation. You can get the current point by calling the currentPoint: method.

These operations are listed in the table below:

Move Operation
A move operation lifts the pen up and puts it at a new location, i.e. changes the current coordinates without drawing. This can be achieved throught the -moveToPoint: method, which takes a point as it's first parameter. It implicitly begins a new sub-path (see below).

Line Operation
A line operation draws a line from the current point to a new point. The current point is set either through a move operation, or through the last point in a previous line or curve operation. Once the line operation is complete, the current point is set as the destination point. We can draw a line using the lineToPoint: method.

Curve Operation
This one is more complex, as it involves the real magic of bezier paths. It consists of four points: the start point, the destination point, and two control points. How this works is beyond the scope of this manual(7) and is not required to draw simple circles, ellipses and arcs. We can draw a circle or an ellipse by calling appendBezierPathWithOvalInRect:, passing in a rectangle for the shape to be drawn in. A few methods are provided for adding arcs, with appendBezierPathWithArcFromPoint: toPoint: radius: useful for adding an arc between two points and the appendBezierPathWithArcWithCenter: radius: startAngle: endAngle: method useful for drawing an arc with a particular centre point. For those familiar with bezier curves or who know their control points, the curveToPoint:controlPoint1:controlPoint2: method can be used to draw curves that way. All curve operations set the current point to the destination of the curve.

Close Path Operation
As bezier paths actually consist of many sub-paths, one can close the current set of path operations with the closePath method to avoid creating a new NSBezierPath method.

A bezier path also consists of a number of sub-paths. After a series of move, line and curve operations, a close path operation is inserted to into the bezier path to indicate the end of a sub path. This concept is important with the filling commands.


7.5.3 Stroking, Filling and Clipping

Once you've assembled a path, you can render it in a number of ways. It can be drawn (stroked), filled, or turned into a clipping region. This is done by calling the relevant methods on the bezier path when it is ready to be used. You can stroke/fill/clip a bezier path more than once (if necessary), making the paths reusable.

A simple stroke operation is induced by calling the stroke operation. It causes the outline described by the path to be drawn using the current pen (which can be set on the bezier path as well).

Filling operations are induced by calling the fill method. It fills in, using the current background colour or pattern, the areas described by the outline of the path. Two winding rules for filling are provided: the even-odd and non-zero winding rules. These affect what areas within the path that are filled, and correspond to their PostScript definitions.

A number of convenience class methods exist for simple drawing operations, setting defaults and getting information about the current state of the drawing view, aka the graphics state.(8) We can call +strokeRect: or +fillRect: directly to add a new rectangle or filled rectangle to the current drawing view. The -clipRect: method can be used to set a smaller clipping rectangle, intersecting with the current clipping rectangle (which is set by default to be the frame of your view), just before a call to -drawRect: is made (see below for information about clipping paths).


7.5.4 Text

You can also render text within a view. For this, you use an instance of the NSText class, which provides advanced text rendering capabilities. It acts as a base for the text view system, which should be used where you require rich text input to your application.


7.5.5 Images

If you just want to display an image in your application, use the NSImageView class. If you want to combine it with other elements in a view (e.g. clip an image or draw on top of it), you can make use of the NSImage class to render an image within your view.

It is described in more detail in See Images and Imageviews.


7.5.6 Affine Transformations

The skewing, rotating, translation and scaling of display objects is represented in the form of an affine transformation. They are encapsulated in an object of NSAffineTransform.

These objects store a mathematical matrix which describes the translation of points and objects within a coordinate system.(9) They are used internally to provide the frame and bounds transformations described earlier, and can be used in your drawing code as well. You can append transformations to the current bounds transformation, to bezier paths and even to text.

A matrix is a two-dimensional table of numbers. It may have any number of rows and columns, and like algebraic terms, can be multiplied and added together. We can pretend the numbers in a two by one (2x1) matrix refer to a point in the cartesian coordinate system, and manipulate them like vectors.

Vectors is another mathematical concept that takes numbers in pairs to describe a point in the cartesian plane. For example, the vector (1, 1) can refer to the same numbered point in the cartesian coordinate system. You can also represent this point as the combination of a length (given a magnitude) and a direction (given as a rotation from the x-axis, anticlockwise). Using this, we can represent (1, 1) as ( \sqrt 2, 45 degrees). This representation is useful for transformation in a matrix. (10)

Matrices can be combined together to produce a new affine transform that will perform the same transformation as if all the original transformations were applied in order. You usually won't need to combine them, unless you have complicated drawing code.


7.6 Clipping

One concept that has been mentioned in this chapter is clipping. It is used extensively throughout the AppKit to control the drawing code that renders it's different graphical elements.

When drawing within a view, you often may specify points outside the visible region of your view's bounds, say to blit an image. What prevents that image from obscuring other parts of the window (and indeed the screen) is clipping. A clip defines what region of the screen at any one time may be drawn on.

For example, when the AppKit calls your drawRect: method to draw onto the screen, it first calls lockFocus. In this method is sets a clipping path defined to the frame of your view's rectangle by default, so that you do not draw outside the frame of your view by accident.

You can define your own clipping paths that further clip the output of your drawing code within your view. It may be a simple rectangle (as used in the case of frame clipping by the AppKit), or a complex path defined by the outline of a bezier path.


This document was generated by Adam Fedor on January, 1 2008 using texi2html