Application Frameworks • 57:28
Learn how to take full advantage of the powerful drawing capabilities of Quartz in your Cocoa applications. This session covers new features in the Cocoa view subsystem, including topics such as progressive image loading, and presents tips and techniques to help you achieve high visual quality and maximal drawing performance in your applications.
Speakers: John Randolph, Troy Stephens
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Well, is my mic on? Can you all hear me? Okay, I want to thank Jason for introducing me. I know he just had to jog down here from a very long QA session in the Cocoa UI techniques part. So I'm here to talk about Cocoa 2D Drawing Techniques. That's me, John Randolph, a senior engineer in a marketing department, which is kind of fun.
And what I'm going to cover in this talk is how the Cocoa classes that reflect the Quartz drawing model are used in 2D rendering. So I'll talk a little bit about images, paths, and text, how we manipulate the coordinate space in Cocoa, generally how to draw using the Cocoa frameworks.
I'll talk a little bit about more sophisticated drawing techniques than I did last year, particularly combining vector and image drawing. And then my colleague Troy will come up and discuss the changes to the Cocoa 2D Drawing API for Panther. So there are a couple of additions to existing classes and some new APIs, and maybe I'll get a demo or two into this.
So where does this all fit in? Before we get to GUIs, we just have the BSD API and we have Darwin. And then we've added all of these amazing graphics technologies like Quartz 2D, OpenGL, and QuickTime for time-based media. So, Java can use them, Carbon can use them, Cocoa can use them, and we use these frameworks talking to the graphics layers to implement Aqua in your applications. And today we're going to talk specifically about Cocoa and Quartz and how you use them to comply with John Galenzi's desires. And you all comply, right? Good.
So, Quartz 2D gives us a couple of very powerful features. We get window management, a very nice... complete vector drawing API, a lot of image handling code, compositing of those images, and that's actually pervasive in our OS, and I believe that we are the only vendor that offers that. And we've got a lot of powerful text rendering capabilities also in the Quartz 2D layer.
The Quartz 2D compositor, what we call Quartz Services now, is the Windows server that takes care of blending those windows together on the screen. We have hardware accelerated window compositing. We have transparency of windows. We have windows mapped into OpenGL texture memory, and you've seen that in other sessions.
Besides that, we have the PostScript PDF style vector-based drawing model, which you all should be familiar with from your days with the Apple LaserWriter, some, what, 20 years ago now? And this is based on paths that can be filled or stroked. They are resolution independent, and on Mac OS X, you get that anti-aliasing for free. The coordinate system that we get from Quartz 2D and that we use in Cocoa views when we're drawing, is based on a floating point system.
The origin is in the lower left corner, just like it is in math. I want to make the point that on today's displays, the whole number of values land exactly between screen pixels. And sometimes you might adjust your drawing to make sure that lines that you draw exactly take one pixel.
I'm going to tell you not to rely on this because you're not always drawing to the screen. Sometimes you're going to be drawing for a printing context. You're going to be drawing in much higher resolution. You're going to be drawing in much higher resolution contexts, like when you're imaging for a printer or when you're imaging for a high-priced typesetter. So when you're doing these drawing techniques, please check to see whether you actually need to do them.
More features of Quartz 2D. We can scale and rotate images. We can map them to what other color depth is available on the display where they're being drawn. We've got color managed rendering of images, which you might have just seen across the hall in the QuickTime session. We've got JPEG decoding, and all of this has been optimized to use the velocity engine on the G4s if it's present. And, of course, we've got people working very hard on making that go as fast as it possibly can on the G5s.
So, more about Quartz 2D features. We've got a very rich set of compositing operators. There are 14 different modes. The original Porter Duff paper on the subject described 12 modes, and we invented two more. Our compositing takes into account both the source and the destination alpha values. Again, this is all optimized to use whatever hardware you have in the machine.
And I'm going to point out a couple of examples here. On any machine that's got the Mac OS X developer tools installed, you can go to Developer Examples App Kit Composite Lab, and there's an app there that lets you try out all the compositing modes and see what their effects are. And then there's a simpler app on the DTS sample code website, actually two of them, Tinted Image and Cropped Image, which both show you things that you can do by compositing.
Quartz 2D also gives us text rendering, as I described before. I'm not going to get much into text because it's a pretty big subject. I urge you to go to the Cocoa text session. I will just mention that the anti-aliasing and the LCD text and so forth is also velocity engine optimized. Let me just grab a little drink here.
So in the Cocoa framework, we do window management through instances of the NSWindow class. We do vector drawing mostly with the NSBezierPath class. Images and compositing are done with the NSImageRep and NSImage class. Text is done with NSText, NSString, NSAttributedString, and a lot of other classes that I won't be going into here.
So when you're going to start rendering the images or the vector art that makes your app unique, you need to know where you're going to draw. Now, if you're down at the Quartz level, you'd be drawing into a CG window, but if you're a Cocoa coder, it's a little simpler than that. You have an NSWindow object that manages the CG windows. It will get your events. It will talk to the Windows server and tell it when something has changed and needs to be updated.
And it has a drawing context, so it has things like what image interpolation is in effect, what the current line drawing width is, what the current drawing color is, and so forth. Most of the drawing in your apps is going to happen in NSView instances. A drawing destination in Cocoa basically is anything that can react to lock focus and unlock focus. And NSImage is also a place to draw that you'll use if you want to build something in an off-screen buffer. the way you might have done in the G world in earlier versions of Mac OS.
So let me mention NSView here. I like to think of NSView as a rectangle of responsibility within a window. It owns some portion of the window, and any events that happen in that area are going to come to this NSView object. Things like keystrokes, things like mouse events, and so forth.
Things like drags into that part of the window. NSView's got its own coordinate space, not necessarily its own drawing context. Views live in a hierarchy, so every view has a super view and any number of sub views. Most of the drawing in NSViews are going to happen within the context of the draw rect method, and some changes to the way NSView objects behave are coming up in Panther, and Troy will fill you in on that. So let me go over something here that seems to be a point of confusion for a number of new Cocoa developers.
There are two rectangles that are very significant for NS views. There are the frame and the bounds rectangles. The bounds determines your coordinate space. It tells you how big you are and where your coordinate space origin is as far as your point of view is concerned. Determines the scaling and the rotation of your coordinate space.
You can change it around to suit your drawing with methods like set bound size and rotate by angle and so forth. So let's say we've got a view that's going to draw this nice little farmhouse here, and the bounds of this obviously are the borders of the image. Now if I go and put that in a scroll view, well, it's still this big.
But it's only going to draw in this area that's outlined by the solid rectangle, even though it thinks it's still as big as the outer rectangle. The frame is what's actually going to enclose the area that gets drawn. The bounds is the largest area that you might ever draw.
Let me give a little bit of advice for writing NS views. Something that's very tempting is when there's a change in your model, in your code, you're going to want to draw immediately. And I'm going to ask you, for performance reasons, to not do that. Calling lock focus and unlock focus is usually not necessary. Now, there are times when it is necessary, like if you have a live animation going, or like we do in the kit where we have that default button pulsing.
So if you must draw right away, you can go ahead and use lock focus and unlock focus. But most of the time, in your apps, it's not going to be necessary. And all you'll need to do is call set needs display or set needs display in rect, and then the view updating machinery can take it from there. And the main piece of advice I want to give you here is let the framework do the driving. It really is a benefit for performance to do that wherever you can. Okay.
So whenever you're going to do the kind of drawing that you used to do with PostScript code, you use the NS BezierPath class. It's got the familiar path construction operators such as move2 and line2 and relative line2 and curve2 and so forth. It also has convenience methods such as BezierPath with oval and rect, or BezierPath, there are convenience methods that will give you a rectangle. And I happen to like to add methods to BezierPath to give me other kinds of shapes that I might use frequently in an app.
You can use a path as an object that will add to your clipping boundaries, and you can make it the current clipping path with setclip, or add it to the current clipping path with addclip. NS BezierPath can also give you character glyph outlines. You can ask for these from NS BezierPath after you've gotten a glyph from NS font.
And one thing that I've seen happen very often, and in fact I've made this mistake myself in a lot of code, is that people will often create paths and then just throw them away, and this isn't actually necessary. Since the NS BezierPath object is a container for these path building commands, you can just empty it and reuse it, and that's a little more memory efficient.
BezierPath, of course, does Bezier cubic lines, thus the name of the class. And it's got the other PostScript marking features that you might expect, like setting the line caps, setting the flatness of curves when you render them, and so forth. So most of the things that you would look for in PostScript or in PDF, when you're drawing in a Cocoa view, you probably want to look to the NSBezier class.
And let me also mention there are a lot of really simple and convenient drawing functions in the app kit. If you just need to fill up a rectangle with a color or fill it with a color using a compositing operation, whenever you think that there probably should be a function to do what you're about to do, take the time to look for it, because it probably is there.
We're going to mention NSImage. Now, I often hear the question, how do I get to this particular pixel of my NSImage instance? And the answer is, you don't, because NSImages don't have pixels. Not all NSImages are just lists of values over space and a color space. A lot of NSImages, for example, PDF or resolution independent. So some NSImage reps have pixels, not all do, and whenever you're going to try to get the values right, you're going to have to do a lot of work.
If you're going to try to get the value of the color at any particular location, you're going to have to actually render that image first and then try to read the value back. So the point I wanted to emphasize here is that some NSImage reps have pixels, some don't, but NSImages, the containers for NSImage reps themselves, do not have pixels. Now, NSImage is also something you can use as a destination for off-screen drawing, and in my demo, I'll show you a little example of how I did that in one case.
Something to remember, when you're drawing into an NSImage, it's not an NSView, so the NSView methods for manipulating the coordinate space are not there. The Quartz2D functions for manipulating your coordinate space are still there, and you can still call all of those functions, and whenever you've got focus locked on a view, you have an active Quartz drawing context in all of the coordinates.
So you can use that to draw any of those functions that directly manipulate that context are still valid. Now, another thing that's kind of handy here is that you can use the NSAffineTransform class to describe any linear transformation of coordinates, and NSAffineTransform can be applied in any drawing context.
A couple more things about NSImage. In Jaguar, we gained the ability to draw NSImages progressively, so if you're loading something expensively over the net, you can get notifications that tell you, I know enough about it to tell you how big it is, I know enough to render a band of pixels, I know enough to actually render the entire image. And every time you get this notification, you can just draw the image, and it will draw as much of it as you have at that point.
We've also got features added in Panther for multi-frame images, so if you have an NSPDF image rep that you've loaded from a PDF file, you can set the current page that it will draw when it's told to draw by calling the set current page method. And if you're going to go through something like a PNG or GIF file that's got multiple frames, you actually can set them, well, you go through set property with value.
to get to the individual frames and set which frame you're looking at. The other thing that you would have in a GIF image, for example, is the current frame duration, which you can query. So if you're going to animate a GIF in Jaguar, this is how you'd get at it, basically as a dictionary of attributes of the image rep.
I want to talk a bit about the compositing of NS images. The one I tend to use most often is Composite to Point from Rect Operation Fraction. And you could also use Draw in Rect. I will point out that the Composite to Point and the Draw in Rect functions do not behave the same way. One is more expensive than the other.
Draw in Rect will honor the fact that you've rotated the coordinate space if you've done so. Composite to Point doesn't bother. It's the fast path, so if you don't need to actually fit that image into a rectangle that's different from its original size, then that's the one you would prefer. If you want to just composite a solid color, you can call NS Fill Rect Using Operation, which apparently a number of people never found while they were reading through all of the AppKit functions that I just suggested everyone read. up on.
And let me just give you my little demonstration of combining Vector and Raster Drawing. So this is a sample that is just sitting out there at the DTS website right now. You can go to the DTS sample code page and go to Cocoa and pick this project up called Cropped Image. And what it will do is it will take a path, draw it into an image, and then use that image as a map. And then it will take a mask to composite another image. So if I want to, for example, just select the mandrel's eyes here.
What's happening here is that that path that I showed you before is being composited against the mandrel image, and this is the result. Now I can turn edge smoothing on or off here, that's a graphics context function. But the point I'm trying to get to here is that this kind of thing, combining the vector with the image drawing, is a way to get some fairly sophisticated effects, and it's not a terribly expensive thing to do. Let me just show you the code for that.
So I have a path here that I got from those mouse events. And what I do to make that cropped image is I make a new image that is the same size as the image that I'm going to be cutting down with the path. I lock focus on that new image, fill it up with a solid color, and then I take the existing image, composite with the source in operator, and the result is that basically that mask punches out the area that I've selected. Let me show you one other thing that I was able to do by compositing. Actually, I wanted to show you the running app, not the project here.
Okay, here's another copy of the traditional mascot of image processing code. And what I've done here is I've taken this original image and basically composited against solid colors using the plus darker method to extract each of the components in turn. And then back here I reassemble them using the plus lighter method. So I can just turn these on and off and see the effects of various color components being composited back or not. So let's go back to the slides, please.
Okay, I will mention, as I always do in my demos, the things that I did wrong in that demo. First of all, I was drawing way too much, and if you go and get that code, you'll see where I made the mistakes. Basically, every time I was drawing the cropped image, I was redrawing the entire area covered by the image view.
Also, I'm pretty sloppy with the Bezier paths. I'm actually creating them and throwing them away every time there's a mouse event. I can get away with that on G4, but probably not on like a Bondi Blue iMac. I also didn't bother with scaling the path, but you can see that when you go get the sample yourself and try resizing those image views.
will just briefly mention text. It's a pretty big subject, and Doug Davidson is going to be talking about that in session 427, which will be the last session in the big room upstairs on Friday. There's a lot of power in the Cocoa text system, and if you really want to dig into the meat of that, Doug can fill you in on how to do that. I'll just mention that if you just want to draw a label or something, you can just send messages to NSString or NSAttributedString to have them render in your view.
If you're going to do text that has maybe a couple of lines to it, or maybe you want to edit the text in your custom views, NSL objects are kind of handy for this because they know how to deal with that. For more information on the text, please catch Doug's session.
A couple more AppKit classes that are involved in 2D rendering. There's the Affine Transform, which, as I said, will contain any combination of rotation, scaling, and shearing, and you can apply it to the current graphics context. There's the NSGraphicsContext method, which is where you store current drawing attributes, such as what your current drawing color is, what your current transformation matrix is, and what level of image interpolation quality you may have on.
There's the NSScreen class, which you use to get information about just what displays are available on your particular system that you're running on. And Troy Stephens is going to come up now and talk about the details of what's changed in NSVU and how you may want to alter your drawing when you're getting ready to deploy your apps for Panther. So Troy? Thank you, John.
Thanks very much for that. Hello, everyone. My name's Troy Stephens. I'm a software engineer in the Cocoa Frameworks group at Apple. And among other things there, I work on the NSView class. NSView, as you may know, is the base class for NSControl, as well as for pretty much every other kind of object in a Cocoa application that has some notion of where it lives in a window, that has the ability to receive and process mouse events, keyboard events, and that can draw itself.
So there are a couple of topics relating to NSView that I want to address with you today. First of all, we'll touch on a technique-- actually, a couple of techniques-- for hiding views when you want them to temporarily disappear from your user interface. So we'll talk briefly about that.
And then for the meat of today's talk, we're going to talk about some techniques that you can use, both on shipping versions of Mac OS X, on Jaguar and earlier, and then starting on Panther, some new features that we've added to help you to actively optimize your view drawing, to draw as little as possible so that you can really have your application scream, even if you're only running on, say, a G4.
[Transcript missing]
One thing you can do to very easily get around this is to take some sort of a proxy view and replace it, substitute it, for your hidden view. You take the hidden view out, and you put, say, an instance of NSView, which doesn't draw anything by default.
That'll do fine. You take an instance of NSView, set it up with the same spring settings in code, give it the same origin and same frame size as your view. And as the super view is resized, as the user drags the corner of the window to resize things, your proxy view will get the effects of the auto sizing. Then when you want to show your hidden view once again, you simply take the size and position of the proxy view, swap them onto the hidden view, reinsert it in super view, so you've solved that problem.
Another thing you may need to be concerned about, however, is your key loop. Interface Builder enables you to explicitly define a tabbing order for your controls. When users hit Tab or Shift Tab on the keyboard, they may want to be able to navigate among your controls without using the mouse, without clicking in them. If you've defined an explicit key loop, you take the view out of its parent, well, it's lost its position in the key loop. So when you go to show it again, you may need to figure out where does it go in the key loop.
You'll have to reinsert it explicitly where it belongs in the key loop. Also, if the view, if you're using tool tips, if you have any cursor rects or tracking rectangles that you've defined on that view, you may also need to manage them when you hide and show the view.
Now, it's pretty rare that you'll have a given view that you want to hide for which all these things apply. You may not be using all of these features. So in general, the remove from super view technique works quite well, and you usually don't have to worry about all this complexity. John Randolph, Troy Stephens Another thing you may need to be concerned about, however, is your key loop. Interface Builder enables you to explicitly define a tabbing order for your controls. When you hit Tab and you hit Save, it will automatically show the key loop.
However, you may be wondering at this point, well, this is a little strange for Cocoa, a little atypical. Usually, Cocoa makes simple things simple, right? So can't we make this a little simpler? Can't we have a simpler way where we can just send a message to a view and ask it to hide itself, let AppKit take care of all of it? Something maybe sort of like this. Well, by popular demand, this is a new API in Panther, as Elie Ozer mentioned in the Cocoa update talk, set hidden. You'll notice the method signature is the same as the set enabled call.
That's a new API. And you'll notice that the method signature is the same as the set enabled call that's available in the app. And you'll notice that the method signature is the same as the set enabled call that's available in the app. And you'll notice that the method signature is the same as the set enabled call that's available on NS Control. You send the same message to show a view again as you send to hide it. You simply vary the bool parameter.
So to hide a view, you send it a set hidden message with a parameter of yes. The view immediately disappears from the user's perspective. It's gone. In addition, any subviews that that view may have, and any subviews that they may in turn have, will also disappear. Those views are implicitly hidden by virtue of being contained within that view.
From your perspective as a developer, however, that view is still very much there. If you look in the views parents subviews list, you will still see it there. As a result, it gets auto-sizing behavior when its parent view is resized. It also stays in the key loop. You don't have to worry about it. It's ignored when the user is tabbing through control, so you don't go off into the weeds, as Ali said.
You don't have to worry about that. And also, AppKit automatically takes care of any tool tips, cursor wrecks, or tracking wrecks you may have. Those are temporarily disabled while the view is hidden. Pretty much everything that you would expect to have to worry about, AppKit will automatically take care of for you. In addition, if you have a view such as an OpenGL view or an NSMovie view that renders to a hardware surface, that surface will be ordered out and automatically ordered back in as appropriate.
So on Jaguar and earlier, you can use the remove from super view technique. We've seen some ways that you can deal with some of the details that you may have to worry about when you're doing that. So that works fine on Jaguar and earlier. On Panther going forward, we have the set hidden API that takes care of all of this stuff for you. It works with any NS view class. Hiding views is dead simple in Panther.
We've given you a way to set a new kind of state on an object, the concept of being hidden, so it's only natural that we should provide some getter accessor methods, and we have two in this case. The reason for this is the fact that I mentioned earlier, that hiding a view is, in effect, hiding the entire view hierarchy that may be rooted at that view. An ordinary control doesn't have subviews, but you may have a box or some other type of view that has subviews.
When you hide that view, setHidden, I want to clarify, is not a recursive operation. When you send setHidden to a view, it's not like setHidden is then recursively sent to all its subviews. We don't do it that way, but rather we implicitly take that knowledge that some superview of a view is hidden, and we apply that as state to the view. So you can think of isHidden, the main accessor method, as the atomic counterpart to setHidden.
The value that it returns reflects the state of the object you are asking, and no other. By contrast, we also have this method isHidden, or hasHiddenAncestor. This answers the question that you probably more commonly will want to be asking about a view. I don't care if it's hidden because somebody asked it specifically to hide, or if it has some other view in the view hierarchy that has been asked to hide. I just want to know if it's hidden from my window. And so isHidden, or hasHiddenAncestor, is the message.
You can use this message to send when you want to do that. We try to be concise when we choose method names, but above all else, we also like to be clear. So isHidden, or hasHiddenAncestor, answers that question. This is, in some sense, sort of a convenience method.
You could implement this yourself in terms of isHidden quite easily by walking up the view hierarchy, but it's such a common question to want to ask. We've implemented this for you, and in fact, that state is cached, so it's a very inexpensive constant time operation to ask a view if it's been hidden, if it has a hidden ancestor, whatever. So that's view hiding in Panther.
That was view hiding in Panther. Now I'd like to move on to talking about optimizing view drawing. John showed us some fantastic things that you can do with the spectacular drawing capabilities of Quartz in Cocoa. There are some, all of these things, all of these drawing operations have an associated cost. And whenever possible, you of course want to avoid doing work that you don't have to do.
You want to avoid sending instructions to the Quartz graphics pipeline that don't really have to be processed. Maybe that drawing is going to be clipped out. And if you can figure that out at a much higher level, then your applications can run much more quickly and be more responsive when the user drags things around in your view and so on.
So we'll look at some techniques that you can use to optimize view drawing on all versions of Mac OS X. And then we'll also look at some of the improvements we've been working on for Panther to help automatically improve performance for you. Whenever possible, we like to try to make optimizations that require no work on your part, that enable your applications to automatically inherit the benefit with full compatibility and without having to do any additional using of new API and so on. But we also do provide new API that we'll see that can help you to more tightly constrain the drawing you're doing so that you're sending less down to the Quartz graphics pipeline and your applications can respond faster when the user's dragging objects around and so on.
So first, some things you can do on all versions of OS X. The first thing I want to encourage you to do here today is to be lazy. And you can go back and tell your manager that I said that. I mean this in the most positive sense of the word. As developers, we all know that laziness can be a virtue.
Aversion to doing work on your applications part, anyhow, can help you to avoid-- Having the system, having the CPU, having the graphics system do work that is unnecessary or redundant. In particular, and John touched on this earlier, you may have noticed that NSView has some display methods. There is display, display if needed, display in rect, display if needed in rect. What these methods do is effectively demand that a view immediately display some portion of itself.
In other words, this is a synchronous call. By the time the message send returns to you, that view has been drawn. But instead, instead of invoking those display methods directly, although they are there for you to use when appropriate, when that's really what you want, we recommend that you mark areas of a view as needing display. You can use set needs display in rect, preferably. Rectangles are the primitive for invalidating parts of views when some state in the view or its object model changes.
And we also have the set needs display method, which you can use more generically to just say, well, draw this whole view. This view needs drawing at some point in the future. This is, of course, a common paradigm that you'll be familiar with from other graphic systems, other windowing systems.
By deferring drawing until later, until, say, the end of the current run loop cycle, what you're doing is enabling the app kit to potentially coalesce, to combine, to satisfy multiple requests, to draw a single view or part of a view with a single drawing operation. If you tell a view to display itself three times, it's going to draw itself three times. If it receives 100 set needs display in rect calls, it will draw itself at the end of the run loop once. So use set needs display in rect.
Defer your drawing until later when possible. One of the things that we've seen on the mailing list is people have recommended display, use of the immediate display methods, as a means to get around a problem we call the coalescing problem, an inefficiency that occasionally appears in certain applications in the drawing system.
We have fixed that inefficiency in Panther, and so we're encouraging you. We're saying you no longer really need to do that. Those methods are there for you to use when you need them, when you really need immediate display. When you don't, use set needs display in rect and so on.
Another thing that you can do to help us out is only invalidate areas of your views that really need to be drawn. Ask yourself, is set needs display, yes, really the best that I can do? Oftentimes, that is the case when you're a client external to the view object.
But if you're the view itself, and some part of your state has changed, well, maybe you know that you only need to repaint some corner of yourself, and you don't need to redraw your text label. Maybe you just need to change your icon to a different state. So rather than marking your entire self generically-- I know that's always the easiest thing to do, just say, I need display-- mark specific areas as needing display. And that will save you drawing work when your drawRect method gets called further on down the line.
In addition, sometimes people as an optimization have consolidated multiple rectangles on their end before sending a set needs display and rect method message to the app kit in order to avoid method call overhead. Let's say you may have a list of rectangles that you need to draw, and you may simply take their union, take a bounding rectangle that contains all those rectangles, and make only one set needs display and rect call to that view, telling it just draw everything in here. We want to encourage you to send those individual rects to the app kit now, because as of Panther and later, we can now take advantage of that information to help you draw less.
A couple of other things you can do. On the receiving end, when your application receives a draw rect call, Note that you get a parameter. DrawRect gives you a bounding rectangle that asks you what to draw. There are a lot of applications out there where we see people are just drawing the entire contents of the view.
For some views, that's not very expensive if their content isn't very complex, but whenever possible, you should try to only draw whatever's in that rectangle, because that's all you're really being asked to do. Any other drawing you do is going to be clipped out automatically anyway, so it's cheaper to eliminate it higher up in the drawing pipeline.
So here we have an example DrawRect implementation. Supposing we have a view here, say like SketchView, if you've seen the Sketch example, that draws a list of objects, has a set of objects that it draws, so we simply have a loop where we iterate over those objects, and for each object, we use the NSIntersectsRect method. Hopefully you're familiar with the convenience functions that are present in NSGeometry.h. That's a good place to look if you're looking for geometry functions.
It's a function, not a method. And so for each object that we're potentially going to draw, we ask, well, is it inside this rectangle? If not, well, then don't even bother drawing it, because it's just going to get clipped out. Doing this alone can save you a fair amount of drawing.
One other thing you can use is isOpaque. And this is somewhat mysterious maybe because of the name. isOpaque is defined by the NSView class. There's a default implementation that returns no. You'll note that most of the controls in Aqua require some drawing to be done behind them in order to show their complete image. Things have rounded corners. Even if they don't really use transparency, they don't cover the entire rectangle that they own. They don't cover their entire frame rectangle with drawing.
By default, therefore, the app kit cannot assume that a view can be drawn without also having to cause drawing of all the views behind it, because they may provide the background. You may have the window itself trying to draw a textured metal background so that then your checkboxes, checkbox image, and then text can be drawn over that.
So we can't make that assumption by default, but by overwriting isOpaque to return yes in your custom view classes, you can potentially save a great deal of drawing. You tell the app kit that you're going to cover all of the pixels in your view with 100% opacity, so you don't need any drawing to be done behind you.
This can obviously be a savings if you're implementing a document view, like, say, Sketch View. And you have this large document view that's often covering your entire window, and your user maximizes your window on a cinema display, and maybe your window is a metal window. I mean, you obviously want app kit to know that it doesn't have to draw that entire textured metal pattern behind every part of your view that is asked to draw, because it's never going to show through. So if the first thing you do in your DrawRect implementation is to cover everything with 100% opaque fill, then overwrite isOpaque, this one line of code, will be one of the most worthwhile things that you've done in your drawing.
So what's new in Panther? What have we changed? One of the important things we've done in the view system is that we're preserving areas marked as needing display in greater detail. In Jaguar and earlier, you may have noticed by experiment that each view really only kept a single rectangle that was marked as needing to be drawn for each view instance.
Any time we would get a set needs display in rect method, every time we would get a set needs display in rect message, we would cumulatively union that rectangle into any existing rectangle that was marked as needing drawn. This could lead to unnecessary drawing, in particular, Going to a more sophisticated, more detailed representation of areas needing drawing enabled us to solve what's called the coalescing problem. And you may have seen this in your app.
Very simply, you would sometimes see drawing of views that were in between other views that had been asked to draw within the same trip through the run loop. For example, we have a UI here where we have a checkbox in the upper left and a button in the lower right and a table view in between. And let's say that this is wired up in code so that toggling the checkbox toggles the button to be enabled and disabled. So when you toggle the checkbox, it needs drawing to reflect its new state. The button needs drawing to reflect its new state.
And in the old system, we would end up drawing the table view also because we have to clear the window background behind that entire rectangle. So the fact that we were only maintaining a single rectangle per view and only clipping to that rectangle would sometimes lead to unnecessary drawing. No more in Panther. We've addressed this problem, and we've enabled views to draw their contents much more selectively.
A lot hasn't changed, however. We try not to change things when we don't have to. Rectangles are still the invalidation primitive. You still use set needs display in rect to mark areas of your view as needing drawing. DrawRect is still the basic method to override when you're creating a custom view subclass. So that's where you put your drawing instructions.
It is still called once each time the view needs drawing, even though we have this list of rectangles that need to be drawn. We're only calling DrawRect once to ask you to do all your drawing for that pass. In addition, of course, for compatibility, we had to make sure that your existing DrawRect implementations required no modification whatsoever for your apps to work on Panther.
So your applications, I want to emphasize, will inherit a great deal of the benefit of this automatically. But we do provide API for you to get at this information. We're keeping around more information about what's dirty in a view, what needs to be drawn. And should you take an interest in that information and want to use it to more tightly constrain your drawing, it is available to you. We have this new API, this new method on NSView, getRectsBeingDrawnCount. It returns by reference a C style array and a count of elements in that array of NSRect structures.
You call this on entry into your drawRect method, and it gives you back a list of rectangles that more tightly bounds the area needing drawing. AppKit automatically clips to that list of rectangles. This is how we're compatible with existing drawRect implementations. So even if you draw everything in your view, you can even ignore the arect parameter to drawRect. You will be clipped down to just the area that AppKit wants you to draw.
Also, you'll note that NS Rect, that Draw Rect's existing NS Rect parameter is still useful as a bounding box on the area on the set of rectangles that you're retrieving. So you can use it to do trivial rejection testing, as we'll see, and more easily reject objects very quickly that can't possibly be within the list that we need to draw.
Also, you don't need to worry about managing memory for the Rect list. That's taken care of for you. It exists for the lifetime of the Draw Rect invocation. So to help make this more concrete, hopefully, here's an illustration we can look at. Let's say we have a view, and we have a list of rectangles that have been marked dirty. Each of these corresponds to a set needs display and rect call for the view.
And there's some redundancy there. What would happen on Jaguar and earlier, as I said, is these would be coalesced into a single rectangle that would be sent to draw rect as its parameter. That is the rectangle we would clip to, and that is the rectangle that you're being asked to draw. Beginning on Panther, however, we have a list of rectangles that we are clipping to. We have a list of rectangles that really more accurately bounds the area you're being asked to draw.
As I said, you still get the bounding rectangle of those rectangles as your parameter to draw rect. And you'll notice that there's some simplification that goes on. This isn't just the same thing as the list of rectangles that's been marked as needing display. We've eliminated the redundancy. And also, these rectangles do not overlap each other. They may abut one another exactly, but they do not overlap. So there's no redundancy there.
So how would you use this API? Well, here we have a sample drawRect method for a view, like a sketch view, that draws some objects. The first thing we do, you'll note, on entry into drawRect, is we invoke getRectsBeingDrawnCount. We get the list of rectangles that are being drawn. And then we go into that same loop over the list of objects that we know we need to display in our document.
For each of those objects, we first intersect it, not with any of the rectangles in the list, but with the single rectangle parameter to draw rect, with the bounding rect. This is called the Trivial Rejection Test. We know that if that object that we're trying to draw does not intersect that overall bounding rectangle, there's no possible way that it could intersect any of the sub-rectangles that we're really needing to draw here. So we can reject the object out of hand.
If it does pass that test, we then go in and test it against the list of rectangles, usually unless you have a pathological case of invalidation. That is a list of a handful of rectangles, and if the object intersects any of those rectangles, you go ahead and draw it then.
So it's sort of a weeding out process. In 3D graphics, this is often called culling, to distinguish it from the much lower level process of clipping, where you're talking about actual fragments being drawn, being clipped out. At the graphic system level, if we can eliminate things at the document object model level, then we can eliminate a lot of potential processing that would have to be done for nothing, because it's just going to be clipped out anyway.
Oh, jumped the gun on myself. So this is a common thing to want to do, to first do the trivial rejection test, and then test against a list of rectangles only if necessary. So we provided a convenience API to make this easier to do. Needs to draw rect. You pass it in a rectangle that bounds the thing that you want to draw or want to find out if you really need to draw.
And if that method returns a Boolean yes, you go ahead and you draw the object. So we've taken this code and reduced it down to this for the simple case. You'll note that if you're running on Panther and later, you know that this is really no more complicated than testing against the arect parameter that we get to draw arect. We've got the same number of lines of code, so you can take advantage of this to very easily weed out stuff with almost no effort that you really don't need to draw.
So maybe we don't have a list of objects. What if we have sort of a monolithic object, like, say, a PDF image or some other type of image? How can we take advantage of this list of rectangles to draw more efficiently, to reduce the amount of drawing we do to the absolute minimum? Well, for an image, for example, we can look at that rectangle list as specifying sections of the image that we can sort of cookie cutter out and blit into just the areas that we're being asked to draw. And that's what we do here in this draw rec method, which is somewhat more complex, but I want to focus your attention on just the parts in orange. Again, on entry into draw rec, we're getting a list of rectangles that we're being asked to draw.
[Transcript missing]
However, if you have something like a document view, like a sketch view that draws a lot of complex objects, maybe an iCal calendar view, each of those objects in an iCal calendar, well, it's got nice rounded corners to it, and it's got both fill and an outline, and it can be semi-transparent, and it's got text on it, and maybe another little icon in there.
Each of those things is costly to draw, and you potentially have a lot of them, and maybe you can have this view size pretty large on your cinema display, and be dragging calendar items around, and for each drag, for each time the mouse moves a little bit, you're going to have to do some redrawing.
Erase the thing in the old position, draw it in the new position, that's potentially a lot of drawing. If you're dragging big distances in one jump, that can be coalesced into a lot of drawing. So, cases like that, complex document views that have a lot of drawing to do, that have complex content. Those are cases where you would expect that you can get some benefit from this slightly additional complexity in your draw rec method.
There are some observations we can make about this last example. For one thing, since we're doing this sort of cookie cutter technique that I described of just drawing sections of the image, we don't really need the clipping that AppKit is providing for us. AppKit is enforcing clipping to the list of rectangles that we're being asked to draw. But clipping is an expensive operation, potentially. It costs a certain amount to set up that clipping state in Quartz.
And if you have a disjoint set of rectangles that you're clipping to, that can be more expensive than, say, clipping to a single rectangle, or better yet, not clipping at all. So if we don't need this clipping, isn't there some way we can avoid the performance cost that's involved in setting it up and using it? And in fact, we can now with a new API in Panther, wants default clipping.
NSView has a default implementation that returns yes. You can override this to return no to tell the AppKit that your view is going to be responsible for itself, that you promise you're not going to draw, that you're not going to draw anything that lies outside of those recs. You're going to draw exactly within those boundaries. And by doing so, you can reap the benefits of not having that clipping set up for you.
Whether this is important or not, again, whether this makes a difference in your application depends a great deal on how often your view is asked to draw, how much it's asked to draw, how complex its content is. But this is something that's there for you to try out if you're really focused in on optimizing drawing performance in a particular view class.
There are some different possible implementation strategies that this WANs default clipping method frees you to use. For example, one thing that we could do, as we did with the image view, we can make our outer loop be over the list of rectangles to be drawn, rather than some list of objects to be drawn. And we can choose to clip.
We can enforce our own clipping to one rectangle at a time. Just because you override WANs default clipping and you return no doesn't mean you can't enforce your own clipping. And maybe you want to use simpler clipping, clipping to a single rectangle at a time. Again, this is very application dependent.
Depends on the content of your view. You have to use your own judgment. We encourage you to use profiling tools like Quartz Debug and Sampler to figure out, well, is your app spending a lot of time drawing, first of all? And which views is it spending a lot of time drawing during different activities? Figure out where you can get the benefit, and then try using these APIs.
So we've seen a few new APIs in Panther that we hope you'll find useful. We have the concept of being hidden that is now supported. Makes it very easy to hide views on Panther going forward. And we also looked at some techniques that you can use to almost as easily hide your views in Jaguar applications.
We also introduced some new APIs that you should be aware of that can enable you to do less drawing, to avoid unnecessary or redundant drawing in your custom view classes. These are there for you to use if you want to. I don't want you to have to worry about them. If you're implementing a simple little custom control that just draws a few things, covers a very small area, you're probably not going to get much benefit from these.
You don't need to worry about it, and particularly for new Cocoa developers who may be with us today, we want to emphasize that you can implement your draw rec methods as before, you can ignore this list of rectangles, you don't even have to ask for it, you can draw and we will automatically clip for you. But it's there for you to use if you'd like to. And with that, I'd like to invite John Randolph back up to do the wrap up, after which we'll be happy to take your questions. John? Oh.
Okay, thank you, Troy. And I will just mention that all we've talked about today about optimizing your view drawing, the easiest way to optimize all your drawing is just deploy only on new G5s. We're working on it. Believe me, there are people in production who are burning the midnight oil on that.
So we talked a little bit today about how to draw using classes in the Cocoa framework and how that's changing for Panther. And I did show you one example of how to do some fairly sophisticated things without a lot of code. So we'll just repeat the Cocoa mantra here. Simple things simple and complex things possible.
Let me mention the roadmap here. Some of these we've already gone past. Here are the ones that are coming up that you might be interested in. Cocoa Performance Techniques tomorrow morning. Cocoa Tips and Tricks, which has a lot of the details about how particular clever things were drawn in some Cocoa apps that Apple has put out.
Doug Davidson will be giving you the in-depth talk on what's new in Cocoa Text in session 427. If you want to talk to us about what you'd like to see in Cocoa, what you'd like changed and so forth, you can come to the Cocoa Feedback Forum. Who to contact about this, you can reach me often on the Cocoa Dev mailing list.
If you have questions about our 2D drawing API and our 3D drawing API, Travis Brown is the Graphics and Imaging Evangelist in Apple Developer Relations. His boss, John Galenzi, is the guy to go to for other questions having to do with how... how we're evolving our software offerings. John is also the guy to go to to get a UI review of your app. He is still the user experience evangelist. And you can also send your Cocoa questions to developer tech support at [email protected].
So there's also this mailing list called Cocoa Feedback at group.apple.com. I know the managers of the Cocoa Frameworks teams read that list. Most of the engineers do, too. If you want to get an idea into our collective consciousness, then you can send it to Cocoa Feedback. I would suggest that you also file feature requests at bugreporter.apple.com. And for more information on Cocoa, there's a host of documentation on any machine that's got the developer tools installed. Well, the AppKit reference and so forth.
There's a lot of sample code right there on your machine in Developer Examples AppKit. There's a lot of other samples up on the DTS website, so if you go to developer.apple.com, click the sample code link, click Cocoa after that, a lot of examples and more being added all the time. Today, there's a very good selection. We're going to take a look at the collection of Cocoa books out. This was not the case two years ago.
Today, there are a lot of them to choose from. I see some of the authors in the audience here. And let's see, there's a couple more book titles here. I suggest that you join the Cocoa Dev mailing list. It's a very good place to get help when you're learning how to code in Cocoa or when you're going for more advanced subjects.
And I also recommend the mailing list that's run by our friends at the Omni Group, which you can get at the Omni Group website. It's a very good place to get help when you're learning how to code in Cocoa or when you're going for more advanced subjects. I also recommend the mailing list that's run by our friends at the Omni Group, which you can get at the Omni Group website. can get to at omnigroup.com.