Программирование — страница 30 из 57

Графические классы

“Язык, не изменяющий ваш образ мышления,

изучать не стоит”.

Расхожее мнение


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

Point
,
Color
,
Polygon
и
Open_polyline
, а также методам их использования. В следующей главе будут изложены идеи, связанные с проектированием связанных классов, а также описаны другие их методы реализации.

13.1. Обзор графических классов

Библиотеки графики и графического пользовательского интерфейса предоставляют множество возможностей. Слово “множество” означает сотни классов, часто содержащих десятки функций. Их описания, справочные руководства и документация напоминают учебники по ботанике, в которых перечислены тысячи растений, упорядоченных в соответствии с устаревшей классификацией. Это обескураживает! Обзор возможностей совершенных библиотек графики и графического пользовательского интерфейса может быть увлекательным занятием. Он может вызвать у читателей ощущения ребенка, попавшего в кондитерскую лавку и не понимающего, с чего начать и понравится ли ему то, что он выберет.

Цель нашей интерфейсной библиотеки — компенсировать шок, вызванный сложностью библиотек графики и графического пользовательского интерфейса. Мы опишем только два десятка классов с немногими операциями. Тем не менее они позволяют создавать полезные графические приложения. Кроме того, эти классы позволяют ввести ключевые понятия графики и графического пользовательского интерфейса. С их помощью читатели уже могут представлять результаты своей работы в виде простых графиков. Прочитав эту главу, вы сможете расширить спектр своих приложений и удовлетворить больше требований. Прочитав к тому же главу 14, вы освоите основные идеи и методы проектирования, которые позволят вам глубже разобраться в графических библиотеках и создать еще более сложные приложения. Этого можно достичь либо включив в свои программы описанные здесь классы, либо адаптировав другие библиотеки графики и графического пользовательского интерфейса.

Основные интерфейсные классы перечислены в следующей таблице.



Классы

Function
и
Axis
описываются в главе 15. В главе 16 рассматриваются основные интерфейсные классы.



Исходный код состоит из следующих файлов.



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

Shape
или
Widget
.



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

• Продемонстрировать связь между кодом и создаваемыми рисунками.

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

• Научить вас размышлять о проектировании программ, в частности о том, как выразить понятия в виде классов. Почему эти классы устроены так, а не иначе? Как еще их можно было бы написать? Вы можете принять много-много проектных решений, и в большинстве своем они будут отличаться от наших незначительно, а в некоторых случаях — кардинально.


Итак, пожалуйста, не торопитесь, иначе пропустите нечто важное и не сможете выполнить упражнения. 

13.2. Классы Point и Line

 Самой главной частью любой графической системы является точка. Определив это понятие, вы определите все ваше геометрическое пространство. В данной книге мы используем обычное, компьютерно-ориентированное двумерное представление точек в виде пары целочисленных координат (x, y). Как указано в разделе 12.5, координаты x изменяются от нуля (левого края экрана) до

x_max()
(правого края экрана); координаты y изменяются от нуля (верхнего края экрана) до
y_max()
(нижнего края экрана).

Как определено в файле

Point.h
, класс
Point
— это просто пара чисел типа
int
(координаты).


struct Point {

  int x, y;

  Point(int xx, int yy):x(xx), y(yy) { }

  Point() :x(0), y(0) { }

};


bool operator==(Point a, Point b) { return a.x==b.x && a.y==b.y; }

bool operator!=(Point a, Point b) { return !(a==b); }


В файле

Graph.h
определены также класс
Shape
, подробно описанный в главе 14, и класс
Line
.


struct Line:Shape {       // класс Line — это класс Shape,

                          // определенный двумя точками

Line(Point p1, Point p2); // создаем объект класса Line

                          // из двух объектов класса Points

};


Класс

Line
— это разновидность класса
Shape
. Именно это означает строка
“:Shape”
. Класс
Shape
называют базовым (base class) по отношению к классу
Line
. В принципе класс
Shape
содержит возможности, чтобы упростить определение класса
Line
. Как только мы столкнемся с конкретными фигурами, например
Line
или
Open_polyline
, то увидим, что это значит (см. главу 14).

Класс

Line
определяется двумя объектами класса
Point
. Оставляя в стороне “леса” (директивы #include и прочие детали, описанные в разделе 12.3), мы можем создать линию и нарисовать ее на экране.


// рисуем две линии

Simple_window win1(Point(100,100),600,400,"Two lines");

Line horizontal(Point(100,100),Point(200,100)); // горизонтальная 

                                                // линия

Line vertical(Point(150,50),Point(150,150));    // вертикальная

                                                // линия

win1.attach(horizontal);                        // связываем их

                                                // с экраном

win1.attach(vertical);

win1.wait_for_button();                         // изобразить!


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

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


Line vertical(Point(150,50),Point(150,150));


создает (вертикальную) линию, соединяющую точки (150,50) и (150,150). Разумеется, существуют детали реализации, но вам необязательно знать их, чтобы создавать линии. Реализация конструктора класса

Line
довольно проста.



Line::Line(Point p1, Point p2) // создаем линию по двум точкам

{

  add(p1); // добавляем точку p1

  add(p2); // добавляем точку p2

}


Иначе говоря, конструктор просто добавляет две точки. Добавляет куда? И как объект класса

Line
рисуется в окне? Ответ кроется в классе
Shape
. Как будет описано в главе 14, класс
Shape
может хранить точки, определяющие линии, знает, как рисовать линии, определенные парами точек, и имеет функцию
add()
, позволяющую добавлять объекты в объекты класса
Point
. Основной момент здесь заключается в том, что определение класса
Line
тривиально. Большая часть работы по реализации выполняется системой, поэтому программист может сосредоточиться на создании простых классов, которые легко использовать.

С этого момента оставим в стороне определение класса

Simple_window
и вызовы функции
attach()
. Они не более чем “леса”, необходимые для завершения программы, но ничего не добавляющие к специфике объектов класса
Shape

13.3. Класс Lines

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

Lines
.


struct Lines:Shape {            // связанные друг с другом линии

  void draw_lines() const;

  void add(Point p1, Point p2); // добавляем линию, заданную

                                // двумя точками

};


Объект класса

Lines
представляет собой коллекцию линий, каждая из которых определена парой объектов класса
Point
. Например, если бы мы рассматривали две линии из примера в разделе 13.2 как часть отдельного графического объекта, то могли бы дать такое определение:


Lines x;

x.add(Point(100,100), Point(200,100)); // первая линия: 
горизонтальная

x.add(Point(150,50), Point(150,150));  // вторая линия: вертикальная


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

Line
.



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

 Разница между совокупностью объектов класса

Line
и совокупностью линий в объекте класса
Lines
заключается лишь в нашей точке зрения на то, что должно произойти. Используя класс
Lines
, мы выражаем наше мнение, что две линии образуют одно целое и должны обрабатываться одновременно. Например, мы можем изменить цвет всех линий, являющихся частью объекта
Lines
, с помощью одной команды. С другой стороны, мы можем присвоить каждой линии, являющейся отдельным объектом класса
Line
, разные цвета. В качестве более реалистичного примера рассмотрим определение сетки. Сетка состоит из большого количества горизонтальных и вертикальных линий, проведенных на одинаковых расстояниях друг от друга. Однако мы считаем сетку одним целым, поэтому определяем ее линии как части объекта класса
Lines
, который называется
grid
.


int x_size = win3.x_max(); // определяем размер нашего окна

int y_size = win3.y_max();

int x_grid = 80;

int y_grid = 40;


Lines grid;

for (int x=x_grid; x

grid.add(Point(x,0),Point(x,y_size)); // вертикальная линия

for (int y = y_grid; y

grid.add(Point(0,y),Point(x_size,y)); // горизонтальная линия


Обратите внимание на то, как мы определили размеры нашего окна с помощью функций

x_max()
и
y_max()
. Это первый пример, в котором мы написали код, вычисляющий объект, подлежащий выводу на экран. Было бы невыносимо скучно определять сетку, вводя именованные переменные для каждой линии, из которых она состоит. Данный фрагмент кода создает следующее окно.



Вернемся к классу

Lines
. Как реализованы функции-члены класса
Lines
? Класс
Lines
выполняет только две операции. Функция
add()
просто добавляет линию, определенную парой точек, к набору линий, которые будут выведены на экран.


void Lines::add(Point p1, Point p2)

{

  Shape::add(p1);

  Shape::add(p2);

}


Да, квалификатор

Shape::
необходим, поскольку в противном случае компилятор рассматривал бы выражение
add(p1)
как недопустимую попытку вызвать функцию
add()
из класса
Lines
, а не из класса
Shape
.

Функция

draw_lines()
рисует линии, определенные с помощью функции
add()
.


void Lines::draw_lines() const

{

  if (color().visibility())

  for (int i=1; i

  fl_line(point(i–1).x,point(i–1).y,

  point(i).x,point(i).y);

}


Иначе говоря, функция

Lines::draw_lines()
на каждом шаге цикла получает две точки (начиная с точек
0
и
1
) и рисует линию, соединяющую эти точки с помощью библиотечной функции
fl_line()
. Видимость (visibility) — это свойство объекта класса
Color
(раздел 13.4), поэтому, прежде чем рисовать эти линии, мы должны проверить, что они являются видимыми.

Как будет показано в главе 14, функция

draw_lines()
вызывается системой. Мы не обязаны проверять, является ли количество точек четным, так как функция
add()
класса
Lines
может добавлять только пары точек. Функции
number_of_points()
и
point()
определены в классе
Shape
(см. раздел 14.2), и их смысл очевиден. Эти две функции обеспечивают доступ к точкам объекта класса
Shape
только для чтения. Функция-член
draw_lines()
определена как
const
(см. раздел 9.7.4), поскольку она не изменяет фигуру.

 Мы не предусмотрели в классе

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

13.4. Класс Color

Color
— это тип, описывающий цвет. Его можно использовать примерно так:


grid.set_color(Color::red);


Эта инструкция окрашивает линии, определенные в объекте

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

Класс

Color
определяет понятие цвета и приписывает символические имена нескольким наиболее распространенным цветам.



struct Color {

  enum Color_type {

    red=FL_RED,

    blue=FL_BLUE,

    green=FL_GREEN,

    yellow=FL_YELLOW,

    white=FL_WHITE,

    black=FL_BLACK,

    magenta=FL_MAGENTA,

    cyan=FL_CYAN,

    dark_red=FL_DARK_RED,

    dark_green=FL_DARK_GREEN,

    dark_yellow=FL_DARK_YELLOW,

    dark_blue=FL_DARK_BLUE,

    dark_magenta=FL_DARK_MAGENTA,

    dark_cyan=FL_DARK_CYAN

  };


  enum Transparency { invisible = 0, visible=255 };


  Color(Color_type cc) :c(Fl_Color(cc)), v(visible) { }

  Color(Color_type cc, Transparency vv) :c(Fl_Color(cc)), v(vv)
 { }

  Color(int cc) :c(Fl_Color(cc)), v(visible) { }

  Color(Transparency vv) :c(Fl_Color()), v(vv) { } // цвет по

                                                   // умолчанию

  int as_int() const { return c; }


  char visibility() const { return v; }

  void set_visibility(Transparency vv) { v=vv; }

private:

  char v; // видимый или невидимый

  Fl_Color c;

}; 


Предназначение класса

Color
заключается в следующем.

• Скрыть реализацию цвета в классе

Fl_Color
из библиотеки FLTK.

• Задать константы, соответствующие разным цветам.

• Обеспечить простую реализацию прозрачности (видимый или невидимый).


Цвет можно выбрать следующим образом.

• Выбрать константу из списка, например

Color::dark_blue
.

• Выбрать цвет из небольшой палитры, которую большинство программ выводит на экран (им соответствуют значения в диапазоне от 0–255; например, выражение

Color(99)
означает темно-зеленый цвет). Пример такой программы приведен в разделе 13.9.

• Выбрать значение в системе RGB (Red, Green, Blue — красный, зеленый, синий), которую мы здесь обсуждать не будем. При необходимости читатели сами в ней разберутся. В частности, можно просто ввести запрос “RGB color” в поисковую веб-машину. Среди прочих вы получите ссылки www.hyperso-lutions.org/rgb.html и www.pitt.edu/~nisg/cis/web/cgi/rgb.html. См. также упр. 13 и 14.


 Обратите внимание на конструкторы класса

Color
, позволяющие создавать объекты как из объектов типа
Color_type
, так и из обычных чисел типа
int
. Каждый конструктор инициализирует член
c
. Вы можете возразить, что переменная c названа слишком коротко и непонятно, но, поскольку она используется в очень небольшой части класса
Color
и не предназначена для широкого использования, это не является недостатком. Мы поместили член
c
в закрытый раздел, чтобы защитить его от непосредственного обращения пользователей. Для представления члена c мы используем тип
Fl_Color
, определенный в библиотеке FLTK, который хотели бы скрыть от пользователей. Однако очень часто этот тип интерпретируется как целочисленное представление значения RGB (или другого значения), поэтому на этот случай мы предусмотрели функцию
as_int()
. Обратите внимание на то, что функция
as_int()
является константной функцией-членом, поскольку она не изменяет объект класса
Color
, который ее использует.

Прозрачность задается членом

v
, который может принимать значения
Color::visible
и
Color::invisible
, имеющие очевидный смысл. Вы можете удивиться: зачем нужен “невидимый цвет”. Оказывается, он может быть очень полезен для того, чтобы скрыть часть фигуры на экране. 

13.5. Класс Line_style

Нарисовав на экране несколько линий, мы можем различать их по цвету, стилю или по обоим этим признакам. Стиль линии — это шаблон, задающий ее внешний вид. Класс

Line_style
используется приблизительно так:


grid.set_style(Line_style::dot);


Эта инструкция выводит на экран линии, заданные в объекте

grid
, как последовательность точек, а не как сплошную линию.



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

Класс

Line_style
выглядит так:


struct Line_style {

  enum Line_style_type {

    solid=FL_SOLID,           // -------

    dash=FL_DASH,             // - - - -

    dot=FL_DOT,               // .......

    dashdot=FL_DASHDOT,       // - . - .

    dashdotdot=FL_DASHDOTDOT, // -..-..

  };


  Line_style(Line_style_type ss):s(ss), w(0) { }

  Line_style(Line_style_type lst, int ww):s(lst), w(ww) { }

  Line_style(int ss):s(ss), w(0) { }

  int width() const { return w; }

  int style() const { return s; }

private:

  int s;

  int w;

};


Методы программирования, использованные для определения класса

Line_style
, ничем не отличаются от методов, использованных для класса
Color
. Здесь мы снова скрываем тот факт, что для представления стилей линии библиотека FKTK использует тип
int
. Почему стоит скрывать эту информацию? Потому что эти способы представления при модификации библиотеки могут измениться. В следующей версии библиотеки FLTK может появиться тип
Fl_linestyle
, да и мы сами можем перенастроить наш интерфейс на другую библиотеку. В любом случае не стоит замусоривать свой код переменными типа
int
только потому, что мы знаем, как они задают стиль линий.

 Как правило, мы не заботимся о стиле вообще; мы просто полагаемся на параметры, заданные по умолчанию (сплошные линии, ширина которых задана по умолчанию). Если мы не указываем ширину линии явно, то она задается конструктором. Установка значений по умолчанию — это одно из предназначений конструктора, а правильно выбранные значения, задаваемые по умолчанию, могут значительно облегчить работу пользователей.

Класс

Line_style
состоит из двух “компонентов”: характеристики стиля (например, пунктирные или сплошные линии) и ширины (толщина линий). Ширина измеряется в целых числах. По умолчанию ширина равна единице. Если нам нужна более широкая линия, то ее толщину можно задать следующим образом:


grid.set_style(Line_style(Line_style::dash,2));


В итоге получим следующее изображение:



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

Lines
,
Open_polyline
или
Polygon
. Если мы хотим управлять цветом или стилем линий по отдельности, то их следует задать как отдельные объекты класса
Line
. Рассмотрим пример.


horizontal.set_color(Color::red);

vertical.set_color(Color::green);


На экране откроется окно, приведенное ниже.



13.6. Класс Open_polyline

Класс

Open_polyline
определяет фигуру, состоящую из ряда отрезков линий, соединенных между собой и заданных последовательностью точек. Слово
poly
имеет греческое происхождение и означает “много”, а
polyline
— это удобное имя для фигуры, состоящей из многих линий. Рассмотрим пример.


Open_polyline opl;

opl.add(Point(100,100));

opl.add(Point(150,200));

opl.add(Point(250,250));

opl.add(Point(300,200));



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

В принципе

Open_polyline
— это выдуманное слово, которое мы позаимствовали из детской игры “Connect the Dots” (“Соедини точки”).

Класс

Open_polyline
определен следующим образом:


struct Open_polyline:Shape { // открытая последовательность линий

  void add(Point p) { Shape::add(p); }

};


Да-да, это все определение. В нем практически ничего нет, кроме указания имени класса и того факта, что он является наследником класса

Shape
. Функция
add()
класса
Open_polyline
просто позволяет пользователям получить доступ к функции
add()
из класса
Shape
(т.е.
Shape::add()
). Нам даже не нужно определять функцию
draw_lines()
, так как класс
Shape
по умолчанию интерпретирует добавленные точки как последовательность линий, соединенных друг с другом. 

13.7. Класс Closed_polyline

Класс

Closed_polyline
похож на класс
Open_polyline
, за исключением того, что последняя точка соединяется с первой. Например, можно было бы создать объект класса
Closed_polyline
из тех же точек, из которых был построен объект класса
Open_polyline
в разделе 13.6.


Closed_polyline cpl;

cpl.add(Point(100,100));

cpl.add(Point(150,200));

cpl.add(Point(250,250));

cpl.add(Point(300,200));


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

Определение класса

Closed_polyline
приведено ниже.


struct Closed_polyline:Open_polyline { // замкнутый ряд линий

  void draw_lines() const;

};


void Closed_polyline::draw_lines() const

{

  Open_polyline::draw_lines(); // сначала рисуем открытый ряд линий,

                               // затем рисуем замыкающую линию:

  if (color().visibility())

    fl_line(point(number_of_points()–1).x,

            point(number_of_points()–1).y,

            point(0).x,

            point(0).y);

}



В классе

Closed_polyline
нужна отдельная функция
draw_lines()
, рисующая замыкающую линию, которая соединяет последнюю точку с первой. К счастью, для этого достаточно реализовать небольшую деталь, которая отличает класс
Closed_polyline
от класса
Shape
. Этот важный прием иногда называют “программированием различий“ (“programming by difference”). Нам нужно запрограммировать лишь то, что отличает наш производный класс (
Closed_polyline
) от базового (
Open_polyline
).

Итак, как же нарисовать замыкающую линию? Воспользуемся функцией

fl_line()
из библиотеки FLTK. Она получает четыре аргументы типа
int
, задающих четыре точки. И здесь нам снова понадобится графическая библиотека. Однако обратите внимание на то, что, как и во многих других ситуациях, упоминание библиотеки FLTK скрыто от пользователей. В программе пользователя нет никаких ссылок на функцию
fl_line()
, и ей неизвестно ничего о неявном представлении точек в виде пар целых чисел. При необходимости мы могли бы заменить библиотеку FLTK другой библиотекой графического пользовательского интерфейса, а пользователи этого почти не заметили бы.

13.8. Класс Polygon

Класс Polygon очень похож на класс

Closed_polyline
. Единственная разница состоит в том, что в классе
Polygon
линии не могут пересекаться. Например, объект класса
Closed_polyline
, изображенный выше, был многоугольником, но если к нему добавить еще одну точку, то ситуация изменится.


cpl.add(Point(100,250));


Результат изображен ниже.



В соответствии с классическими определениями объект класса

Closed_polyline
многоугольником не является. Как определить класс
Polygon
так, чтобы он правильно отображал связь с классом
Closed_polyline
, не нарушая правил геометрии? Подсказка содержится в предыдущем описании. Класс
Polygon
— это класс
Closed_polyline
, в котором линии не пересекаются. Иначе говоря, мы могли бы подчеркнуть способ образования фигуры из точек и сказать, что класс
Polygon
— это класс
Closed_polyline
, в который невозможно добавить объект класса
Point
, определяющий отрезок линии, пересекающийся с одной из существующих линий в объекте класса
Polygon
.

Эта идея позволяет описать класс

Polygon
следующим образом:


struct Polygon:Closed_polyline { // замкнутая последовательность

                                 // непересекающихся линий

  void add(Point p);

  void draw_lines() const;

};


void Polygon::add(Point p)

{

  // проверка того, что новая линия не пересекает существующие

  // (код скрыт)

  Closed_polyline::add(p);

}


Здесь мы унаследовали определение функции

draw_lines()
из класса
Closed_polyline
, сэкономив усилия и избежав дублирования кода. К сожалению, мы должны проверить каждый вызов функции
add()
. Это приводит нас к неэффективному алгоритму, сложность которого оценивается как N в квадрате, — определение объекта класса
Polygon
, состоящего из N точек, требует N*(N–1)/2 вызовов функции
intersect()
. По существу, мы сделали предположение, что класс
Polygon
будет использоваться для создания многоугольников с меньшим количеством точек.

 Например, для того чтобы создать объект класса

Polygon
, состоящего из 24 точек, потребуется 24*(24–1)/2 == 276 вызовов функции
intersect()
. Вероятно, это допустимо, но если бы мы захотели создать многоугольник, состоящий из 2000 точек, то вынуждены были бы сделать около 2 000 000 вызовов. Мы должны поискать более эффективный алгоритм, который может вынудить нас модифицировать интерфейс.

В любом случае можем создать следующий многоугольник:


Polygon poly;

poly.add(Point(100,100));

poly.add(Point(150,200));

poly.add(Point(250,250));

poly.add(Point(300,200));


Очевидно, что этот фрагмент создает объект класса

Polygon
, идентичный (вплоть до последнего пикселя) исходному объекту класса
Closed_polyline
.

Проверка того, что объект класса

Polygon
действительно представляет собой многоугольник, оказывается на удивление запутанной. Проверка пересечений, которая реализована в функции
Polygon::add()
, является наиболее сложной во всей графической библиотеке. Если вас интересуют кропотливые геометрические манипуляции с координатами, взгляните на код. И это еще не все. Посмотрим, что произойдет, когда мы попытаемся создать объект класса
Polygon
лишь из двух точек. Лучше предусмотреть защиту от таких попыток.


void Polygon::draw_lines() const

{

  if (number_of_points() < 3)

    error("Меньше трех точек вводить нельзя.");

  Closed_polyline::draw_lines();
}



 Проблема заключается в том, что инвариант класса

Polygon
— “точки образуют многоугольник” — невозможно проверить, пока не будут определены все точки. Иначе говоря, в соответствии с настоятельными рекомендациями мы не задаем проверку инварианта в конструкторе класса
Polygon
. И все же “предупреждение о трех точках” в классе
Polygon::draw_lines()
— совершенно недопустимый трюк. (См. также упр. 18.) 

13.9. Класс Rectangle

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


struct Rectangle:Shape {

  Rectangle(Point xy, int ww, int hh);

  Rectangle(Point x, Point y);

  void draw_lines() const;


  int height() const { return h; }

  int width() const { return w; }

private:

  int h; // высота

  int w; // ширина

};


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


Rectangle::Rectangle(Point xy,int ww,int hh)
:w(ww),h(hh)

{

  if (h<=0 || w<=0)

    error("Ошибка: отрицательная величина");

  add(xy);

}


Rectangle::Rectangle(Point x,Point y)
:w(y.x–x.x),h(y.y–x.y)

{

  if (h<=0 || w<=0)

    error("Ошибка: отрицательная ширина или длина.");

  add(x);

}


Каждый конструктор соответствующим образом инициализирует члены

h
и
w
(используя синтаксис списка инициализации; см. раздел 9.4.4) и хранит верхнюю левую точку отдельно в базовом классе
Shape
(используя функцию
add()
). Кроме того, в конструкторах содержится проверка ширины и длины — они не должны быть отрицательными.

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

Polygon
и
Circle
. По этой причине понятие “заполнение цветом” — т.е. закраска пространства внутри прямоугольника — чаще применяется по отношению к прямоугольникам, чем к другим фигурам.

Заполнение цветом можно реализовать в конструкторе или в виде отдельной функции

set_fill_color()
(предусмотренной в классе
Shape
наряду с другими средствами для работы с цветом).


Rectangle rect00(Point(150,100),200,100);

Rectangle rect11(Point(50,50),Point(250,150));

Rectangle rect12(Point(50,150),Point(250,250)); // ниже rect11

Rectangle rect21(Point(250,50),200,100);        // правее rect11

Rectangle rect22(Point(250,150),200,100);       // ниже rect21


rect00.set_fill_color(Color::yellow);

rect11.set_fill_color(Color::blue);

rect12.set_fill_color(Color::red);

rect21.set_fill_color(Color::green);


В итоге получаем следующее изображение:



Если заполнение цветом не требуется, то прямоугольник считается прозрачным; вот почему вы видите желтый угол объекта

rect00
.

Фигуры можно передвигать в окне (см. раздел 14.2.3). Рассмотрим пример.


rect11.move(400,0); // вправо от rect21

rect11.set_fill_color(Color::white);

win12.set_label("rectangles 2");


В итоге получим изображение, приведенное ниже.



Заметьте, что только часть белого прямоугольника

rect11
помещается в окне. То, что выходит за пределы окна, “отрезается”; иначе говоря, на экране эта часть не отображается.

 Обратите внимание на то, как фигуры накладываются одна на другую. Это выглядит так, будто вы кладете на стол один лист бумаги на другой. Первый лист окажется в самом низу. Наш класс

Window
(раздел Д.3) реализует простой способ размещения фигуры поверх другой (используя функцию
Window::put_on_top()
). Рассмотрим пример.


win12.put_on_top(rect00);

win12.set_label("rectangles 3");


В итоге получаем следующее изображение:



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


rect00.set_color(Color::invisible);

rect11.set_color(Color::invisible);

rect12.set_color(Color::invisible);

rect21.set_color(Color::invisible);

rect22.set_color(Color::invisible);


Это приводит к следующему результату:



Обратите внимание на то, что цвет заполнения и цвет линии заданы параметром

invisible
, поэтому прямоугольник
rect22
на экране больше не виден.

Поскольку мы должны работать как с цветом линии, так и с цветом заполнения, функция-член

draw_lines()
класса
Rectangle
становится немного запутанной.


void Rectangle::draw_lines() const

{

  if (fill_color().visibility()) { // заполнение

    fl_color(fill_color().as_int());

    fl_rectf(point(0).x,point(0).y,w,h);

  }

  if (color().visibility()) { // линии поверх заполнения

    fl_color(color().as_int());

    fl_rect(point(0).x,point(0).y,w,h);

  }

}


Как видим, библиотека FLTK содержит функции для рисования как заполненных прямоугольников (

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

13.10. Управление неименованными объектами

До сих пор мы именовали все наши графические объекты. Когда же объектов много, то присваивать всем им имена становится нецелесообразно. В качестве примера нарисуем простую цветную диаграмму, состоящую из 256 цветов, предусмотренных в палитре библиотеки, иначе говоря, раскрасим 256 квадратов и нарисуем их в матрице 16×16.

Вот что у нас получится.



Называть все эти 256 квадратов было бы не только утомительно, но и глупо. Очевидно, что “имя” левого верхнего квадрата в матрице определяется его местоположением в точке (0,0), а все остальные квадраты можно точно так же идентифицировать с помощью пар координат (i, j). Итак, нам необходим эквивалент матрицы объектов. Сначала мы подумали о векторе

vector
, но оказалось, что он недостаточно гибок. Например, было бы неплохо иметь коллекцию неименованных объектов (элементов), не все из которых имеют одинаковые типы. Проблему гибкости мы обсудим в разделе 14.3, а здесь продемонстрируем наше решение: векторный тип, хранящий именованные и неименованные объекты.


template class Vector_ref {

public:

  // ...

  void push_back(T&);   // добавляет именованный объект

  void push_back(T*);   // добавляет неименованный объект

  T& operator[](int i); // индексация: доступ для чтения и записи

  const T& operator[](int i) const;

  int size() const;

};


Наше определение очень похоже на определение типа

vector
из стандартной библиотеки.


Vector_ref rect;

Rectangle x(Point(100,200),Point(200,300));


// добавляем именованные объекты

rect.push_back(x);


// добавляем неименованные объекты

rect.push_back(new Rectangle(Point(50,60),Point(80,90)));


// используем объект rect

for (int i=0; i


 Оператор new описан в главе 17, а реализация класса

Vector_ref
— в приложении Д. Пока достаточно знать, что мы можем использовать его для хранения неименованных объектов. За оператором new следует имя типа (в данном случае
Rectangle
) и, необязательно, список инициализации (в данном случае
(Point(50,60),Point(80,90))
).

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

Rectangle
и
Vector_ref
мы можем экспериментировать с цветами. Например, можем нарисовать простую диаграмму, состоящую из 256 цветов.


Vector_ref vr;

for (int i = 0; i<16; ++i)

  for (int j = 0; j<16; ++j) {

    vr.push_back(new Rectangle(Point(i*20,j*20),20,20));

    vr[vr.size()–1].set_fill_color(Color(i*16+j));

    win20.attach(vr[vr.size()–1]);

}


Мы создали объект класса

Vector_ref
, состоящий из 256 объектов класса
Rectangle
, организованный в объекте класса
Window
в виде матрицы 16×16. Мы приписали объектам класса
Rectangle
цвета 0, 1, 2, 3, 4 и т.д. После создания каждого из объектов этого типа они выводятся на экран.



13.11. Класс Text

Очевидно, что нам необходимо выводить на экран текст. Например, мы могли бы пометить “странный” объект класса

Closed_polyline
из раздела 13.8.


Text t(Point(200,200),"A closed polyline that isn't a polygon");

t.set_color(Color::blue);


В этом случае мы получим такое изображение.



В принципе объект класса

Text
определяет строку текста, начиная с точки, заданной объектом класса
Point
. Этот объект класса
Point
находится в левом нижнем углу текста. Мы ограничиваемся одной строкой, поскольку хотим, чтобы наша программа выполнялась на многих компьютерах. Не пытайтесь вставлять в окно символ перехода на новую строку. Для создания объектов класса
string
, подлежащих выводу на экран в объектах класса
Text
(см. примеры в разделах 12.7.7 и 12.7.8), очень полезны строковые потоки (см. раздел 11.4).


struct Text:Shape {

  // точка в левом нижнем углу первой буквы

  Text(Point x, const string& s)

    :lab(s), fnt(fl_font()), fnt_sz(fl_size()) 
{ add(x); }


  void draw_lines() const;

  void set_label(const string& s) { lab = s; }

  string label() const { return lab; }


  void set_font(Font f) { fnt = f; }

  Font font() const { return fnt; }


  void set_font_size(int s) { fnt_sz = s; }

  int font_size() const { return fnt_sz; }

private:

  string lab; // label

  Font fnt;

  int fnt_sz;

};


Класс

Text
имеет свою собственную функцию-член
draw_lines()
, поскольку только он знает, как хранится его строка.


void Text::draw_lines() const

{

  fl_draw(lab.c_str(),point(0).x,point(0).y);

}


Цвет символов определяется точно так же, как в фигурах, состоящих из линий (например,

Open_polyline
и
Circle
), поэтому можем выбирать новый цвет с помощью функции
set_color()
, а определять текущий цвет — с помощью функции
color()
. Размер и шрифт символов выбираются аналогично. В классе предусмотрено небольшое количество заранее определенных шрифтов.


class Font { // шрифт символа

public:

  enum Font_type {

    helvetica=FL_HELVETICA,

    helvetica_bold=FL_HELVETICA_BOLD,

    helvetica_italic=FL_HELVETICA_ITALIC,

    helvetica_bold_italic=FL_HELVETICA_BOLD_ITALIC,

    courier=FL_COURIER,

    courier_bold=FL_COURIER_BOLD,

    courier_italic=FL_COURIER_ITALIC,

    courier_bold_italic=FL_COURIER_BOLD_ITALIC,

    times=FL_TIMES,

    times_bold=FL_TIMES_BOLD,

    times_italic=FL_TIMES_ITALIC,

    times_bold_italic=FL_TIMES_BOLD_ITALIC,

    symbol=FL_SYMBOL,

    screen=FL_SCREEN,

    screen_bold=FL_SCREEN_BOLD,

    zapf_dingbats=FL_ZAPF_DINGBATS

  };


  Font(Font_type ff):f(ff) { }

  Font(int ff) :f(ff) { }


  int as_int() const { return f; }

private:

  int f;

};


Стиль определения класса

Font
совпадает со стилями определения классов
Color
(см. раздел 13.4) и
Line_style
(см. раздел 13.5). 

13.12. Класс Circle

 Просто для того чтобы показать, что не все фигуры в мире являются прямоугольными, мы создали классы

Circle
и
Ellipse
. Объект класса
Circle
определяется центром и радиусом.


struct Circle:Shape {

  Circle(Point p, int rr); // центр и радиус


  void draw_lines() const;


  Point center() const;

  int radius() const { return r; }

  void set_radius(int rr) { r=rr; }

private:

  int r;

};


Использовать класс

Circle
можно следующим образом:


Circle c1(Point(100,200),50);

Circle c2(Point(150,200),100);

Circle c3(Point(200,200),150);


Эти инструкции рисуют три окружности разных радиусов, центры которых лежат на горизонтальной линии.



Основной особенностью реализации класса

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


Circle::Circle(Point p, int rr) // центр и радиус

       :r(rr)

{

  add(Point(p.x–r,p.y–r));      // хранит левый верхний угол

}


Point Circle::center() const

{

  return Point(point(0).x+r, point(0).y+r);

}


void Circle::draw_lines() const

{

  if (color().visibility())

    fl_arc(point(0).x,point(0).y,r+r,r+r,0,360);

}


Обратите внимание на использование функции

fl_arc()
, рисующей окружность. Первые два аргумента задают левый верхний угол, вторые два — ширину и высоту наименьшего прямоугольника, окаймляющего окружность, а последние два аргумента задают начальный и последний углы. Для того чтобы нарисовать окружность, нужно обойти вокруг ее центра все 360 градусов, но с помощью функции
fl_arc()
можно нарисовать только часть окружности (и часть эллипса); см. упр. 1. 

13.13. Класс Ellipse

Эллипс похож на окружность, но он определяется большой и малой осями, а не радиусом. Иначе говоря, для того чтобы определить эллипс, мы должны задать координаты центра, а также расстояние от центра до точки на оси x и расстояние от центра до точки на оси y.


struct Ellipse:Shape {

  // центр, минимальное и максимальное расстояние от центра

  Ellipse(Point p, int w, int h);


  void draw_lines() const;


  Point center() const;

  Point focus1() const;

  Point focus2() const;


  void set_major(int ww) { w=ww; }

  int major() const { return w; }


  void set_minor(int hh) { h=hh; }

  int minor() const { return h; }

private:

  int w;

  int h;

};


Класс

Ellipse
можно использовать следующим образом:


Ellipse e1(Point(200,200),50,50);

Ellipse e2(Point(200,200),100,50);

Ellipse e3(Point(200,200),100,150);


Этот фрагмент программы рисует три эллипса с общим центром и разными осями.



Объект класса

Ellipse
, для которого выполняется условие
major()==minor()
, выглядит как окружность. Эллипс можно также задать с помощью двух фокусов и суммы расстояний от точки до фокусов. Имея объект класса
Ellipse
, можем вычислить фокус. Рассмотрим пример.


Point Ellipse::focus1() const

{

  return Point(center().x+sqrt(double(w*w–h*h)),center().y);

}


 Почему класс

Circle
не является наследником класса
Ellipse
? С геометрической точки зрения каждая окружность является эллипсом, но не каждый эллипс является окружностью. В частности, окружность — это эллипс, у которого оба фокуса совпадают. Представьте себе, что мы определили класс
Circle
как разновидность класса
Ellipse
. В этом случае нам пришлось включать в представление дополнительные величины (окружность определяется центром и радиусом; для определения эллипса необходимы центр и пара осей). Мы не приветствуем излишние затраты памяти там, где они не нужны, но основная причина, по которой класс
Circle
не сделан наследником класса
Ellipse
, заключается в том, что мы не можем определить его, не заблокировав каким-то образом функции
set_major()
и
set_minor()
. Кроме того, фигура не была бы окружностью (что легко распознают математики), если бы мы использовали функцию
set_major()
, чтобы обеспечить выполнение условия
major()!=minor()
, — по крайней мере, после этого фигура перестанет быть окружностью. Нам не нужен объект, который иногда относится к одному типу (когда
major()!=minor()
), а иногда к другому (когда
major()==minor()
). Нам нужен объект (класса
Ellipse
), который иногда выглядит как окружность. С другой стороны, объект класса
Circle
никогда не превратится в эллипс с двумя неравными осями.

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

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

13.14. Класс Marked_polyline

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

Marked_polyline
. Рассмотрим пример.


Marked_polyline mpl("1234");

mpl.add(Point(100,100));

mpl.add(Point(150,200));

mpl.add(Point(250,250));

mpl.add(Point(300,200));


В результате выполнения этого фрагмента программы получим следующий результат:



Определение класса

Marked_polyline
имеет следующий вид:


struct Marked_polyline:Open_polyline {

  Marked_polyline(const string& m):mark(m)

  {

  if (m=="") mark = "*";

  }

  void draw_lines() const;

private:

  string mark;

};


Поскольку этот класс является наследником класса

Open_polyline
, можем свободно обрабатывать объекты класса
Point
, и все что нам для этого необходимо — иметь возможность ставить метки. В частности, функция
draw_lines()
примет следующий вид:


void Marked_polyline::draw_lines() const

{

  Open_polyline::draw_lines();

  for (int i=0; i

    draw_mark(point(i),mark[i%mark.size()]);

}


Вызов функции

Open_polyline::draw_lines()
рисует линии, так что остается просто расставить метки. Эти метки представляют собой строки символов, которые используются в определенном порядке: команда
mark[i%mark.size()]
выбирает символ, который должен быть использован следующим, циклически перебирая символы, хранящиеся в объекте класса
Marked_polyline
. Оператор
%
означает деление по модулю (взятие остатка). Для вывода буквы в заданной точке функция
draw_lines()
использует вспомогательную функцию меньшего размера
draw_mark()
.


void draw_mark(Point xy, char c)

{

  static const int dx = 4;

  static const int dy = 4;

  string m(1,c);

  fl_draw(m.c_str(),xy.x–dx,xy.y+dy);

}


Константы

dx
и
dy
используются для центрирования буквы относительно заданной точки. Объект
m
класса хранит единственный символ
c

13.15. Класс Marks

Иногда необходимо вывести метки отдельно от линий. Для этого предназначен класс

Marks
. Например, мы можем пометить четыре точки, использованные в предыдущих примерах, не соединяя их линиями.


Marks pp("x");

pp.add(Point(100,100));

pp.add(Point(150,200));

pp.add(Point(250,250));

pp.add(Point(300,200));


В итоге будет получено следующее изображение:



Очевидно, что класс

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

Класс

Marks
— это разновидность класса
Marked_polyline
с невидимыми линиями.


struct Marks : Marked_polyline {

  Marks(const string& m) :Marked_polyline(m)

  {

    set_color(Color(Color::invisible));

  }

};

13.16. Класс Mark

Объект класса

Point
задает координаты в объекте класса
Window
. Иногда мы их отображаем, а иногда нет. Если возникает необходимость пометить отдельную точку, чтобы ее увидеть, мы можем изобразить ее в виде крестиков, как показано в разделе 13.2, или воспользоваться классом
Marks
. Это объяснение слегка многословно, поэтому рассмотрим простой объект класса
Marks
, инициализированный точкой и символом.

Например, мы могли бы пометить центры окружностей, изображенных в разделе 13.12, следующим образом:


Mark m1(Point(100,200),'x');

Mark m2(Point(150,200),'y');

Mark m3(Point(200,200),'z');

c1.set_color(Color::blue);

c2.set_color(Color::red);

c3.set_color(Color::green);


В итоге мы получили бы изображения, приведенные ниже.



Класс

Mark
— это разновидность класса
Marks
, в котором при создании объекта немедленно задается начальная (и, как правило, единственная) точка.


struct Mark : Marks {

  Mark(Point xy, char c) : Marks(string(1,c))

  {

    add(xy);

  }

};


Функция

string(1,c)
— это конструктор класса
string
, инициализирующий строку, содержащую единственный символ
c
.

Класс

Mark
всего лишь позволяет легко создать объект класса
Marks
с единственной точкой, помеченной единственным символом. Стоило ли тратить силы, чтобы определять такой класс? Или он является следствием “ложного стремления к усложнениям и недоразумениям”? Однозначного и логичного ответа на этот вопрос нет. Мы много думали над этим и в конце концов решили, что для пользователей этот класс был бы полезен, а определить его было совсем нетрудно.

Почему в качестве метки используется символ? Можно было бы нарисовать любую маленькую фигуру, но символы нагляднее и проще. Они часто позволяют отделить одно множество точек от другого. К тому же такие символы, как

x
,
o
,
+
и
*
, обладают центральной симметрией. 

13.17. Класс Image

Файлы в типичном персональном компьютере хранят тысячи изображений. Кроме того, миллионы изображений доступны в сети веб. Естественно, мы хотели бы отображать содержимое этих файлов на экране с помощью относительно простых программ. Например, ниже продемонстрирован рисунок (

rita_path.gif
), иллюстрирующий путь урагана “Рита”, пришедшего из Мексиканского залива.

Мы можем выбрать часть этого изображения и добавить фотографию урагана, сделанную из космоса (

rita.jpg
).


Image rita(Point(0,0),"rita.jpg");

Image path(Point(0,0),"rita_path.gif");

path.set_mask(Point(50,250),600,400); // выбираем желательную область

win.attach(path);

win.attach(rita);


Операция

set_mask()
выбирает часть рисунка, которую следует изобразить на экране. В данном случае мы выбрали изображение размером 600×400 пикселей из файла
rita_path.gif
(загруженный как объект
path
) и показали его в области, левый верхний угол которой имеет координаты (50,250). Выбор части рисунка — довольно распространенный прием, поэтому мы предусмотрели для него отдельную операцию.



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

path
оказался на самом “дне”, просто потому, что он был связан с окном до объекта
rita
. Изображения могут кодироваться во множестве форматов. Здесь мы используем только два из них: JPEG и GIF.


struct Suffix {

  enum Encoding { none, jpg, gif };

};


В нашей библиотеке графического интерфейса изображение в памяти представляется как объект класса

Image
.


struct Image:Shape {

  Image(Point xy, string file_name,

    Suffix::Encoding e = Suffix::none);

  ~Image() { delete p; }

  void draw_lines() const;

  void set_mask(Point xy, int ww, int hh)

    { w=ww; h=hh; cx=xy.x; cy=xy.y; }

private:

  int w,h; // определяем "маскировочное окно" внутри изображения

           // по отношению к позиции (cx,cy)

  int cx,cy;

  Fl_Image* p;

  Text fn;

};


Конструктор класса

Image
пытается открыть файл с указанным именем, затем создать рисунок, используя кодировку, указанную в дополнительном аргументе или (как правило) в расширении файла. Если изображение невозможно вывести на экран (например, потому, что файл не найден), класс
Image
выводит на экран объект
Bad_image
. Определение класса
Bad_image
выглядит так:


struct Bad_image:Fl_Image {

  Bad_image(int h, int w):Fl_Image(h,w,0) { }

  void draw(int x,int y, int, int, int, int) { draw_empty(x,y); 
}

};


Работа с изображениями в графической библиотеке довольно сложна, но основная сложность класса

Image
кроется в файле, который обрабатывает его конструктор.


// более сложный конструктор, потому что ошибки,

// связанные с графическими файлами, трудно найти

Image::Image(Point xy, string s, Suffix::Encoding e)

      :w(0), h(0), fn(xy,"")

{

  add(xy);

  if (!can_open(s)) {         // можно ли открыть файл s?

    fn.set_label("Невозможно открыть \""+s+" ");

    p = new Bad_image(30,20); // ошибка графики

    return;

  }


  if (e == Suffix::none) e = get_encoding(s);


  switch(e) {                 // проверка кодировки

  case Suffix::jpg:

    p = new Fl_JPEG_Image(s.c_str());

    break;

  case Suffix::gif:

    p = new Fl_GIF_Image(s.c_str());

    break;

  default:                    // неприемлемая кодировка

    fn.set_label("Неприемлемый тип файла \""+s+" ");

    p = new Bad_image(30,20); // ошибка графики

  }

}


Расширение файла используется для того, чтобы определить вид объекта, создаваемого для хранения изображения ( Fl_JPEG_Image или Fl_GIF_Image). Этот объект создается с помощью оператора

new
и связывается с указателем. Подробности его реализации (в главе 17 рассматривается оператор new и указатели) связаны с организацией библиотеки FLTK и не имеют для нас большого значения.

Теперь настало время реализовать функцию

can_open()
, проверяющую, можно ли открыть файл для чтения.


bool can_open(const string& s)

  // проверка, существует ли файл s и можно ли его открыть

  // для чтения

{

  ifstream ff(s.c_str());

  return ff;

}


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

Если хотите, можете посмотреть на определение функции

get_encoding()
: она просто анализирует суффикс и ищет соответствие в таблице заранее заданных суффиксов. Эта таблица реализована с помощью стандартного типа
map
(подробнее об этом — в разделе 21.6).


Задание

1. Создайте объект класса

Simple_window
размером 800×1000 пикселей.

2. Разместите сетку размером 88 пикселей в левой части окна размером 800 на 800 пикселей (так что каждый квадрат сетки имеет размер 100×100 пикселей).

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

Rectangle
).

4. Подберите изображение размером 200×200 пикселей (в формате JPEG или GIF) и разместите три его копии поверх сетки (каждое изображение покроет четыре квадрата). Если вы не найдете изображения, размеры которого точно равнялись бы 200 пикселям, то, используя функцию

set_mask()
, вырежьте соответствующий фрагмент более крупного изображения. Не закрывайте красные квадраты.

5. Добавьте изображение размером 100×100 пикселей. Перемещайте его с одного квадрата на другой, щелкая на кнопке Next. Для этого поместите вызов функции

wait_for_button()
в цикл, сопроводив его командами, выбирающими новый квадрат для вашего изображения.


Контрольные вопросы

1. Почему мы просто не используем какую-нибудь коммерческую или бесплатную графическую библиотеку?

2. Сколько классов из библиотеки графического интерфейса нам понадобится, чтобы создать простой вывод графической информации?

3. Какие заголовочные файлы нужны для использования библиотеки графического интерфейса?

4. Какие классы определяют замкнутые фигуры?

5. Почему мы не используем класс

Line
для рисования любой фигуры?

6. Что означают аргументы конструктора класса

Point
?

7. Перечислите компоненты класса

Line_style
.

8. Перечислите компоненты класса

Color
.

9. Что такое система RGB?

10. В чем заключается разница между двумя объектами класса

Line
и объектом
Lines
, содержащим две линии?

11. Какие свойства можно задать для любого объекта класса

Shape
?

12. Сколько сторон объекта класса

Closed_polyline
определяются пятью объектами класса
Point
?

13. Что мы увидим на экране, если определим объект класса

Shape
, но не свяжем его с объектом класса
Window
?

14. Чем объект класса

Rectangle
отличается от объекта класса
Polygon
с четырьмя объектами класса
Point
(углами)?

15. Чем объект класса

Polygon
отличается от объекта класса
Closed_polyline
?

16. Что расположено сверху: заполненная цветом область или границы фигуры?

17. Почему мы не определили класс

Triangle
(ведь мы определили класс
Rectangle
)?

18. Как переместить объект класса

Shape
в другое место окна?

19. Как пометить объект класса

Shape
строкой текста?

20. Какие свойства текстовой строки можно задать в классе

Text
?

21. Что такое шрифт и зачем он нужен?

22. Для чего нужен класс

Vector_ref
и как его использовать?

23. В чем заключается разница между классами

Circle
и
Ellipse
?

24. Что произойдет, если мы попытаемся изобразить объект класса

Image
с заданным именем файла, а заданное имя файла не относится к файлу, содержащему изображение?

25. Как вывести на экран часть изображения?


Термины


Упражнения

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

1. Определите класс

Arc
, рисующий часть эллипса. Подсказка:
fl_arc()
.

2. Нарисуйте окно с закругленными углами. Определите класс Box, состоящий из четырех линий и четырех дуг.

3. Определите класс

Arrow
, рисующий стрелки.

4. Определите функции

n()
,
s()
,
e()
,
w()
,
center()
,
ne()
,
se()
,
sw()
и
nw()
. Каждая из них должна получать аргумент типа
Rectangle
и возвращать объект типа
Point
. Эти функции должны определять точки соединения, расположенные на границах и внутри прямоугольника. Например,
nw(r)
— это левый верхний угол объекта класса
Rectangle
с именем
r
.

5. Определите функции из упр. 4 для классов

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

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

Box
, объект которого представляет собой прямоугольник с текстовой меткой.

7. Создайте цветную диаграмму RGB (поищите пример в вебе).

8. Определите класс

Regular_hexagon
(шестиугольник — это правильный шестисторонний многоугольник). В качестве аргументов конструктора используйте центр и расстояние от центра до угловой точки.

9. Покройте часть окна узорами в виде объектов класса

Regular_hexagon
(используйте не меньше восьми шестиугольников).

10. Определите класс

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

11. Нарисуйте эллипс размером 300×200 пикселей. Нарисуйте ось x длиной 400 пикселей и ось y размером 300 пикселей, проходящие через центр эллипса. Пометьте фокусы. Отметьте точку на эллипсе, которая не принадлежит ни одной из осей. Соедините эту точку с фокусами двумя линиями.

12. Нарисуйте окружность. Заставьте метку перемещаться по окружности (пусть она перемещается каждый раз, когда вы щелкаете на кнопке Next).

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

14. Определите класс для прямоугольного треугольника. Составьте восьмиугольник из восьми прямоугольных треугольников разного цвета.

15. Покройте окно узорами в виде маленьких прямоугольных треугольников.

16. Покройте окно узорами в виде маленьких шестиугольников.

17. Покройте окно узорами в виде маленьких разноцветных шестиугольников.

18. Определите класс

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

19. Определите класс

Star
. Одним из его параметров должно быть количество точек. Нарисуйте несколько звездочек с разным количеством точек, разноцветными линиями и разными цветами заполнения.


Послесловие

В главе 12 мы играли роль пользователей классов. В этой главе мы перешли на один уровень вверх по “пищевой цепочке” программистов: здесь мы стали разработчиками классов и пользователями инструментов программирования.

Глава 14