Cocoa is cool and Objective-C is a great language, but I like/prefer Python and the PyObjC bridge makes my life easier and programming actually interesting/fun for me.
This Xcode project is an example of how easy it is to create useful custom views in PyObjC with an example of a graph view that displays a circular queue/buffer. Everything is done starting with an Xcode project which is available for download (76KB compressed archive) and modification. Here is a preview of what you can use out of the box:


The easiest and smallest part of the project is the RingBuffer class that implements a very simple circular queue/buffer and provides direct access to the list that stores the information:
# # RingBuffer.py class RingBuffer: def __init__(self, size): """ init with # of elements in the queue """ self.data = [ None for i in xrange(size) ] def append(self, x): """ take away one and put one in """ self.data.pop(0) self.data.append(x) def get(self): """ return the list so we can manipulate it """ return self.data
There's not a lot of rocket science there, but it's amazing how much code you can eliminate by taking advantage of Python's inherent capabilities. In this case, list initialization is a breeze and removal/insertion of new elements could not be easier.
Next up is the CBGraphView class itself:
# # CBGraphView.py from objc import YES, NO, IBAction, IBOutlet from Foundation import * from AppKit import * import RingBuffer class CBGraphView(NSView): dataQueue = None # holds the data we'll be graphing gradientGray = None # the gray color of the black->gray gradient we are using lineColor = None # the color to make the bars grad = None # the gradient object lineWidth = 1.0 # default bar width (1 "pixel") lineSpacing = 0.0 # default spacing between bars to no space def initWithFrame_(self, frame): """ basic constructor for views. here we init colors and gradients """ self = super(CBGraphView, self).initWithFrame_(frame) if self: self.gradientGray = NSColor.colorWithCalibratedRed_green_blue_alpha_(50/255.0, 50/255.0, 50/255.0, 1.0) self.lineColor = NSColor.colorWithCalibratedRed_green_blue_alpha_(33/255.0, 104/255.0, 198/255.0, 1.0) self.borderColor = NSColor.whiteColor() self.grad = NSGradient.alloc().initWithStartingColor_endingColor_(NSColor.blackColor(), self.gradientGray) self.grad.retain() return self def setDataQueue_(self, dq): """ set the data object we are graphig """ self.dataQueue = dq self.setNeedsDisplay_(YES) def setLineWidth_(self, width): """ let user change line (bar) width """ self.lineWidth = width def setLineSpacing_(self, spacing): """ let user change spacing bewteen bars (lines) """ self.lineSpacing = spacing def setLineColor_(self, color): """ let user change line (bar) color """ self.lineColor = color def setBorderColor_(self, color): """ let user change border color """ self.borderColor = color def setBackgroundGradient_(self, startColor, endColor): """ let user change the gradient colors """ self.grad.release() self.grad = NSGradient.alloc().initWithStartingColor_endingColor_(startColor, endColor) self.grad.retain() def isOpaque(self): """ are we opaque? why, of course we are! """ return YES def dealloc(self): """ default destructor """ self.grad.release() super(CBGraphView, self).dealloc() def drawRect_(self, rect): """ we raw the background gradient and graph outline then clip the inner rect and draw the bars """ bounds = self.bounds() # get our view bounds insetBounds = NSInsetRect(bounds, 2, 2) # set the inside ortion r = NSBezierPath.bezierPathWithRect_(bounds) # creatre a new bezier rect self.grad.drawInBezierPath_angle_(r, 90.0) # and draw gradient in it self.borderColor.set() # set border to white NSBezierPath.setDefaultLineWidth_(3.0) # set line width for outline NSBezierPath.strokeRect_(bounds) # draw outline NSBezierPath.clipRect_(insetBounds) # set the clipping path insetBounds.size.height -= 2 # leave room at the top (purely my personal asthetic buf = None # init the list structure we will be using if self.dataQueue: buf = self.dataQueue.get() # get teh list if buf: rbuf = [ q for q in buf if q ] # filter "None" from the list rbuf.reverse() # reverse the list self.lineColor.set() # set drawing color barRect = NSRect() # init the rect maxB = max(rbuf) # find out the max value so we can scale the graph # disable anti-aliasing since it looks bad shouldAA = NSGraphicsContext.currentContext().shouldAntialias() NSGraphicsContext.currentContext().setShouldAntialias_(NO) # draw each bar barRect.origin.x = insetBounds.size.width - self.lineWidth + 2 for b in rbuf: if b: barRect.origin.y = insetBounds.origin.y barRect.size.width = self.lineWidth barRect.size.height = ((int(b) * insetBounds.size.height) / maxB) NSBezierPath.fillRect_(barRect) barRect.origin.x = barRect.origin.x - self.lineWidth - self.lineSpacing NSGraphicsContext.currentContext().setShouldAntialias_(shouldAA)
This is the class you would use in your own projects. I like to to initialize the instance variables at the head of the class so you have an idea of what it is using. I find this much more helpful than watching for all of the "self.xyz" calls in init methods. What is laid out is a view that lets the programmer control the background gradient of the graph (pass in the same color for startColor and endColor in setBackgroundGradient_ to use a solid color, or – as you will see – no color), the border and line color along with the thickness of the bars and spacing between bars. You have to appreciate the fact that Cocoa just gives us the ability to create and apply gradients to paths with just 2 lines of code (true for Objective-C as well as Python).
The main work is done in drawRect_ (as it is in most NSViews). It begins with getting the bounds which lets us be a bit dynamic since you will probably create and size the views in Interface Builder. I deliberately hardcoded some values for this example so you had some work to do by making the border width more configurable, but we use NSInsetRect to get the inner rectangle where we will draw the graph leaving the outer area for a border.

We create a NSBezierPath rectangular path and fill it with a gradient then draw the border and set the clipping region to the inset rect we made previously. You never (or rarely) call drawRect_ yourself and it should only redraw when necessary. In this case it is necessary to redraw whenever the data changes, so your code should call drawRect_ method will then be called due to the self.setNeedsDisplay_(YES) call in setDataQueue_
We strip out the non-existent values, get the max value in the queue (so we can scale the graph height accordingly and dynamically) and disable anti-aliasing (you can remove that bit of code to see if you like the results...I did not). We then make NSRects for each bar and fill the bar with the designated color. This code could easily be modified to keep track of what is visible on screen and allow mouse-overs to get the resultant data displayed in a pop-up or tool-tip of some sort.
Since this test is simple, all the work is done by the app delegate. We first create a nib with two windows, one normal and one HUD. We add some generic views to each and then tell IB that they are of type CBGraphView:

# # CBGraphViewAppDelegate.py from objc import YES, NO, IBAction, IBOutlet from Foundation import * from AppKit import * from RingBuffer import RingBuffer import os, sys, random class CBGraphViewAppDelegate(NSObject): gv1 = objc.IBOutlet() gv2 = objc.IBOutlet() gv3 = objc.IBOutlet() gv4 = objc.IBOutlet() values = None DataTimer = None @objc.IBAction def windowWillClose_(self, aNotification): NSApp.terminate_(self) def getData_(self, notification): pool = NSAutoreleasePool.alloc().init() self.values.append(random.randint(0,2000)) self.gv1.setDataQueue_(self.values) self.gv2.setDataQueue_(self.values) self.gv3.setDataQueue_(self.values) self.gv4.setDataQueue_(self.values) pool.release() def applicationWillTerminate_(self, sender): self.DataTimer.invalidate() def applicationDidFinishLaunching_(self, sender): random.seed() self.values = RingBuffer(500) self.startTimeNS = NSDate.date() self.gv1.setLineWidth_(5.0) self.gv1.setLineSpacing_(2.0) self.gv1.setBorderColor_(NSColor.blackColor()) self.gv1.setBackgroundGradient_(NSColor.whiteColor(), NSColor.whiteColor()) self.gv2.setLineWidth_(1.0) self.gv2.setLineSpacing_(0.0) self.gv2.setBorderColor_(NSColor.blueColor()) self.gv2.setBackgroundGradient_(NSColor.yellowColor(), NSColor.whiteColor()) self.gv2.setLineColor_(NSColor.orangeColor()) self.gv3.setLineWidth_(8.0) self.gv3.setLineSpacing_(4.0) self.gv3.setBorderColor_(NSColor.clearColor()) self.gv3.setBackgroundGradient_(NSColor.clearColor(), NSColor.clearColor()) self.gv3.setLineColor_(NSColor.redColor()) self.gv4.setLineWidth_(3.0) self.gv4.setLineSpacing_(1.0) self.DataTimer = NSTimer.alloc().initWithFireDate_interval_target_selector_userInfo_repeats_(self.startTimeNS, 1.0, self, 'getData:', None, True) NSRunLoop.currentRunLoop().addTimer_forMode_(self.DataTimer, NSDefaultRunLoopMode) self.DataTimer.fire()
When the app is finished launching, we initialize (poorly) the Python random number generator and create a 500 element ring buffer. Your ring buffer should be a bit bigger than the graph display to be useful (it will never make it to the other edge if not). We then initialize the custom IBOutlets with various properties from the graphing class. The one interesting feature here is the g3 graph that has a NSImage behind it in the nib. By using NScolor.clearColor() we can make the background and border transparent. A very nice Cocoa feature.
Finally, we create a NSTimer that fires every second and calls getData_ which adds a new random number and sets the data queue for all the graphs we are managing. In this example, all the graphs use the same data source which gives you multiple views into the same information.
When the app is ready to quit, we release the thread and are done.
There is tons of room for expansion to this class, but you should be able to use the base functionality without issue. Remember to add CBGraphView to your main.py for the magic nib-to-app-fu to work.
If you have questions, thoughts or have enhanced the code in anyway (or use it in an app), drop a note in the comments.
This really helped. I've had
This really helped. I've had a hard time finding PyObjC samples that use XCode and IB.
Had to change setBackgroundGradient_ to setBackgroundGradientStart_andEnd_ to get it to work.
Cheers.