A Dash of Quartz Drawing by Seth Willits
01-28-06




Today we're just going to take a brief look at drawing in Quartz thanks to the new Graphics.Handle feature in REALbasic 2006r1. What we'll do is setup a Quartz CGContextRef for a Canvas and clip it to the bounds and then draw a rectangle with a shadow in it.




A Bunch of Declares

Let's first get the declares out of the way. You really don't need to know how to create these, if you do because you plan to make more, well, this isn't quite the place to learn since it requires a bunch of background knowledge anyway.

Declare Function QDBeginCGContext Lib "Carbon" _
  (CGrafPtr as Integer, ByRef CGContextRef as Integer) as Integer
Declare Function QDEndCGContext Lib "Carbon" _
  (CGrafPtr as Integer, ByRef CGContextRef as Integer) as Integer
Declare Sub CGContextSaveGState Lib "Carbon" _
  (CGContextRef as Integer)
Declare Sub CGContextRestoreGState Lib "Carbon" _
  (CGContextRef as Integer)
Declare Sub CGContextClipToRect Lib "Carbon" _
  (CGContextRef as Integer, rect as CGRect)
Declare Sub CGContextTranslateCTM Lib "Carbon" _
  (CGContextRef as Integer, tx as Single, ty as Single)
Declare Sub CGContextFillRect Lib "Carbon" _
  (CGContextRef as Integer, rect as CGRect )
Declare Sub CGContextSetRGBFillColor Lib "Carbon" _
  (CGContextRef as Integer, Red as Single, Green as Single, Blue as Single, Alpha as Single)
Declare Sub CGContextSetShadow Lib "Carbon" _
  (CGContextRef as Integer, offset as CGPoint, Blur as Single)


The Drawing Code

Ok, now to the code to draw in the canvas. The first thing we do is just draw the white background and declare a few variables. Nothing fancy there. CGContextRef will hold the pointer to the CGContext variable which is synonymous to the "Graphics" class of Quartz.

Sub Paint(g As Graphics)
  *<the declares go here>*

  // Background
  g.ForeColor = &cFFFFFF
  g.FillRect 0, 0, g.Width, g.Height
  
  
  dim err, CGContextRef as Integer
  dim rect as CGRect
  const noErr = 0
  
  
  // Open CGContext
  err = QDBeginCGContext(g.Handle(Graphics.HandleTypeCGrafPtr), CGContextRef)
  if err <> noErr then
    break
    return
  end if
  CGContextSaveGState(CGContextRef)


The secret is right above here. What we do is call the function QDBeginCGContext which gets a CGContextRef from an old-style QuickDraw "CGrafPtr". Now the CGContextRef here is for the entire window. The reason it's for the window and not the canvas is that the canvas is (apparently) just an interface to drawing into the window's buffer at differtent times and with different clipping rects.

So what we need to do is first clip the bounds of the context (that is, limit the area we can draw into) and then translate the coordinate system so that it matches that of the canvas. One thing to note is that Quartz uses a bottom-left coordinate system. That means that it's like a normal Cartesian coordinate system. It's like making an "L" with your left hand - your thumb points to positive X and your first finger points to positive Y. This is really useful in some cases but in others its not. Unfortunately, I haven't been able to find a way to flip the coordinate system of a CGContext so for now we're just going to stick to the Quartz system and realize that when we're drawing with Quartz we're using its coordinate system.


  // Clip to the Canvas's bounds
  rect.origin.x = me.Left
  rect.origin.y = self.Height - (me.Top + me.Height)
  rect.dimens.width = me.Width
  rect.dimens.height = me.Height
  CGContextClipToRect(CGContextRef, rect)
  CGContextTranslateCTM(CGContextRef, me.Left, self.Height - (me.Top + me.Height))


After that, we're going to setup the offset point for the shadow of and the dimensions for our rectangle and then draw it.


  dim offset as CGPoint
  offset.x = OffsetX
  offset.y = -OffsetY 'negative because quartz and RB use two different coordinate systems
  rect.origin.x = g.Width  / 2 - 50.0
  rect.origin.y = g.Height / 2 - 50.0
  rect.dimens.width = 100.0
  rect.dimens.height = 100.0
  
  // Draw Rect
  CGContextSetShadow(CGContextRef, offset, Blur)
  CGContextSetRGBFillColor(CGContextRef, 1.0, 0, 0, 1.0)
  CGContextFillRect(CGContextRef, rect)
  
  
  // Close CGContext
  CGContextRestoreGState(CGContextRef)
  err = QDEndCGContext(g.Handle(Graphics.HandleTypeCGrafPtr), CGContextRef)
  if err <> noErr then
    break
    return
  end if
  


After restoring the state of our context (since we changed the clipping and origin we need to save the existing state before we do that, and then restore it after we're done) we close it (which then synchronizes the drawing to the window's buffer) and we're all done. After that we can go back to using REALbasic drawing code.


  // REALbasic Drawing -- Unfortunately you can't draw using
  // RB while the CGContext is open. If you could, you could set
  // the shadow and then draw using RB and those objects could
  // get shadows, but it doesn't work that way, despite RB using
  // Quartz to draw
  
  // Border
  g.ForeColor = &c555555
  g.DrawRect 0, 0, g.Width, g.Height
  
  // Random Circle
  g.ForeColor = &c0000FF
  g.FillOval 30, 200, 40, 40
End Sub

Finished

I wanted to have much more complex project, but I ran into some annoyingly serious problems as I was writing the project, so it will have to wait for a future release of REALbasic to have them fixed. In the mean time, you can download this project.