Cocoa Drawing Guide
Cocoa Drawing Guide
2011-01-18
Apple Inc. 2005, 2011 Apple Inc. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, mechanical, electronic, photocopying, recording, or otherwise, without prior written permission of Apple Inc., with the following exceptions: Any person is hereby authorized to store documentation on a single computer for personal use only and to print copies of documentation for personal use provided that the documentation contains Apples copyright notice. The Apple logo is a trademark of Apple Inc. No licenses, express or implied, are granted with respect to any of the technology described in this document. Apple retains all intellectual property rights associated with the technology described in this document. This document is intended to assist application developers to develop applications only for Apple-labeled computers. Apple Inc. 1 Infinite Loop Cupertino, CA 95014 408-996-1010 Apple, the Apple logo, Carbon, Cocoa, ColorSync, Mac, Mac OS, Macintosh, Objective-C, Quartz, QuickDraw, QuickTime, Spaces, and Xcode are trademarks of Apple Inc., registered in the United States and other countries. Adobe, Acrobat, and PostScript are trademarks or registered trademarks of Adobe Systems Incorporated in the U.S. and/or other countries. Helvetica is a registered trademark of Heidelberger Druckmaschinen AG, available from Linotype Library GmbH. OpenGL is a registered trademark of Silicon Graphics, Inc.
Even though Apple has reviewed this document, APPLE MAKES NO WARRANTY OR REPRESENTATION, EITHER EXPRESS OR IMPLIED, WITH RESPECT TO THIS DOCUMENT, ITS QUALITY, ACCURACY, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. AS A RESULT, THIS DOCUMENT IS PROVIDED AS IS, AND YOU, THE READER, ARE ASSUMING THE ENTIRE RISK AS TO ITS QUALITY AND ACCURACY. IN NO EVENT WILL APPLE BE LIABLE FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES RESULTING FROM ANY
DEFECT OR INACCURACY IN THIS DOCUMENT, even if advised of the possibility of such damages. THE WARRANTY AND REMEDIES SET FORTH ABOVE ARE EXCLUSIVE AND IN LIEU OF ALL OTHERS, ORAL OR WRITTEN, EXPRESS OR IMPLIED. No Apple dealer, agent, or employee is authorized to make any modification, extension, or addition to this warranty. Some states do not allow the exclusion or limitation of implied warranties or liability for incidental or consequential damages, so the above limitation or exclusion may not apply to you. This warranty gives you specific legal rights, and you may also have other rights which vary from state to state.
Contents
Introduction
Chapter 1
Chapter 2
Graphics Contexts 25
Graphics Context Basics 25 The Current Context 26 Graphics State Information 27 Screen Canvases and Print Canvases 29 Graphics Contexts and Quartz 30 Modifying the Current Graphics State 30 Setting Colors and Patterns 30 Setting Path Attributes 31 Setting Text Attributes 31 Setting Compositing Options 31 Setting the Clipping Region 34 Setting the Anti-aliasing Options 36 Creating Graphics Contexts 37 Creating a Screen-Based Context 37 Creating a PDF or PostScript Context 37
3
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CONTENTS
Chapter 4
4
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CONTENTS
Converting Between Color Spaces 65 Mapping Physical Colors to a Color Space 65 Chapter 5
Paths 67
Path Building Blocks 67 The NSBezierPath Class 67 Path Elements 68 Subpaths 69 Path Attributes 70 Winding Rules 75 Manipulating Geometric Types 77 Drawing Fundamental Shapes 78 Adding Points 78 Adding Lines and Polygons 79 Adding Rectangles 79 Adding Rounded Rectangles 80 Adding Ovals and Circles 81 Adding Arcs 82 Adding Bezier Curves 84 Adding Text 84 Drawing the Shapes in a Path 85 Drawing Rectangles 86 Working with Paths 87 Building Paths 87 Improving Rendering Performance 87 Manipulating Individual Path Elements 89 Transforming a Path 89 Creating a CGPathRef From an NSBezierPath Object 90 Detecting Mouse Hits on a Path 91
Chapter 6
Images 93
Image Basics 93 Image Representations 94 Images and Caching 96 Image Size and Resolution 98 Image Coordinate Systems 99 Drawing Versus Compositing 99 Supported Image File Formats 100 Basic Formats 100 TIFF Compression 101 Support for Other File Formats 102 Guidelines for Using Images 103 Creating NSImage Objects 103 Loading an Existing Image 104
5
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CONTENTS
Loading a Named Image 104 Drawing to an Image 105 Creating a Bitmap 105 Creating a PDF or EPS Image Representation 108 Using a Quartz Image to Create an NSImage 109 Working with Images 110 Drawing Images into a View 110 Drawing Resizable Textures Using Images 110 Creating an OpenGL Texture 112 Applying Core Image Filters 113 Getting and Setting Bitmap Properties 114 Converting a Bitmap to a Different Format 114 Associating a Custom Color Profile With an Image 114 Converting Between Color Spaces 115 Premultiplying Alpha Values for Bitmaps 119 Creating New Image Representation Classes 120 Chapter 7
Text 121
Text Attributes 121 Simple Text Drawing 121 Advanced Text Drawing 122
Chapter 8
Chapter 9
6
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CONTENTS
Graphics Type Conversions 136 Getting a Quartz Graphics Context 137 Creating a Cocoa Graphics Context Using Quartz 137 Modifying the Graphics State 138 Using OpenGL in Your Application 138 Using NSOpenGLView 138 Creating an OpenGL Graphics Context 139 Using QuickTime in Your Application 140 Using the QuickTime Kit 140 Using QuickTime C-Based Functions 140 Using Quartz Composer Compositions 140 Choosing the Right Imaging Technology 141
7
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CONTENTS
8
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
Chapter 2
Graphics Contexts 25
Figure 2-1 Figure 2-2 Figure 2-3 Table 2-1 Table 2-2 Compositing operations in Cocoa 33 Clipping paths and winding rules 35 A comparison of aliased and anti-aliased content 36 Graphics state information 27 Mathematical equations for compositing colors 33
Chapter 3
Chapter 4
Chapter 5
Paths 67
Figure 5-1 Figure 5-2 Figure 5-3 Path elements for a complex path 69 Line cap styles 71 Line join styles 72
9
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
Figure 5-4 Figure 5-5 Figure 5-6 Figure 5-7 Figure 5-8 Figure 5-9 Figure 5-10 Figure 5-11 Table 5-1 Table 5-2 Table 5-3 Table 5-4 Listing 5-1 Listing 5-2 Listing 5-3 Listing 5-4 Listing 5-5 Listing 5-6 Listing 5-7 Listing 5-8 Listing 5-9 Listing 5-10 Listing 5-11 Listing 5-12 Listing 5-13 Listing 5-14 Listing 5-15 Chapter 6
Line dash patterns 73 Flatness effects on curves 74 Miter limit effects 75 Applying winding rules to a path 76 Inscribing the corner of a rounded rectangle 81 Creating arcs 83 Cubic Bezier curve 84 Stroking and filling a path. 85 Path element commands 68 Winding rules 76 Commonly used geometry functions 77 Rectangle frame and fill functions 86 Creating a complex path 69 Setting the line width of a path 70 Setting the line cap style of a path 71 Setting the line join style of a path 72 Adding a dash style to a path 73 Setting the flatness of a path 74 Setting the miter limit for a path 75 Drawing a point 78 Using lines to draw a polygon 79 Drawing a rectangle 80 Drawing a rounded rectangle 81 Creating three arcs 83 Changing the control point of a curve path element 89 Creating a CGPathRef from an NSBezierPath 90 Detecting hits on a path 92
Images 93
Figure 6-1 Figure 6-2 Figure 6-3 Table 6-1 Table 6-2 Table 6-3 Table 6-4 Table 6-5 Table 6-6 Table 6-7 Listing 6-1 Listing 6-2 Listing 6-3 Listing 6-4 Listing 6-5 Listing 6-6 Image orientation in an unflipped view 99 Drawing a three-part image 111 Drawing a nine-part image 111 Image representation classes 94 Image caching modes 96 Implied cache settings 96 Image interpolation constants 98 Cocoa supported file formats 101 TIFF compression settings 101 Additional formats supported by Cocoa 102 Drawing to an image 105 Capturing the contents of an existing image 107 Drawing to an offscreen window 107 Drawing directly to a bitmap 108 Creating PDF data from a view 109 Creating an OpenGL texture from an image 112
10
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
Listing 6-7 Listing 6-8 Listing 6-9 Listing 6-10 Listing 6-11 Chapter 8
Adding a ColorSync profile to an image 114 Creating a bitmap with a custom color profile 115 Converting a bitmap to a different color space 116 Using a CGImageRef object to create an NSImage object 118 Creating a color space from a custom color profile 119
Chapter 9
11
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
12
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
INTRODUCTION
High-quality graphics are an important part of a well-designed application. In fact, high-quality graphics is one of the things that sets Mac OS X apart from many other operating systems. While some operating systems rely on flat colors and rectangular objects, Mac OS X uses color, transparency, and its advanced compositing system to give programs a more fluid and inviting appearance.
Overview of Cocoa Drawing (page 15) introduces drawing-related concepts and the Cocoa support for drawing. Graphics Contexts (page 25) describes the drawing environment and provides examples of how you configure the environment to suit your needs. Coordinate Systems and Transforms (page 39) describes the coordinate systems used for drawing and provides examples of how you manipulate your content using transforms. Color and Transparency (page 59) provides basic information about color and shows you how to use the color-related Cocoa objects. Paths (page 67) describes the basic drawing tools found in Cocoa and provides detailed information about how to create and manipulate everything from simple shapes to Bezier paths. Images (page 93) describes the image classes found in Cocoa and provides examples of how to create and manipulate images in your application. Text (page 121) provides an overview of text and its relationship to the Cocoa drawing environment.
13
INTRODUCTION
Advanced Drawing Techniques (page 123) demonstrates some advanced drawing-related techniques, including full-screen drawing, animation, gradients, and performance tuning. Incorporating Other Drawing Technologies (page 135) provides information and examples on how to integrate advanced technologies, such as Quartz, OpenGL, and QuickTime, into your Cocoa application.
See Also
Drawing is only one step in the process of creating a fully functional Cocoa view. Understanding view hierarchies and how events interact with views are two other critical steps. For information about these other subjects, consult the following documents:
Cocoa Fundamentals Guidefor general information about views View Programming Guidefor information about creating and managing views Cocoa Event-Handling Guidefor information about event handling
Because Cocoa drawing is based on Quartz, many Quartz behaviors (though not all) are also relevant to Cocoa. This document describes the different behaviors provided by Cocoa, but for additional information about Quartz behavior, consult the following documents:
Quartz 2D Programming Guidefor conceptual information related to Quartz. Programming with Quartz, by David Gelphman and Bunny Ladenan excellent third-party book written by members of the Apple development team. It provides detailed explanations and examples of how to draw using Quartz in Mac OS X v10.4.
14
See Also
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 1
Drawing is a fundamental part of most Cocoa applications. If your application uses only standard system controls, then Cocoa does all of the drawing for you. If you use custom views or controls, though, then it is up to you to create their appearance using drawing commands. The following sections provide a quick tour of the drawing-related features available in Cocoa. Subsequent chapters provide more details about each feature, and also include examples for many common tasks you might perform during drawing.
Path-based drawing (also known as vector-based drawing) Image creation, loading and display Text layout and display PDF creation and display Transparency Shadows Color management Transforms Printing support Anti-aliased rendering
15
CHAPTER 1
OpenGL support
Like Quartz, the Cocoa drawing environment takes advantage of graphics hardware wherever possible to accelerate drawing operations. This support is automatic. You do not have to enable it explicitly in your code. For information about the classes available in Cocoa, see Application Kit Framework Reference and Foundation Framework Reference. For information on how to integrate C-based technologies into your Cocoa application, see Incorporating Other Drawing Technologies (page 135).
Drawing order
Drawing order
Result
16
CHAPTER 1
Windows (and their views) Images (including bitmaps of all kinds) Printers Files (PDF, EPS) OpenGL surfaces
By far, the most common drawing destination is your application's windows, and by extension its views. Cocoa maintains graphics context objects on a per-window, per-thread basis for your application. This means that for a given window, there are as many graphics contexts for that window as there are threads in your application. Although most drawing occurs on your application's main thread, the additional graphics context objects make it possible to draw from secondary threads as well. For most other drawing operations, Cocoa creates graphics contexts as needed and configures them before calling your drawing code. In some cases, actions you take may create a graphics context indirectly. For example, when creating a PDF file, you might simply request the PDF data for a certain portion of your view object. Behind the scenes, Cocoa actually creates a graphics context object and calls your view's drawing code to generate the PDF data. You can also create graphics contexts explicitly to handle drawing in special situations. For example, one way to create a bitmap image is to create the bitmap canvas and then create a graphics context that draws directly to that canvas. There are other ways to create graphics context objects explicitly, although most involve drawing to the screen or to an image. It is very rare that you would ever create a graphics context object for printing or generating PDF or EPS data. For information about graphics contexts, see Graphics Contexts (page 25).
17
CHAPTER 1
the graphics state can then be undone quickly by simply restoring the previous graphics state. This ability to save and restore the graphics state provides a simple way for your drawing code to return to a known set of attributes. Cocoa manages some attributes of the graphics state in a slightly different way than Quartz does. For example, the current stroke and fill color are set using the NSColor class, and most path-based parameters are set using the NSBezierPath class. This shift of responsibility reflects the more object-oriented nature of Cocoa. For more information about the attributes that comprise the current graphics state, and the objects that manage them, see Graphics State Information (page 27).
Transforms
A transform is a mathematical construct used to manipulate coordinates in two-dimensional space. Transforms are used extensively in graphics-based computing to simplify the drawing process. Coordinate values are multiplied through the transform's mathematical matrix to obtain a modified coordinate that reflects the transform's properties. In Cocoa, the NSAffineTransform class implements the transform behavior. You use this class to apply the following effects to the current coordinate system:
18
CHAPTER 1
You can combine the preceding effects in different combinations to achieve interesting results. During drawing, Cocoa applies the effects to the content you draw, imparting those characteristics on your shapes and images. Because all coordinates are multiplied through a transform at some point during rendering, the addition of these effects has little effect on performance. In fact, manipulating your shapes using transforms is often faster than manipulating your source data directly. For more information about transforms, including how they affect your content and how you use them, see Coordinate Systems and Transforms (page 39).
Geometry Support
Cocoa provides its own data structures for manipulating basic geometric information such as points and rectangles. Cocoa defines the data types listed in Table 1-1. The member fields in each of these data structures are floating-point values. Table 1-1 Type Primitive data types Description
NSPoint A point data type consists of an x and y value. Points specify the coordinates for a rendered
element. For example, you use points to define lines, to specify the start of a rectangle, to specify the angle of an arc, and so on.
NSSize
A size data type consists of a width and height field. Sizes are used to specify dimensions of a target. For example, a size data type specifies the width and height of a rectangle or ellipse.
19
CHAPTER 1
Type
NSRect
Description A rectangle data type is a compound structure composed of an origin point and a size. The origin field specifies the location of the rectangles bottom-left corner in the current coordinate system. The size field specifies the rectangles height and width relative to the origin point and extending up and to the right. (Note, in flipped coordinate spaces, the origin point is in the upper-left corner and the rectangles height and width extend down and to the right.)
For information on how to manipulate point, rectangle, and size data types, see Manipulating Geometric Types (page 77).
Shape Primitives
Cocoa provides support for drawing shape primitives with the NSBezierPath class. You can use this class to create the following basic shapes, some of which are shown in Figure 1-2.
Figure 1-2
Bezier path objects store vector-based path information, making them compact and resolution independent. You can create paths with any of the simple shapes or combine the basic shapes together to create more complex paths. To render those shapes, you set the drawing attributes for the path and then stroke or fill it to paint the path to your view. Note: You can also add glyph outlines to a Bezier path object using the methods of NSBezierPath. For most text handling, though, you should use the Cocoa text system, which is introduced in Text (page 121). For more information about drawing shapes, see Paths (page 67).
20
CHAPTER 1
Images
Support for images is provided by the NSImage class and its associated image representation classes (NSImageRep and subclasses). The NSImage class contains the basic interface for creating and managing image-related data. The image representation classes provide the infrastructure used by NSImage to manipulate the underlying image data. Images can be loaded from existing files or created on the fly. Figure 1-3 shows some bitmap images loaded from files. Figure 1-3 Examples of bitmap images
Cocoa supports many different image formats, either directly or indirectly. Some of the formats Cocoa supports directly include the following:
Images based on Encapsulated PostScript (EPS) data Images based on Portable Document Format (PDF) data Images based on PICT data Core Image images
21
CHAPTER 1
Because they support many types of data, you should not think of image objects strictly as bitmaps. Image objects can also store path-based drawing commands found in EPS, PDF, and PICT files. They can render data provided to them by Core Image. They can interpolate image data as needed and render the image at different resolutions as needed. For detailed information about Cocoa support for images and the ways to use images in your code, see Images (page 93).
Gradients
In Mac OS X v10.5 and later, you can use the NSGradient class to create gradient fill patterns.
Text
Cocoa provides an advanced text system for drawing everything from simple strings to formatted text flows. Figure 1-4 shows some basic examples of stylized text that you can create. Figure 1-4 Examples of text
Because text layout and rendering using the Cocoa text system is a very complicated process, it is already well documented elsewhere and is not covered in great detail in this document. For basic information about drawing text and for links to more advanced text-related documents, see Text (page 121).
22
CHAPTER 1
The NSView class is the base class for all view-related objects. Cocoa defines several types of views for displaying standard content, including text views, split views, tab views, ruler views, and so on. Cocoa controls are also based on the NSView class and implement interface elements such as buttons, scrollers, tables, and text fields. In addition to the standard views and controls, you can also create your own custom views. You create custom views in cases where the behavior you are looking for is not provided by any of the standard views. Cocoa notifies your view that it needs to draw itself by sending your view a drawRect: message. Your implementation of the drawRect: method is where all of your drawing code goes. Note: Although you can also subclass the standard views and controls to implement custom behavior, it is recommended that you try to use a delegate object whenever possible instead. If you do subclass a standard control, avoid changing the appearance of that control. Doing so goes against the guidance in Mac OS X Human Interface Guidelines. By default, window updates occur only in response to user actions. This means that your views drawRect: method is called only when something about your view has changed. For example, Cocoa calls the method when a scrolling action causes a previously hidden part of your view to be exposed. Cocoa also calls it in response to requests from your own code. If the information displayed by your custom view changes, you must tell Cocoa explicitly that you want the appropriate parts of your view updated. You do so by invalidating parts of your views visible area. Cocoa collects the invalidated regions together and generates appropriate drawRect: messages to redraw the content. Although there are numerous ways to draw, a basic drawRect: method has the following structure:
- (void)drawRect:(NSRect)rect { // Draw your content }
That's it! By the time your drawRect: method is called, Cocoa has already locked the drawing focus on your view, saved the graphics state, adjusted the current transform matrix to your view's origin, and adjusted the clipping rectangle to your view's frame. All you have to do is draw your content. In reality, your drawRect: method is often much more complicated. Your own method might use several other objects and methods to handle the actual drawing. You also might need to save and restore the graphics state one or more times. Because this single method is used for all of your view's drawing, it also has to handle several different situations. For example, you might want to do more precise drawing during printing or use heavily optimized code during a live resizing operation. The options are numerous and covered in more detail in subsequent chapters. For additional information about views and live resizing, see View Programming Guide. For more information about printing in Cocoa, see Customizing a Views Drawing for Printing in Printing Programming Topics for Cocoa.
23
CHAPTER 1
Common tasks and solutions How to accomplish Implement a drawRect: method in your custom view. Use your implementation of this method to draw content using paths, images, text, or any other tools available to you in Cocoa, Quartz, or OpenGL. Send a setNeedsDisplayInRect: or setNeedsDisplay: message to the view. Sending either of these messages marks part or all of the view as invalid and in need of an update. Cocoa responds by sending a drawRect: message to your view during the next update cycle. Use Core Animation, set up a timer, or use the NSAnimation or NSViewAnimation classes, to generate notifications at a desired frame rate. Upon receiving the timer notification, invalidate part or all of your view to force an update. For information about Core Animation, see Core Animation Programming Guide. For more information about animating with timers, see Using NSTimer for Animated Content (page 132). For information about using NSAnimation objects, see Using Cocoa Animation Objects (page 133).
Draw during a live resize. Use the inLiveResize method of NSView to determine if a live resize is happening. If it is, draw as little as possible while ensuring your view has the look you want. For more information about live resizing optimizations, see Drawing Performance Guidelines. Draw during a printing operation. Use the currentContextDrawingToScreen class method or isDrawingToScreen instance method of NSGraphicsContext to determine if a print operation is underway. Use the attributes method of NSGraphicsContext to retrieve (as needed) any additional information about the current print job. Draw images at the best possible resolution. Adjust your graphics in any other ways you think are appropriate to achieve the best possible appearance on the target device. For more information about printing, see Printing Programming Topics for Cocoa. Use the dataWithPDFInsideRect: or dataWithEPSInsideRect: method to obtain the data. In your drawRect: method use the currentContextDrawingToScreen class method or isDrawingToScreen instance method of NSGraphicsContext to determine if a print operation is underway.
24
CHAPTER 2
Graphics Contexts
Graphics contexts are a fundamental part of the drawing infrastructure in Cocoa applications. As the name suggests, a graphics context provides the context for subsequent drawing operations. It identifies the current drawing destination (screen, printer, file, and so on), the coordinate system and boundaries for the underlying canvas, and any graphics attributes associated with the destination. For most of the drawing you do in Cocoa, you never need to create a graphics context yourself. The normal drawing cycle in Cocoa automatically creates and configures a graphics context for you to use. For some advanced drawing, however, you may need to create your own graphics context prior to drawing. In a Cocoa application, graphics contexts for nearly all types of canvas are represented by the NSGraphicsContext class. You use graphics context objects to manipulate graphics attributes and to get information about the current drawing environment. Note: For OpenGL drawing, you use the NSOpenGLContext class instead of NSGraphicsContext for the graphics context object. OpenGL drawing, and use of the NSOpenGLContext class, are covered in Using OpenGL in Your Application (page 138). This chapter provides an overview of Cocoa graphics contexts and how you use them in your application. It includes information on how to create custom graphics contexts and when it might be appropriate to do so.
25
CHAPTER 2
Graphics Contexts
After your drawRect: method returns, Cocoa goes through the process of resetting the drawing environment for the next view. It reverts any changes you made to the drawing environment and sets up the coordinate transform and clipping region for the next view, giving it its own pristine environment in which to work. This process then repeats itself during each update cycle in your application.
The currentContext method always returns the Cocoa graphics context object that is appropriate for the current drawing environment. This object keeps track of the current graphics state, lets you save and restore graphics state information, and lets you modify many graphics state attributes. The changes you make to the graphics state affect all subsequent drawing calls. If you change an attribute more than once, only the most recent setting is used. To save the current graphics state, you use the saveGraphicsState method of NSGraphicsContext. This method essentially pushes a copy of the current state onto a stack, leaving you free to make changes to the current state. When you want to revert back to the previous state, you simply call the restoreGraphicsState method to pop the current graphics state (including all changes since the last save) off of the stack and restore the previous state. If you plan to change the current graphics state significantly, it is a good idea to save the current state before making your changes. Modifying one or two attributes usually may not merit saving the graphics state, since you can reset or change those individual attributes easily. However, if you are changing more than one or two attributes, it is usually easier to save and restore the entire graphics state. You can call the saveGraphicsState method as often as needed in your code to save snapshots of the current graphics state, but you must be sure to balance each call with a matching call to restoreGraphicsState. Note: The saveGraphicsState and restoreGraphicsState methods are available both as class methods and as instance methods. The class method versions simply save and restore the graphics state of the current context. The instance methods let you save the state of a specific context object, although in most cases this should be the current context. The following example shows a simple drawRect: method that iterates over an array of developer-defined objects, each of which is drawn with a different set of attributes. The graphics state is saved and restored during each loop iteration, ensuring that each object starts from the same graphics state.
- (void)drawRect:(NSRect)rect { NSGraphicsContext* theContext = [NSGraphicsContext currentContext]; int i; int numObjects = [myObjectArray count]; // Iterate over an array of objects // Set the attributes for each before drawing for (i = 0; i < numObjects; i++) {
26
CHAPTER 2
Graphics Contexts
[theContext saveGraphicsState]; // Set the drawing attributes // Draw the object [theContext restoreGraphicsState]; } }
Warning: When saving and restoring the graphics state, you must balance all calls to saveGraphicsState with a corresponding call to restoreGraphicsState. Failure to do so can result in unexpected changes to the appearance of any windows that use that view.
Clipping area
Line width
27
CHAPTER 2
Graphics Contexts
Description Determines when lines should be joined with a bevel instead of a miter. Applies only when the line join style is set to NSMiterLineJoinStyle. The length of the miter is divided by the line width. If the resulting value is greater than the miter limit, a bevel is used. The default value is 10.0 but you can modify this value using an NSBezierPath object. For more information, see Miter Limits (page 74). Specifies the accuracy with which curves are rendered. (It is also the maximum error tolerance, measured in pixels.) Smaller numbers result in smoother curves at the expense of more calculations. The interpretation of this value may vary slightly on different rendering devices. The default value is 0.6 but you can modify this value using an NSBezierPath object. For more information, see Line Flatness (page 73). Specifies the color used for rendering paths. This color applies only to the path line itself, not the area the path encompasses. You can specify colors using any of the system-supported color spaces. This value includes alpha information. Color information is managed by the NSColor class. For more information, see Setting Colors and Patterns (page 30). Specifies the color used to fill the area enclosed by a path. You can specify colors using any of the system-supported color spaces. This value includes alpha information. Color information is managed by the NSColor class. For more information, see Setting Colors and Patterns (page 30). Specifies the shadow attributes to apply to rendered content. You set shadows using the NSShadow class. For more information, see Adding Shadows to Drawn Paths (page 123). Specifies the technique used to map in-gamut colors to the gamut of the current color space. Cocoa does not support setting this attribute directly. Instead, you must use Quartz. For more information, see Mapping Physical Colors to a Color Space (page 65). Specifies the font to use when drawing text. You modify font information using the NSFont class. For more information on drawing text, see Text Attributes (page 121). Specifies the font size to use when drawing text. You modify font information using the NSFont class. For more information on drawing text, see Text Attributes (page 121). Specifies the character spacing to use when drawing text. (This attribute is supported only indirectly by Cocoa.) For more information on drawing text, see Text Attributes (page 121).
Flatness value
Stroke color
Fill color
Shadow
Rendering intent
Font name
Font size
Text drawing mode Specifies how to render the text. (This attribute is supported only indirectly by Cocoa.) For more information on drawing text, see Text Attributes (page 121). Image interpolation Specifies the process used to interpolate images during rendering. You use the quality NSGraphicsContext class to change this setting. For more information, see Image Size and Resolution (page 98)
28
CHAPTER 2
Graphics Contexts
Description Specifies the process used to composite source and destination material together. (The compositing operations supported by Cocoa are related to the Quartz blend modes but differ in their usage and behavior.) You use the NSGraphicsContext class to set the default value for this setting. Some rendering methods and functions may let you specify a different option. For more information, see Setting Compositing Options (page 31). Specifies a global alpha (transparency) value to apply in addition to the alpha value for a given color. Cocoa does not support this attribute directly. If you want to set it, you must use the CGContextSetAlpha function in Quartz.
Global alpha
Anti-aliasing setting Specifies whether paths use aliasing to smooth lines as they cross pixel boundaries. You use the NSGraphicsContext class to change this setting. For more information, see Setting the Anti-aliasing Options (page 36).
Note: The winding rule used to fill paths is not stored as part of the current graphics state. You can set a default winding rule for NSBezierPath objects but doing so affects content rendered using those objects. For more information, see Winding Rules (page 75).
Using OpenGL commands to render your view content Drawing to an offscreen bitmap Creating PDF or EPS data Initiating a print job programmatically
Using the class methods of NSGraphicsContext, you can create graphics context objects for drawing to screen-based canvases. You cannot use these methods for print-based canvas, however. Cocoa routes all printing operations through the Cocoa printing system, which handles the task of setting up the graphics context object for you. This means that if you want to generate PDF data, EPS data, or print to a printer, you must use the methods of the NSPrintOperation class to create a print job for your target. It also means that your views should provide some minimal printing support if you want to produce well-formatted output for print-based canvases.
29
CHAPTER 2
Graphics Contexts
Note: Although Cocoa does provide some support for creating OpenGL graphics contexts automatically, the default pixel format options are usually limited. In most cases, you will want to create a custom OpenGL graphics context with the pixel format options you need for drawing. For more information, see Creating an OpenGL Graphics Context (page 139). You can determine the type of canvas being managed by the current graphics context using the isDrawingToScreen instance method or currentContextDrawingToScreen class method of NSGraphicsContext. For print-based canvases, you can use the attributes method to get additional information about the canvas, such as whether it is being used to generate a PDF or EPS file. For more information about obtaining contexts for both screen-based and print-based canvases, see Creating Graphics Contexts (page 37).
30
CHAPTER 2
Graphics Contexts
To set the current stroke or fill attributes, create an NSColor object and send it a set, setStroke, or setFill message. The stroke and fill attributes define the color or pattern for paths and the areas they enclose. The current stroke and fill colors affect all drawn content except text, which requires the application of text attributes; see Applying Color to Text (page 63). For more information about colors and how to create them, see Color and Transparency (page 59).
31
CHAPTER 2
Graphics Contexts
The Cocoa compositing options differ from the blend modes used in Quartz, although the two perform basically the same task. The Cocoa options are inherited from the NextStep environment, whereas the Quartz blend modes are part of the newer PDF-based rendering model. Despite their historical legacy, the Cocoa options are still a very powerful way to composite content, and may even be a little easier to understand than their Quartz counterparts. Important: Despite their similarities, there is no direct mapping between the Cocoa compositing options and the Quartz blend modes. In addition, when drawing to a print-based canvas, you should use only the NSCompositeCopy or the NSCompositeSourceOver operators. (For PDF content, you should use only the NSCompositeSourceOver operator or the Quartz blend modes.) If you need to use any other compositing operators, you should render your content to an image and then draw the image to the printing context using one of the supported operators. If your application relies heavily on PDF blend modes, you may want to use Quartz for your drawing instead. Figure 2-1 shows the Cocoa compositing options and how they affect rendered content. At the top of the figure are the source and destination content being rendered. The veins of the leaf are completely transparent while the rest of the leaf is opaque. In the destination image, the color is rendered at partial opacity. Below that are the results for each of the supported compositing operations.
32
CHAPTER 2
Graphics Contexts
Figure 2-1
Opacity 85%
Source
Destination
Copy
Clear
Source over
Source atop
Source in
Source out
Destination over
Destination atop
Destination in
Destination out
Xor
Plus darker
Plus lighter
Table 2-2 lists the mathematical equations used to compute pixel colors during compositing operations. In each equation, R is the resulting (premultiplied) color, S is the source color, D is the destination color, Sa is the alpha value of the source color, and Da is the alpha value of the destination color. All color component values and alpha values are in the range 0 to 1 for these computations. Table 2-2 Para
NSCompositeClear NSCompositeCopy NSCompositeSourceOver NSCompositeSourceIn
33
CHAPTER 2
Graphics Contexts
Para
NSCompositeSourceOut NSCompositeSourceAtop
Para
R = S*(1 - Da) R = S*Da + D*(1 - Sa)
NSCompositeDestinationAtop R = S*(1 - Da) + D*Sa NSCompositeXOR NSCompositePlusDarker NSCompositePlusLighter R = S*(1 - Da) + D*(1 - Sa) R = MAX(0, (1 - D) + (1 - S)) R = MIN(1, S + D)
To set the current compositing operation, you use the setCompositingOperation: method of NSGraphicsContext. This sets the global compositing option to use if no other operator is specified. The default compositing option is NSCompositeSourceOver.
34
CHAPTER 2
Graphics Contexts
Figure 2-2 shows the effects of applying a clipping path to an image. The top images show the image to be clipped and the path to use for the clip shape, which in this case consists of two shapes inside a single NSBezierPath object. Although the clip shape is the same in both cases, the resulting clip region is different. This is because clipping takes into account the current winding rule when calculating the clipping region. Figure 2-2 Clipping paths and winding rules
Nonzero Even-odd
The following example shows you how to create the clip region shown in Figure 2-2. The clip region is composed of an overlapping square and circle, so you simply add a rectangle and oval with the appropriate sizes to a Bezier path object and call the addClip method.
// If you plan to do more drawing later, it's a good idea // to save the graphics state before clipping. [NSGraphicsContext saveGraphicsState]; // Create the path and add the shapes NSBezierPath* clipPath = [NSBezierPath bezierPath]; [clipPath appendBezierPathWithRect:NSMakeRect(0.0, 0.0, 100.0, 100.0)]; [clipPath appendBezierPathWithOvalInRect:NSMakeRect(50.0, 50.0, 100.0, 100.0)]; // Add the path to the clip shape. [clipPath addClip]; // Draw the image.
35
CHAPTER 2
Graphics Contexts
[NSGraphicsContext restoreGraphicsState];
Warning: Although you can also use the setClip method of NSBezierPath to modify the clipping region, doing so is not recommended. The setClip method replaces the entire clipping region with the area you specify. If the new clipping region extends beyond the bounds of your view, this could lead to portions of your content spilling over into neighboring views.
Anti-aliased
To enable or disable anti-aliasing, use the setShouldAntialias: method of NSGraphicsContext. Even with anti-aliasing disabled, it may still appears as if Cocoa is drawing content using aliasing. When drawing content on non-pixel boundaries, Cocoa may opt to split the line over multiple pixels, which can give the impression of aliasing. For more information about how to avoid this situation, see Doing Pixel-Exact Drawing (page 55).
36
CHAPTER 2
Graphics Contexts
37
CHAPTER 2
Graphics Contexts
To create a print job manually, you use the NSPrintOperation class. This class offers several class methods for creating print jobs for a particular view and outputting the job to a printer, PDF file, or EPS file. Once you have an instance of the NSPrintOperation class, you can set the print information and use the runOperation method to start the print job, at which point Cocoa takes over. Important: You cannot create a viable graphics context for PDF or PostScript canvases using the graphicsContextWithAttributes: method. You must go through the Cocoa Printing system instead. During the execution of a print job, Cocoa calls several methods of your view to handle page layout and drawing. These methods are called for all printing paths, so implementing them for printing will also support PDF and EPS. For information on how to implement these methods, see Printing Programming Topics for Cocoa.
38
CHAPTER 3
Coordinate spaces simplify the drawing code required to create complex interfaces. In a standard Mac OS X application, the window represents the base coordinate system for drawing, and all content must eventually be specified in that coordinate space when it is sent to the window server. For even simple interfaces, however, it is rarely convenient to specify coordinates relative to the window origin. Even the location of fixed items can change and require recalculation when the window resizes. This is where Cocoa makes things simple. Each Cocoa view you add to a window maintains its own local coordinate system for drawing. Rather than convert coordinate values to window coordinates, you simply draw using the local coordinate system, ignoring any changes to the position of the view. Before sending your drawing commands to the window server, Cocoa automatically corrects coordinate values and puts them in the base coordinate space. Even with the presence of local coordinate spaces, it is often necessary to change the coordinate space temporarily to affect certain behaviors. Changing the coordinate space is done using mathematical transformations (also known as transforms). Transforms convert coordinate values from one coordinate space to another. You can use transforms to alter the coordinate system of a view in a way that affects subsequent rendering calls, or you can use them to determine the location of points in the window or another view. The following sections provide information about how Cocoa manages the local coordinate systems of your views and how you can use transforms to affect your drawing environment.
39
CHAPTER 3
Note: If a computer has multiple monitors attached, those monitors can be set to mirror each other or to display one contiguous desktop. In mirroring mode, every screen has an origin of (0, 0). In contiguous mode, one screen has an origin of (0, 0) but other screens have origins that are offset from that of the first screen. Figure 3-1 shows the coordinate-system origin points of the screen, a window, and a view. In each case, the value to the bottom-left of each point is the coordinate measured in its parent coordinate system. (The screen does not have a parent coordinate system, so both coordinate values are 0). The windows parent is the screen and the views parent is the window. Figure 3-1 Screen, window, and view coordinate systems on the screen
(0,0) (0,0) x
Mapping from screen coordinates to local window or view coordinates takes place in the current transformation matrix (CTM) of the Cocoa graphics context object. Cocoa applies the CTM automatically to any drawing calls you make, so you do not need to convert coordinate values yourself. You can modify the CTM though to change the position and orientation of the coordinate axes inside your view. (For more information, see Transformation Operations (page 43).)
40
CHAPTER 3
target device, such as a monitor or printer. The advantage of this model is that graphics drawn using vector commands scale nicely to any resolution device. As the device resolution increases, the system is able to use any extra pixels to create a crisper look to the graphics. In order to maintain the precision inherent with a vector-based drawing system, drawing coordinates are specified using floating-point values instead of integers. The use of floating-point values for Mac OS X coordinates makes it possible for you to specify the location of your program's content very precisely. For the most part, you do not have to worry about how those values are eventually mapped to the screen or other output device. Instead, Cocoa takes care of this mapping for you. Even though the drawing model is based on PDF, there are still times when you need to render pixel-based content. Bitmap images are a common way to create user interfaces, and your drawing code may need to make special adjustments to ensure that any bitmap images are drawn correctly on different resolution devices. Similarly, you may want to ensure that even your vector-based graphics align properly along pixel boundaries so that they do not have an anti-aliased appearance. Mac OS X provides numerous facilities to help you draw pixel-based content the way you want it. The following sections provide more detail about the coordinate spaces used for drawing and rendering content. There also follows some tips on how to deal with pixel-specific rendering in your drawing code.
User Space
The user coordinate space in Cocoa is the environment you use for all your drawing commands. It represents a fixed scale coordinate space, which means that the drawing commands you issue in this space result in graphics whose size is consistent regardless of the resolution of the underlying device. Units in the user space are based on the printer's point, which was used in the publishing industry to measure the size of content on the printed page. A single point is equivalent to 1/72 of an inch. Points were adopted by earlier versions of Mac OS as the standard resolution for content on the screen. Mac OS X continues to use the same effective resolution for user-space drawing. Although a single point often corresponded directly to a pixel in the past, in Mac OS X, that may not be the case. Points are not tied to the resolution of any particular device. If you draw a rectangle whose width and height are exactly three points, that does not mean it will be rendered on the screen as a three-pixel by three-pixel rectangle. On a 144 dpi screen, the rectangle might be rendered using six pixels per side, and on a 600-dpi printer, the rectangle would require 25 pixels per side. The actual translation from points to pixels is device dependent and handled for you automatically by Mac OS X. For all practical purposes, the user coordinate space is the only coordinate space you need to think about. There are some exceptions to this rule, however, and those are covered in Doing Pixel-Exact Drawing (page 55).
Device Space
The device coordinate space refers to the native coordinate space used by the target device, whether it be a screen, printer, file, or some other device. Units in the device coordinate space are specified using pixels and the resolution of this space is device dependent. For example, most monitors have resolutions in the 100 dpi range but printers may have resolutions exceeding 600 dpi. There are some devices that do not have a fixed resolution, however. For example, PDF and EPS files are resolution independent and can scale their content to any resolution.
41
CHAPTER 3
For Cocoa users, the device coordinate space is something you rarely have to worry about. Whenever you generate drawing commands, you always specify positions using user space coordinates. The only time that you might need to know about device space coordinates is when you are adjusting your drawn content to map more cleanly to a specific target device. For example, you might use device coordinates to align a path or image to specific pixel boundaries in order to prevent unwanted anti-aliasing. In such a situation, you can adjust your user space coordinates based on the resolution of the underlying device. For information on how to do this, see Doing Pixel-Exact Drawing (page 55)
Transform Basics
Transforms are a tool for manipulating coordinates (and coordinate systems) quickly and easily in your code. Consider a rectangle whose origin is at (0, 0). If you wanted to change the origin of this rectangle to (10, 3), it would be fairly simple to modify the rectangles origin and draw it. Suppose, though, that you wanted to change the origin of a complex path that incorporated dozens of points and several Bezier curves with their associated control points. How easy would it be to recalculate the position of each point in that path? It would probably take a lot of time and require some pretty sophisticated calculations. Enter transforms. A transform is two-dimensional mathematical array used to map points from one coordinate space to another. Using transforms, you can scale, rotate, and translate content freely in two-dimensional space using only a few methods and undo your changes just as quickly.
42
Transform Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 3
Support for transforms in Cocoa is provided by the NSAffineTransform class. The following sections provide background information about transforms and their effects. For additional information about how to use transforms in your code, see Using Transforms in Your Code (page 47).
Transformation Operations
For two-dimensional drawing, you can transform content in several different ways, including translating, scaling, and rotating. Transforms modify the coordinate system for the current drawing environment and affect all subsequent drawing operations. Before applying a transform, it is recommended that you save the current graphics state. The following sections describe each type of transformation and how it affects rendered content.
Translation
Translation involves shifting the origin of the current coordinate system horizontally and vertically by a specific amount. Translation is probably used the most because it can be used to position graphic elements in the current view. For example, if you create a path whose starting point is always (0, 0), you could use a translation transform to move that path around your view, as shown in Figure 3-2. Figure 3-2 Translating content
y' y y
x' x x
To translate content, use the translateXBy:yBy: method of NSAffineTransform. The following example changes the origin of the current context from (0, 0) to (50, 20) in the view's coordinate space:
NSAffineTransform* xform = [NSAffineTransform transform]; [xform translateXBy:50.0 yBy:20.0];
Transform Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
43
CHAPTER 3
[xform concat];
Scaling
Scaling lets you stretch or shrink the units of the user space along the x and y axes independently. Normally, one unit in user space is equal to 1/72 of an inch. If you multiple the scale of either axis by 2, one unit on that axis becomes equal to 2/72 of an inch. This makes content drawn with scale factors greater than 1 appear magnified and content drawn with scale factors less than 1 appear shrunken. Figure 3-3 shows the effects of scaling on content. In the figure, a translation transform has already been applied so that the origin is located at (1, 1) in the original user space coordinate system. After applying the scaling transform, you can see the modified coordinate system and how it maps to the original coordinate system. Figure 3-3 Scaling content
y '' y y
y'
2
4 3 2 1
4 3 2
x'
1
1
x
x '' x
2 x2
Although you might normally scale proportionally by applying the same scale factor to both the horizontal and vertical axes, you can assign different scale factors to each axis to create a stretched or distorted image. To scale content proportionally, use the scaleBy: method of NSAffineTransform. To scale content differently along the X and Y axes, use the scaleXBy:yBy: method. The following example demonstrates the scale factors shown in Figure 3-3:
NSAffineTransform* xform = [NSAffineTransform transform]; [xform scaleXBy:2.0 yBy:1.5]; [xform concat];
Note: Scaling does not change the origin of the coordinate system.
Rotation
Rotation changes the orientation of the coordinate axes by rotating them around the current origin, as shown in Figure 3-4. You can change the orientation through a full circle of motion.
44
Transform Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 3
Figure 3-4
y
Rotated content
y
Unrotated axes
y'
x'
Rotated 45
To rotate content, use the rotateByDegrees: or rotateByRadians: methods of NSAffineTransform. Positive rotation values proceed counterclockwise around the current origin. For example, to rotate the current coordinate system 45 degrees around the current origin point (as shown in Figure 3-4), you would use the following code:
NSAffineTransform* xform = [NSAffineTransform transform]; [xform rotateByDegrees:45]; [xform concat];
Note: Combining a non-uniform scaling transform with a rotation transform can also give your content a skewed effect.
Transformation Ordering
The implementation of transforms uses matrix multiplication to map an incoming coordinate point to a modified coordinate space. Although the mathematics of matrices are covered in Transform Mathematics (page 46), an important factor to note is that matrix multiplication is not always a commutative operationthat is, a times b does not always equal b times a. Therefore, the order in which you apply transforms is often crucial to achieving the desired results. Figure 3-5 shows the two transformations applied to a path in two different ways. In the top part of the figure, the content is translated by 60 points along the X axis and then rotated 45 degrees. In the bottom part of the figure, the exact same transformations are reversed with the rotation preceding the translation. The end result is two different coordinate systems.
Transform Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
45
CHAPTER 3
Figure 3-5
y
Transform ordering
y' y y'
x'
y'
x'
x'
'
2 Rotate 45
'
y'
x'
y'
y'
x'
1 Rotate 45
The preceding figure demonstrates the key aspect of transformation ordering. Each successive transformation is applied to the coordinate system created by the previous transformations. When you translate and then rotate, the rotation begins around the origin of the translated coordinate system. Similarly, when you rotate and then translate, the translation occurs along the axes of the rotated coordinate system. For transformations of the same type, the order of the transformations does not matter. For example, three rotations in a row creates a coordinate system whose final rotation is equal to the final sum of the three rotation angles. There may be other cases (such as scaling by 1.0) where the order of the transforms does not matter, but you should generally assume that order is significant.
Transform Mathematics
All transform operations contribute to the building of a mathematical matrix that is then used by the graphics system to compute the screen location of individual points. The NSAffineTransform class uses a 3 x 3 matrix to store the transform values. Figure 3-6 shows this matrix and identifies the key factors used to apply transforms. The m11, m12, m21, and m22 values control both the scaling and rotation factors while tx and ty control translation.
46
Transform Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
x'
'
'
CHAPTER 3
Figure 3-6
Using linear algebra, it is possible to multiply a coordinate vector through the transform matrix to obtain a new coordinate vector whose position is equal to the original point in the new coordinate system. Figure 3-7 shows the matrix multiplication process and the resulting linear equations. Figure 3-7 Mathematical conversion of coordinates
If you are already familiar with transform structures and the mathematics, you can set the values of a transform matrix directly using the setTransformStruct: method of NSAffineTransform. This method replaces the six key transform values with the new ones you specify. Replacing all of the values at once is much faster than applying individual transformations one at a time. It does require you to precompute the matrix values, however. For more information about the mathematics behind matrix multiplications, see Quartz 2D Programming Guide.
47
CHAPTER 3
Calling concat adds your transformations to the CTM of the current graphics context. The modifications stay in effect until you explicitly undo them, as described in Undoing a Transformation (page 48), or a previous graphics state is restored. The following example creates a new transform object and adds several transformations to it.
NSAffineTransform* xform = [NSAffineTransform transform]; // Add [xform [xform [xform the transformations translateXBy:50.0 yBy:20.0]; rotateByDegrees:90.0]; // counterclockwise rotation scaleXBy:1.0 yBy:2.0];
Undoing a Transformation
Once applied, a transform affects all subsequent drawing calls in the current context. To undo a set of transformations, you can either restore a previous graphics state or apply an inverse transform. Both techniques have their advantages and disadvantages, so you should choose a technique based on your needs and the available information. Restoring a previous graphics state is the simplest way to undo a transformation but has other side effects. In addition to undoing the transform, restoring the graphics state reverts all other attributes in the current drawing environment back to their previous state. If you want to undo only the current transformation, you can add an inverse transform to the CTM. An inverse transform negates the effects of a given set of transformations using a complementary set of transformations. To create an inverse transform object, you use the invert method of the desired transform object. You then apply this modified transform object to the current context, as shown in the following example:
NSAffineTransform* xform = [NSAffineTransform transform]; // Add [xform [xform [xform the transformations translateXBy:50.0 yBy:20.0]; rotateByDegrees:90.0]; // counterclockwise rotation concat];
// Draw content... // Remove the transformations by applying the inverse transform. [xform invert]; [xform concat];
You might use this latter technique to draw multiple items using the same drawing attributes but at different positions in your view. Depending on the type of transformations you use, you might also be able to do incremental transformations. For example, if you are calling translateXBy:yBy: only to reposition the origin, you could move the origin incrementally for each successive item. The following example, shows how you might position one item at (10, 10) and the next at (15, 10):
[NSAffineTransform* xform = [NSAffineTransform transform]; // Draw item 1 [xform translateXBy:10.0 yBy:10.0]; [xform concat];
48
CHAPTER 3
[item1 draw]; //Draw [xform [xform [item2 item 2 translateXBy:5.0 yBy:0.0]; // Translate relative to the previous concat]; draw];
element.
Remember that the preceding techniques are used in cases where you do not want to modify your original items directly. Cocoa provides ways to modify geometric coordinates without modifying the current transformation matrix. For more information, see Transforming Coordinates (page 49). It is also worth noting that the effectiveness of an inverse transform is limited by mathematical precision. For rotation transforms, which involve taking sines and cosines of the desired rotation angle, an inverse transform may not be precise enough to undo the original rotation completely. In such a situation, you may want to simply save and restore the graphics state to undo the transform.
Transforming Coordinates
If you do not want to change the coordinate system of the current drawing environment, but do want to change the position or orientation of a single object, you have several options. The NSAffineTransform class includes the transformPoint: and transformSize: methods for changing coordinate values directly. Using these methods does not change the CTM of the current graphics context. If you want to alter the coordinates in a path, you can do so using the transformBezierPath: method of NSAffineTransform. This method returns a transformed copy of the specified Bezier path object. This method differs slightly from the transformUsingAffineTransform: method of NSBezierPath, which modifies the original object.
49
CHAPTER 3
(0,0) (0,0)
y Flipped coordinates
Flipping the coordinate system can make drawing easier in some situations. Text systems in particular use flipped coordinates to simplify the placement of text lines, which flow from top to bottom in most writing systems. Although you are encouraged to use the standard Cartesian (unflipped) coordinate system whenever possible, you can use flipped coordinates if doing so is easier to support in your code. Configuring a view to use flipped coordinates affects only the content you draw directly in that view. Flipped coordinate systems are not inherited by child views. The content you draw in a view, however, must be oriented correctly based on the current orientation of the view. Failing to take into account the current view orientation may result in incorrectly positioned content or content that is upside down. The following sections provide information about Cocoa support for flipped coordinates and some of the issues you may encounter when using flipped coordinate systems. Wherever possible, these sections also offer guidance on how to solve issues that arise due to flipped coordinate systems.
50
CHAPTER 3
Override your views isFlipped method and return YES. Apply a flip transform to your content immediately prior to rendering.
If you plan to draw all of your views content using flipped coordinates, overriding the views isFlipped method is by far the preferred option. Overriding this method lets Cocoa know that your view wants to use flipped coordinates by default. When a views isFlipped method returns YES, Cocoa automatically makes several adjustments for you. The most noticeable change is that Cocoa adds the appropriate conversion transform to the CTM before calling your views drawRect: method. This behavior eliminates the need for your drawing code to apply a flip transform manually. In addition, many Cocoa objects automatically adjust their drawing code to account for the coordinate system of the current view. For example, the NSFont object automatically takes the orientation of the coordinate system into account when setting the current font. This prevents text from appearing upside down when drawn in your view. If you draw only a subset of your views content using flipped coordinates, you can use a flip transform (instead of overriding isFlipped) to modify the coordinate system manually. A flip transform lets you adjust the current coordinate system temporarily and then undo that adjustment when it is no longer needed. You would apply this transform to your views coordinate system immediately prior to drawing the relevant flipped content. For information on how to create a flip transform, see Creating a Flip Transform (page 54).
51
CHAPTER 3
Drawing Images
When rendering images in your custom views, you must pay attention to the relative orientation of your view and any images you draw in that view. If you draw an image in a flipped view using the drawInRect:fromRect:operation:fraction: method, your image would appear upside down in your view. You could fix this problem using one of several techniques:
You could apply a flip transform immediately prior to drawing the image; see Creating a Flip Transform (page 54). You could use one of the compositeToPoint methods of NSImage to do the drawing. You could invert the image data itself. (Although a suitable fix, this is usually not very practical.)
Using a flip transform to negate the effects of a flipped view ensures that your image contents are rendered correctly in all cases. This technique retains any previous transformations to the coordinate system, including scales and rotations, but removes the inversion caused by the view being flipped. You should especially use this technique if you needed to draw your image using the drawInRect:fromRect:operation:fraction: method of NSImage. This method lets you scale your image to fit the destination rectangle and is one of the more commonly used drawing methods for images. Although the compositeToPoint methods of NSImage provide you with a way to orient images properly without a flip transform, their use is not recommended. There are some side effects that make drawing with these methods more complicated. The compositeToPoint methods work by removing any custom scaling or rotation factors that you applied to the CTM. These methods also remove any scaling (but not translations) applied by any flip transforms, whether the transform was supplied by you or by Cocoa. (The methods also do not remove the scale factor in effect from resolution independence.) Any custom translation factors you applied to the CTM are retained, however. Although this behavior is designed to ensure that images are not clipped by your views bounding rectangle, if you do not compensate for the flip transforms translation factor, clipping may still occur. Figure 3-9 shows what happens when you render an image in an unflipped view, and then in a flipped view, using the compositeToPoint:fromRect:operation: method. In the unflipped view, the image renders as expected at the specified point in the view. In the flipped view, the scale factor for the y-axis is removed but the translation factor is not, which results in the image being clipped because it appears partially outside the views visible bounds. To compensate, you would need to adjust the y-origin of the image by subtracting the original value from the view height to get the adjusted position.
52
CHAPTER 3
Figure 3-9
(0, 100)
(0, 0)
(0, 0)
(0, 0)
(0, 100)
(0, 100)
The issues related to the drawing of images in a flipped coordinate system are essentially independent of how you create those images in the first place. Images use a separate coordinate system internally to orient the image data. Whether you load the image data from an existing file or create the image by locking focus on it, once the image data is loaded or you unlock focus, the image data is set. At that point, you must choose the appropriate drawing method or adjust the coordinate system yourself prior to drawing to correct for flipped orientation issues. Important: Although the setFlipped: method of NSImage might seem like a good way to change the orientation of an image after the fact, that is not actually the case. The setFlipped: method is there to let you specify the orientation of the image data before you issue a lockFocus call and draw into the image. Using that method to correct for flipped coordinate systems during drawing might seem to work at times, but it is not a reliable way to flip images and its use in that capacity is highly discouraged. For more information about images and their internal coordinate systems, see Image Coordinate Systems (page 99). Important: Regardless of whether the contents of the image are flipped or unflipped, you always specify the location and size of the image using the coordinate system of the current context.
Drawing Text
The text rendering facilities in Cocoa take their cues for text orientation from the current view. If your views isFlipped method returns YES, Cocoa automatically inverts the text drawn in that view to compensate for its flipped coordinate system. If you apply a flip transform manually from your drawing code, however, Cocoa does not know to compensate when drawing text. Any text you render after applying a flip transform manually therefore appears upside down in your view. These rules apply whether you are using the Cocoa text system or the drawing facilities of NSString to draw your text. If you lock focus on an image and draw some text into it, Cocoa uses the internal coordinate system of the NSImage object to determine the correct orientation for the text. As with other image content, if you subsequently render the image in a flipped view, the text you drew is flipped along with the rest of the image data.
53
CHAPTER 3
For more information about working with text, see Text (page 121).
- (void)drawRect:(NSRect)rect { NSRect frameRect = [self bounds]; NSAffineTransform* xform = [NSAffineTransform transform]; [xform translateXBy:0.0 yBy:frameRect.size.height]; [xform scaleXBy:1.0 yBy:-1.0]; [xform concat]; // Draw flipped content. }
The flip transform merely toggles the orientation of the current coordinate system. If your view already draws using flipped coordinates, because its isFlipped method returns YES, applying a flip transform reverts the coordinate system back to the standard orientation.
54
CHAPTER 3
Some Cocoa classes support flipped coordinates but do not use them all the time. The following list includes the known cases where flipped-coordinate support depends on other mitigating factors.
Images do not use flipped coordinates by default; however, you can flip the images internal coordinate system manually using the setFlipped: method of NSImage. All representations of an NSImage object use the same orientation. For more information about images and flipped coordinates, see Image Coordinate Systems (page 99). The Cocoa text system takes cues from the current context to determine whether text should be flipped. If the text is to be displayed in an NSTextView object, text system objects (such as NSFont) also uses flipped coordinates to ensure that text is rendered right-side up. If you are drawing text in a custom view that uses standard coordinate, the text system objects do not use flipped coordinates. An NSClipView object determines whether to use flipped coordinates by looking at the coordinate system of its document view. If the document view uses flipped coordinates, so does the clip view. Using the same coordinate system ensures that the scroll origin matches the bounds origin of the document view. Graphics convenience functions, such as those declared in NSGraphics.h, take flipped coordinate systems into account when drawing. For information about the available graphics convenience functions, see Application Kit Functions Reference.
As new controls and views are introduced in Cocoa, those objects may also support flipped coordinates. Check the class reference documentation for any subclassing notes on whether a class supports flipped coordinates. You can also invoke the views isFlipped method at runtime to determine if it uses flipped coordinates.
55
CHAPTER 3
Use high-resolution images. During layout, make sure views and images are positioned on integral pixel boundaries. When creating tiled background images for custom controls, use the NSDrawThreePartImage and NSDrawNinePartImage methods to draw your background rather than trying to draw it yourself. Use antialiased text rendering modes for non-integral scale factors and be sure to lay out your text views on pixel boundaries. Test your applications with non-integral scale factors such as 1.25 and 1.5. These factors tend to generate odd numbers of pixels, which can reveal potential pixel cracks.
If you are using OpenGL for drawing, you should also be aware that in Mac OS X v10.5, the bounding rectangle of a view drawn into an NSOpenGLContext is measured in pixels and not in points (as it is in non OpenGL situations). This support may change in the future, however, so OpenGL developers should be sure to convert coordinates directly using the coordinate conversion methods of NSView. For example, the following conversion code for a view object is guaranteed to return the correct values needed by OpenGL.
NSSize boundsInPixelUnits = [self convertRect:[self bounds] toView:nil]; glViewport(0, 0, boundsInPixelUnits.size.width, boundsInPixelUnits.size.height);
For more information about resolution independence and how it affects rendered content, see Resolution Independence Guidelines.
56
CHAPTER 3
The best way to get the correct device-space rectangle is to use the centerScanRect: method of NSView. This method takes a rectangle in user space coordinates, performs the needed calculations to adjust the position of rectangles based on the current scale factor and device, and returns the resulting user space rectangle. For layout, you can also use the methods described in Converting Coordinate Values (page 57). If you want more control over the precise layout of items in device space, you can also adjust coordinates yourself. Mac OS X provides several functions for normalizing coordinate values once they are in device space, including the NSIntegralRect and CGRectIntegral functions. You can also use the ceil and floor functions in math.h to round device space coordinates up or down as needed.
57
CHAPTER 3
convertSizeFromBase: convertRectFromBase:
These convenience methods make it possible to convert values to and from the base (device) coordinate system. They take into account the current backing store configuration for the view, including whether it is backed by a layer. To change the coordinate values of an NSPoint structure, the beginning of your views drawRect: method might have code similar to the following:
- (void)drawRect:(NSRect)rect { NSPoint myPoint = NSMakePoint(1.0, 2.0); CGFloat scaleFactor = [[self window] userSpaceScaleFactor]; if (scaleFactor != 1.0) { NSPoint tempPoint = [self convertPointToBase:myPoint]; tempPoint.x = floor(tempPoint.x); tempPoint.y = floor(tempPoint.y); myPoint = [self convertPointFromBase:tempPoint]; } // Draw the content at myPoint }
It is up to you to determine which normalization function is best suited for your drawing code. The preceding example uses the floor function to normalize the origin of the given shape but you might use a combination of floor and ceil depending on the position of other content in your view.
58
CHAPTER 4
One of the keys to creating interesting graphics is the effective use of color and transparency. In Mac OS X, both are used to convey information and provide an inherent appeal to your creations. Good color usage usually results in an interface that is pleasing to the user and helps call out information when it is needed.
In Cocoa, the NSColorSpace class handles the information associated with a particular color space. You can create instances of this class to represent individual color spaces. Cocoa provides methods for retrieving color space objects representing the standard color spaces. You can also create custom color space objects using a ColorSync profile reference or International Color Consortium (ICC) profile data.
59
CHAPTER 4
For detailed information about color spaces and color models in Mac OS X, see Color Management Overview.
Color Objects
The NSColor class in Cocoa provides the interface you need to create and manage individual colors. The NSColor class is itself a factory class for creating the actual color objects. The class methods of NSColor create color objects that are actually based on specific subclasses of NSColor, where each subclass implements the behavior for a specific color space. Because a color object must represent a single color space, you cannot use all of the methods of NSColor from the same object. For a given color object, you can use only the methods that are relevant to colors in that objects color space. For example, if you create an CMYK-based color object, you cannot use the getRed:green:blue:alpha: method to retrieve RGB values. Methods that are unsupported in the current color space raise an exception. For more information on how to create and use colors, see Creating Colors (page 62).
Transparency
In addition to the component values used to identify a particular color in a color space, Mac OS X colors also support an alpha component for identifying the transparency of that color. Transparency is a powerful effect used to give the illusion of light passing through a particular area instead of reflecting off of it. When you render an object using a partially transparent color, the object picks up some color from the object directly underneath it. The amount of color it picks up depends on the value of the colors alpha component and the compositing mode. Like color components, the alpha component is specified as a floating-point value in the range 0.0 to 1.0. You can think of the alpha component as specifying the amount of light being reflected back from the objects surface. An alpha value of 1.0 represents a 100% reflection of all light and is equivalent to the object being opaque. An alpha value of 0.0 represents 0% reflection of light and all color coming from the content underneath. An alpha value of 0.5 represents 50% reflection, with half the color being reflected off the object and half coming from the content underneath.
60
CHAPTER 4
You specify transparency when you create a color object. If you create a color using component values, you can specify an alpha value directly. If you have an existing color object, you can use the colorWithAlphaComponent: method to create a new color object with the same color components as the original but with the alpha value you specify.
Pattern Colors
In addition to creating monochromatic colors, you can also create pattern colors using images. Pattern colors are most applicable as fill colors but can be used as stroke colors as well. When rendered, the image you specify is drawn on the path or its fill area instead of a solid color. If an image is too small to fill the given area, it is tiled vertically and horizontally, as shown in Figure 4-1. Figure 4-1 Drawing with a pattern
For information on how to create pattern colors, see Creating Colors (page 62).
Color Lists
A color list is a dictionary-like object (implemented by the NSColorList class) that contains an ordered list of NSColor objects, identified by keys. You can retrieve colors from the color list by key. You can also organize the colors by placing them at specific indexes in the list. Color lists are available as a tool to help you manage any document-specific colors. They are also used to customize the list of colors displayed in a color panel. You can use the attachColorList: method of NSColorPanel to add any colors your application uses to the panel. For more information about using color lists and color panels, see Color Programming Topics.
61
CHAPTER 4
Color Matching
Cocoa provides automatic color matching whenever possible using ColorSync. Color matching ensures that the colors you use to draw your content look the same on different devices. Cocoa provides full support for creating and getting color profile information using the NSColorSpace class. Cocoa supports both ColorSync profile references and ICC profiles as well as calibrated and device-specific profiles for the RGB, CMYK, and gray color spaces. Because color matching is automatic, there is nothing to do in your code except use the colors you want. For information about ColorSync, see ColorSync Manager Reference. For information on ICC profiles, see the International Color Consortium website: http://www.color.org/.
Creating Colors
The NSColor class supports the creation of several different types of color objects:
Commonly used colors, such as red, green, black, or white System colors, such as the current control color or highlight color, whose values are set by user preferences Calibrated colors belonging to a designated color space Device colors belonging to the designated device color space Pattern colors
To create most color objects, simply use the appropriate class method of NSColor. The class defines methods for creating preset colors or for creating colors with the values you specify. To create a pattern color, load or create the desired image and pass it to the colorWithPatternImage: method of NSColor. For more information, see the NSColor class reference. For information on how to load and create images, see Images (page 93). Important: Never allocate and initialize an NSColor object directly. Because it is a base class, the implementation of NSColor is minimal and simply throws an exception in situations where actual color information is required. In Mac OS X v10.5 and later, Cocoa provides support for gradient fill patterns through the NSGradient class. Prior to version 10.5, if you want to use a gradient to fill or stroke a path, you must use Quartz instead. For examples of how to create and use gradients, see Creating Gradient Fills (page 124).
62
Creating Colors
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 4
Sets both the stroke and fill color to the same value. Sets the fill color. Sets the stroke color.
For example, the following code sets the stroke color to black and the fill color to the background color for controls.
[[NSColor blackColor] setStroke]; [[NSColor controlBackgroundColor] setFill];
All subsequent drawing operations in the current context would use the specified colors. If you do not want any color to be drawn for the stroke or fill, you can set the current stroke or fill to a completely transparent color, which you can get by calling the clearColor method of NSColor. You can also create a transparent color by setting the alpha of any other color to 0. Note: Stroke and fill colors do not affect the appearance of text. To apply color to text, you must change the attributes associated with the text.
63
CHAPTER 4
The NSColor class also provides methods for accessing individual component values, rather than all of the components together. For more information, see the NSColor class reference. Important: It is a programming error to ask an NSColor object for components that are not defined in the color space of its current color, and making such a request raises an exception. If you need a specific set of components, you should first convert the color to the appropriate color space using the colorUsingColorSpaceName: method. For more information, see Converting Between Color Spaces (page 65).
Choosing Colors
Applications that need to present a color picker interface to the user can use either a color well or a color panel. A color well is a control that displays a single color. You can embed this control in your windows and use it to show a currently selected color. When clicked, a color well displays the system color panel, which provides an interface for picking a color. You can also use the color panel on its own to prompt the user for a color. For information about how to use color wells and the color panel in your application, see Color Programming Topics.
64
CHAPTER 4
Rendering intent
kCGRenderingIntentDefault
kCGRenderingIntentAbsoluteColorimetric
kCGRenderingIntentRelativeColorimetric
kCGRenderingIntentPerceptual
kCGRenderingIntentSaturation
65
CHAPTER 4
To change the rendering intent, you must get a Quartz graphics context for the current drawing environment and call the CGContextSetRenderingIntent function, as shown in the following example:
- (void) drawRect:(NSRect)rect { CGContextRef theCG = [[NSGraphicsContext currentContext] graphicsPort]; // Change the rendering intent. CGContextSetRenderingIntent(theCG, kCGRenderingIntentPerceptual); // Draw your content. }
66
CHAPTER 5
Paths
Cocoa provides support for drawing simple or complex geometric shapes using paths. A path is a collection of points used to create primitive shapes such as lines, arcs, and curves. From these primitives, you can create more complex shapes, such as circles, rectangles, polygons, and complex curved shapes, and paint them. Because they are composed of points (as opposed to a rasterized bitmap), paths are lightweight, fast, and scale to different resolutions without losing precision or quality. The following sections focus primarily on the use of the NSBezierPath class, which provides the main interface for creating and manipulating paths. Cocoa also provides a handful of functions that offer similar behavior for creating and drawing paths but do not require the overhead of creating an object. Those functions are mentioned where appropriate, but for more information, see Foundation Framework Reference and Application Kit Framework Reference.
67
CHAPTER 5
Paths
Path Elements
An NSBezierPath object uses path elements to build a path. A path element consists of a primitive command and one or more points. The command tells the path object how to interpret the associated points. When assembled, a set of path elements creates a series of line segments that form the desired shape. The NSBezierPath class handles much of the work of creating and organizing path elements initially. Knowing how to manipulate path elements becomes important, however, if you want to make changes to an existing path. If you create a complex path based on user input, you might want to give the user the option of changing that path later. Although you could create a new path object with the changes, it is far simpler to modify the existing path elements. (For information on how to modify path elements, see Manipulating Individual Path Elements (page 89).) The NSBezierPath class defines only four basic path element commands, which are listed in Table 5-1. These commands are enough to define all of the possible path shapes. Each command has one or more points that contain information needed to position the path element. Most path elements use the current drawing point as the starting point for drawing. Table 5-1 Command Path element commands Number Description of points 1 Moves the path objects current drawing point to the specified point. This path element does not result in any drawing. Using this command in the middle of a path results in a disconnected line segment. Creates a straight line from the current drawing point to the specified point. Lines and rectangles are specified using this path element. Creates a curved line segment from the current point to the specified endpoint using two control points to define the curve. The points are stored in the following order: controlPoint1, controlPoint2, endPoint. Ovals, arcs, and Bezier curves all use curve elements to specify their geometry. Marks the end of the current subpath at the specified point. (Note that the point specified for the Close Path element is essentially the same as the current point.
NSMoveToBezierPathElement
NSLineToBezierPathElement
NSCurveToBezierPathElement
NSClosePathBezierPathElement
When you add a new shape to a path, NSBezierPath breaks that shape down into one or more component path elements for storage purposes. For example, calling moveToPoint: or lineToPoint: creates a Move To element or Line To element respectively. In the case of more complex shapes, like rectangles and ovals, several line or curve elements may be created. Figure 5-1 shows two shapes and the resulting path elements. For the curved segment, the figure also shows the control points that define the curve.
68
CHAPTER 5
Paths
Figure 5-1
(2, 21)
Elements Shape 1 Move To Line To Curve To (0, 0) (10, 10) (6, 2) (28, 10) (18, 21) (2, 16) (10, 16) (10, 21) (2, 21) (2, 21) (2, 16)
(2, 16)
(10, 16)
Shape 2
(6, 2) (0, 0)
Listing 5-1 shows the code that creates the path shown in Figure 5-1. Listing 5-1 Creating a complex path
NSBezierPath* aPath = [NSBezierPath bezierPath]; [aPath moveToPoint:NSMakePoint(0.0, 0.0)]; [aPath lineToPoint:NSMakePoint(10.0, 10.0)]; [aPath curveToPoint:NSMakePoint(18.0, 21.0) controlPoint1:NSMakePoint(6.0, 2.0) controlPoint2:NSMakePoint(28.0, 10.0)]; [aPath appendBezierPathWithRect:NSMakeRect(2.0, 16.0, 8.0, 5.0)];
Subpaths
A subpath is a series of connected line and curve segments within an NSBezierPath object. A single path object may contain multiple subpaths, with each subpath delineated by a Move To or Close Path element. When you set the initial drawing point (typically using the moveToPoint: method), you set the starting point of the first subpath. As you draw, you build the contents of the subpath until you either close the path (using the closePath method) or add another Move To element. At that point, the subpath is considered closed and any new elements are added to a new subpath. Some methods of NSBezierPath automatically create a new subpath for you. For example, creating a rectangle or oval results in the addition of a Move To element, several drawing elements, and a Close Path and Move To element (see Figure 5-1 (page 69) for an example). The Move To element at the end of the list of elements ensures that the current drawing point is left in a known location, which in this case is at the rectangles origin point.
69
CHAPTER 5
Paths
Subpaths exist to help you distinguish different parts of a path object. For example, subpaths affect the way a path is filled; see Winding Rules (page 75). The division of a path into subpaths also affects methods such as bezierPathByReversingPath, which reverses the subpaths one at a time. In other cases, though, subpaths in an NSBezierPath object share the same drawing attributes.
Path Attributes
An NSBezierPath object maintains all of the attributes needed to determine the shape of its path. These attributes include the line width, curve flatness, line cap style, line join style, and miter limit of the path. You set these values using the methods of NSBezierPath. Path attributes do not take effect until you fill or stroke the path, so if you change an attribute more than once before drawing the path, only the last value is used. The NSBezierPath class maintains both a custom and default version of each attribute. Path objects use custom attribute values if they are set. If no custom attribute value is set for a given path object, the default value is used. The NSBezierPath class does not use path attribute values set using Quartz functions. Note: Path attributes apply to the entire path. If you want to use different attributes for different parts of a path, you must create two separate path objects and apply the appropriate attributes to each. The following sections describe the attributes you can set for a path object and how those attributes affect your rendered paths.
Line Width
The line width attribute controls the width of the entire path. Line width is measured in points and specified as a floating-point value. The default width for all lines is 1. To change the default line width for all NSBezierPath objects, you use the setDefaultLineWidth: method. To set the line width for the current path object, you use the setLineWidth: method of that path object. To set the default line width for shapes rendered without an NSBezierPath object, you must use the CGContextSetLineWidth function in Quartz. Fractional line widths are rendered as close as possible to the specified width, subject to the limitations of the destination device, the position of the line, and the current anti-aliasing setting. For example, suppose you want to draw a line whose width is 0.2 points. Multiplying this width by 1/72 points per inch yields a line that is 0.0027778 inches wide. On a 90 dpi screen, the smallest possible line would be 1 pixel wide or 0.0111 inches. To ensure your line is not hidden on the screen, Cocoa nominally draws it at the screens larger minimum width (0.0111 inches). In reality, if the line straddles a pixel boundary or anti-aliasing is enabled, the line might affect additional pixels on either side of the path. If the output device were a 600 dpi printer instead, Quartz would be able to render the line closer to its true width of 0.0027778 inches. Listing 5-2 draws a few paths using different techniques. The NSFrameRect function uses the default line width to draw a rectangle, so that value must be set prior to calling the function. Path objects use the default value only if a custom value has not been set. You can even change the line width of a path object and draw again to achieve a different path width, although you would also need to move the path to see the difference. Listing 5-2 Setting the line width of a path
// Draw a rectangle using the default line width: 2.0. [NSBezierPath setDefaultLineWidth:2.0]; NSFrameRect(NSMakeRect(20.0, 20.0, 10.0, 10.0));
70
CHAPTER 5
Paths
// Set the line width for a single NSBezierPath object. NSBezierPath* thePath = [NSBezierPath bezierPath]; [thePath setLineWidth:1.0]; // Has no effect. [thePath moveToPoint:NSMakePoint(0.0, 0.0)]; [thePath lineToPoint:NSMakePoint(10.0, 0.0)]; [thePath setLineWidth:3.0]; [thePath lineToPoint:NSMakePoint(10.0, 10.0)]; // Because the last value set is 3.0, all lines are drawn with // a width of 3.0, not just the second line. [thePath stroke]; // Changing the width and stroking again draws the same path // using the new line width. [thePath setLineWidth:4.0]; [thePath stroke]; // Changing the default line width has no effect because a custom // value already exists. The path is rendered with a width of 4.0. [thePath setDefaultLineWidth:5.0]; [thePath stroke];
NSRoundLineCapStyle
NSSquareLineCapStyle
To set the line cap style for a NSBezierPath object, use the setLineCapStyle: method. The default line cap style is set to NSButtLineCapStyle. To change the default line cap style, use the setDefaultLineCapStyle: method. Listing 5-3 demonstrates both of these methods: Listing 5-3 Setting the line cap style of a path
[// Set the default line cap style [NSBezierPath setDefaultLineCapStyle:NSButtLineCapStyle]; // Customize the line cap style for the new object. NSBezierPath* aPath = [NSBezierPath bezierPath]; [aPath moveToPoint:NSMakePoint(0.0, 0.0)]; [aPath lineToPoint:NSMakePoint(10.0, 10.0)]; [aPath setLineCapStyle:NSSquareLineCapStyle]; [aPath stroke];
71
CHAPTER 5
Paths
NSBevelLineJoinStyle
NSMiterLineJoinStyle
NSRoundLineJoinStyle
To set the line join style for an NSBezierPath object, use the setLineJoinStyle: method. The default line join style is set to NSMiterLineJoinStyle. To change the default line join style, use the setDefaultLineJoinStyle: method. Listing 5-4 demonstrates both of these methods: Listing 5-4 Setting the line join style of a path
[// Set the default line join style [NSBezierPath setDefaultLineJoinStyle:NSMiterLineJoinStyle]; // Customize the line join style for a new path. NSBezierPath* aPath = [NSBezierPath bezierPath]; [aPath moveToPoint:NSMakePoint(0.0, 0.0)]; [aPath lineToPoint:NSMakePoint(10.0, 10.0)]; [aPath lineToPoint:NSMakePoint(10.0, 0.0)]; [aPath setLineJoinStyle:NSRoundLineJoinStyle]; [aPath stroke];
72
CHAPTER 5
Paths
Figure 5-4
Pattern: dash 40, gap 12, dash 8, gap 12, dash 8, gap 12
The NSBezierPath class does not support the concept of a default line dash style. If you want a line dash style, you must apply it to a path explicitly using the setLineDash:count:phase: method as shown in Listing 5-5, which renders the last pattern from the preceding figure. Listing 5-5 Adding a dash style to a path
void AddDashStyleToPath(NSBezierPath* thePath) { // Set the line dash pattern. float lineDash[6]; lineDash[0] lineDash[1] lineDash[2] lineDash[3] lineDash[4] lineDash[5] = = = = = = 40.0; 12.0; 8.0; 12.0; 8.0; 12.0;
Line Flatness
The line flatness attribute determines the rendering accuracy for curved segments. The flatness value measures the maximum error tolerance (in pixels) to use during rendering. Smaller values result in smoother curves but require more computation time. Larger values result in more jagged curves but are rendered much faster. Line flatness is one parameter you can tweak when you want to render a large number of curves quickly and do not care about accuracy. For example, you might increase this value during a live resize or scrolling operation when accuracy is not as crucial. Regardless, you should always measure performance to make sure such a modification actually saves time. Figure 5-5 shows how changing the default flatness affects curved surfaces. The figure on the left shows a group of curved surfaces rendered with the flatness value set to 0.6 (its default value). The figure on the right shows the same curved surfaces rendered with the flatness value set to 20. The curvature of each surface is lost and now appears to be a set of connected line segments.
73
CHAPTER 5
Paths
Figure 5-5
Flatness: 0.6
Flatness: 20.0
To set the flatness for a specific NSBezierPath object, use the setFlatness: method. To set the default flatness value, use setDefaultFlatness:, as shown in Listing 5-6: Listing 5-6 Setting the flatness of a path
[- (void) drawRect:(NSRect)rect { if ([self inLiveResize]) { // Adjust the default flatness upward to reduce // the number of required computations. [NSBezierPath setDefaultFlatness:10.0]; // Draw live resize content. } // ... }
Miter Limits
Miter limits help you avoid spikes that occur when you join two line segments at a sharp angle. If the ratio of the miter lengththe diagonal length of the miterto the line thickness exceeds the miter limit, the corner is drawn using a bevel join instead of a miter join. Note: Miter limits apply only to paths rendered using the miter join style. Figure 5-6 shows an example of how different miter limits affect the same path. This path consists of several 10-point wide lines connected by miter joins. In the figure on the left, the miter limit is set to 5. Because the miter lengths exceed the miter limit, the line joins are changed to bevel joins. By increasing the miter limit to 16, as shown in the figure on the right, the miter joins are restored but extend far beyond the point where the two lines meet.
74
CHAPTER 5
Paths
Figure 5-6
Miter limit: 5
Miter limit: 16
To set the miter limits for a specific NSBezierPath object, use the setMiterLimit: method. To set the default miter limit for newly created NSBezierPath objects, use setDefaultMiterLimit:. Listing 5-7 demonstrates both of these methods: Listing 5-7 Setting the miter limit for a path
// Increase the default limit [NSBezierPath setDefaultMiterLimit:20.0]; // Customize the limit for a specific path with sharp angles. NSBezierPath* aPath = [NSBezierPath bezierPath]; [aPath moveToPoint:NSMakePoint(0.0, 0.0)]; [aPath lineToPoint:NSMakePoint(8.0, 100.0)]; [aPath lineToPoint:NSMakePoint(16.0, 0.0)]; [aPath setLineWidth:5.0]; [aPath setMiterLimit:5.0]; [aPath stroke];
Winding Rules
When you fill the area encompassed by a path, NSBezierPath applies the current winding rule to determine which areas of the screen to fill. A winding rule is simply an algorithm that tracks information about each contiguous region that makes up the path's overall fill area. A ray is drawn from a point inside a given region to any point outside the path bounds. The total number of crossed path lines (including implicit lines) and the direction of each path line are then interpreted using the rules in Table 5-2, which determine if the region should be filled.
75
CHAPTER 5
Paths
NSNonZeroWindingRule Count each left-to-right path as +1 and each right-to-left path as -1. If the sum
of all crossings is 0, the point is outside the path. If the sum is nonzero, the point is inside the path and the region containing it is filled. This is the default winding rule.
NSEvenOddWindingRule Count the total number of path crossings. If the number of crossings is even,
the point is outside the path. If the number of crossings is odd, the point is inside the path and the region containing it should be filled. Fill operations are suitable for use with both open and closed subpaths. A closed subpath is a sequence of drawing calls that ends with a Close Path path element. An open subpath ends with a Move To path element. When you fill a partial subpath, NSBezierPath closes it for you automatically by creating an implicit (non-rendered) line from the first to the last point of the subpath. Figure 5-7 shows how the winding rules are applied to a particular path. Subfigure a shows the path rendered using the nonzero rule and subfigure b shows it rendered using the even-odd rule. Subfigures c and d add direction marks and the hidden path line that closes the figure to help you see how the rules are applied to two of the paths regions. Figure 5-7 Applying winding rules to a path
Even-odd winding rule
a)
b)
Start
End
Start
End
c)
d)
76
CHAPTER 5
Paths
To set the winding rule for an NSBezierPath object, use the setWindingRule: method. The default winding rule is NSNonZeroWindingRule. To change the default winding rule for all NSBezierPath objects, use the setDefaultWindingRule: method.
Returns a properly formatted NSSize data structure with the specified width and height. Returns a properly formatted NSRect data structure with the specified origin (x, y) and size (width, height).
NSRect NSMakeRect(x, y, w, h)
Equality
BOOL NSEqualPoints(p1, p2) Returns YES if the two points are the same. BOOL NSEqualSizes(s1, s2) Returns YES if the two size types have identical widths
and heights.
BOOL NSEqualRects(r1, r2) Returns YES, if the two rectangles have the same origins
and the same widths and heights. Rectangle BOOL NSContainsRect(r1, manipulations r2) Returns YES if rectangle 1 completely encloses rectangle 2.
NSRect NSInsetRect(r, dX, Returns a copy of the specified rectangle with its sides moved inward by the specified delta values. Negative dY)
delta values move the sides outward. Does not modify the original rectangle.
NSRect Returns the intersection of the two rectangles. NSIntersectionRect(r1, r2) NSRect NSUnionRect(r1, r2) Returns the union of the two rectangles. BOOL NSMouseInRect(p, r, flipped)
Tests whether the point lies within the specified view rectangle. Adjusts the hit-detection algorithm to provide consistent behavior from the users perspective.
77
CHAPTER 5
Paths
Operation
Function
BOOL NSPointInRect(p, r)
Description Tests whether the point lies within the specified rectangle. This is a basic mathematical comparison.
Because paths are specified mathematically, they scale easily to different resolutions. Thus, the same path objects can be used for screen and print-based drawing. The geometry information associated with a path requires much less storage space than most image data formats. Rendering paths is often faster than compositing a comparable image. It takes less time to transfer path data to the graphics hardware than it takes to transfer the texture data associated with an image.
The following sections provide information about the primitive shapes you can draw using paths. You can combine one or more of these shapes to create a more complex path and then stroke or fill the path as described in Drawing the Shapes in a Path (page 85). For some shapes, there may be more than one way to add the shape to a path, or there may be alternate ways to draw the shape immediately. Wherever possible, the benefits and disadvantages of each technique are listed to help you decide which technique is most appropriate in specific situations.
Adding Points
An NSPoint structure by itself represents a location on the screen; it has no weight and cannot be drawn as such. To draw the equivalent of a point on the screen, you would need to create a small rectangle at the desired location, as shown in Listing 5-8. Listing 5-8 Drawing a point
void DrawPoint(NSPoint aPoint) { NSRect aRect = NSMakeRect(aPoint.x, aPoint.y, 1.0, 1.0); NSRectFill(aRect); }
Of course, a more common use for points is to specify the position of other shapes. Many shapes require you to specify the current point before actually creating the shape. You set the current point using the moveToPoint: or relativeMoveToPoint: methods. Some shapes, like rectangles and ovals, already contain location information and do not require a separate call to moveToPoint:.
78
CHAPTER 5
Paths
Important: You must specify a starting point before drawing individual line, arc, curve, and glyph paths. If you do not, NSBezierPath raises an exception.
Create single horizontal and vertical lines by filling a rectangle using NSRectFill. This technique is less precise but is often a little faster than creating an NSBezierPath object. To create diagonal lines using this technique, you must apply a rotation transform before drawing. This technique is not appropriate for creating connected line segments. Use the lineToPoint:, relativeLineToPoint:, or strokeLineFromPoint:toPoint: methods of NSBezierPath to create individual or connected line segments. This technique is fast and is the most precise option for creating lines and complex polygons. Use the appendBezierPathWithPoints:count: method to create a series of connected lines quickly. This technique is faster than adding individual lines.
Polygons are composed of multiple connected lines and should be created using an NSBezierPath object. The simplest way to create a four-sided nonrectangular shape, like a parallelogram, rhombus, or trapezoid, is using line segments. You could also create these shapes using transforms, but calculating the correct skew factors would require a lot more work. Listing 5-9 shows code to draw a parallelogram using NSBezierPath. The method in this example inscribes the parallelogram inside the specified rectangle. The withShift parameter specifies the horizontal shift applied to the top left and bottom right corners of the rectangular area. Listing 5-9 Using lines to draw a polygon
void DrawParallelogramInRect(NSRect rect, float withShift) { NSBezierPath* thePath = [NSBezierPath bezierPath]; [thePath moveToPoint:rect.origin]; [thePath lineToPoint:NSMakePoint(rect.origin.x - withShift, rect.origin.y)]; [thePath lineToPoint:NSMakePoint(NSMaxX(rect), NSMaxY(rect))]; [thePath lineToPoint:NSMakePoint(rect.origin.x + withShift, NSMaxY(rect))]; [thePath closePath]; [thePath stroke]; }
Adding Rectangles
Because rectangles are used frequently, there are several options for drawing them.
Use the methods of NSBezierPath to create your rectangle. The following methods are reasonably fast and offer the best precision:
79
CHAPTER 5
Paths
Create rectangles using the Cocoa functions described in Drawing Rectangles (page 86). These functions draw rectangles faster than, but with less precision than, the methods of NSBezierPath. Create a rectangle using individual lines as described in Adding Lines and Polygons (page 79). You could use this technique to create diagonally oriented rectanglesthat is, rectangles whose sides are not parallel to the x and y axeswithout using a rotation transform.
Listing 5-10 shows a simple function that fills and strokes the same rectangle using two different techniques. The current fill and stroke colors are used when drawing the rectangle, along with the default compositing operation. In both cases, the rectangles are drawn immediately; there is no need to send a separate fill or stroke message. Listing 5-10 Drawing a rectangle
bezierPathWithRoundedRect:xRadius:yRadius: appendBezierPathWithRoundedRect:xRadius:yRadius:
These methods create rectangles whose corners are curved according to the specified radius values. The radii describe the width and height of the oval to use at each corner of the rectangle. Figure 5-8 shows how this inscribed oval is used to define the path of the rectangles corner segments.
80
CHAPTER 5
Paths
Figure 5-8
y x
Listing 5-11 shows a code snippet that creates and draws a path with a rounded rectangle. Listing 5-11 Drawing a rounded rectangle
void DrawRoundedRect(NSRect rect, CGFloat x, CGFloat y) { NSBezierPath* thePath = [NSBezierPath bezierPath]; [thePath appendBezierPathWithRoundedRect:rect xRadius:x yRadius:y]; [thePath stroke]; }
bezierPathWithOvalInRect: appendBezierPathWithOvalInRect:
Both methods inscribe an oval inside the rectangle you specify. You must then fill or stroke the path object to draw the oval in the current context. The following example creates an oval from the specified rectangle and strokes its path.
void DrawOvalInRect(NSRect ovalRect) { NSBezierPath* thePath = [NSBezierPath bezierPath]; [thePath appendBezierPathWithOvalInRect:ovalRect]; [thePath stroke]; }
You could also create an oval using arcs, but doing so would duplicate what the preceding methods do internally and would be a little slower. The only reason to add individual arcs is to create a partial (non-closed) oval path. For more information, see Adding Arcs (page 82).
81
CHAPTER 5
Paths
Adding Arcs
To draw arcs, use the following methods of NSBezierPath:
The appendBezierPathWithArcFromPoint:toPoint:radius: method creates arcs by inscribing them in an angle formed by the current point and the two points passed to the method. Inscribing a circle in this manner can result in an arc that does not intersect any of the points used to specify it. It can also result in the creation of an unwanted line from the current point to the starting point of the arc. Figure 5-9 shows three different arcs and the control points used to create them. For the two arcs created using appendBezierPathWithArcFromPoint:toPoint:radius:, the current point must be set before calling the method. In both examples, the point is set to (30, 30). Because the radius of the second arc is shorter, and the starting point of the arc is not the same as the current point, a line is drawn from the current point to the starting point.
82
CHAPTER 5
Paths
Figure 5-9
(0,60)
Creating arcs
r=
30
(0,30)
(30,30)
(30,40) (30,30)
r=
(70,30)
20
(30,30)
Listing 5-12 shows the code snippets you would use to create each of the arcs from Figure 5-9. (Although the figure shows the arcs individually, executing the following code would render the arcs on top of each other. ) Listing 5-12 Creating three arcs
arcPath1 = [NSBezierPath bezierPath]; arcPath2 = [NSBezierPath bezierPath];
NSBezierPath* NSBezierPath*
[[NSColor blackColor] setStroke]; // Create the first arc [arcPath1 moveToPoint:NSMakePoint(30,30)]; [arcPath1 appendBezierPathWithArcFromPoint:NSMakePoint(0,30) toPoint:NSMakePoint(0,60) radius:30]; [arcPath1 stroke]; // Create the second arc. [arcPath2 moveToPoint:NSMakePoint(30,30)];
r = 30
83
CHAPTER 5
Paths
[arcPath2 appendBezierPathWithArcFromPoint:NSMakePoint(30,40) toPoint:NSMakePoint(70,30) radius:20]; [arcPath2 stroke]; // Clear the old arc and do not set an initial point, which prevents a // line being drawn from the current point to the start of the arc. [arcPath2 removeAllPoints]; [arcPath2 appendBezierPathWithArcWithCenter:NSMakePoint(30,30) radius:30 startAngle:45 endAngle:135]; [arcPath2 stroke];
Control point 1
Control point 2
Adding Text
Because NSBezierPath only supports path-based content, you cannot add text characters directly to a path; instead, you must add glyphs. A glyph is the visual representation of a character (or partial character) in a particular font. For glyphs in an outline font, this visual representation is stored as a set of mathematical paths that can be added to an NSBezierPath object.
84
CHAPTER 5
Paths
Note: Using NSBezierPath is not the most efficient way to render text, but can be used in situations where you need the path information associated with the text. To obtain a set of glyphs, you can use the Cocoa text system or the NSFont class. Getting glyphs from the Cocoa text system is usually easier because you can get glyphs for an arbitrary string of characters, whereas using NSFont requires you to know the names of individual glyphs. To get glyphs from the Cocoa text system, you must do the following: 1. 2. Create the text system objects needed to manage text layout. For a tutorial on how to do this, see Assembling the Text System by Hand in Text System Overview. Use the glyphAtIndex: or getGlyphs:range: method of NSLayoutManager to retrieve the desired glyphs. Add the glyphs to your NSBezierPath object using one of the following methods:
3.
appendBezierPathWithGlyph:inFont: appendBezierPathWithGlyphs:count:inFont:
When added to your NSBezierPath object, glyphs are converted to a series of path elements. These path elements simply specify lines and curves and do not retain any information about the characters themselves. You can manipulate paths containing glyphs just like you would any other path by changing the points of a path element or by modifying the path attributes.
Path
85
CHAPTER 5
Paths
Drawing Rectangles
Cocoa provides several functions for drawing rectangles to the current context immediately using the default attributes. These functions use Quartz primitives to draw one or more rectangles quickly, but in a way that may be less precise than if you were to use NSBezierPath. For example, these routines do not apply the current join style to the corners of a framed rectangle. Table 5-4 lists some of the more commonly used functions for drawing rectangles along with their behaviors. You can use these functions in places where speed is more important than precision. The syntax for each function is provided in a shorthand notation, with parameter types omitted to demonstrate the calling conventions. For a complete list of available functions, and their full syntax, see Application Kit Functions Reference. Table 5-4 Function
void NSEraseRect(aRect) void NSFrameRect(aRect)
Rectangle frame and fill functions Description Fills the specified rectangle with white. Draws the frame of the rectangle using the current fill color, the default line width, and the NSCompositeCopy compositing operation.
void NSFrameRectWithWidth(aRect, Draws the frame of the rectangle using the current fill color, the specified width, and the NSCompositeCopy compositing width)
operation.
void NSFrameRectWithWidthDraws the frame of the rectangle using the current fill color, UsingOperation(aRect, width, op) the specified width, and the specified operation. void NSRectFill(aRect)
Fills the rectangle using the current fill color and the NSCompositeCopy compositing operation. Fills the rectangle using the current fill color and specified compositing operation. Fills the C-style array of rectangles using the current fill color and the NSCompositeCopy compositing operation.
void NSRectFillListFills the C-style array of rectangles using the corresponding list WithColors(rects, colors, count) of colors. Each list must have the same number of entries. void NSRectFillListUsingOperation(rects, count, op) void NSRectFillListWithColorsUsingOperation(rects, colors, count, op)
Fills the C-style array of rectangles using the current fill color and the specified compositing operation. Fills the C-style array of rectangles using the corresponding list of colors and the specified compositing operation. The list of rectangles and list of colors must contain the same number of items.
86
Drawing Rectangles
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 5
Paths
Important: You may have noticed that the NSFrameRect, NSFrameRectWithWidth, and NSFrameRectWithWidthUsingOperation functions draw the rectangle using the fill color instead of the stroke color. These methods draw the rectangles frame by filling four sub-rectangles, one for each side of the rectangle. This differs from the way NSBezierPath draws rectangles and can sometimes lead to confusion. If your rectangle does not show up the way you expected, check your code to make sure you are setting the drawing color using either the set or setFill method of NSColor.
Building Paths
Building a path involves creating an NSBezierPath object and adding path elements to it. All paths must start with a Move To element to mark the first point of the path. In some cases, this element is added for you but in others you must add it yourself. For example, methods that create a closed path (such as an oval or rectangle) insert a MoveTo element for you. A single NSBezierPath object may have multiple subpaths. Each subpath is itself a complete path, meaning the subpath may not appear connected to any other subpaths when drawn. Filled subpaths can still interact with each other, however. Overlapping subpaths may cancel each others fill effect, resulting in holes in the fill area. All subpaths in an NSBezierPath object share the same drawing attributes. The only way to assign different attributes to different paths is to create different NSBezierPath objects for each.
87
CHAPTER 5
Paths
Note: As with any determination of performance, you should measure the speed of your drawing operations before making any changes. If the amount of time spent inside the methods of NSBezierPath becomes significant, simplifying your paths might offer better performance. Limiting the total amount of drawing you do during an update cycle might also improve performance.
Use the available update rectangles to draw only what has changed. Use different NSBezierPath objects for each part of the screen rather than one large object that covers everything. For more information, see Reduce Path Complexity (page 88). During scrolling, live resizing, or other time-critical operations, consider the following options:
If your screen contains animated content, pause the animation until the operation is complete. Try temporarily increasing the flatness value for curved paths. The default flatness value is set to 0.6, which results in nice smooth curves. Increasing this value above 1.0 may make your curves look more jagged but should improve performance. You may want to try a few different values to determine a good tradeoff between appearance and speed. Disable anti-aliasing. For more information, see Setting the Anti-aliasing Options (page 36).
When drawing rectangles, use NSFrameRect and NSRectFill for operations where the highest quality is not required. These functions offer close approximations to what you would get with NSBezierPath but are often a little faster.
88
CHAPTER 5
Paths
- (void)replaceLastControlPointWithPoint:(NSPoint)newControl inPath:(NSBezierPath*)thePath { int elemCount = [thePath elementCount]; NSBezierPathElement elemType = [thePath elementAtIndex:(elemCount - 1)]; if (elemType != NSCurveToBezierPathElement) return; // Get the current points for the curve. NSPoint points[3]; [thePath elementAtIndex:(elemCount - 1) associatedPoints:points]; // Replace the old control point. points[1] = newControl; // Update the points. [thePath setAssociatedPoints:points atIndex:(elemCount - 1)]; }
Transforming a Path
The coordinate system of an NSBezierPath object always matches the coordinate system of the view in which it is drawn. Thus, given a path whose first point is at (0, 0) in your NSBezierPath object, drawing the path in your view places that point at (0, 0) in the views current coordinate system. To draw that path in a different location, you must apply a transform in one of two ways:
89
CHAPTER 5
Paths
Apply the transform to the view coordinate system and then draw the path. For information on how to apply transforms to a view, see Creating and Applying a Transform (page 47). Apply the transform to the NSBezierPath object itself using the transformUsingAffineTransform: method and then draw it in an unmodified view.
Both techniques cause the path to be drawn at the same location in the view; however, the second technique also has the side effect of permanently modifying the NSBezierPath object. Depending on your content, this may or may not be appropriate. For example, in an illustration program, you might want the user to be able to drag shapes around the view; therefore, you would want to modify the NSBezierPath object to retain the new position of the path.
@implementation NSBezierPath (BezierPathQuartzUtilities) // This method works only in Mac OS X v10.2 and later. - (CGPathRef)quartzPath { int i, numElements; // Need to begin a path here. CGPathRef immutablePath = NULL; // Then draw the path elements. numElements = [self elementCount]; if (numElements > 0) { CGMutablePathRef path = CGPathCreateMutable(); NSPoint points[3]; BOOL didClosePath = YES; for (i = 0; i < numElements; i++) { switch ([self elementAtIndex:i associatedPoints:points]) { case NSMoveToBezierPathElement: CGPathMoveToPoint(path, NULL, points[0].x, points[0].y); break;
90
CHAPTER 5
Paths
case NSLineToBezierPathElement: CGPathAddLineToPoint(path, NULL, points[0].x, points[0].y); didClosePath = NO; break; case NSCurveToBezierPathElement: CGPathAddCurveToPoint(path, NULL, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y); didClosePath = NO; break; case NSClosePathBezierPathElement: CGPathCloseSubpath(path); didClosePath = YES; break; } } // Be sure the path is closed or Quartz may not do valid hit detection. if (!didClosePath) CGPathCloseSubpath(path); immutablePath = CGPathCreateCopy(path); CGPathRelease(path); } return immutablePath; } @end
The code from the preceding example closes only the last open path by default. Depending on your path objects, you might also want to close intermediate subpaths whenever a new Move To element is encountered. If your path objects typically contain only one path, you do not need to do so, however.
91
CHAPTER 5
Paths
Important: Quartz considers a point to be inside a path only if the path is explicitly closed. If you are converting your NSBezierPath objects to Quartz paths for use in hit detection, be sure to close any open subpaths either prior to or during the conversion. If you do not, points lying inside your path may not be correctly identified as such. Listing 5-15 shows an example of how you might perform advanced hit detection on an NSBezierPath object. This example adds a method to the NSBezierPath class using a category. The implementation of the method adds a CGPathRef version of the current path to the current context and calls the CGContextPathContainsPoint function. This function uses the specified mode to analyze the location of the specified point relative to the current path and returns an appropriate value. Modes can include kCGPathFill, kCGPathEOFill, kCGPathStroke, kCGPathFillStroke, or kCGPathEOFillStroke. Listing 5-15 Detecting hits on a path
@implementation NSBezierPath (BezierPathQuartzUtilities) // Note, this method works only in Mac OS X v10.4 and later. - (BOOL)pathContainsPoint:(NSPoint)point forMode:(CGPathDrawingMode)mode { CGPathRef path = [self quartzPath]; // Custom method to create a CGPath CGContextRef cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; CGPoint cgPoint; BOOL containsPoint = NO; cgPoint.x = point.x; cgPoint.y = point.y; // Save the graphics state before doing the hit detection. CGContextSaveGState(cgContext); CGContextAddPath(cgContext, path); containsPoint = CGContextPathContainsPoint(cgContext, cgPoint, mode); CGContextRestoreGState(cgContext); return containsPoint; } @end
92
CHAPTER 6
Images
Important: This chapter has not yet been updated to describe how images work in Mac OS X v10.6. Significant changes were made to image processing in Mac OS X v10.6. See Application Kit Release Notes (Lion) for details. Images are an important part of any Mac OS X application. In Cocoa, images play a very important, but flexible, role in your user interface. You can use images to render preexisting content or act as a buffer for your application's drawing commands. At the heart of Cocoa's image manipulation code is the NSImage class. This class manages everything related to image data and is used for the following tasks:
Loading existing images from disk. Drawing image data into your views. Creating new images. Scaling and resizing images. Converting images to any of several different formats.
You can use images in your program for a variety of tasks. You can load images from existing image files (such as JPEG, GIF, PDF, and EPS files) to draw elements of your user interface that might be too difficult (or too inefficient) to draw using primitive shapes. You can also use images as offscreen or temporary buffers and capture a sequence of drawing commands that you want to use at a later time. Although bitmaps are one of the most common types of image, it is important not to think of the NSImage class as simply managing photographic or bitmap data. The NSImage class in Cocoa is capable of displaying a variety of image types and formats. It provides support for photograph and bitmap data in many standard formats. It also provides support for vector, or command-based data, such as PDF, EPS, and PICT. You can even use the NSImage class to represent an image created with the Core Image framework.
Image Basics
The NSImage class provides the high-level interface for manipulating images in many different formats. Because it provides the high-level interface, NSImage knows almost nothing about managing the actual image data. Instead, the NSImage class manages one or more image representation objectsobjects derived from the NSImageRep class. Each image representation object understands the image data for a particular format and is capable of rendering that data to the current context. The following sections provide insight into the relationship between image objects and image representations.
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
93
CHAPTER 6
Images
Image Representations
An image representation object represents an image at a specific size, using a specific color space, and in a specific data format. Image representations are used by an NSImage object to manage image data. An image representation object knows how to read image data from a file, write that data back to a file, and convert the image data to a raw bitmap that can then be rendered to the current context. Some image representations also provide an interface for manipulating the image data directly. For file-based images, NSImage creates an image representation object for each separate image stored in the file. Although most image formats support only a single image, formats such as TIFF allow multiple images to be stored. For example, a TIFF file might store both a full size version of an image and a thumbnail. If you are creating images dynamically, you are responsible for creating the image representation objects you need for your image. As with file-based images, most of the images you create need only a single image representation. Because NSImage is adept at scaling and adjusting images to fit the target canvas, it is usually not necessary to create different image representations at different resolutions. You might create multiple representations in the following situations, however:
For printing, you might want to create a PDF representation or high-resolution bitmap of your image. You want to provide different content for your image when it is scaled to different sizes.
When you draw an image, the NSImage object chooses the representation that is best suited for the target canvas. This choice is based on several factors, which are explained in How an Image Representation Is Chosen (page 95). If you want to ensure that a specific image representation is used, you can use the drawRepresentation:inRect: method of NSImage.
NSCIImageRep
N/A
NSPDFImageRep
94
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 6
Images
Class
NSEPSImageRep
Description Handles the display of PostScript or encapsulated PostScript data. Handles custom image data by passing it to a delegate object you provide. Handles the display of PICT format version 1, version 2, and extended version 2 pictures. The PICT format is a legacy format described in the Carbon QuickDraw Manager documentation.
NSCustomImageRep Custom
NSPICTImageRep
PICT
In most situations, you do not need to know how an image representation is created. For example, if you load an existing image from a file, NSImage automatically determines which type of image representation to create based on the file data. All you have to do is draw the image in your view. If you want to support new image formats, you can create a new image representation class. The NSImage class and its NSImageRep subclasses do not follow the class cluster model found in several other Cocoa classes. Creating new image representations is relatively straightforward and is explained in Creating New Image Representation Classes (page 120).
2.
You can change the order in which these rules are applied using the methods of NSImage. For example, if you want to invert the first and second rules, pass NO to the setPrefersColorMatch: method. Doing so causes NSImage to match the resolution before the color space.
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
95
CHAPTER 6
Images
If these rules fail to narrow the choice to a single representationfor example, if the NSImage object has two color TIFF representations with the same resolution and depththe chosen representation is operating-system dependent.
NSImageCacheDefault Use the default caching mode appropriate for the image representation. This
is the default value. For more information, see Table 6-3 (page 96).
NSImageCacheAlways NSImageCacheBySize
Always caches a version of the image. Creates a cached version of the image if the size set for the image is smaller than the size of the actual image data. Does not cache the image. The image data is rasterized every time it is drawn.
NSImageCacheNever
Table 6-3 lists the behavior of each image representation when its cache mode is set to NSImageCacheDefault. Table 6-3 Implied cache settings
Behaves as if the NSImageCacheBySize setting were in effect. Creates a cached copy if the bitmap depth does not match the screen depth or if the bitmap resolution is greater than 72 dpi. Not applicable. This class is used to implement caching. Behaves as if the NSImageCacheBySize setting were in effect. Creates a cached copy if the bitmap depth does not match the screen depth or if the bitmap resolution is greater than 72 dpi. Behaves as if the NSImageCacheAlways setting were in effect. Behaves as if the NSImageCacheAlways setting were in effect. Behaves as if the NSImageCacheAlways setting were in effect. Behaves as if the NSImageCacheBySize setting were in effect. Creates a cached copy of the PICT image if it contains a bitmap whose depth does not match the screen depth or if that bitmap resolution is greater than 72 dpi.
NSCachedImageRep NSCIImageRep
96
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 6
Images
Caching is a useful step toward preparing an image for display on the screen. When first loaded, the data for an image representation may not be in a format that can be rendered directly to the screen. For example, PDF data, when loaded into a PDF image representation, must be rasterized before it can be sent to the graphics card for display. With caching enabled, a NSPDFImageRep object rasterizes the PDF data before drawing it to the screen. The image representation then saves the raster data to alleviate the need to recreate it later. If you disable caching for such images, the rasterization process occurs each time you render the image, which can lead to a considerable performance penalty. For bitmap image representations, the decision to cache is dependent on the bitmap image data. If the bitmaps color space, resolution, and bit depth match the corresponding attributes in the destination device, the bitmap may be used directly without any caching. If any of these attributes varies, however, the bitmap image representation may create a cached version instead. Important: It is important to remember that caching is aimed at improving performance during screen updates. During printing, Cocoa uses the native image data and resolution whenever possible and uses cached versions of the image only as a last resort. If you change the contents of an image representation object directly, you should invoke the recache method of the owning NSImage object when you are done and want the changes to be reflected on the screen. Cocoa does not automatically track the changes you make to your image representation objects. Instead, it continues to use the cached version of your image representation until you explicitly clear that cache using the recache method.
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
97
CHAPTER 6
Images
Interpolation constant
The preceding interpolation settings control both the quality and the speed of the interpolation algorithm. To change the current interpolation setting, use the setImageInterpolation: method of NSGraphicsContext. Note: Scaling affects the in-memory copy of image data only. It does not change data stored on-disk. With data retention disabled in an image, scaling the image multiple times can seriously degrade the resulting image quality. Making an image smaller through scaling is a lossy operation. If you subsequently make the image larger again by scaling, the results are based on the scaled-down version of the image. If you need several different sizes of an image, you might want to create multiple image representation objects, one for each size, to avoid any lossy behavior. Alternatively, you can use the setDataRetained: method to ensure that the caching system has access to the original image data.
98
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
CHAPTER 6
Images
(36,30)
(137,30)
Flipped image
Unflipped image
Image Basics
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
99
CHAPTER 6
Images
The drawing methods are among the most commonly-used methods of NSImage because of their basic safety. Images are typically rendered in offscreen windows and then copied to the screen as needed. In some cases, several images may be composited into the same window for efficiency reasons. The draw methods use extra safety checking to ensure that only the contents of the current image are ever drawn in one of your views. The same is not true of compositing methods, of which there are the following:
These methods can be more efficient than the drawing methods because they perform fewer checks on the image bounds. These methods do have other behaviors that you need to understand, however. The most important behavior is that the compositing methods undo any scale or rotation factors (but not translation factors) applied to the CTM prior to drawing. If you are drawing in a flipped view or manually applied scaling or rotation values to the current context, these methods will ignore those transformations. Although this might seem like a problem, it actually can be a very useful tool. For example, if your program is scaling a graphic element, you might want to add a scale value to your transform to do the scaling (at least temporarily). If your element uses image-based selection handles, you could use the compositing methods to prevent those handles from being scaled along with the rest of your graphic element. The other thing to remember about the compositing methods is that none of them allow you to scale your image to a target rectangle. Cocoa composites the entire image (or designated portion thereof ) bit-for-bit to the specified location. This is in contrast to the drawInRect:fromRect:operation:fraction: method, which lets you scale all or part of your image to the designated target rectangle in your view. Note: The dissolveToPoint:fraction: and dissolveToPoint:fromRect:fraction: methods behave in a similar manner as the corresponding compositing methods. Their use is generally more limited though and better support for dissolving images is available through Core Image.
Basic Formats
Table 6-5 lists the formats supported natively by Cocoa. (Uppercase versions of the filename extensions are also recognized.)
100
CHAPTER 6
Images
UTI
com.adobe.pdf
Tagged Image File Format (TIFF) Joint Photographic Experts Group (JPEG), JPEG-2000 Graphic Interchange Format (GIF) Portable Network Graphic (PNG) Macintosh Picture Format (PICT) Windows Bitmap Format (DIB) Windows Icon Format Icon File Format
TIFF Compression
TIFF images can be read from compressed data, as long as the compression algorithm is one of the four schemes described in Table 6-6. Table 6-6 TIFF compression settings
Compression Description LZW Compresses and decompresses without information loss, achieving compression ratios up to 5:1. It may be somewhat slower to compress and decompress than the PackBits scheme. Compresses and decompresses without information loss, but may not achieve the same compression ratios as LZW. JPEG compression is no longer supported in TIFF files, and this factor is ignored. Compresses and decompresses 1 bit gray-scale images using international fax compression standards CCITT3 and CCITT4.
PackBits
JPEG CCITTFAX
An NSImage object can also produce compressed TIFF data using any of these schemes. To get the TIFF data, use the TIFFRepresentationUsingCompression:factor: method of NSImage.
101
CHAPTER 6
Images
QuickTime Import Format .qti, .qtif Radiance SGI Sony RAW Targa Windows Cursor XWindow bitmap
.hdr .sgi .srf .targa, .tga .cur .xbm
The Image I/O framework is part of Quartz, although the actual framework is part of the Application Services framework. Image I/O handles the importing and exporting of many file formats. To use Quartz directly, you read image data using the CGImageSourceRef opaque type and write using the CGImageDestinationRef type. For more information on using the Image I/O framework to read and write images, see CGImageSource Reference and CGImageDestination Reference.
102
CHAPTER 6
Images
Use the NSImage interface whenever possible. The goal of NSImage is to simplify your interactions with image data. Working directly with image representations should be done only as needed. Treat NSImage and its image representations as immutable objects. The goal of NSImage is to provide an efficient way to display images on the target canvas. Avoid manipulating the data of an image representation directly, especially if there are alternatives to manipulating the data, such as compositing the image and some other content into a new image object. For screen-based drawing, it is best to use the built-in caching mechanism of NSImage. Using an NSCachedImageRep object is more efficient than an NSBitmapImageRep object with the same data. Cached image representations store image data using a CGImageRef object, which can be stored directly on the video card by Quartz. There is little benefit to storing multiple representations of the same image (possibly at different sizes) in a single NSImage. Modern hardware is powerful enough to resize and scale images quickly. The only reason to consider storing multiple representations is if each of those representations contains a customized version of the image. If caching is enabled and you modify an image representation object directly, be sure to invoke the recache method of the owning NSImage object. Cocoa relies on cached content wherever possible to improve performance and does not automatically recreate its caches when you modify image representations. You must tell the image object to recreate its caches explicitly. Avoid recreating art that is already provided by the system. Mac OS X makes several standard pieces of artwork available for inclusion in your own interfaces. This artwork ranges from standard icons to other elements you can integrate into your controls. You load standard images using the imageNamed: method. For a list of standard artwork, see the constants section in NSImage Class Reference.
Mac OS X defines several technologies for working with images. Although the NSImage class is a good general purpose class for creating, manipulating, and drawing images, there may be times when it might be easier or more efficient to use other imaging technologies. For example, rather than manually dissolving from one image to another by drawing partially transparent versions of each image over time, it would be more efficient to use Core Image to perform the dissolve operation for you. For information about other image technologies, and when you might use them, see Choosing the Right Imaging Technology (page 141).
103
CHAPTER 6
Images
System images stored in the Resources directory of the Application Kit framework Your applications icon or other images located in the Resources directory of your main bundle.
To retrieve images from the registry, you use the imageNamed: class method of NSImage. This method looks in the registry for an image associated with the name you provide. If none is found, it looks for the image among the Application Kit's shared resources. After that, it looks for a file in the Resources directory of your application bundle, and finally it checks the Application Kit bundle. If it finds an image file, it loads the image, adds it to the registry, and returns the corresponding NSImage object. As long as the corresponding image object is retained somewhere by your code, subsequent attempts to retrieve the same image file return the already-loaded NSImage object. To retrieve your application icon, ask for an image with the constant, NSImageNameApplicationIcon (on versions of Mac OS X before v10.6, you can use the string @"NSApplicationIcon"). Your application's custom icon is returned, if it has one; otherwise, Cocoa returns the generic application icon provided by the system. For a list of image names you can use to load other standard system images, see the constants section in NSImage Class Reference. In addition to loading existing image files, you can also add images to the registry explicitly by sending a setName: message to an NSImage object. The setName: method adds the image to the registry under the designated name. You might use this method in cases where the image was created dynamically or is not located in your application bundle.
104
CHAPTER 6
Images
Note: When adding images to the registry explicitly, choose a name that does not match the name of any image in your application bundle. If you choose a name that is used by a bundle resource, the explicitly added image supersedes that resource. You can still load the resource using the methods of NSBundle, however.
Drawing to an Image
It is possible to create images programmatically by locking focus on an NSImage object and drawing other images or paths into the image context. This technique is most useful for creating images that you intend to render to the screen, although you can also save the resulting image data to a file. Listing 6-1 shows you how to create a new empty image and configure it for drawing. When creating a blank image, you must specify the size of the new image in pixels. If you lock focus on an image that contains existing content, the new content is composited with the old content. When drawing, you can use any routines that you would normally use when drawing to a view. Listing 6-1 Drawing to an image
100.0)];
NSImage* anImage = [[NSImage alloc] initWithSize:NSMakeSize(100.0, [anImage lockFocus]; // Do your drawing here... [anImage unlockFocus]; // Draw the image in the current context. [anImage drawAtPoint:NSMakePoint(0.0, 0.0) fromRect: NSMakeRect(0.0, 0.0, 100.0, 100.0) operation: NSCompositeSourceOver fraction: 1.0];
Drawing to an image creates an NSCachedImageRep object or uses an existing cached image representation, if one exists. Even when you use the lockFocusOnRepresentation: method to lock onto a specific image representation, you do not lock onto the representation itself. Instead, you lock onto the cached offscreen window associated with that image representation. This behavior might seem confusing but reinforces the notion of the immutability of images and their image representations. Images and their representations are considered immutable for efficiency and safety reasons. If you consider the image files stored in your application bundle, would you really want to make permanent changes to the original image? Rather than change the original image data, NSImage and its image representations modify a copy of that data. Modifying a cached copy of the data is also more efficient for screen-based drawing because the data is already in a format ready for display on the screen.
Creating a Bitmap
There are a few different ways to create bitmaps in Cocoa. Some of these techniques are more convenient than others and some may not be available in all versions of Mac OS X, so you should consider each one carefully. The following list summarizes the most common techniques and the situations in which you might use them:
105
CHAPTER 6
Images
To create a bitmap from the contents of an existing CIImage object (in Mac OS X v10.5 and later), use the initWithCIImage: method of NSBitmapImageRep. To create a bitmap from the contents of a Quartz image (in Mac OS X v10.5 and later), use the initWithCGImage: method of NSBitmapImageRep. When initializing bitmaps using this method, you should treat the returned bitmap as a read-only object. In addition, you should avoid accessing the bitmap data directly, as doing so requires the unpacking of the CGImageRef data into a separate set of buffers. To capture the contents of an existing view or image, use one of the following techniques:
Lock focus on the desired object and use the initWithFocusedViewRect: method of NSBitmapImageRep. In Mac OS X v10.4 and later, use the bitmapImageRepForCachingDisplayInRect: and cacheDisplayInRect:toBitmapImageRep: methods of NSView. The first method creates a bitmap image representation suitable for use in capturing the view's contents while the second draws the view contents to the bitmap. You can reuse the bitmap image representation object to update the view contents periodically, as long as you remember to clear the old bitmap before capturing a new one.
To draw directly into a bitmap, create a new NSBitmapImageRep object with the parameters you want and use the graphicsContextWithBitmapImageRep: method of NSGraphicsContext to create a drawing context. Make the new context the current context and draw. This technique is available only in Mac OS X v10.4 and later. Alternatively, you can create an NSImage object (or an offscreen window), draw into it, and then capture the image contents. This technique is supported in all versions of Mac OS X.
To create the bitmap bit-by-bit, create a new NSBitmapImageRep object with the parameters you want and manipulate the pixels directly. You can use the bitmapData method to get the raw pixel buffer. NSBitmapImageRep also defines methods for getting and setting individual pixel values. This technique is the most labor intensive but gives you the most control over the bitmap contents. For example, you might use it if you want to decode the raw image data yourself and transfer it to the bitmap image representation.
The sections that follow provide examples on how to use the first two techniques from the preceding list. For information on how to manipulate a bitmap, see NSBitmapImageRep Class Reference. Important: In many operating systems, offscreen bitmaps are used to buffer the actual content of a view or window. In Mac OS X, you should generally not use offscreen bitmaps in this way. Most Mac OS X windows are already double-buffered to prevent rendering artifacts caused by drawing during a refresh cycle. Adding your own offscreen bitmap would result in your window being triple-buffered, which is a waste of memory.
106
CHAPTER 6
Images
Before attempting to capture the contents of a view, you should consider invoking the views canDraw method to see if the view should be drawn. Cocoa views return NO from this method in situations where the view is currently hidden or not associated with a valid window. If you are trying to capture the current state of a view, you might use the canDraw method to prevent your code from capturing the view when it is hidden. Once you have your view or image, lock focus on it and use the initWithFocusedViewRect: method of NSBitmapImageRep to capture the contents. When using this method, you specify the exact rectangle you want to capture from the view or image. Thus, you can capture all of the contents or only a portion; you cannot scale the content you capture. The initWithFocusedViewRect: method captures the bits exactly as they appear in the focused image or view. Listing 6-2 shows how to create a bitmap representation from an existing image. The example gets the image to capture from a custom routine, locks focus on it, and creates the NSBitmapImageRep object. Your own implementation would need to replace the call to myGetCurrentImage with the code to create or get the image used by your program. Listing 6-2 Capturing the contents of an existing image
NSImage* image = [self myGetCurrentImage]; NSSize size = [image size]; [image lockFocus]; NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect: NSMakeRect(0,0,size.width,size.height)]; [image unlockFocus];
To capture the content of an onscreen view, you would use code very much like the preceding example. After locking focus on the view, you would create your NSBitmapImageRep object using the initWithFocusedViewRect: method. To capture the content of a detached (offscreen) view, you must create an offscreen window for the view before you try to capture its contents. The window object provides a backing buffer in which to hold the views rendered content. As long as you do not order the window onto the screen, the origin you specify for your window does not really matter. The example in Listing 6-3 uses large negative values for the origin coordinates (just to make sure the window is not visible) but could just as easily use the coordinate (0, 0). Listing 6-3 Drawing to an offscreen window
NSRect offscreenRect = NSMakeRect(-10000.0, -10000.0, windowSize.width, windowSize.height); NSWindow* offscreenWindow = [[NSWindow alloc] initWithContentRect:offscreenRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreRetained defer:NO]; [offscreenWindow setContentView:myView]; [[offscreenWindow contentView] display]; // Draw to the backing
buffer
// Create the NSBitmapImageRep [[offscreenWindow contentView] lockFocus]; NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect: NSMakeRect(0, 0, windowSize.width, windowSize.height)];
107
CHAPTER 6
Images
// Clean up and delete the window, which is no longer needed. [[offscreenWindow contentView] unlockFocus]; [offscreenWindow release];
NSRect offscreenRect = NSMakeRect(0.0, 0.0, 500.0, 500.0); NSBitmapImageRep* offscreenRep = nil; offscreenRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil pixelsWide:offscreenRect.size.width pixelsHigh:offscreenRect.size.height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bitmapFormat:0 bytesPerRow:(4 * offscreenRect.size.width) bitsPerPixel:32]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:offscreenRep]]; // Draw your content... [NSGraphicsContext restoreGraphicsState];
Once drawing is complete, you can add the bitmap image representation object to an NSImage object and display it like any other image. You can use this image representation object as a texture in your OpenGL code or examine the bits of the bitmap using the methods of NSBitmapImageRep.
For PDF data, use the dataWithPDFInsideRect: method of NSView. For EPS data, use the dataWithEPSInsideRect: method of NSView.
108
CHAPTER 6
Images
When you send one of these messages to your view, Cocoa launches the printing system, which drives the data generation process. The printing system handles most of the data generation process, sending appropriate messages to your view object as needed. For example, Cocoa sends a drawRect: message to your view for each page that needs to be drawn. The printing system also invokes other methods to compute page ranges and boundaries. Note: The NSView class provides a default pagination scheme. To provide a custom scheme, your view must override the knowsPageRange: and rectForPage: methods at a minimum. For more information about printing and pagination, see Printing Programming Topics for Cocoa. After the printing system finishes, the code that called either dataWithPDFInsideRect: or dataWithEPSInsideRect: receives an NSData object with the PDF or EPS data. You must then pass this object to the imageRepWithData: method of either NSEPSImageRep or NSPDFImageRep to initialize a new image representation object, which you can then add to your NSImage object. Listing 6-5 shows the basic steps for creating a PDF image from some view content. The view itself must be one that knows how to draw the desired content. This can be a detached view designed solely for drawing the desired content with any desired pagination, or it could be an existing view in one of your windows. Listing 6-5 Creating PDF data from a view
MyPDFView* myView = GetMyPDFRenderView(); NSRect viewBounds = [myView bounds]; NSData* theData = [myView dataWithPDFInsideRect:viewBounds]; NSPDFImageRep* pdfRep = [NSPDFImageRep imageRepWithData:theData]; // Create a new image to hold the PDF representation. NSImage* pdfImage = [[NSImage alloc] initWithSize:viewBounds.size]; [pdfImage addRepresentation:pdfRep];
If you choose to use an existing onscreen view, your views drawing code should distinguish between content drawn for the screen or for the printing system and adjust content accordingly. Use the currentContextDrawingToScreen class method or the isDrawingToScreen instance method of NSGraphicsContext to determine whether the current context is targeted for the screen or a print-based canvas. These methods return NO for operations that generate PDF or EPS data. Important: When drawing in a printing context, the only supported compositing operators are NSCompositeCopy and NSCompositeSourceOver. If you need to render content using any other operators, you must composite them to an image or offscreen window first and then render the resulting image to the printing context using one of the supported operators.
109
CHAPTER 6
Images
drawAtPoint:fromRect:operation:fraction: drawInRect:fromRect:operation:fraction:
These methods offer a simple interface for rendering your images, but more importantly, they ensure that only your image content is drawn. Other methods, such as the compositeToPoint:operation: method and its variants, are fast at drawing images but they do not check the images boundaries before drawing. If the drawing rectangle strays outside of the image bounds, it is possible to draw content not belonging to your image. If the image resides on a shared offscreen window, which many do, it is even possible to draw portions of other images. For more information about the differences between these methods, see Drawing Versus Compositing (page 99). With one exception, all of the drawing and compositing methods choose the image representation that is best suited for the target canvassee How an Image Representation Is Chosen (page 95). The one exception is the drawRepresentation:inRect: method, which uses the image representation object you specify. For more information about the use of these methods, see the NSImage reference. Images support the same set of compositing options as other Cocoa content, with the same results. For a complete list of compositing options and an illustration of their effects, see Setting Compositing Options (page 31).
NSDrawThreePartImage NSDrawNinePartImage
110
CHAPTER 6
Images
Drawing multipart images cleanly on high resolution screens can be very challenging. If you are not careful about aligning images correctly on integral boundaries, the resulting texture might contain pixel cracks or other visual artifacts. The AppKit functions take into account all of the factors required to draw multipart images correctly in any situation, including situations where resolution independence scale factors other than 1.0 are in use. Figure 6-2 shows how you assemble a three-part image for a horizontally resizable control. The two end caps are fixed size images that provide the needed decoration for the edges of the background. The center fill portion then resizes appropriately to fit the bounding rectangle you pass into the NSDrawThreePartImage function. (If you wanted the control to be resizable in the vertical direction, you would stack these images vertically instead of horizontally.) After drawing the background with this function, you would then layer any additional content on top of the background as needed. Figure 6-2
startCap
Figure 6-3 shows you how to assemble a nine-part image for a control that can be resized both vertically and horizontally. In this case, the size of the corner pieces stays fixed but the five remaining fill images vary in size to fit the current bounding rectangle. Figure 6-3 Drawing a nine-part image
centerFill topLeftCorner topEdgeFill topRightCorner
leftEdgeFill
rightEdgeFill
bottomLeftCorner
bottomEdgeFill
bottomRightCorner
For more information about these functions, see their descriptions in Application Kit Functions Reference.
111
CHAPTER 6
Images
- (void)textureFromImage:(NSImage*)theImg textureName:(GLuint*)texName { NSBitmapImageRep* bitmap = [NSBitmapImageRep alloc]; int samplesPerPixel = 0; NSSize imgSize = [theImg size]; [theImg lockFocus]; [bitmap initWithFocusedViewRect: NSMakeRect(0.0, 0.0, imgSize.width, imgSize.height)]; [theImg unlockFocus]; // Set proper unpacking row length for bitmap. glPixelStorei(GL_UNPACK_ROW_LENGTH, [bitmap pixelsWide]); // Set byte aligned unpacking (needed for 3 byte per pixel bitmaps). glPixelStorei (GL_UNPACK_ALIGNMENT, 1); // Generate a new texture name if one was not provided. if (*texName == 0) glGenTextures (1, texName); glBindTexture (GL_TEXTURE_RECTANGLE_EXT, *texName); // Non-mipmap filtering (redundant for texture_rectangle). glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, GL_TEXTURE_MIN_FILTER, samplesPerPixel = [bitmap samplesPerPixel]; // Nonplanar, RGB 24 bit bitmap, or RGBA 32 bit bitmap. if(![bitmap isPlanar] && (samplesPerPixel == 3 || samplesPerPixel == 4)) { glTexImage2D(GL_TEXTURE_RECTANGLE_EXT, 0, samplesPerPixel == 4 ? GL_RGBA8 : GL_RGB8, [bitmap pixelsWide], [bitmap pixelsHigh], 0, samplesPerPixel == 4 ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE, [bitmap bitmapData]);
GL_LINEAR);
112
CHAPTER 6
Images
In the preceding code, there are some additional points worth mentioning:
The gluScaleImage() function can be used to scale non-PoT textures into PoT dimensions for hardware that doesn't support GL_TEXTURE_RECTANGLE_EXT. When you call this method, the current context must be a valid OpenGL context. For more information about OpenGL graphics contexts, see Using OpenGL in Your Application (page 138). Upon completion, the texture is bound to the value in texName. If you specified 0 for the texName parameter, a new texture ID is generated for you and returned in texName.
For more information on Apple's support for OpenGL, see OpenGL Programming Guide for Mac OS X.
113
CHAPTER 6
Images
The first set of methods generate TIFF data for the bitmap. For all other supported formats, you use the representationOfImageRepsInArray:usingType:properties: and representationUsingType:properties: methods. These methods support the conversion of bitmap data to BMP, GIF, JPEG, PNG, and TIFF file formats. All of the preceding methods return an NSData object with the formatted image data. You can write this data out to a file or use it to create a newNSBitmapImageRep object.
114
CHAPTER 6
Images
// Build an NSData object using the specified ColorSync profile id profile = [NSData dataWithContentsOfFile: pathToProfile]; // Set the ColorSync profile for the object [result setProperty:NSImageColorSyncProfileData withValue:profile]; return [result autorelease]; } @end
In Mac OS X v10.5, it is also possible to associate a custom ICC color profile with an NSBitmapImageRep object. To do so, you must initialize your NSBitmapImageRep instance using the calibrated RGB colorspace (NSCalibratedRGBColorSpace). After that, you load the profile and associate the corresponding data object with the NSImageColorSyncProfileData key exactly as you would for a ColorSync profile.
CGContextRef CreateCGBitmapContextWithColorProfile(size_t width, size_t height, CMProfileRef profile, CGImageAlphaInfo alphaInfo) { size_t bytesPerRow = 0; size_t alphaComponent = 0; // Get the type of the color space. CMAppleProfileHeader header; if (noErr != CMGetProfileHeader(profile, &header)) return nil; // Get the color space info from the profile. CGColorSpaceRef csRef = CGColorSpaceCreateWithPlatformColorSpace(profile); if (csRef == NULL) return NULL;
115
CHAPTER 6
Images
// Add 1 channel if there is an alpha component. if (alphaInfo != kCGImageAlphaNone) alphaComponent = 1; // Check the major color spaces. OSType space = header.cm2.dataColorSpace; switch (space) { case cmGrayData: bytesPerRow = width; // Quartz doesnt support alpha for grayscale bitmaps. alphaInfo = kCGImageAlphaNone; break; case cmRGBData: bytesPerRow = width * (3 + alphaComponent); break; case cmCMYKData: bytesPerRow = width * 4; // Quartz doesnt support alpha for CMYK bitmaps. alphaInfo = kCGImageAlphaNone; break; default: return NULL; } // Allocate the memory for the bitmap. void* bitmapData = malloc(bytesPerRow * height); CGContextRef theRef = CGBitmapContextCreate(bitmapData, width, height, 8, bytesPerRow, csRef, alphaInfo);
// Cleanup if an error occurs; otherwise, the caller is responsible // for releasing the bitmap data. if ((!theRef) && bitmapData) free(bitmapData); CGColorSpaceRelease(csRef); return theRef; }
Once you have a Quartz bitmap context, you can create a new Cocoa graphics context object and use it for drawing. To create the NSGraphicsContext object, you use the graphicsContextWithGraphicsPort:flipped: method, which takes a CGContextRef object as a parameter. You then use the setCurrentContext: method to make it current and begin drawing. When you are done drawing, you use Quartz to create a CGImageRef object containing the results. Listing 6-9 shows this process. Listing 6-9 Converting a bitmap to a different color space
116
CHAPTER 6
Images
{ if (!theRep) return nil; // Map the Cocoa constants returned by -bitmapFormat to their // Quartz equivalents. CGImageAlphaInfo alphaInfo = GetAlphaInfoFromBitmapImageRep(theRep); // Get the rest of the image info. NSSize imageSize = [theRep size]; size_t width = imageSize.width; size_t height = imageSize.height; CMProfileRef profile = (CMProfileRef)[colorspace colorSyncProfile]; // Create a new 8-bit bitmap context based on the image info. CGContextRef cgContext = CreateCGBitmapContextWithColorProfile(width, height, profile, alphaInfo); if (cgContext == NULL) return NULL; // Create an NSGraphicsContext that draws into the CGContext. NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:cgContext flipped:NO]; // Make the NSGraphicsContext current and draw into it. [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setCurrentContext:graphicsContext]; // Create a new image for rendering the original bitmap. NSImage* theImage = [[[NSImage alloc] initWithSize:imageSize] autorelease]; [theImage addRepresentation:theRep]; // Draw the original image in the Quartz bitmap context. NSRect imageRect = NSMakeRect(0.0, 0.0, imageSize.width, imageSize.height); [theImage drawAtPoint:NSMakePoint(0.0, 0.0) fromRect:imageRect operation: NSCompositeSourceOver fraction: 1.0]; [NSGraphicsContext restoreGraphicsState]; // Create a CGImage from the CGContext's contents. CGImageRef cgImage = CGBitmapContextCreateImage(cgContext); // Release the context. Note that this does not release the bitmap data. CGContextRelease(cgContext); return cgImage; }
There are two ways to get an NSImage object from a CGImageRef type. In Mac OS X v10.5 and later, you can create an NSBitmapImageRep object using its initWithCGImage: method and then add that image representation to an NSImage object. If your code needs to run in versions of Mac OS X v10.4 or earlier, however, you can lock focus on an NSImage object and use the CGContextDrawImage function to draw the Quartz image into the image. This latter technique creates a copy of the image data and requires more
117
CHAPTER 6
Images
effort than using the initWithCGImage: method but is available on all versions of Mac OS X. Listing 6-10 shows a sample method that demonstrates both approaches but always uses the best approach available for the target platform. Listing 6-10 Using a CGImageRef object to create an NSImage object
- (NSImage*)imageFromCGImageRef:(CGImageRef)image { NSImage* newImage = nil; #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 NSBitmapImageRep* newRep = [[NSBitmapImageRep alloc] initWithCGImage:image]; NSSize imageSize; // Get the image dimensions. imageSize.height = CGImageGetHeight(image); imageSize.width = CGImageGetWidth(image); newImage = [[NSImage alloc] initWithSize:imageSize]; [newImage addRepresentation:newRep]; #else NSRect imageRect = NSMakeRect(0.0, 0.0, 0.0, 0.0); CGContextRef imageContext = nil; // Get the image dimensions. imageRect.size.height = CGImageGetHeight(image); imageRect.size.width = CGImageGetWidth(image); // Create a new image to receive the Quartz image data. newImage = [[NSImage alloc] initWithSize:imageRect.size]; [newImage lockFocus]; // Get the Quartz context and draw. imageContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; CGContextDrawImage(imageContext, *(CGRect*)&imageRect, image); [newImage unlockFocus]; #endif return [newImage autorelease]; }
118
CHAPTER 6
Images
Listing 6-11 shows you how to create a CGColorSpaceRef object from an ICC profile. This code uses several ColorSync Manager functions to create a CMProfileRef object, from which you can then extract the color space object. Mac OS X includes several standard ICC profiles in the /System/Library/ColorSync/Profiles/ directory. Listing 6-11 Creating a color space from a custom color profile
CGColorSpaceRef CreateColorSpaceForProfileAtPath(NSString* path) { CMProfileLocation profileLoc; CMProfileRef profileRef; CGColorSpaceRef csRef = NULL; // Specify where the ICC profile data file is located. profileLoc.locType = cmPathBasedProfile; strncpy(profileLoc.u.pathLoc.path, [path fileSystemRepresentation], // Get the ColorSync profile information from the data file. CMOpenProfile(&profileRef, &profileLoc); // Use the profile to create the color space object. csRef = CGColorSpaceCreateWithPlatformColorSpace(profileRef); CMCloseProfile(profileRef); return csRef; }
255);
For more information on ColorSync and its functions, see ColorSync Manager Reference.
119
CHAPTER 6
Images
These methods provide the basic interface that the parent NSImageRep class needs to interact with your subclass. The methods provide information about what image data formats your class supports along with entry points for initializing your object and drawing the image. Before your subclass can be used, it must be registered with the Application Kit. You should do this early in your applications execution by invoking the registerImageRepClass: class method of NSImageRep. Registering your class lets Cocoa know that your class exists and that it can handle a specific set of file types. Your implementation of the imageUnfilteredTypes method should return an array of UTI types corresponding to the image file types your class supports directly. Another method you should always override is the canInitWithData: method. Once your image representation class has been identified as handling a particular type of data, Cocoa may notify it when data of the appropriate type is received. At that time, Cocoa passes a data object to your canInitWithData: method. Your implementation of this method should examine the data quickly and verify that it can really handle the format. Note: If your subclass is capable of reading multiple images from a single file, you should also implement the imageRepsWithData: method. This method must parse the image data and check to see if it indeed contains multiple images. For each separate image, you should create an instance of your subclass and initialize it with the appropriate subset of the image data. Once your class is chosen to handle the image data, Cocoa looks for an initWithData: method and uses it to initialize your object with the image data. Your implementation of this method should retain the data and use it to initialize the object. At some point later, your draw method may be called to render the data in the current context. Your draw method should render the data at the current origin point and with the current size and settings specified by the NSImageRep parent class.
120
CHAPTER 7
Text
Text rendering is a special type of drawing that is an important part of most applications. Cocoa provides a range of options for rendering text that should satisfy the needs of most developers. The following sections cover these options briefly. For more detailed information, you should see the documents in Reference Library > Cocoa > Text & Fonts.
Text Attributes
Cocoa provides support for programmatically getting font information using the NSFont class. You can apply fonts as attributes to strings or use them to set the default font in the current context. The Cocoa text system also uses font objects for formatting text. You request NSFont objects from Cocoa using the name and size of the font you want, as shown in the following example.
NSFont* font1= [NSFont fontWithName:@"Helvetica" size:9.0]; NSFont* font2 = [NSFont fontWithName:@"Helvetica Bold" size:10.0];
The NSFont class does not provide a programmatic way to modify other text attributes, such as the character spacing and text drawing mode. Cocoa does, however, provide a system Font panel that you can display to the user. From this panel, the user can make changes to the current font attributes. You can also set most text options using the Cocoa text system, which is described in Advanced Text Drawing (page 122). Although you usually specify font attributes directly when drawing NSString and NSAttributedString objects, you can also change the font and font size information in the current graphics state. To change these values, you create an NSFont object and invoke its set method. For information about working with fonts and font objects, see Font Handling. For information about how to display the Font panel, see Font Panel.
Text Attributes
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
121
CHAPTER 7
Text
significantly and is useful in many situations; of course, you should always measure the performance yourself and see if it is adequate for your program. If you need to do more complex text layout, you should still consider using the Cocoa text system. For information on string drawing methods, see NSString Application Kit Additions Reference or NSAttributedString Application Kit Additions Reference in Application Kit Framework Reference.
122
CHAPTER 8
Creating an effective and beautiful Mac OS X application often requires the use of many different techniques. Beyond the basic drawing of paths and images in views, there are other ways to create more complex imagery for your application. The following sections cover many of the most commonly used techniques supported by Cocoa.
A shadow effect consists of horizontal and vertical offset values, a blur value, and the shadow color. These effects combine to give the illusion that there is a light above the canvas that is shining down on the shapes below. The offset and blur values effectively determine the position of the light and the height of the shapes above the canvas. Shadow positions always use the base coordinate system of the view, ignoring any transforms you apply to the shapes in your view. This means that no matter how you manipulate the shapes in a view, the apparent position of the light generating the shadows never changes. If you want to change the apparent position of the light, you must change the shadow object attributes and apply the updated shadow object to the current graphics context before drawing your content.
123
CHAPTER 8
To create a shadow, you create an NSShadow object and call its methods to set the desired shadow attributes. If you anticipate one or more paths overlapping each other, you should be sure to choose a color that has an alpha value; otherwise, shadows that intersect other objects might look flat and ruin the effect. To apply the shadow, invoke its set method. Listing 8-1 shows the code used to create the shadow for the paths in Figure 8-1 (page 123). A partially transparent color is used to allow for overlapping paths and shadows. Listing 8-1 Adding a shadow to a path
[NSGraphicsContext saveGraphicsState]; // Create the shadow below and to the right of the shape. NSShadow* theShadow = [[NSShadow alloc] init]; [theShadow setShadowOffset:NSMakeSize(10.0, -10.0)]; [theShadow setShadowBlurRadius:3.0]; // Use a partially transparent color for shapes that overlap. [theShadow setShadowColor:[[NSColor blackColor] colorWithAlphaComponent:0.3]]; [theShadow set]; // Draw your custom content here. Anything you draw // automatically has the shadow effect applied to it. [NSGraphicsContext restoreGraphicsState]; [theShadow release];
Shadow effects are stored as part of the graphics state, so once set, they affect all subsequent rendering commands in the current context. This is an important thing to remember because it might force you to think about the order in which you draw your content. For example, if you set up a shadow, fill a path, and then stroke the same path, you do not get a single shape with an outline, fill color, and shadow. Instead, you get two shapesan outline and a fill shapeand two shadows, one for each shape. If you stroke the path after filling it, the shadow for the stroked path appears on top of the filled shape. In Figure 8-1 (page 123), the desired effect was achieved by applying the shadow to only the fill shape of each path. Note: Another way to a single shadow for multiple paths is using a Quartz transparency layer. For more information about using transparency layers, see Quartz 2D Programming Guide.
124
CHAPTER 8
creating both a white circle in the very center of the gradient and a black border surrounding the gradient. For gradient d, the center points of the circles used to draw the gradient are offset, creating a different sort of shading effect. Figure 8-2 Different types of gradients
In Mac OS X v10.5 and later, Cocoa provides support for drawing gradients using the NSGradient class. If your software runs on earlier versions of Mac OS X, you must use Quartz or Core Image to perform gradient fills.
125
CHAPTER 8
Although you cannot change the colors of a gradient object after you initialize it, you can get information about the colors it contains using accessor methods. The numberOfColorStops method returns the number of colors that the gradient uses to draw itself and the getColor:location:atIndex: method retrieves the color stop information for each of those colors. If you want to know what color would be drawn for the gradient in between two color stops, you can use the interpolatedColorAtLocation: method to get it.
These convenience methods are easily identified by the fact that they take either an NSBezierPath or a rectangle as their first parameter. This parameter is used as a clipping region for the gradient when it is drawn. You might use these methods to draw a gradient fill inside an existing shape in your interface. Listing 8-2 shows some code that draws an axial gradient pattern. The NSBezierPath object containing the rounded rectangle shape acts as the clipping region for the gradient when it is drawn. Figure 8-3 (page 127) shows the resulting gradient.
126
CHAPTER 8
Listing 8-2
- (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; NSBezierPath* clipShape = [NSBezierPath bezierPath]; [clipShape appendBezierPathWithRoundedRect:bounds xRadius:40 yRadius:30];
NSGradient* aGradient = [[[NSGradient alloc] initWithColorsAndLocations:[NSColor redColor], (CGFloat)0.0, [NSColor orangeColor], (CGFloat)0.166, [NSColor yellowColor], (CGFloat)0.33, [NSColor greenColor], (CGFloat)0.5, [NSColor blueColor], (CGFloat)0.75, [NSColor purpleColor], (CGFloat)1.0, nil] autorelease]; [aGradient drawInBezierPath:clipShape angle:0.0]; }
Figure 8-3
drawFromPoint:toPoint:options: drawFromCenter:radius:toCenter:radius:options:
These methods give you more flexibility over the gradient parameters, including the ability to extend the gradient colors beyond their start and end points. Unlike the high-level routines, these methods do not change the clip region prior to drawing. If you do not set up a custom clip region prior to drawing, the resulting gradient could potentially expand to fill your entire view, depending on the gradient options. Listing 8-3 shows the code for drawing a radial gradient in a view using the primitive drawing routine of NSGradient. The second circle in the gradient is offset from the first one by 60 points in both the horizontal and vertical directions, causing the overall gradient to skew towards the upper-right of the circle. Because
127
CHAPTER 8
the code passes the value 0 for the options parameter, the gradient does not draw beyond the start and end colors and therefore does not fill the entire view. Figure 8-4 (page 128) shows the gradient resulting from this code. Listing 8-3 Drawing a radial gradient using primitive routine
- (void)drawRect:(NSRect)rect { NSRect bounds = [self bounds]; NSGradient* aGradient = [[[NSGradient alloc] initWithStartingColor:[NSColor orangeColor] endingColor:[NSColor cyanColor]] autorelease]; NSPoint centerPoint = NSMakePoint(NSMidX(bounds), NSMidY(bounds)); NSPoint otherPoint = NSMakePoint(centerPoint.x + 60.0, centerPoint.y + 60.0); CGFloat firstRadius = MIN( ((bounds.size.width/2.0) - 2.0), ((bounds.size.height/2.0) -2.0) ); [aGradient drawFromCenter:centerPoint radius:firstRadius toCenter:otherPoint radius:5.0 options:0]; }
Figure 8-4
128
CHAPTER 8
2. 3. 4.
Create a CGShadingRef using Quartz; see Gradients in Quartz 2D Programming Guide. Configure the current clipping path to the desired shape for your shading; see Setting the Clipping Region (page 34). Draw the shading using CGContextDrawShading.
For information on using Core Image to create images with gradient fills, see Core Image Programming Guide.
After capturing the screen, the way you configure your drawing environment depends on whether you are using Cocoa or OpenGL to draw. In OpenGL, you create an NSOpenGLContext object and invoke several of its methods to enter full-screen mode. In Cocoa, you have to create a window that fills the screen and configure that window.
129
CHAPTER 8
To release a display you previously captured, use the CGDisplayRelease function. If you captured all of the active displays, you can release them all by calling the CGReleaseAllDisplays function. For more information about capturing and manipulating screens, see Quartz Display Services Reference.
NSOpenGLPFAFullScreen NSOpenGLPFAScreenMask NSOpenGLPFAAccelerated NSOpenGLPFANoRecovery (only if your OpenGL graphics context is shared)
Listing 8-4 shows the basic steps for capturing all displays and setting up the OpenGL context for full-screen drawing. For information on how to create an NSOpenGLContext object, see Creating an OpenGL Graphics Context (page 139). Listing 8-4 Creating an OpenGL full-screen context
NSOpenGLContext* CreateScreenContext() { CGDisplayErr err; err = CGCaptureAllDisplays(); if (err != CGDisplayNoErr) return nil; // Create the context object. NSOpenGLContext* glContext = CreateMyGLContext(); // If the context is bad, release the displays. if (!glContext) { CGReleaseAllDisplays(); return nil; } // Go to full screen mode. [glContext setFullScreen]; // Make this context current so that it receives OpenGL calls. [glContext makeCurrentContext];
130
CHAPTER 8
return glContext; }
Once you go into full-screen mode with your graphics context, your application has full control of the screen. To exit full-screen mode, invoke the clearDrawable method of your OpenGL context and call the CGReleaseAllDisplays function to release the screens back to the system. For detailed sample code showing you how to enter full-screen mode using OpenGL and Cocoa, see the NSOpenGL Fullscreen sample in Sample Code > Graphics & Imaging > OpenGL.
- (IBAction)goFullScreen:(id)sender { // Get the screen information. NSScreen* mainScreen = [NSScreen mainScreen]; NSDictionary* screenInfo = [mainScreen deviceDescription]; NSNumber* screenID = [screenInfo objectForKey:@"NSScreenNumber"]; // Capture the screen. CGDirectDisplayID displayID = (CGDirectDisplayID)[screenID longValue]; CGDisplayErr err = CGDisplayCapture(displayID); if (err == CGDisplayNoErr) { // Create the full-screen window if it doesnt already exist. if (!myScreenWindow) { // Create the full-screen window. NSRect winRect = [mainScreen frame]; myScreenWindow = [[NSWindow alloc] initWithContentRect:winRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO screen:[NSScreen mainScreen]];
131
CHAPTER 8
// Establish the window attributes. [myScreenWindow setReleasedWhenClosed:NO]; [myScreenWindow setDisplaysWhenScreenProfileChanges:YES]; [myScreenWindow setDelegate:self]; // Create the custom view for the window. MyFullScreenView* theView = [[MyFullScreenView alloc] initWithFrame:winRect]; [myScreenWindow setContentView:theView]; [theView setNeedsDisplay:YES]; [theView release]; } // Make the screen window the current document window. // Be sure to retain the previous window if you want to use it again. NSWindowController* winController = [[self windowControllers] objectAtIndex:0]; [winController setWindow:myScreenWindow]; // The window has to be above the level of the shield window. int32_t shieldLevel = CGShieldingWindowLevel(); [myScreenWindow setLevel:shieldLevel]; // Show the window. [myScreenWindow makeKeyAndOrderFront:self]; } }
To exit full screen mode using Cocoa, simply release the captured display, resize your window so that it does not occupy the entire screen, and set its level back to NSNormalWindowLevel. For more information about the shield window, see Quartz Display Services Reference.
132
CHAPTER 8
Note: For finite-length animations, you can also use an NSAnimation object to control the animation timing. For more information, see Using Cocoa Animation Objects (page 133). The NSTimer class provides a mechanism for generating periodic events in your application. When a preset time is reached, the timer object sends a message to your application, giving you the chance to perform any desired actions. For animations, you would use a timer to tell your application that it is time to draw the next frame. There are two steps involved with getting a timer to run. The first step is to create the NSTimer object itself and specify the object to notify, the message to send, the time interval for the notification, and whether the timer repeats. The second step is to install that timer object on the run loop of your thread. The methods scheduledTimerWithTimeInterval:invocation:repeats: and scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: perform both of these steps for you. Other methods of NSTimer create the timer but do not install it on the run loop. For information and examples on how to create and use timers, see Timer Programming Topics.
Draw Minimally
Even with modern graphics hardware, drawing is still an expensive operation. The best way to reduce the amount of time spent in your drawing code is to draw only what is needed in the first place. During a view update, the drawRect: method receives a rectangle that specifies the portion of the view that needs to be updated. This rectangle is always limited to the portion of the view that is currently visible and in some cases may be even smaller. Your drawing code should pay attention to this rectangle and avoid drawing content outside of it. Because the rectangle passed to drawRect: might be a union of several smaller rectangles, an even better approach is to call the views getRectsBeingDrawn:count: method and constrain your drawing to the exact list of rectangles returned by that method.
133
CHAPTER 8
134
CHAPTER 9
Cocoa was designed to integrate well with other technologies in Mac OS X. Many technologies are packaged as Objective-C frameworks, which makes including them in Cocoa easy. You are not limited to the use of Objective-C frameworks, though. Cocoa itself uses Quartz internally to implement most drawing routines. You can use Quartz and other C-based technologies, such as OpenGL and QuickTime, from your code with little extra effort. The sections that follow provide information about how to incorporate some of the more important drawing technologies available in Mac OS X.
Layers Gradients (also called shadings) Image data sources Blend modes (Cocoa uses compositing modes instead) Masking images Transparency layers (for grouping content) Arbitrary patterns (other than images)
135
CHAPTER 9
In each case, you are free to use Quartz functions to take advantage of these features. Some features can produce data types that you can then incorporate back into a Cocoa object. (For example, you can use an image data source to obtain a Quartz image (CGImageRef), which you can then use to create an NSImage object.) In some cases, however, you may need to perform the entire operation using Quartz functions. For information on how to use Quartz features, see Quartz 2D Programming Guide.
Although in each case the structure layout is the same, you cannot pass the Quartz data type directly to a method expecting the Cocoa type. To convert, you must cast from one type to another, as shown in the following example:
NSRect cocoaRect = *(NSRect*)&myCGRect;
Table 9-2 lists the Cocoa classes that approximate the behavior of specific Quartz data types. In some cases, the Cocoa class wraps an instance of its Quartz counterpart, but that is not always true. In the case of shadows, Quartz provides no direct data type for managing the shadow parameters; you must set shadows attributes in Quartz using several different functions. In the case of layers, there are no Cocoa equivalents. Table 9-2 Cocoa type
NSGraphicsContext NSAffineTransform NSColor NSFont NSGlyph NSImage NSBezierPath NSShadow
136
CHAPTER 9
Cocoa type
NSGradient (Mac OS X v10.5 and later)
Quartz type
CGShadingRef CGLayerRef
No equivalent
Because Cocoa types often wrap equivalent Quartz types, you should look at the Cocoa reference documentation for information about how to get equivalent Quartz objects, if any. In many cases, Cocoa classes do not offer direct access to their Quartz equivalent and you may need to create the Quartz type based on information in the Cocoa object, such as in the following cases:
To create a CGPathRef object from an NSBezierPath object, you must redraw the path using Quartz function calls. Use the elementAtIndex:associatedPoints: method of NSBezierPath to retrieve the paths point information. To convert back and forth between CGColorRef and NSColor objects, get the color component values from one object and use those values to create the other object. When creating colors, you may also need to specify the color space for that color. For the most part, Quartz and Cocoa support the same color spaces. If a color uses a custom color space, you can use the available ICC profile data to create the appropriate color space object. To create an NSImage object from a Quartz image, you need to create the image object indirectly. For information on how to do this, see Using a Quartz Image to Create an NSImage (page 109). To create Quartz shadows, you can use the methods of NSShadow to retrieve the color, offset, and blur radius values prior to calling CGContextSetShadow or CGContextSetShadowWithColor.
137
CHAPTER 9
Using NSOpenGLView
One way to do OpenGL drawing is to add an OpenGL view (an instance of NSOpenGLView) to your window. An OpenGL view behaves like any other view but also stores a pointer to an OpenGL graphics context object (an instance of NSOpenGLContext). Storing the graphics context in the view eliminates the need for your code to recreate the context during each drawing cycle, which can be expensive. To use an OpenGL view in your program, you create a subclass of NSOpenGLView and add that view to your window, either programmatically or using Interface Builder. When creating an OpenGL view programmatically, you specify the pixel format object you want to associate with the view. A pixel format object (an instance of NSOpenGLPixelFormat) specifies the buffers and other rendering attributes of the OpenGL graphics context. For information on the meaning of different pixel format attributes, see OpenGL Programming Guide for Mac OS X. If you use Interface Builder to add your view to a window, you specify the pixel format information using the inspector for your view. Interface Builder lets you specify some pixel attributes, but not all. To support other attributes, you must replace the views pixel format object at runtime using the setPixelFormat: method.
138
CHAPTER 9
Important: If you set the pixel format attributes programmatically, you must do so before getting the OpenGL graphics context using the openGLContext method. The graphics context is created with the current pixel format information and is not recreated if that information changes. Alternatively, you can change the OpenGL graphics context at any time using the setOpenGLContext: method. As with other views, you use your OpenGL views drawRect: method to draw the content of your view. When your drawRect: method is invoked, the environment is automatically configured for drawing using the OpenGL graphics context associated with your view. Unlike with other graphics contexts, you do not need to restore the previous OpenGL graphics context when you are done drawing. OpenGL does not maintain a stack of graphics contexts that need to be popped as they are no longer needed. Instead, it simply uses the most recent context that was made current.
- (NSOpenGLContext*)getMyContext { // Specify the pixel-format attributes. NSOpenGLPixelFormatAttribute attrs[] = { NSOpenGLPFAFullScreen, NSOpenGLPFADoubleBuffer, NSOpenGLPFADepthSize, 32, 0 }; // Create the pixel-format object. NSOpenGLContext* myContext = nil; NSOpenGLPixelFormat* pixFmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; // If the pixel format is valid, create the OpenGL context. if (pixFmt != nil) { myContext = [[NSOpenGLContext alloc] initWithFormat:pixFmt shareContext:NO]; } [pixFmt release]; return myContext; }
139
CHAPTER 9
Because the creation of OpenGL graphics contexts depends on the currently available renderers, your code should always verify that the desired objects were created before trying to use them. If creating an object fails, you can always try to create it again using a different set of attributes.
140
CHAPTER 9
Quartz Composer is especially suited for applications that want to perform complex image manipulations. Through it, you gain easy access to features of Quartz 2D, Core Image, Core Video, OpenGL, QuickTime, MIDI System Services, and Real Simple Syndication (RSS). Your application can render compositions for display or provide the user with controls for manipulating the composition parameters. For a detailed example showing you how to run a composition from your Cocoa application, see the chapter Using QCRenderer to Play a Composition in Quartz Composer Programming Guide.
Quartz images are immutable data types that you use to manipulate bitmap data in Quartz. Although NSImage is easier to use and provides automatic support for resolution independence, you might need to create Quartz images if another API you are using expects them. You can create a Quartz image by drawing into a NSBitmapImageRep object or Quartz bitmap context and then extracting a CGImageRef from there. Quartz images are part of the Application Services framework. Quartz layers are a mutable alternative to Quartz images. You can draw to layers much like you would draw to an NSImage object. You do so by creating a context, locking focus on that context, drawing, and retrieving an image object from the results. Because they are implemented in video memory, layers can be very efficient to use, especially if you need to draw the same image repeatedly. Quartz layers are available in Mac OS X v10.4 and later as part of the Application Services framework. The Core Image framework is geared toward processing image data. You would use this technology to apply visual effects or filters to existing bitmap images. Because it is explicitly designed for manipulating bitmap images, you must convert your images to a CIImage object before you do any processing. Core Image is available in Mac OS X v10.4 and later as part of the Quartz Core framework. The Image I/O framework is geared towards developers who need more direct control over reading and writing image data. You might use this framework to convert images from one format to another or you might use it to add metadata to an image created by your program. The features of Image I/O are available in Mac OS X v10.4 and later as part of the Application Services framework.
Image I/O
141
CHAPTER 9
Description
While not explicitly an imaging technology, the Core Animation framework is a smart and efficient way to render images and other data inside a view. The framework provides a cached backing store that makes it possible to do animations with a minimal amount of redrawing. You might use this technology in place of NSImage or other imaging technologies to create animation effects or other rapidly changing graphics. It offers respectable animation performance without requiring you to use low-level APIs such as OpenGL. The Core Animation framework is available in Mac OS X v10.5 and later as part of the Quartz Core framework.
142
REVISION HISTORY
This table describes the changes to Cocoa Drawing Guide. Date 2011-01-18 2009-10-19 2009-01-06 2008-10-15 Notes Corrected reference to application icon image name constant. Corrected typos. Updated the guidelines associated with resolution independent drawing. Updated advice for creating an NSImage from a CGImageRef. Updated the discussion of screen coordinates. Updated the content for Mac OS X v10.5. Added information about NSGradient and rounded rectangle support. Updated the information about flipped coordinate systems. Fixed bugs in several code examples. Added guidance about which imaging technologies work best for different types of operations. Added the mathematical equations corresponding to the available compositing operations. 2006-10-03 Fixed several code examples and added information about how to add a ColorSync profile to a bitmap. Changed matrix values to match the values in NSAffineTransformStruct. Fixed example for casting a CGRect to an NSRect. Moved animation object details to "Animation Programming Guide." New document that describes how to draw content from a Cocoa application. This document replaces information about Cocoa drawing that was previously published in Basic Drawing, Drawing and Images, The Drawing Environment, and OpenGL.
2007-10-31
2006-06-28
2006-04-04 2006-03-08
143
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.
REVISION HISTORY
144
2011-01-18 | 2005, 2011 Apple Inc. All Rights Reserved.