Программирование для Mac OS X Cocoa - пишем скринсейвер

В этой статье мы рассмотрим написание полноценного скринсейвера для Mac OS X (правда пока без использования OpenGL) в виде вращающегося 4-мерного куба (гиперкуба).

Как и в предыдущей статье, запустим XCode и создадим новый проект, но на этот раз в качестве типа проекта выберем Screen Saver (см. рис 1).

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

В качестве имени для проекта зададим Cube4D и зададим какое-нибудь место для проекта в личном каталоге.

Рис 2. Задание имени проекта и его местоположения.

После открытия проекта (см. рис. 3) обратите внимание, что в качестве цели проекта (target) выступает файл Cube4D.saver - когда он будет собран достаточно будет двойного щелчка мышью по нему для запуска настроек скринсейвера в системе.

Также обратите внимание, что по сравнению с предыдущим нашим проектом добавился ScreenSaver.framework - специальная библиотека для написания скринсейверов.

Рис 3. Окно проекта Cube4D.

При этом в проект сразу же вошли файлы Cube4DView.h и Cube4DView.m, но нет ни одного nib-файла (как нет и файла main.m).

Фактически файлы Cube4DView.h и Cube4DView.m содержат готовый скелет для скринсейвера, нам остается лишь добавить реализации методов (АТД).

Поскольку скринсейвер, который мы будем писать, будет строить "летающее" изображение гиперкуба, то было бы очень удобно воспользоваться возможностью языка С++ по работе с абстрактными типами данных.

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

Поскольку у меня уже есть готовая реализация на С++ для этого класса (Vector4D, содержится в исходном коде к статье), то возьмем его и добавим к нашему проекту выбрав в popup-меню для узла Cube4D (в дереве проекта) команду Add/Existing Files... (см. рис. 4).

Рис 4. Добавление уже существующего файла к проекту.

После этого в диалоге выберем файл Vector4D.h (рис 5) и зададим подключение к текущему проекту (при необходимости установив флаг копирования файла в каталог проекта) (рис 6).

Рис 5. Выбор файла Vector4D.h для добавления к текущему проекту.

Рис 6. Задание параметров для подключения файла к проекту.

Поскольку мы хотим использовать класс языка С++ (Vector4D) в программе на Objective-C мы воспользуемся поддержкой компилятором объединения этих языков, называемое Objective-C++.

Программы на Objective-C++ (соответствующие исходные файлы имеют расширение mm) позволяют одновременно использовать как конструкции языка Objective-C, так и конструкции языка С++.

Тем не менее совмещать эти два языка надо очень аккуратно, т.к. они сильно отличаются (например хотя они оба поддерживают исключения, но исключения С++ можно обрабатывать только при помощи механизма С++ для работы с исключениями, аналогично исключения Objective-C можно обрабатывать только конструкциями языка Objective-C). Кроме того возможны проблемы при вставке объектов С++ внутрь объектов Objective-C, например деструктор для объекта С++ не будет автоматически вызван при уничтожении содержащего его объекта Objective-C.

К счастью нам не нужны ни исключения С++, ни деструкторы - все, что нам нужно - это поддержка простого класса (фактически структуры с переопределенными операторами).

Конечно можно вместо класса из языка С++ воспользоваться либо обычной структурой и набором функций, реализующих основные операции над векторами, или же реализовать класс как объект Objective-C.

К сожалении оба подхода в данном случае оказываются довольно неудобными, растет объем кода и уменьшается его читаемость.

Поскольку мы хотим использовать класс из С++ в файле Cube4DView.m нам надо изменить его расширение на mm, чтобы компилятор понял, что имеет дело с программой на Objective-C++.

Для этого щелкнув правой кнопкой мыши по этому файлу в правой части окна проекта выберем из popup-меню команду Rename.

Рис 7. Popup-меню для файла в проекте.

После того имя файла Cube4DView.m станет выделенным мы сможем легко изменить его расширение на .mm.

Рис 8. Переименование файла Cube4DView.m.

Фактически каждый скринсейвер для Mac OS X представляет собой объект, унаследованный от класса ScreenSaverView. Задачей класса является переопределение ряда методов, связанных с анимацией и построением изображения. Ниже приводятся описания основных методов этого класса.

– (id) initWithFrame: (NSRect) frame;
– (id) initWithFrame: (NSRect) frame isPreview: (BOOL) flag;
– (NSTimeInterval) animationTimeInterval;
- (void)setAnimationTimeInterval:(NSTimeInterval)timeInterval
- (void)animateOneFrame;
- (void)startAnimation;
- (void)stopAnimation;
- (void)drawRect:(NSRect)rect;
- (BOOL)isPreview;
- (BOOL)hasConfigureSheet;
- (NSWindow *)configureSheet;

При создании (т.е. в методе initWithFrame:isPreview:) при помощи сообщения setAnimationTimeInterval: можно задать частоту кадров для анимации скринсейвера (т.к. это сообщение позволяет задать интервал времени в секундах между соседними кадрами).

[self setAnimationTimeInteral: 1.0/24.0];

Методы startAnimation и stopAnimation служат для начала и остановки анимации, т.е. посылки сообщения animateOneFrame с заданной частотой.

Простейшая реализация метода animateOneFrame просто сообщает системе, что данный объект необходимо перерисовать:

- (void) animateOneFrame
{
    [self setNeedsDisplay: YES];
}

Это приводит к последующей посылке объекту сообщения drawRect:, сообщающего, что j,]trn должен нарисовать свое содержимое.

Таким образом именно функция drawRect: и является ядром скринсейвера, т.к. она отвечает за последовательное построение изображений, образующих анимацию.

Что такое 4-мерный куб (гиперкуб) и как с ним работать

Отвлечемся немного от Objective-C и Mac OS X и рассмотрим, что же представляет собой гиперкуб - объект, который мы хотим вывести.

Любой куб заданной размерности n задается 2n вершин, каждая из которых является n-мерным вектором, состоящим из нулей и единиц.

Так одномерный куб состоит всего из двух вершин, каждая из которых является одномерным вектором - (0) и (1). Двухмерный куб состоит из четырех вершин - (0,0), (0,1), (1,0) и (1,1).

Если две вершины отличаются значениями всего в одной координате, то они соединены ребром. Множество этих ребер мы и будем называть гиперкубом.

Рис 9. Кубы размерности 1, 2 и 3.

Простейшим способом получения всех вершин n-мерного куба является использование записей в двоичной системе счисления всех чисел от нуля до 2n-1.

Это справедливо поскольку каждое целое число из этого диапазона может быть представлено ровно n упорядоченными битами (т.е. числами 0 или 1), перебирающими все их возможные комбинации.

Следующий фрагмент кода на С++ демонстрирует получение всех вершин для 4-мерного куба.

for ( i = 0; i < 15; i++ )
{
    Vector4D v;
                                   // fill v with binary representation of i
    v.w = (i & 1 ? 1 : 0);
    v.z = (i & 2 ? 1 : 0);
    v.y = (i & 4 ? 1 : 0);
    v.x = (i & 8 ? 1 : 0);

    . . .
}

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

Следующий фрагмент кода демонстрирует получение всех ребер для гиперкуба.

for ( i = 0; i < 15; i++ )
{
    Vector4D v;
                                   // fill v with binary representation of i
    v.w = (i & 1 ? 1 : 0);
    v.z = (i & 2 ? 1 : 0);
    v.y = (i & 4 ? 1 : 0);
    v.x = (i & 8 ? 1 : 0);

    for ( j = 0; j < 4; j++ )
    {
        Vector4D v2 = v;

        if ( v [j] == 1 )
            continue;

        v2 [j] = 1;
     
        . . .
    }
}

Осталось рассмотреть как такой объект можно спроектировать на двухмерную плоскость и поворачивать.

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

В более общем случае параллельное проектирование (а это именно оно) может быть задано при помощи двух взаимно перпендикулярных векторов e1 и e2 (той же размерности, что и вершины куба).

Тогда в качестве проекции произвольно вершины v на двухмерную плоскость будут точка с координатами ((v,e1),(v,e2)), т.е. образованная двумя скалярными произведениями вершины с v выбранными нами векторами e1 и e2.

Можно слегка изменить способ проектирования добавив в него перспективу. Для этого достаточно покомпонентно разделить получившийся двухмерный вектор ((v,e1),(v,e2)) на величину вида a+b*v.w.

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

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

Как известно, в двухмерном случае поворот на заданный угол осуществляется при помощи умножения на следующую матрицу 2х2:

Действуя по аналогии можно утверждать что частным случаем поворота в 4-мерном пространстве будет преобразование, задаваемое следующей матрицей:

Здесь через R1 и R2 обозначены двухмерные матрицы (размера 2х2) поворота на какие-то (не связанные между собой) углы, а через 0 - нулевая матрица размером 2х2.

Таким образом можно реализовать вращение гиперкуба путем построения для каждого кадра новой матрицы 4х4 приведенного выше вида (при этом используемые углы должны линейно зависеть от времени или номера кадра) и умножения векторов e1 и e2 на эту матрицу.

После этого осуществляется проектирование вершин куба с использованием повернутых векторов e1 и e2.

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

Теперь нам осталось рассмотреть вопрос о том, как именно осуществляется рисование в Mac OS X.

Рисование Mac OS X

Наверное первым, что бросается в глаза в Mac OS X, это ее великолепная графика.

Одной из особенностей графики в Mac OS X является то, что она фактически базируется на стандарте PDF (вобравшим в себя многое из PostScript-а)(в свое время система NextStep в качестве основы для рисования использовала Display PostScript).

Как следствие графика не зависит от устройства и разрешения. Для обеспечения независимости от разрешения при рисовании все функции используют физические единицы - points (1/72 дюйма). Это позволяет легко использовать один и тот же код как для вывода на экран, та и для вывода на печать (или в pdf-файл).

Под независимостью от устройства также понимается возможность использования device-calibrated-colors (например через метод calibratedColorWithRed:green:blue:alpha: класса NSColor), так и некалиброванных цветов (colorWithDeviceRed:green:blue:alpha:), хотя в последнем случае не гарантируется, что заданный цвет будет одинакового выглядеть на различных устройствах.

Графика в Mac OS X полностью поддерживает полупрозрачность - каждый цвет всегда задается вместе с альфа-компонентой как RGBA-вектор с float-компонентами.

Основой для рисования являются так называемые кривые Безье (Bezier path), для удобства работы завернутые в объекты класса NSBezierPath.

Заданная составная кривая Безье может быть нарисована (с использованием заданного стиля линии) и/или ограниченная ею область может быть заполнена заданным цветом.

Вполне естественным является полная поддержка произвольных аффинных преобразований (через объекты класса NSAffineTransform) составных кривых Безье.

Также вводится несколько вспомогательных структур и функций для обозначения точек, размеров и прямоугольников (использование полноценных объектов в этом случае нецелесообразно).

Структура NSPoint, служащая для представления точек на плоскости, состоит из двух вещественных (float) полей - x и y.

Для создания этой структуры по координатам служит функция NSMakePoint.

NSPoint pt;
NSPoint p = NSMakePoint ( 10, 7.5 );

pt.x = p.y + 0.7;

Структура NSSize, служащая для задания размеров двухмерных объектов, также состоит из двух float-полей width и height. Для создания экземпляра данной структуры служит функция NSMakeSize.

Для задания прямоугольников служит структура NSRect, состоящая из двух полей - origin типа NSPoint и size типа NSSize. Для создания экземпляра структуры по координатам начала и размеру служит функция NSMakeRect.

Для построения простейших рисунков нам понадобятся объекты двух классов - NSColor (для задания цвета) и NSBezierPath (для задания линий).

Простейшим способом получения объекта класса NSColor, соответствующего заданному цвету, является использование метода класса (т.е. посылаемого не объектам класса, а самому class object'у) colorWithDeviceRed:green:blue:alpha::

NSColor * color = [NSColor colorWithDeviceRed:0.3 green: 0.7 blue: 0.7 alpha: 0.5];

Также класс NSColor содержит методы класса, позволяющие получать распространенные цвета - blackColor, whiteColor, greenColor и т.д.).

Если послать объекту класс NSColor сообщение set, то это выберет данный цвет как текущий, т.е. все последующие команды рисования будут использовать именно этот цвет.

[[NSColor blackColor] set];

Обратите внимание, что возвращаемые методами класса NSColor объекты уже помечены для удаления и если они Вам нужны, то Вы должны явно послать им сообщение retain. В противном случае в начале следующей итерации цикла обработки сообщений эти объекты удаляются.

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

Для создания объекта класса NSBezierPath служит метод класса bezierPath:

NSBezirePath * path = [NSBezierPath bezierPath];

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

Так простейшие сообщения moveToPoint: и relativeMoveToPoint: позволяют перемещать текущую точку в новое положение, либо явно заданное, либо заданное по отношению к текущему положению.

- (void) moveToPoint: (NSPoint) pt;
- (void) relativeMoveToPoint: (NSPoint) pt;

Для добавления отрезка прямой к составной кривой Безье можно использовать следующие методы.

- (void) lineToPoint: (NSPoint) pt;
- (void) relativeLineToPoint: (NSPoint) pt;

Добавление произвольного фрагмента кривой Безье осуществляется при помощи следующего сообщения.

- (void) curveToPoint: (NSPoint) pt controlPoint1: (NSPoint) c1 controlPoint2: (NSPoint) c2;
- (void) relaticeCurveToPoint: (NSPoint) pt controlPoint1: (NSPoint) c1 controlPoint2: (NSPoint) c2;

Эти сообщения добавляют кривую Безье общего вида, описываемую следующим уравнением:

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

Сообщение closePath замыкает составную кривую, добавляя отрезок от текущего положения "пера" к началу кривой. В результате чего мы получаем замкнутую кривую, которая ограничивает некоторую область.

У объектов типа NSBezierPath есть еще много полезных методов, далее мы воспользуемся еще одним:

+ (void) bezierPathWithRect: (NSRect) rect;

Этот метод возвращает замкнутую составную кривую Безье, ограничивающую заданный прямоугольник.

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

Обратите внимание, что объект, возвращаемый методами класса NSBezierPath, не нужно явно удалять при помощи сообщения release - он уже помечен, как подлежащий удалению на следующем итерации цикла обработки сообщений.

Однако, если вы планируете использовать такой объект далее, то вы должны послать ему сообщение retain (и не забыть удалить, когда он перестанет быть нужен вам).

После рассмотрения математики, связанной с выводом гиперкуба и основных способов рисования в Mac OS X мы готовы написать функцию, отвечающую за рисование проекции гиперкуба.

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

void drawCube ( NSRect bounds, int brightness, float a1, float a2 )
{
    NSPoint        center;
    NSSize         scale;
    float          matrix [4][4];
    int            i, j;
    float          br   = (float) brightness / 255.0f;
    NSBezierPath * path = [NSBezierPath bezierPath];

    [[NSColor colorWithDeviceRed: br green: br blue: br alpha: 1.0] set];

    center.x     = bounds.origin.x + bounds.size.width  * 0.5;
    center.y     = bounds.origin.y + bounds.size.height * 0.5;
    scale.width  = bounds.size.width  / 45.0;
    scale.height = bounds.size.height / 45.0;

    for ( i = 0; i < 4; i++ )
        for ( j = 0; j < 4; j++ )
            matrix [i][j] = 0;

    matrix [0][0] =  (float) cos ( a1 );
    matrix [1][1] =  (float) cos ( a1 );
    matrix [0][1] =  (float) sin ( a1 );
    matrix [1][0] = -(float) sin ( a1 );
    matrix [2][2] =  (float) cos ( a2 );
    matrix [3][3] =  (float) cos ( a2 );
    matrix [2][3] =  (float) sin ( a2 );
    matrix [3][2] = -(float) sin ( a2 );

    Vector4D b1, b2;

    for ( i = 0; i < 4; i++ )
    {
        b1 [i] = 0;
        b2 [i] = 0;

        for ( int j = 0; j < 4; j++ )
        {
            b1 [i] += e1 [j] * matrix [i][j];
            b2 [i] += e2 [j] * matrix [i][j];
        }
    }

    for ( i = 0; i < 15; i++ )
    {
        Vector4D v;
                                            // fill v with binary representation of i
        v.w = (i & 1 ? 1.0f : 0.0f);
        v.z = (i & 2 ? 1.0f : 0.0f);
        v.y = (i & 4 ? 1.0f : 0.0f);
        v.x = (i & 8 ? 1.0f : 0.0f);
		
        for ( j = 0; j < 4; j++ )
        {
            Vector4D  v2 = v;
            NSPoint   start, end;

            if ( v [j] == 1 )
                continue;

            v2 [j] = 1.0;

            Vector4D offs ( sin ( angle1 * 0.33f + angle2 * 0.2f ) * dir / 4 );
            Vector4D w  = v  + offs;
            Vector4D w2 = v2 + offs;

            start.x = ( w  &  b1 ) / (0.7 + 0.2*v.w);
            start.y = ( w  &  b2 ) / (0.7 + 0.2*v.w);
            end.x   = ( w2 & b1  ) / (0.7 + 0.2*v2.w);
            end.y   = ( w2 & b2  ) / (0.7 + 0.2*v2.w);

                                            // remap projection to bounds
            start.x = center.x + start.x * scale.width;
            start.y = center.y + start.y * scale.height;
            end.x   = center.x + end.x   * scale.width;
            end.y   = center.y + end.y   * scale.height;

            [path moveToPoint: start];
            [path lineToPoint: end];
        }	
    }

    [path closePath];
    [path stroke];
}

Рассмотрим теперь реализацию класса Cube4DView. Наиболее важным для нас методом является drawRect:.

Этот метод определен в базовом класса (NSView) и вызывается когда необходимо определить, что должно быть нарисовано в соответствующей части окна. Параметр rect и задает какую именно область требуется обновить.

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

За счет этого мы получим эффект размытости изображение - как бы за гиперкубом тянется слабый след. Ниже приводится соответствующий текст файла Cube4DView.mm.

@implementation Cube4DView

- (id)initWithFrame: (NSRect)frame isPreview:(BOOL) isPreview
{
    self = [super initWithFrame:frame isPreview:isPreview];

    if ( self )
    {
        shadowSizeValue = 5;

        [self setAnimationTimeInterval:1/20.0];
    }
	
    return self;
}

- (BOOL) hasConfigureSheet
{
    return NO;
}

- (void) startAnimation
{
    [super startAnimation];
}

- (void) stopAnimation
{
    [super stopAnimation];
}

- (void) drawRect:(NSRect)rect
{
    int i;

    [super drawRect:rect];

    [[NSColor blackColor] set];
    [[NSBezierPath bezierPathWithRect: rect] fill];

    for ( i = 1 - shadowSizeValue; i <= 0; i++ )
    {
        int intensity = 255 + (255*i) / shadowSizeValue;

        drawCube ( rect, intensity,  angle1 + i * step1 / 2, angle2 + i * step2 / 2 );	
    }

    angle1 += step1;
    angle2 += step2;
}

- (void)animateOneFrame
{
    [self setNeedsDisplay: YES];

    return;
}

@end

Соберем проект, используя кнопку Build, после чего сделаем двойной щелчок мышью по собранному файлу Cube4D.saver.

Рис 11. Запуск собранного скринсейвера.

Это приведет к запуску System Preferences, которые откроются на разделе Screen Saver. В окне Preview мы увидим наш скринсейвер в действии. Кнопка Test позволяет запустить скринсейвер в полноэкранном режиме, а кнопка Options ... позволяет вызвать панель настроек скринсейвера (которой у нас пока нет).

Рис 12. Окно настроек скринсейверов в системе с нашим скринсейвером.

Обратите внимание, что при повторном двойном щелчке мышью по Cube4D.saver вы получите предупреждение, что скринсейвер с таким именем уже существует и вам необходимо будет нажать на OK для замены уже существующего файла на новый.

Рис 13. При попытке установить уже имеющийся скринсейвер система выдает предупреждение.

Подключение панели настроек

Для подключения панели настроек для нашего скринсейвера служат следующие сообщения, определенные в классе ScreenSaverView - hasConfigSheet и configureSheet.

Первый из них сообщает есть ли у данного скринсейвера панель настроек. Поскольку мы хотим добавить ее, то наша реализация этого метода должна возвращать YES.

Второй метод (configureSheet) служит для возвращения панели настроек (объекта класса NSWindow).

Как всегда, весь интерфейс панели настроек делается в Interface Builder'е. Однако обычно окна, которые не часто используются, помещаются в отдельный nib-файл. Это позволяет сделать загрузку программы более быстрой. Однако при необходимости показать соответствующее окно необходимый nib-файл загружается и обычно при дальнейших обращениях используется уже загруженный файл (точнее, соответствующий объект).

В нашем случае нам достаточно один раз загрузить соответствующую панель и запомнить указатель на нее в instance-переменной объекта Cube4DView. Причем эту переменную можно не инициализировать nil'ом в начале - при создании объектов, все их переменные обнуляются.

Задание настроек мы рассмотрим на примере задания всего одного параметра - размера "следа" за вращающимся гиперкубом. Для поддержки настройки этого параметра нам понадобятся некоторые изменения в заголовочном файле. Новая версия этого файла приводится ниже.

@interface Cube4DView : ScreenSaverView 
{
    IBOutlet id configSheet;
    IBOutlet id shadowSize;
    int         shadowSizeValue;
}

- (IBAction) okClick: (id) sender;
- (IBAction) cancelClick: (id) sender;

@end

Мы добавили два outlet'а - один (configSheet) будет указывать на саму панель настроек (причем установка значения для этого outlet'а будет осуществляться непосредственно в nib-файле), а другой (shadowSize) на поле для ввода текста на нашей панели, содержащей настраиваемое значение.

Также нам понадобятся реализации для методов, соответствующих нажатиям на кнопки OK Cancel, и новые реализации методов hasConfigSheet и configureSheet.

@implementation Cube4DView

static	NSString * cube4DModuleName = @"com.alex.Cube4D";

- (id)initWithFrame: (NSRect)frame isPreview:(BOOL) isPreview
{
    self = [super initWithFrame:frame isPreview:isPreview];

    if (self)
    {
        ScreenSaverDefaults * defs;

        defs = [ScreenSaverDefaults defaultsForModuleWithName: cube4DModuleName];

                                  // register default values
        [defs registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys: 
               [NSNumber numberWithInt: 3], @"ShadowSize", nil]];

        shadowSizeValue = [defs integerForKey: @"ShadowSize"];

        [self setAnimationTimeInterval:1/20.0];
    }

    return self;
}

- (BOOL) hasConfigureSheet
{
    return YES;
}

- (NSWindow *) configureSheet
{
    ScreenSaverDefaults * defs;

    if ( !configSheet )
    {
        if ( ! [NSBundle loadNibNamed: @"Cube4D" owner: self] )
            NSBeep ();
    }

    defs = [ScreenSaverDefaults defaultsForModuleWithName: cube4DModuleName];

    [shadowSize setIntValue: shadowSizeValue];

    return configSheet;
}

- (void) startAnimation
{
    [super startAnimation];
}

- (void) stopAnimation
{
    [super stopAnimation];
}

- (void) drawRect:(NSRect)rect
{
    int i;

    [super drawRect:rect];

    [[NSColor blackColor] set];
    [[NSBezierPath bezierPathWithRect: rect] fill];

    for ( i = 1 - shadowSizeValue; i <= 0; i++ )
    {
        int intensity = 255 + (255*i) / shadowSizeValue;

        drawCube ( rect, intensity,  angle1 + i * step1 / 2, angle2 + i * step2 / 2 );	
    }

    angle1 += step1;
    angle2 += step2;
}

- (void) animateOneFrame
{
    [self setNeedsDisplay: YES];
}

- (IBAction) cancelClick: (id) sender
{
    [[NSApplication sharedApplication] endSheet: configSheet];
}

- (IBAction) okClick: (id) sender
{
    ScreenSaverDefaults * defs;

    defs            = [ScreenSaverDefaults defaultsForModuleWithName: cube4DModuleName];
    shadowSizeValue = [shadowSize intValue];

    [defs setInteger: shadowSizeValue forKey: @"ShadowSize"];
    [defs synchronize];

    [[NSApplication sharedApplication] endSheet: configSheet];
}

@end

Обратите внимание на использование класса NSBundle для загрузки требуемого nib-файла. При этом данная загрузка происходит только в том случае, если панель настроек еще не была загружена. В качестве владельца загруженного файла выступает сам объект класса Cube4DView (что и позволит при загрузке автоматически связать переменную configSheet с указателем на панель).

Еще одним механизмом системы, с которым мы сталкиваемся в этом коде, является система хранения настроек. В данном случае мы используем специализированный вариант системы настроек именно для создания скринсейверов (используя метод класса defaultsForModuleWithName и класс ScreenSaverDefaults).

В общем случае работа системы хранения настроек выглядит аналогичным образом - настройки задаются при помощи словаря, в котором мы можем размещать значения основных классов (NSString, NSNumber, NSData и т.п.).

Настройки сохраняются под определенным именем, обычно имеющем форму перевернутого адреса - "com.company.application". Однако в отличие от традиционных систем мы с самого начала должны задать словарь с настройками по умолчанию (при помощи метода registerDefaults:).

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

Рассмотрим теперь создание интерфейса панели настроек.

Создадим отдельный nib-файл Cube4D.nib, содержащий панель настроек. Для этого запустим Interface Builder и создадим пустой проект (см. рис 14).

Рис 14. Создание пустого проекта в Interface Builder'е.

В результате этого мы получим пустой проект, в разделе Instances которого будет всего два объекта - File's Owner и First Responder.

Рис 15. Состояние перед добавлением панели.

Откроем панель компонент по закладке окон и перетащим из нее объект с меткой Panel в раздел Instances (рис. 16).

Рис 16. Панель перетащена в раздел Instances.

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

Рис 17. Вид панели настроек для Cube4D.

Теперь нам надо сообщить Interface Builder'у, что в качестве владельца данного nib-файла будет выступать объект класса Cube4DView. К сожалению этот класс унаследован от изначально неизвестного Interface Builder'у класса ScreenSaverView.

Поэтому нам нужно сообщить как об этом классе, так и о класса Cube4DView. Для этого воспользуемся командой меню Classes/Read Feles .... Сначала необходимо задать чтение файла ScreenSaverView.h из каталога /Developer/SDKs/Mac OSX10.4u.sdk/System/Library/Frameworks/ScreenSaver.framework/Headers (см. рис 18).

Рис 18. Чтение файла ScreenSaverView.h

После этого опять этой же командой меню прочтем файл Cube4DView.h.

Рис 19. Чтение файла Cube4DView.h.

После этого выберем в разделе Instances объект File's Owner и в инспекторе для него откроем раздел Custom Class, далее выберем из списка классов наш класс Cube4DView.

Рис 20. Явное задание класса для объекта File's Owner.

После того, как мы задали точный класс для File's Owner'а, нам стали доступны методы и outlet'ы этого объекта и мы можем связать их с компонентами на панели настроек.

Сначала свяжем кнопку OK с методом okClick: объекта File's Owner.

Рис 21. Установки связи для кнопки ОК.

Аналогично свяжем кнопку Cancel с методом cancelClick:, а outlet configSheet - самим окном.

Рис 22. Установка связи самой панели с outletconfigSheet.

Поле для ввода текста свяжем с outlet'ом shadowSize.

Рис 23. Подключение outletshadowSize.

Сохраним нам nib-файл под именем Cube4D.nib в каталог проекта Cube4D и при сохранении пометим пункт подключения этого файла к нашему проекту в XCode.

Рис 24. При сохранении проекта Cube4D.nib нам автоматически предложат добавить его к проекту Cube4D в XCode.

Пересоберем проект и как и ранее двойным щелчком по собранному файлу (Cube4D.saver) запустим настройки системы.

Рис 25. Перед вызовом панели настроек нашего скринсейвера.

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

Рис 26. Вызванная панель настроек для Cube4D.

Замечание. Mac OS X обычно поставляет средства для разработки сразу с двумя версиями gcc - 3.3 и 4. Только версия 4 поддерживает язык Objective-C++.

Выбор версии компилятора можно осуществить при помощи команд:

sudo /usr/sbin/gcc_select 3.3
sudo /usr/sbin/gcc_select 4.0

Весь проект можно скачать по этой ссылке.

Valid HTML 4.01 Transitional

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