I’ve previously written about getting fixed-width fonts to display properly on the iPhone. Today I’d like to add a little to that discussion, and show how you might create a (very simple) ncurses-like display on the iPhone.
Header
The class declaration is pretty simple. The w
and h
members track the size of the display (in characters), while screen
buffers the contents of the display as a simple byte array. The font
and p
members track the display’s typeface and current cursor position (in characters), respectively.
@interface CursesView : UIView
{
UIFont* font;
int w;
int h;
char* screen;
CGPoint p;
}
- (void)clear;
- (void)move:(CGPoint)np;
- (void)printw:(char*)fmt, ...;
@end
Implementation
The implementation is also pretty straightforward. It begins with two initializer functions:
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
if (self.frame.size.width != 320)
{
// This class assumes that it's 320 points wide
[self release];
return nil;
}
font = [[UIFont fontWithName:@"CourierNewPSMT" size:16] retain];
w = 33;
h = floor(self.frame.size.height/[font pointSize]);
screen = (char*) malloc(w*h+1);
}
return self;
}
- (id)initWithCoder:(NSCoder*)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
if (self.frame.size.width != 320)
{
// This class assumes that it's 320 points wide
[self release];
return nil;
}
font = [[UIFont fontWithName:@"CourierNewPSMT" size:16] retain];
w = 33;
h = floor(self.frame.size.height/[font pointSize]);
screen = (char*) malloc(w*h+1);
}
return self;
}
Both methods are needed: initWithCoder:
for initialization from NIB files, and initWithFrame:
for programmatic initialization. The methods don’t do anything too fancy — they do enforce a 320-point width assumption, since the font and display width in characters are hard-coded.
The drawRect:
method is the real heart of the matter:
- (void)drawRect:(CGRect)rect
{
// What to render, and how
screen[w*h] = '\0';
NSString* s = [NSString stringWithCString:screen encoding:NSASCIIStringEncoding];
// Dimensions
CGSize size = self.frame.size;
// Get a pointer to context
CGContextRef context = UIGraphicsGetCurrentContext();
// Flop text rightside-up
CGContextSetTextMatrix(context, CGAffineTransformMakeScale(1, -1));
// Clear background
CGContextSetFillColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
// Render text (Setup)
NSUInteger ch = [font pointSize];
CGContextSetTextDrawingMode(context, kCGTextFillStroke);
CGContextSetLineWidth(context, 1.0);
CGContextSelectFont(context, [[font fontName] cStringUsingEncoding:NSASCIIStringEncoding], ch, kCGEncodingMacRoman);
[[UIColor greenColor] set];
// Draw text
NSString* ss;
for (NSUInteger c = 0, n = w, l = [s length], row = ch; c < l; c = n, n += w, row += ch)
{
ss = [s substringWithRange:NSMakeRange(c, (n<l?n:l)-c)];
CGContextShowTextAtPoint(context,
2,
row,
[ss cStringUsingEncoding:NSMacOSRomanStringEncoding],
[ss lengthOfBytesUsingEncoding:NSMacOSRomanStringEncoding]);
}
}
While there’s not really anything here that we haven’t seen before, I would point out the “2” in the call to CGContextShowTextAtPoint()
: This is an experimentally determined “magic value” used to (roughly) center 33 columns of 16-point text on a 320-point display.
Next up are the less-interesting “icurses” functions:
- (void)clear
{
[self setNeedsDisplay];
memset(screen, ' ', w*h);
p = CGPointZero;
}
- (void)move:(CGPoint)np
{
p = np;
}
Followed by printw:
, which updates the screen
buffer for later rendering:
- (void)printw:(char*)fmt, ...
{
[self setNeedsDisplay];
static char s[1024];
va_list ap;
va_start(ap, fmt);
vsnprintf(s, sizeof(s), fmt, ap);
va_end(ap);
int i, l, r, c;
for (i=0, l=strlen(s), r=p.y, c=p.x; i < l; i++)
{
if (s[i] == '\n')
{
r += 1; c = 0;
}
else if (s[i] == '\b')
{
if (c) c--;
}
else if (r*w+c < w*h)
{
screen[r*w+c] = s[i]; c += 1;
if (c >= w)
{
r += 1; c = 0;
}
}
}
p.x = c;
p.y = r;
}
Finally, dealloc
handles any necessary cleanup:
- (void)dealloc
{
[font release];
if (screen) free(screen);
[super dealloc];
}
Usage
You could draw to one of these views with code like the following:
- (void)render
{
clear();
move(3, 2); printw("------------------------");
move(4, 1); printw("|");
move(4, 15); printw("%10d",1);
move(4, 26); printw("|");
move(5, 1); printw("|");
move(5, 15); printw("%10d",100);
move(5, 26); printw("|");
move(6, 1); printw("|");
move(6, 15); printw("%10d",10000);
move(6, 26); printw("|");
move(7, 1); printw("|");
move(7, 15); printw("%10d",1000000);
move(7, 26); printw("|");
move(8, 2); printw("------------------------");
}
… assuming that the render
method belonged to a class with a CursesView
member called display
, and that you’d defined these convenience macros:
#define clear() [display clear]
#define move(r,c) [display move:CGPointMake((c), (r))]
#define printw(...) [display printw:__VA_ARGS__]
Note that you don’t need to trigger drawing explicitly after invoking, e.g., printw:
— there are [self setNeedsDisplay]
calls embedded in the relevant functions.