Mac OS X - Работа с кривыми Безье

В этой статье мы рассмотрим написание простого приложения Cocoa, позволяющее создавать и редактировать кривые Безье - изменять концевые и контрольные точки, цвет, толщину и стиль линии, которой рисуется кривая Безье. Ниже приводится скриншот программы.

Bezier Demo screenshot

Рис 1. Скриншот программы для редактирования кривой Безье.

Создадим новый проект BezierDemo типа Cocoa Application как и в предыдущих статьях.

create new cocoa application

Рис 2. Создание нового проекта.

create new cocoa application

Рис 3. Задание имени проекта.

project window

Рис 4. Готовое окно проекта.

После этого сразу же перейдем к созданию пользовательского интерфейса для нашей программы для чего откроем файл MainMenu.nib из нашего проекта в Interface Builder'е (для этого достаточно два раза щелкнуть мышью по названию этого файла в окне проекта).

Создадим окно с заголовком BezierDemo и начнем располагать в нем компоненты.

Удобно сразу же в Interface Builder'е в закладке Classes выбрать NSView и унаследовать от него новый класс BezierView, который мы и будем использовать для вывода и редактирования кривой Безье.

creating new subclass of NSView

Рис 5. Создание класса BezierView на базе класса NSView.

После создания класса BezierView, начнем располагать компоненты в окне приложения.

Components layout

Рис 6. Расположение компонент в окне.

В верхней части окна разместим метку "Size:", слайдер для управления толщиной линии, метку "Color:", компоненту NSColorWell для выбора цвета линии, метку "Style:" и компоненту NSPopUpButton для выбора стиля (шаблона) линии.

Components layout

Рис 7. Расположение управляющих компонент.

Для каждой из этих компонент в качестве режима autoresize выберем режим, представленный на следующей картинке.

Components layout

Рис 8. Задание режима autoresize для компонент управления кривой.

Под этими компонентами расположим горизонтальную прямую, отделяющую управляющие компоненты с области работы с кривой. Далее под прямой разместим объект CustomView, выровняв его как показано на рисунке 6.

Далее зададим для этого компонента в качестве класса класс BezierView и следующий режим autoresize.

Рис 9. Задание свойств объекта BezierView.

Далее настроим меню, связанное с NSPopUpButton, служащей для выбора стиля линии. Для этого откроем это меню дважды щелкнув по нему мышью и добавим туда недостающие элементы, просто перетащив элемент Item с закладки Cocoa-Menus в палитре компонент в меню нужное число раз.

Для каждого пункта меню уберем текст (вместо него мы будем использовать картинку) и выставим Key Equivalent в цифры от 1 до 6.

Рис 10. Настройка меню стиля линии.

Добавим в ряд картинок, со стилями линии, в наш nib-файл. Для этого откроем закладку Images и просто перетащим туда по одной все картинки style-1.png, style-2.png, ..., style-6.png.

Рис 11. Добавление изображений в nib-файл.

Далее добавим к классу BezierView методы и outlet'ы, показанные на следующих рисунках.

adding outlets adding actions

Рис 12. Методы и outlet'ы класса BezierView.

После этого при помощи команды меню Classes/Create Files for BezierView создадим файлы BezierView.h и BezierView.m, добавив их в наш проект.

Дальше нужно установить связи объектов. Свяжем outlet'ы BezierView colorWell, slider и styleButton с соответствующими объектами в окне.

Аналогично в качестве action'а для этих объектов выберем setColor:, setLineSize: и setLineStyle.

После этого сохраним наш nib-файл и вернемся в XCode. Откроем файлы BezierView.h и BezierView.m и поправим файл BezierView.h, чтобы он выглядел как на следующем листинге.

#import <Cocoa/Cocoa.h>

enum
{
    modeNone = 0,
    modeCreate,
    modeDragControl,
    modeDragPoint
};

struct	LineStyleDef
{
	const float * pattern;
	int           count;
};

@interface BezierView : NSView
{
    NSPoint	p [2];                  // endpoints
    NSPoint	c [2];                  // control points
    NSColor * color;
    float     lineWidth;
    int       index;
    int       mode;
    int       lineStyle;

    IBOutlet NSSlider      * slider;
    IBOutlet NSColorWell   * colorWell;
    IBOutlet NSPopUpButton * styleButton;
}
- (IBAction) setColor:     (id) sender;
- (IBAction) setLineSize:  (id) sender;
- (IBAction) setLineStyle: (id) sender;

- (int)     indexOfPoint: (NSPoint) pt inArray: (NSPoint *) arr;
- (void)    drawPoint: (NSPoint) pt radius: (float) r;

@end

Основой для рисования будет объект класса NSBezierPath. Этот объект представляет собой составную кривую Безье, т.е. набор отрезков прямых и кривых Безье.

В общем случае кривая Безье задается при помощи четырех точек - начальной и конечной точек (p0 и p1) и двух контрольных точек (c0 и c1).

Контрольные точки c0 и c1 позволяют управлять формой кривой, но при этом получаемая кривая обладает следующими свойствами - она всегда лежит в выпуклой оболочке всех четырех точек и в начальной точке касается отрезка p0c0, а в конечной - отрезка p1c1.

Рис 13. Кривая Безье.

Объект класса NSBezierPath позволяет добавлять к составной кривой (представленной данным объектом) новые отрезки прямых (отрезок прямой является частным случаем кривой Безье) и новые кривые Безье. Кроме того можно сразу получать составные кривые Безье, соответствующие элементарным объектам - эллипсам, прямоугольникам и т.п.

Ниже приводится простейший пример кода, строящего ломаную из двух соединенных между собой отрезков.

NSBezierPath * path = [NSBezierPath bezierPath];

[path moveToPoint: NSMakePoint ( 0, 0 )];
[path lineToPoint: NSMakePoint ( 1, 1 )];
[path lineToPoint: NSMakePoint ( 1, 7 )];

Обратите внимание, что объект, возвращенный методом класса bezierPath, уже autoreleas'ed, т.е. он будет автоматически удален на следующем цикле обработки сообщений (если Вы явно не пошлете ему сообщение retain).

После того, как у нас имеется готовая составная кривая Безье ей можно послать сообщение stroke, в результате чего она будет нарисована как линия. Также можно заполнить ограниченную ею область при помощи сообщения fill.

Для задания цвета, которым будет выводиться (заполняться) линия следует использовать метод set объекта NSColor. Так следующий пример рисует уже готовую кривую голубым цветом.

[[NSColor cyanColor] set];
[path stroke];

Для задания толщины выводимой линии ( в point-х[) служит сообщение setLineWidth:, а для задания шаблона, т.е. последовательности рисуемый и не рисуемых участков кривой, служит сообщение setLineDash:count:phase:.

float ptn = { 5, 5 };

[path setLineWidth: 0.5]
[path setLineDash: ptn count: 2 phase: 0];
[path stroke];

Для задания стиля линии удобно использовать введенную ранее структуру LineStyleDef, задающую как последовательной отрезков, так и их число.

Для удобства задания стиля линии лучше "прикрепить" картинки с изображениями стилей (файлы style-n.tiff) линии к пунктам меню в элементе выборе стиля. Поскольку это действительно меню, то его элементы удовлетворяют протоколу NSMenuItem, поддерживающем сообщение setImage:.

Удобнее всего эту "привязку" выполнить в методе awakeFromNib. Этот метод автоматически будет вызван для каждого из содержащихся в nib-файле объектов после того как все они будут разархивированы и восстановлены все связи между ними.

- (void) awakeFromNib
{
    int i;
    
    [self setLineSize: slider];
    [self setColor: colorWell];
    
    for ( i = 0; i < 6; i++ )
    {
        id <NSMenuItem> item = [styleButton itemAtIndex: i];
        NSString * imageName = [NSString stringWithFormat: @"style-%d", i + 1 ];
        NSString * path      = [[NSBundle mainBundle] pathForResource: imageName ofType: @"tiff"];
        NSImage  * image     = [[NSImage alloc] initWithContentsOfFile: path];
        
        NSLog ( @"%@", image );
        
        [item setImage: image];
    }
}

Ниже приводится метод drawRect:, осуществляющей построение кривой Безье по заданным начальной и конечной точкам p[0] и p[1] и контрольным точкам c[0] и c[1] с заданным стилем линии, цветом и ее толщиной. Метод drawPoint: служит для изображения заданной точки (концевой или контрольной).

- (void) drawRect: (NSRect) rect
{
    NSBezierPath * path = [NSBezierPath bezierPath];
    float          r = 5;
    
    [path moveToPoint: p [0]];
    [path curveToPoint: p [1] controlPoint1: c [0] controlPoint2: c [1]];
    [path setLineWidth: lineWidth];
    [color set];
    [path setLineDash: defs [lineStyle].pattern count: defs [lineStyle].count phase: 0.0];
    [path stroke];
    
    [[NSColor cyanColor] set];
    [self drawPoint: p [0] radius: r];
    [self drawPoint: p [1] radius: r];
    
    [[NSColor yellowColor] set];
    [self drawPoint: c [0] radius: r];
    [self drawPoint: c [1] radius: r];
    
    path = [NSBezierPath bezierPath];
    
    [[NSColor blackColor] set];
    [path moveToPoint: p [0]];
    [path lineToPoint: c [0]];
    [path moveToPoint: p [1]];
    [path lineToPoint: c [1]];
    [path setLineWidth: 0.5];
    [path stroke];
}

- (void)    drawPoint: (NSPoint) pt radius: (float) r
{
    [[NSBezierPath bezierPathWithOvalInRect: NSMakeRect ( pt.x - r,  pt.y - r, 2*r, 2*r )] fill];
}

Рассмотрим теперь как можно реализовать перетаскивание точек заданного типа мышью.

Условимся, что для перетаскивания контрольных точек надо удерживать нажатой клавишу Ctrl, а для перетаскивания контрольных точек - клавишу Alt (это связано с тем, что контрольные и концевые точки могут совпадать между собой).

Для проверки нажатия заданной клавиши удобно использовать метод modifierFlags класса NSEvent. Данный метод возвращает целое число, отдельные биты которого несут информацию о статусе (нажата или нет) ряда клавиш (Shift, Alt, Ctrl и т.п.).

Для проверки нажатия заданных клавиш определены константы, такие как NSControlKeyMask. NSAlternateKeyMask (полный список таких констант можно получить в справочной системе в XCode).

Поэтому получение информации о статусе клавиш Alt и Ctrol можно представить следующим фрагментом кода:

BOOL    ctrl = ([event modifierFlags] & NSControlKeyMask  ) != 0;
BOOL    alt  = ([event modifierFlags] & NSAlternateKeyMask) != 0;

Также довольно просто получение текущих координат мыши - для этого служит метод locationInWindow класса NSEvent. Он возвращает координаты с системе координат окна, для переводя их в систему координат, связанную с заданным объектом NSView служит метод convertPoint:fromView: класса NSView.

Если в качестве второго параметра передать nil, то этот метод осуществляет перевод координат точки из системы координат окна.

NSPoint loc = [self convertPoint: [event locationInWindow]fromView: nil];

Осталось только проверить соответствует ли текущему положению курсора мыши какой-либо из точек кривой Безье заданного типа (в зависимости от нажатия клавиш Alt и Ctrol) и сразу получить ее номер при помощи метода indexOfPoint:inArray: или же надо начинать строить новую кривую Безье.

- (void) mouseDown : (NSEvent *) event
{
    NSPoint loc  = [event locationInWindow];
    BOOL    ctrl = ([event modifierFlags] & NSControlKeyMask  ) != 0;
    BOOL    alt  = ([event modifierFlags] & NSAlternateKeyMask) != 0;
    
    loc = [self convertPoint: loc fromView: nil];

    if ( !ctrl && !alt )
    {
        p [0]  = loc;
        p [1]  = loc;
        c [0]  = loc;
        c [1]  = loc;
        index  = -1;
        mode   = modeCreate;
    }
    else
    if ( ctrl )                     // we're moving control points
    {
        index = [self indexOfPoint: loc inArray: c];
        mode  = modeDragControl;
    }
    else
    if ( alt )
    {
        index = [self indexOfPoint: loc inArray: p];
        mode  = modeDragPoint;
    }
    else
    {
        index = -1;
        mode  = modeNone;
    }
    
    [self setNeedsDisplay: YES];
}

Само перемещение точки обрабатывается методом mouseDragged:, который просто передвигает заданную точку и посылает сообщение setNeedsDisplay: для перерисовки содержимого данного объекта view.

- (void) mouseDragged: (NSEvent *) event
{
    NSPoint loc  = [event locationInWindow];
    
	loc = [self convertPoint: loc fromView: nil];

    if ( mode == modeDragControl && index >= 0 )
        c [index] = loc;
    else
    if ( mode == modeDragPoint && index >= 0 )
        p [index] = loc;
    else
    if ( mode == modeCreate )
    {
        p [1] = loc;
        c [1] = loc;
    }
    
    [self setNeedsDisplay: YES];
}

- (void) mouseUp : (NSEvent *) event
{
    index = -1;
}

Наиболее простой получилась реализация метода mouseUp: - он просто должен сбросить состояние передвижения точки. Приятным моментом в обработке сообщений мыши в Mac OS X является то, что получатель сообщения о нажатии кнопки мыши (mouseDown:) всегда получит у уведомление об отпускании кнопки (mouseUp:) мыши вне зависимости от того над каким объектом это произошло (в отличии от убогих форточек:)))).

Ниже приводится полный текст программы BezierDemo.

#import "BezierView.h"

static  const float ptn1 [] = { 5, 0 };
static  const float ptn2 [] = { 5, 5 };
static  const float ptn3 [] = { 5, 2 };
static  const float ptn4 [] = { 3, 3 };
static  const float ptn5 [] = { 2, 5 };
static  const float ptn6 [] = { 7, 2, 7 };

struct LineStyleDef defs [] =
{
    { ptn1, 2 },
    { ptn2, 2 },
    { ptn3, 2 },
    { ptn4, 2 },
    { ptn5, 2 },
    { ptn6, 3 }
};

float   pointDistance ( NSPoint p1, NSPoint p2 )    // Manhattan distance
{
    return fabs ( p1.x - p2.x ) + fabs ( p1.y - p2.y );
}

@implementation BezierView

- (id)initWithFrame:(NSRect)frameRect
{
    if ((self = [super initWithFrame:frameRect]) != nil)
    {
        // Add initialization code here
        lineWidth    = 0.1;
        color        = [[NSColor blackColor] copy];
        index        = -1;
    }
    return self;
}

- (void) dealloc 
{
    [color release];
    [super dealloc];
}

- (void) awakeFromNib
{
    int i;
    
    [self setLineSize: slider];
    [self setColor: colorWell];
    
    for ( i = 0; i < 6; i++ )
    {
        id <NSMenuItem> item = [styleButton itemAtIndex: i];
        NSString * imageName = [NSString stringWithFormat: @"style-%d", i + 1 ];
        NSString * path      = [[NSBundle mainBundle] pathForResource: imageName ofType: @"tiff"];
        NSImage  * image     = [[NSImage alloc] initWithContentsOfFile: path];
        
        NSLog ( @"%@", image );
        
        [item setImage: image];
    }
}

- (void) drawRect: (NSRect) rect
{
    NSBezierPath * path = [NSBezierPath bezierPath];
    float          r = 5;
    
    [path moveToPoint: p [0]];
    [path curveToPoint: p [1] controlPoint1: c [0] controlPoint2: c [1]];
    [path setLineWidth: lineWidth];
    [color set];
    [path setLineDash: defs [lineStyle].pattern count: defs [lineStyle].count phase: 0.0];
    [path stroke];
    
    [[NSColor cyanColor] set];
    [self drawPoint: p [0] radius: r];
    [self drawPoint: p [1] radius: r];
    
    [[NSColor yellowColor] set];
    [self drawPoint: c [0] radius: r];
    [self drawPoint: c [1] radius: r];
    
    path = [NSBezierPath bezierPath];
    
    [[NSColor blackColor] set];
    [path moveToPoint: p [0]];
    [path lineToPoint: c [0]];
    [path moveToPoint: p [1]];
    [path lineToPoint: c [1]];
    [path setLineWidth: 0.5];
    [path stroke];
}

- (void) mouseDown : (NSEvent *) event
{
    NSPoint loc  = [event locationInWindow];
    BOOL    ctrl = ([event modifierFlags] & NSControlKeyMask  ) != 0;
    BOOL    alt  = ([event modifierFlags] & NSAlternateKeyMask) != 0;
    
    loc = [self convertPoint: loc fromView: nil];

    if ( !ctrl && !alt )
    {
        p [0]  = loc;
        p [1]  = loc;
        c [0]  = loc;
        c [1]  = loc;
        index  = -1;
        mode   = modeCreate;
    }
    else
    if ( ctrl )                     // we're moving control points
    {
        index = [self indexOfPoint: loc inArray: c];
        mode  = modeDragControl;
    }
    else
    if ( alt )
    {
        index = [self indexOfPoint: loc inArray: p];
        mode  = modeDragPoint;
    }
    else
    {
        index = -1;
        mode  = modeNone;
    }
    
    [self setNeedsDisplay: YES];
}

- (void) mouseDragged: (NSEvent *) event
{
    NSPoint loc  = [event locationInWindow];
    
    loc = [self convertPoint: loc fromView: nil];

    if ( mode == modeDragControl && index >= 0 )
        c [index] = loc;
    else
    if ( mode == modeDragPoint && index >= 0 )
        p [index] = loc;
    else
    if ( mode == modeCreate )
    {
        p [1] = loc;
        c [1] = loc;
    }
    
    [self setNeedsDisplay: YES];
}

- (void) mouseUp : (NSEvent *) event
{
    index = -1;
}

- (IBAction) setColor: (id) sender
{
    [color autorelease];
    
    color = [[sender color] copy];
    
    [self setNeedsDisplay: YES];
}

- (IBAction) setLineSize: (id) sender
{
    lineWidth = [sender floatValue];
    
    [self setNeedsDisplay: YES];
}

- (int) indexOfPoint: (NSPoint) pt inArray: (NSPoint *) arr
{
    int       i;

    for ( i = 0; i < 2; i++ )
        if ( pointDistance ( arr [i], pt ) < 10 )
            return i;

    return -1;
}

- (void)    drawPoint: (NSPoint) pt radius: (float) r
{
    [[NSBezierPath bezierPathWithOvalInRect: NSMakeRect ( pt.x - r,  pt.y - r, 2*r, 2*r )] fill];
}

- (IBAction) setLineStyle: (id) sender
{
    lineStyle = [sender indexOfSelectedItem];
    
    [self setNeedsDisplay: YES];
}

@end

По этой ссылке можно скачать исходный проект к этой статье.

Valid HTML 4.01 Transitional

Напиши мне
Используются технологии uCoz