GNUstep Developer: Drawing in Custom Views

Copyright (C) 2017 Graham Lee.

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the page entitled “GNU Free Documentation License”.

1 About This Guide

This is a guide to creating custom view classes in GNUstep and drawing your own views.

The guide assumes that you have completed the Introduction to ProjectCenter and gorm, and builds on the project created in that article. You can either work through that article now, or clone the bitbucket project and start where the other guide leaves off.

2 Creating a Clock Face

The digital clock you created in the ProjectCenter and gorm introduction is alright for some, but other people prefer a traditional, analog clock appearance. GNUstep doesn’t contain a ready-made clock view, but it’s easy to make one.

What GNUstep does give you is NSView, a class that represents the abstract essence of something that can be drawn, but has no knowledge of what to draw. Much of what you see on the screen in a GNUstep application is drawn by NSView or its subclasses—a window has a content view which draws its background; textfields, labels, buttons and other controls are all also views.

Subclassing represents an specialisation action: the subclass is a version of its parent class, more specifically suited to a certain task. By creating a new view class as a subclass of NSView, you can create something that works in the same way as all of the other views in GNUstep, but that draws content relevant to your application. Open the Clock project in ProjectCenter, and from the “File” menu choose “New in project”. The “File Type” drop-down lets you choose different kinds of file to add: select “Objective-C Class”. Give the file the name “ClockView.m”, and check the “Add Header File” box as shown in figure 1.


PIC

Figure 1: Creating the ClockView class.


When you have chosen a location for the class, you will see that ProjectCenter has added ClockView.m to the “Classes” group and ClockView.h to the “Headers” group in your project. Before using the class, there’s some housekeeping to do. Open ClockView.h and change it as below, so that the AppKit headers are imported and the class is a subclass of NSView, not of NSObject. You’ll also add some methods and instance variables to set and store the hour, minute and second displayed by the clock.1

#import <AppKit/AppKit.h>  
 
@interface ClockView : NSView  
{  
  int hour;  
  int minute;  
  int second;  
}  
 
- (void)setHour:(int)theHour;  
- (void)setMinute:(int)theMinute;  
- (void)setSecond:(int)theSecond;  
 
@end

Now switch to the implementation file (ClockView.m) and fill in the definitions of the three methods in the @implementation section.

@implementation ClockView  
 
- (void)setHour:(int)theHour  
{  
  if (hour != theHour)  
  {  
    hour = theHour;  
    [self setNeedsDisplay:YES];  
  }  
}  
 
- (void)setMinute:(int)theMinute  
{  
  if (minute != theMinute)  
  {  
    minute = theMinute;  
    [self setNeedsDisplay:YES];  
  }  
}  
 
- (void)setSecond:(int)theSecond  
{  
  if (second != theSecond)  
  {  
    second = theSecond;  
    [self setNeedsDisplay:YES];  
  }  
}  
 
@end

When any of your methods change the time represented by the ClockView, they send the view a -setNeedsDisplay: message. That message indicates to the view that it needs to update its content, so next time the application refreshes its views the clock view will be included as one that needs to be redrawn.

3 Add the Clock View to the user interface

Open gorm, and from the Classes browser’s Operations drop-down select “Load Class”. Navigate to and open your ClockView.h file. Now resize the clock window, and from the controls palette drop a Custom View into the window. In the custom view’s inspector, change its custom class to ClockView (figure 2). Your interface in gorm will look like figure 3.


PIC

Figure 2: Set the custom class to ClockView.



PIC

Figure 3: Add a custom view to the Clock application.


But if you build and launch the clock now, you’ll discover that the custom view remains resolutely empty. You’ll need to tell the clock how to draw itself.

4 Drawing a clock

The GNUstep appkit framework isn’t always drawing views. As you saw when creating ClockView, a -setNeedsDisplay: call tells the view that it needs redrawing, and GNUstep will only draw views when this is the case. In fact, it can be a bit more efficient than that, telling a view to only redraw part of its content if necessary.

The framework will send the view a -drawRect: message, with the parameter being an NSRect representing the part of the view’s bounds that needs updating. To get the clock built, draw the whole view when -drawRect: is called: this fits the “do it right, do it well, do it fast” approach to working.

The -[ClockView drawRect:] method will draw the clock face (a circle, and the ‘ticks’ at each hour mark), and the three hands. Each of these is drawn by creating an NSBezierPath representing the component of the clock face, then telling it to either stroke (draw its line) or fill (paint within itself up to its line).

- (void)drawRect:(NSRect)dirtyRect  
{  
  NSRect bounds = [self bounds];  
  // fill in the background  
  [[NSColor controlBackgroundColor] set];  
  [[NSBezierPath bezierPathWithRect:bounds] fill];  
  // draw and fill the dial  
  float smallerDimension = MIN(NSWidth(bounds), NSHeight(bounds));  
  float dialInset = 2.0f;  
  float dialRadius = smallerDimension/2.0f - dialInset;  
  NSRect dialRect = NSMakeRect(NSMidX(bounds)-dialRadius,  
                               NSMidY(bounds)-dialRadius,  
                               dialRadius * 2.0f,  
                               dialRadius * 2.0f);  
  NSBezierPath *dial = [NSBezierPath bezierPathWithOvalInRect:dialRect];  
  [dial setLineWidth:4.0f];  
  [[NSColor controlLightHighlightColor] set];  
  [dial fill];  
  [[NSColor controlColor] set];  
  [dial stroke];  
  // draw twelve hour markers  
  float markerLength = 12.0f;  
  float innerRadius = dialRadius - markerLength;  
  [[NSColor darkGrayColor] set];  
  NSPoint center = NSMakePoint(NSMidX(dialRect), NSMidY(dialRect));  
  int i;  
  for (i = 0; i < 12; i++)  
  {  
    float angle = (i/12.0f) * 2.0f * M_PI;  
    float cosAngle = cos(angle);  
    float sinAngle = sin(angle);  
    NSPoint farEdge = NSMakePoint(center.x + (dialRadius * cosAngle),  
                                  center.y + (dialRadius * sinAngle));  
    NSPoint nearEdge = NSMakePoint(center.x + (innerRadius * cosAngle),  
                                   center.y + (innerRadius * sinAngle));  
    [NSBezierPath strokeLineFromPoint:nearEdge toPoint:farEdge];  
  }  
  // draw the hour hand  
  [[NSColor blackColor] set];  
  float hourHandLength = dialRadius * 0.6f;  
  float hourHandWidth = 8.0f;  
  float hourHandAngle = M_PI_2 - (((hour % 12)/12.0f) * 2.0f * M_PI);  
  NSPoint hourHandEnd = NSMakePoint(center.x + (hourHandLength * cos(hourHandAngle)),  
                                    center.y + (hourHandLength * sin(hourHandAngle)));  
  NSBezierPath *hourHand = [NSBezierPath bezierPath];  
  [hourHand setLineWidth:hourHandWidth];  
  [hourHand moveToPoint:center];  
  [hourHand lineToPoint:hourHandEnd];  
  [hourHand stroke];  
  // draw the minute hand  
  float minuteHandLength = dialRadius * 0.8f;  
  float minuteHandWidth = 4.0f;  
  float minuteHandAngle = M_PI_2 - (((minute % 60)/60.0f) * 2.0f * M_PI);  
  NSPoint minuteHandEnd = NSMakePoint(center.x + (minuteHandLength * cos(minuteHandAngle)),  
                                      center.y + (minuteHandLength * sin(minuteHandAngle)));  
  NSBezierPath *minuteHand = [NSBezierPath bezierPath];  
  [minuteHand setLineWidth:minuteHandWidth];  
  [minuteHand moveToPoint:center];  
  [minuteHand lineToPoint:minuteHandEnd];  
  [minuteHand stroke];  
  // draw the second hand  
  [[NSColor redColor] set];  
  float secondHandLength = dialRadius * 0.95f;  
  float secondHandWidth = 2.0f;  
  float secondHandAngle = M_PI_2 - (((second % 60)/60.0f) * 2.0f * M_PI);  
  NSPoint secondHandEnd = NSMakePoint(center.x + (secondHandLength * cos(secondHandAngle)),  
                                      center.y + (secondHandLength * sin(secondHandAngle)));  
  NSBezierPath *secondHand = [NSBezierPath bezierPath];  
  [secondHand setLineWidth:secondHandWidth];  
  [secondHand moveToPoint:center];  
  [secondHand lineToPoint:secondHandEnd];  
  [secondHand stroke];  
}

Now you have a clock that draws the time…as long as the time is twelve o’clock. It usually isn’t midnight, so you need the clock controller to tell the view what the time is.

5 Telling the time

In Project Center, switch to ClockController.h and add an instance variable for the clock view.

@interface ClockController : NSObject  
{  
  id timeLabel;  
  id clockView;  
  NSTimer *timer;  
  NSDateFormatter *dateFormatter;  
}  
 
//...

gorm will automatically tell you that it has re-parsed the file and that this might have broken some connections (it won’t have broken any connections, in this case). Switch to gorm, and connect the clockView outlet on the ClockController to the custom view in the Clock window. This connection will appear in the inspector as figure 4.


PIC

Figure 4: Connecting the clock view outlet.


Now the controller has a reference to the clock view, it should tell it the time. This can be done in the same timer method that currently updates the digital clock.

- (void)updateTimeDisplay:(NSTimer *)aTimer  
{  
  NSCalendarDate *now = [NSCalendarDate calendarDate];  
  [timeLabel setStringValue:[dateFormatter stringFromDate:now]];  
  [clockView setHour:[now hourOfDay]];  
  [clockView setMinute:[now minuteOfHour]];  
  [clockView setSecond:[now secondOfMinute]];  
}

Now, when you build and run the application, the clock will be updated in real time, and will look like figure 5.


PIC

Figure 5: Connecting the clock view outlet.


6 Summary

You have created a custom view in a GNUstep application as a subclass of NSView, and used gorm to connect it to your controller. You made the view draw its content using bezier paths when the framework told it that it’s time to draw.

7 Resources

The source code for the Clock application created here is available in its entirety from gs-analog-clock on Bitbucket, under the terms of the GNU General Public License, Version 3.