Uli's Web Site
[ Zathras.de - Uli's Web Site ]
Other Sites: Stories
Pix
Abi 2000
Stargate: Resurgence
Lost? Site Map!
 
 
     home | articles | moose | programming | articles >> blog

 Blog
 
 Blog Topics
 
 Archive
 

15 Most Recent [RSS]

 All you need to know about the Mac keboard
2010-08-09 @488
 
 Review: Sherlock
2010-07-31 @978
 
 Playing with Objective C on Debian
2010-05-08 @456
 
 Fruit vs. Obst
2010-05-08 @439
 
 Mixed-language ambiguity
2010-04-15 @994
 
 Uli's 12:07 AM Law
2010-04-12 @881
 
 Uli's 1:24 AM Law
2010-04-12 @874
 
 Uli's 6:28 AM Law
2010-04-12 @869
 
 Uli's 3:57 PM Law
2010-04-12 @867
 
 Uli's 4:41 PM Law
2010-04-12 @864
 
 Uli's 7:25 AM Law
2010-04-12 @862
 
 Uli's 9:36 PM Law
2010-04-12 @861
 
 Typesafe typecasts
2010-04-12 @471
 
 Porting to the Macintosh
2010-04-09 @592
 
 Uli's source code is on Github!
2010-03-05 @986
 

More...

Cocoa Text System everywhere...

Sometimes, you need to draw text with more control than an NSTextField or NSTextView will let you do, and sometimes you need better performance than the NSStringDrawing category will provide. And maybe you need to draw text into a CGContext or even inside a Carbon application.

You may be thinking about CoreText right now, and how unfortunate it is that you still have to maintain compatibility with Mac OS X 10.4 "Tiger", but there's an easier way:

The Cocoa Text System

Just use the Cocoa Text System. The Cocoa Text System is a group of classes that NSTextView, NSTextField and NSStringDrawing use to actually draw strings. Now, if you look at Apple's docs, you'll first be frightened by how complex this whole system seems to be: there are NSLayoutManagers, NSTextStorages, NSTextContainers, NSGlyphGenerators, NSTypesetters... but don't fear, it's much easier than it looks.

Apple actually provides a great introduction in the Drawing Text with NSLayoutManager entry of the Drawing Strings Task Documentation.

If you read that, you'll see that you actually only need to deal with three of these classes to draw any styled Unicode string wrapped to a text box: NSLayoutManager is kind of the main controller. The string to be drawn and its attributes are stored in an NSTextStorage, and the extents of the area to draw into is specified by the NSTextContainer. What's more, once you've created these objects, you can cache them, and thus speed up repeated drawing of the same string significantly.

You create these objects using +alloc/-init as usual, then tell the layout manager to take ownership of the text container, and the text storage to take ownership of the layout manager. Here's essentially Apple's example:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:@"This is the text string."];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] init];
[layoutManager addTextContainer:textContainer];
[textContainer release];
[textStorage addLayoutManager:layoutManager];
[layoutManager release];

// Use the objects.

[textStorage release];

To actually draw this example string, you simply specify the size of the area to draw in, then ask the layout manager for the range of glyphs to draw, and then tell it to draw those at whatever position you want:

[textContainer setContainerSize: rect.size];
NSRange glyphRange = [layoutManager glyphRangeForTextContainer: textContainer];
[layoutManager drawGlyphsForGlyphRange: glyphRange atPoint: rect.origin];

Pretty simple, isn't it? Two things to watch out for here: Whenever you change the size of the text container, the text needs to be re-wrapped ("re-layouted"), which is a sort of expensive operation that can be sped up by caching. Second, to actually trigger such a re-layout, you call -glyphRangeForTextContainer:.

For drawing styles and images, you simply take advantage of the fact that the NSTextStorage is actually a subclass of NSMutableAttributedString. So, it's trivial to assign styles, fonts, colors and add text attachments.

Measuring Text

For example, if you want to measure how much space text will use, set the text container to have the height or width of the fixed side, and set the other side of the rect to some huge value, like FLT_MAX. Then, you call -glyphRangeForTextContainer: to make sure the text has been layouted, and then call -usedRectForTextContainer: to get the actual dimensions of the text:

[textContainer setContainerSize: NSMakeSize([self bounds].size.width, FLT_MAX)];
(NSRange) [layoutManager glyphRangeForTextContainer: textContainer]; // Cause re-layout.
NSRect neededBox = [layoutManager usedRectForTextContainer: textContainer];

The Cocoa Text System in Quartz or Carbon

Carbon uses the straight Quartz APIs. People who want to draw into OpenGL textures, PDF contexts and other custom locations often also use Quartz directly. So, what if you want to use the above text drawing methods in Quartz? You don't have a an NSGraphicsContext, you only have a CGContext. What to do? Well, easy. Behind every NSGraphicsContext, there is a CGContext, and for every CGContext, you can create an NSGraphicsContext. So, if you have the CGContextRef in a variable named inContext, you can easily do:

[NSGraphicsContext saveGraphicsState];
NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithGraphicsPort: inContext flipped: true];
[NSGraphicsContext setCurrentContext: context];

// Do Cocoa drawing here.

[NSGraphicsContext restoreGraphicsState];

Of course, if you're doing this in Carbon, take care to catch any NSExceptions and create an autorelease pool around calls like these, you wouldn't want to leak Cocoa objects or have a Cocoa exception waltz through the Carbon system libraries.

The neat part about this is not only that the Cocoa Text System is much simpler than ATSUI, but also that the Cocoa Text system is very similar in design to CoreText. So, if you want to move to CoreText a couple releases from now, you'll just have to swap out a few API calls, but the general workings will stay pretty much the same.

Reader Comments: (RSS Feed)
Michael Nickerson writes:
Hey Uli, small typo in your code to resize the text container - that should be NSMakeSize() not NSSizeMake() (been playing around with CG stuff lately?). Also, NSMakeSize() takes float arguments, so that should probably be FLT_MAX, not LONG_MAX. And an interesting bug for everyone - if you don't put together the text system like above, you'll leak a little memory. i.e. if you add the layout manager to the text storage, *then* add the text container to the layout manager. I keep forgetting to submit a bug about that one.
Uli Kusterer replies:
@Michael: Thank you, you're right, I converted some CG-using code that actually used CGSizeMake() and forgot the different naming. And you're right, FLT_MAX was what I was looking for. Couldn't initially find it as it neither came up in a search for 'float', nor was in limits.h... Thanks.
Matt writes:
Great article! Exactly what I was looking for. Thanks!
Comment on this article:
Name:
E-Mail: (not shown, hashed for Gravatar)
Web Site URL: (optional)
Comment: (plain text only)
Please Enter the following word:
Or E-Mail Uli privately.

 
Created: 2008-04-19 @900 Last change: 2010-09-02 @776 | Home | Admin | Edit
© Copyright 2003-2010 by M. Uli Kusterer, all rights reserved.