25.03.2012

Xcode 4

Tutorial: Zeichnen einer Box mit appendBezierPathWithArcFromPoint und NSBezierPath

In diesem Tutorial wird erklärt, wie man mit moveToPoint, lineToPoint und appendBezierPathWithArcFromPoint einen NSBezierPath erstellt, der eine Box in eine Custom-View (NSView) zeichnet. Cocoa stellt zwar mit der Klasse NSBox eine Möglichkeit zur Verfügung, um einen Rahmen mit abgerundeten "Ecken" zu zeichnen, aber es gibt Anwendungsfälle in denen es gut ist, alles selbst in der Hand zu haben.

Im Folgenden wird Quellcode erstellt, der in der Methode draw:(NSRect) einer Custom-View hinzugefügt werden kann, um einen Rahmen zu zeichnen. Das Ergebnis sollte folgendermaßen aussehen:

Vorgehen

Die Skizze zeigt die verschiedenen Abschnitte und Punkte des BezierPath um einen Rahmen zu zeichnen.

Es ist zu sehen, daß insgesammt 12 Punkte (p1-p12) benötigt werden. Für jede Ecke werden jeweils drei Punkte verwendet. Einen Startpunkt, einen Endpunkt und einen "Stützpunkt".

Custom-View erstellen

Der folgende Quellcode zeichnet den Rahmen in einer NSView.

- (void)drawRect:(NSRect)dirtyRect
{

    // radius > disstance!
    int radius = 8;
    int distance = 5; // Distanz zum Rand

    // Linien Korrektur (Links und rechts jeweils
    // die Differenz von radius und distance.
    // Addieren von jeweils einem Korrektur-Pixel
    // links und rechts.
    int corr = ((radius-distance)*2) + 2;

    NSBezierPath *shape = [NSBezierPath bezierPath];

    NSPoint p1 = NSMakePoint(distance, radius+corr);
    NSPoint p2 = NSMakePoint(distance, distance);
    NSPoint p3 = NSMakePoint(radius,distance);

    NSPoint p4 = NSMakePoint([self bounds].size.width-radius-corr, distance);
    NSPoint p5 = NSMakePoint([self bounds].size.width-distance, distance);
    NSPoint p6 = NSMakePoint([self bounds].size.width-distance,radius);

    NSPoint p7 = NSMakePoint([self bounds].size.width-distance, [self bounds].size.height-radius-corr);
    NSPoint p8 = NSMakePoint([self bounds].size.width-distance,[self bounds].size.height-distance);
    NSPoint p9 = NSMakePoint([self bounds].size.width-radius,[self bounds].size.height-distance);

    NSPoint p10 = NSMakePoint(radius+corr, [self bounds].size.height-distance);
    NSPoint p11 = NSMakePoint(distance,[self bounds].size.height-distance);
    NSPoint p12 = NSMakePoint(distance,[self bounds].size.height-radius);

    // Links unten
    [shape moveToPoint:p1]; // Startpunkt
    [shape appendBezierPathWithArcFromPoint:p2 toPoint:p3 radius:radius]; //Bogen 1
    [shape lineToPoint:p4]; // Linie 2

    // Rechts unten
    [shape appendBezierPathWithArcFromPoint:p5 toPoint:p6 radius:radius]; //Bogen 3
    [shape lineToPoint:p7]; // Linie 4

    // Rechts oben
    [shape appendBezierPathWithArcFromPoint:p8 toPoint:p9 radius:radius]; // Bogen 5
    [shape lineToPoint:p10]; // Linie 6

    // Links oben
    [shape appendBezierPathWithArcFromPoint:p11 toPoint:p12 radius:radius]; // Bogen 7
    [shape lineToPoint:p1]; // Linie 8

    // Liniebreite des Rahmen
    [shape setLineWidth:5];
    [[NSColor darkGrayColor] set];
    [shape stroke]; //Zeichne Rahmen
    [[NSColor lightGrayColor] set];
    [shape fill]; //Fuelle die Flaeche

}

Als erstes wird ein NSBezierPath, ein Radius und die Distanz zum Rand (Rahmenabstand) für die Ecken definiert. Anschließend werden 12 Objekte vom Typ NSPoint erzeugt.

Zu beachten ist, daß eine Variable corr berechnet und beim Setzen der Punkte verwendet wird. Sie dient dazu, die korrekte Linienlänge zu bestimmen. Da die Länge der Linie abhängig vom Radius und dem Rahmenabstand ist, werden diese beiden Werte jeweils zweimal (links und rechts) eingerechnet. Außerdem wird noch eine Korrektur von zwei Pixel benötigt.

Mit der Methode moveToPoint:NSPoint bewegt man sich zum Startpunkt des BezierPaths. Von dort wird der erste Bogen mit appendBezierPathWithArcFromPoint:toPoint:radius gezeichnet. Im Anschluß wird eine Linie mit lineToPoint: zu Punkt p4 gezogen. Analog werden die weiteren Bögen und Linien gezeichnet, bis der Ausgangspunkt p1 erreicht ist.

Abschließend wird die Breite und die Farbe für die Umrandungslinie gewählt und mit der Methode stroke wird der BezierPath gezeichnet.

In dem Beispiel wird außerdem noch eine Hintergrundfarbe festgelegt und mit der Methode fill wird der BezierPath ausgefüllt.

Der Quellcode oben kann einfach in der Methode drawRect: einer Custom-View ergänzt werden.

Vorteile

Der Vorteil, wenn nicht die NSBox verwendet wird, sondern eine individuelle Zeichenroutine ist, daß das Aussehen sehr leicht angepaßt werden kann. Beispielsweise kann der Radius des Bogen verändert werden. Folgende Screenshots zeigen dies mit unterschiedlichen Radien:

Radius=5 Radius=10
Radius=50 Radius=86

Es ist zu beachten, daß das Aussehen der verschiedenen Werte für die Radien auch von der Fensterbreite und Fensterhöhe abhängen. Bei den Screenshots wurde eine Fensterhöhe von 172px verwendet. Wenn man für das Beispiel einen Radius größer als 86 verwendet, kann so etwas passieren:

Radius=150

Andere Lösung

Eine andere Lösung findet man in verschiedenen Foren und Tutorials. Diese kompaktere Implementierung bietet sich an, wenn man keinen Rahmen um die Box benötigt. Der Quellcode dazu sieht folgendermaßen aus:

- (void)drawRect:(NSRect)rect
{
    float radius = 20;

    NSBezierPath *path = [NSBezierPath bezierPath];

    NSPoint p2 = NSMakePoint(NSMinX(rect), NSMinY(rect));
    NSPoint p5 = NSMakePoint(NSMaxX(rect), NSMinY(rect));
    NSPoint p11 = NSMakePoint(NSMinX(rect), NSMaxY(rect));
    NSPoint p8 = NSMakePoint(NSMaxX(rect), NSMaxY(rect));

    [path moveToPoint: NSMakePoint(p2.x + radius, p2.y)];

    [path appendBezierPathWithArcFromPoint: p5 toPoint: p8 radius: radius];
    [path appendBezierPathWithArcFromPoint: p8 toPoint: p11 radius: radius];
    [path appendBezierPathWithArcFromPoint: p11 toPoint: p2 radius: radius];
    [path appendBezierPathWithArcFromPoint: p2 toPoint: p5 radius: radius];

    [path closePath];

    [[NSColor lightGrayColor] set];
    [path fill]; //Fuelle die Flaeche
    [path setLineWidth:5];
    [[NSColor lightGrayColor] set];
    [path stroke]; //Zeichne Rahmen

}

Das Ergebnis sieht dann so aus:

Update - 17.06.2012