В этой главе представлена библиотека Graphics, она входит является частью дистрибутива языка Objective CAML. Эта библиотека одинаково работает в графических интерфейсах основных платформ: Windows, MacOS, Unix с оболочкой X-Windows. С помощью Graphics мы можем реализовать графические рисунки с текстом, картинками, управлять различными базовыми событиями как нажатие на кнопку мыши или клавиатуры.
Модель программирования графических рисунков — “модель художника”: последний нарисованный слой стирает предыдущий. Это императивная модель, графическое окно в ней представляется как вектор пикселей, которые физически изменяются различными графическими функциями. Взаимодействие с мышью и клавиатурой подчиняется модели программирования событиями: главная функция программы это бесконечный цикл, в котором мы ожидаем действие пользователя.
Несмотря на простоту библиотеки Graphics, она вполне достаточна для введения концепций программирования графических интерфейсов с одной стороны и с другой стороны содержит базовые элементы простые в использовании программистом, при помощи которых мы можем реализовать более сложные интерфейсы.
В первом разделе мы объясним как пользоваться этой библиотекой на различных системах. Во втором изучим основы графического программирования: точка, рисунок, заполнение, цвет, bitmap. Для того чтобы проиллюстрировать эти концепции, в третьем разделе опишем и реализуем функции рисования “блоков” (boxes). В четвертом разделе увидим анимацию графических объектов и их взаимодействие с фоном экрана или другими анимационными объектами. В пятом разделе представлен стиль программирования событиями, а так же скелет любого графического приложения. И наконец, в последнем разделе мы воспользуемся библиотекой Graphics для реализации интерфейса калькулятора приведенного на стр. ??.
Использование библиотеки зависит от операционной системы и способа компиляции. Здесь мы рассмотрим только программы используемые в интерактивном цикле Objective CAML. В операционных систем Windows и MacOS интерактивное рабочее окружение само загружает библиотеку. Для систем Unix необходимо создать новый toplevel1, который зависит от того где находится библиотека X11. Если она находится в одном из каталогов по умолчанию, где ищутся библиотеки C, то командная строка будет выглядеть так:
Этим мы создаем команду mytoplevel с включенной библиотекой X11. Запуск этой комманды осуществляется так:
Если же библиотека расположена в другом каталоге, как в Linux, необходимо его явно указать:
В этом примере файл libX11.a ищется в каталоге /usr/X11/lib.
Более подробный пример команды ocamlmktop приведен в главе 6.
Программирование графических интерфейсов изначально связано с развитием аппаратных средств, в частности мониторов и графических карт. Для того чтобы изображени имело хорошее качество, необходимо чтобы оно регулярно и часто обновлялось (перерисовывалось), как в кино. Для рисования на экране существует два основных метода: либо используя список видимых сегментов, при этом только нужная часть изображения рисуется на экране, либо рисуя все точки экрана (экран bitmap). На обычных компьютерах используется последний способ.
Экран bitmap можно рассматривать как прямоугольник точек, называемые пиксел от английского picture element. Они являются базовыми элементами изображения. Высота и ширина экрана называется разрешением основного bitmap. Размер этого bitmap зависит от памяти занимаемой пикселом. Для черно–белого изображения пиксел может занимать один бит. Для цветных или серых изображений размер пиксела зависит от числа оттенков ассоциированных пикселу. Для bitmap 320x640 пикселей по 256 цветов в каждом, нам понадобится 8 битов на каждый пиксел, соответственно размер видео–памяти должен быть 480*640 байтов = 307200 байтов ≃ 300 ko. Это разрешения до сих пор используется некоторыми программами в MS-DOS.
В следующем списке приведены базовые операции над bitmap из библиотеки Graphics:
Каждая из этих операций использует координаты точки изображения для задания места рисования. Некоторые характеристики графических операций образуют графический контекст: толщина линии, соединение линий, выбор шрифта и его размер, стиль заливки. Графическая операция всегда выполняется в определенном контексте и ее результат зависит от этого контекста. Графический контекст библиотеки Graphics содержит лишь текущие точку, цвет, шрифт и его размер.
При графическом выводе следующие элементы являются базовыми: ориентир (начальная точка?) (reference point), графический контекст, цвет, изображение, заполнение замкнутых фигур, тексты и bitmap–ы.
Библиотека Graphics управляет единственным и главным окном. Координаты ориентира окна могут меняться от нижней левой точки (0,0) до верхнего правого угла. Вот основные операции над этим окном:
Размер окна определяется функциями size_x и size_y.
Строка, которую мы передаем функции open_graph, зависит от графической оболочки машины где запускается программа, то есть она платформенно–зависимая. Если строка пустая, то полученное окно будет иметь свойства по умолчанию. Мы можем явно указать размер окна; в X-Windows строка “ 200x300” создаст окно размером 200 пискелей по ширине и 300 по высоте. Заметьте, пробел — первый символ строки “ 200x300”, обязателен.
В графическом контексте есть несколько компонентов которые можно просмотреть/изменить:
текущая точка: | current_point : unit -> int * int |
moveto : int -> int -> unit | |
текущий цвет : | set_color : color -> unit |
ширина текущей линии : | set_line_width : int -> unit |
текущий шрифт : | set_font : string -> unit |
размер этого шрифта : | set_text_size : int -> unit |
Цвет представлен тремя байтами, каждый из которых хранит яркость основного цвета в модели RGB (red, green, blue) в интервале от 0 до 255. Функция rgb (с типом int -> int -> int -> color) создает цвет с тремя указанными значениями. Если все три значения равны, то мы получим серый цвет более или менее светлый, в зависимости от значений. Черный соответствует минимуму яркости для каждой компоненты цвета rgb(0,0,0), белый цвет — rgb(255,255,255). Есть некоторые предопределенные цвета: black, white, red, green, blue, yellow, cyan, magenta.
Переменные foreground и background соответствуют текущему цвету и цвету фона. При стирании экрана, осуществляется заполнение цветом фона.
Цвет (значение типа color) это на самом деле целое число, которое мы можем разбить на три части (from_rgb) или инвертировать (inv_color).
Функция point_color, типа int -> int -> color, возвращает цвет точки, координаты которой указаны на входе.
Функция рисования выводит линию на экран, при этом для толщины и цвета используются текущие значения. Функция заливки закрашивает замкнутую форму текущим цветом. Различные функции рисования и заливки приведены в таблице 4.1.
рисунок заполнение тип plot int -> int -> unit lineto int -> int -> unit fill_rect int -> int -> int -> int -> unit fill_poly ( int * int) array -> unit draw_arc fill_arc int -> int -> int -> int -> int -> unit draw_ellipse fill_ellipse int -> int -> int -> int -> unit draw_circle fill_circle int -> int -> int -> unit
Table 4.1: Функции рисования и заливки
Заметьте, что функция lineto имеет побочный эффект: она изменяет положение текущей точки.
В следующем примере, мы добавим некоторые функции рисования, которые не существуют в библиотеке. Многоугольник определяется вектором его вершин.
Обратите внимание на то что эти функции берут такие же аргументы что и существующие функции заливки. Так они не изменяют состояние текущей точки.
Следующий пример иллюстрирует сеть эстафетного кольца (token ring) (рис. 4.1). Каждый компьютер представлен в виде небольшого круга, все компьютеры соединены между собой и сеть образует кольцо. Текущее положение маркера в сети изображено черным кругом.
Функция net_points создает координаты всех компьютеров сети, эти данные будут храниться в векторе.
Функция draw_net выводит соединения, компьютеры и маркер.
При следующем вызове получим левую картинку на рисунке 4.1.
Заметим что порядок вывода на экран важен — сначала соединения, а затем узлы. Изображение узлов сети скрывает (в тексте стирает, прим. пер.) часть отрезков соединений. Таким образом нет надобности вычислять точки пересечения отрезков и кругов. На правой картинке рисунка 4.1 результат обратного порядка вывода на экран, где мы видим пересечения отрезков внутри кругов, представляющих узлы.
Две следующие функции вывода текста на экран чрезвычайно просты: draw_char (с типом char -> unit) и draw_string (с типом string -> unit) выводят на экран один символ и строку соответственно. Вывод осуществляется на текущую позицию экрана, также эти обе функции учитывают значение текущего шрифта и его размер.
Замечание
Вывод текста на экран может зависеть от графической оболочки.
Для переданой в аргументе строки, функция text_size возвращает пару целых
чисел, которые соответствуют размерам выведенной строки с текущим шрифтом и
размером.
В следующем примере функция draw_string_v вертикально выводит на экран строку начиная с текущей позиции. Результат изображен на рисунке 4.2, каждая буква выводится отдельно, меняя вертикальные координаты.
Эта функция изменяет текущую позицию, после вывода позиция перемещается на расстояние равное ширине символа.
Следующая программа выводит легенду вдоль координатных осей (рис. 4.2).
Для того чтобы вывести вертикальный текст, необходимо помнить что функция draw_string_v изменяет текущую позицию. Для этого определим функцию draw_text_v с двумя аргументами: расстояние между столбцами и список слов.
Если необходимо реализовать другие манипуляции с текстом, такие, как вращение, мы должны использовать bitmap каждой буквы и произвести вращение всех пикселей.
Bitmap может быть представлен либо как матрица цветов (color array array), либо как значение абстрактного типа2 image из библиотеки Graphics. Имена и типы функций приведены в таблице 4.2.
Функции make_image и dump_image конвертируют из одного типа в другой — image и color array array. Функция draw_image выводит на экран bitmap начиная с координат его нижнего левого угла.
При помощи функции get_image мы можем захватить прямоугольную часть экрана и создать таким образом изображение, для этого необходимо указать нижнюю левую и правую верхнюю углы зоны захвата. Функция blit_image захватывает экран часть экрана и сохраняет изображение которое мы передали в виде аргумента (тип image), второй аргумент указывает нижний левый угол части экрана, которую мы желаем захватить. Размер захватываемой части зависит от размеров изображения, переданого в аргументе. Функция create_image инициализирует изображение, для этого необходимо указать его будущий размер. Позднее, это изображение мы можем изменить функцией blit_image.
Предопределенный цвет transp позволяет сделать точки изображения прозрачными. Это позволяет нам вывести изображение лишь на части прямоугольника, прозрачные точки не изменяют начальный экран.
В следующем примере мы инвертируем цвет точек bitmap-а, для чего будем использовать функцию, которая была представлена на ??, для каждого пиксела bitmap-а.
На рисунке 4.3, картинка слева — начальное изображение и правое — новое, “освещенное солнцем”, после использование функции inv_image.
В этом примере мы попытаемся определить несколько полезных функций вывода рельефных блоков. Блоком мы называем общий объект, который послужит в дальнейшем. Блок вписывается в прямоугольник, который задан начальной точкой, высотой и шириной.
Для того чтобы придать блоку рельефный вид, достаточно добавить две трапеции светлых тонов и две более темных.
Инвертируя цвета можно создать впечатление вогнутого или выпуклого блока.
Добавим новые свойства блока: толщина окантовки, тип вывода на экран (выпуклый, вогнутый или плоский), цвета окантовки и фона. Вся эта информация сгруппирована в записи.
Только поле r может быть изменено. Для вывода на экран мы используем функцию рисующую прямоугольник draw_rect, которую мы определили на ??.
Для удобства, определим функцию рисующую контур блока.
Функция вывода на экран состоит из трех частей: рисование первой окантовки, затем второй и внутренней части блока.
Контур блока подсвечен черным цветом. Для того чтобы стереть блок, достаточно заполнить пространство фоновым цветом.
И наконец, определим функцию выводящую текст выровненный слева, справа или по центру блока. Для описания позиции текста в блоке определим тип position.
Чтобы продемонстрировать использование блоков, выведем на экран поле игры “крестики–нолики”, как на рисунке 4.4. Для упрощения задачи, определим цвета для игры.
Теперь, напишем функцию рисующую матрицу блоков одного размера.
Создадим список блоков.
Изображение 4.4 соответствует результату следующих вызовов:
Анимация на экране компьютера использует ту же технику что и мультипликационные фильмы. Большая часть изображения не меняется, только подвижная часть изменяет цвет своих пикселей. При работе с анимацией, сразу же возникает проблема скорости рисования. Она зависит от сложности вычислений и производительности процессора. Поэтому, чтобы анимация была переносима и имела одинаковый эффект, необходимо учитывать производительность процессора. Чтобы получить плавную анимацию, желательно вывести на экран на новое положение анимационный объект, затем стереть старый, учитывая при этом пересечение областей обоих объектов.
Для упрощения задачи, мы будем перемещать объекты простой формы, как прямоугольник. Остается проблема заполнения экрана за перемещенным объектом.
Мы хотим перемещать прямоугольник в замкнутом пространстве. Объект перемещается с определенной скоростью в направлении X и Y. Если он дойдет до границы графического окна, объект отскочит он него на определенный угол отражения. Объект перемещается из одного положения в другое без перекрывания между новой и старой позицией. В зависимости от текущего положения (x,y), размера объекта (sx,sy) и его скорости (dx,dy) функция calc_pv вычисляет, учитывая границы окна, новое положение и новую скорость.
Функция перемещая прямоугольник, указанный положением pos и размером size, n раз, полученная траектория учитывает скорость speed и границы окна. Путь перемещения, изображенный на рисунок 4.5, получен инверсией bitmap–а, который соответствует перемещенному прямоугольнику.
Следующий код соответствует изображениям на рисунке 4.5, первое получено на красном фоне, второе — перемещением объекта по картинке Jussieu.
Наша задача была упрощена тем, что не было пересечений между новым и старым положением объекта. В противном случае, необходимо написать функцию вычисляющую это пересечение, что может быть более или менее сложно в зависимости от формы объекта. В настоящем примере с квадратом, при пересечении квадратов получается прямоугольник, который необходимо стереть.
Для того чтобы создать программу взаимодействующую с пользователем необходимо обрабатывать события произошедшие в графическом окне. Следующие события обрабатываются библиотекой Graphics: нажатие на клавишу клавиатуры, на кнопку мыши и ее перемещение.
Стиль и устройство программы изменяются: программа превращается в бесконечный цикл ожидающий события. После обработки нового события, программа вновь возвращается в бесконечный цикл, если только это он не было предвиденно для остановки программы.
Существует следующая главная функция ожидания события: wait_next_event с типом list -> status.
Различные события определены типом сумма event:
Первые четыре значения соответствуют нажатию и отпусканию кнопки мыши, нажатие на клавишу клавиатуры и перемещение мыши. Если конструктор Poll добавить в список событий, то ожидание не будет блокирующим. Функция возвращает значение типа status.
Поля этой записи содержат координаты курсора мыши, булево значение равное истине если кнопка мыши была нажата, такое же значение для клавиатуры и последнее значение содержит символ нажатой клавиши. Следующие функции используют значения этой записи.
Библиотека Graphics обрабатывает события на минимальном уровне, однако полученный код переносим на различные платформы: Windows, MacOS или X-Windows. Именно по этой причине данная библиотека не различает кнопки мыши. На компьютерах Mac существует всего одна кнопка. Другие события, как exposing a window или изменение размеров окна не доступны и оставлены на обработку библиотекой.
Все программы с пользовательским интерфейсом содержат цикл, потенциально бесконечный, осуществляющий ожидание событий от пользователя. Как только оно возникает, программа выполняет связанное с ним действие. У следующей функции 5 функциональных аргументов. Первый и второй служат для запуска и остановки программы, два следующие это функции обрабатывающие события связанные с клавиатурой и мышью. Последний аргумент служит для управления исключениями, которые могут возникнуть во время вычислений. События, заканчивающие программу, возбуждают исключение End.
Этот скелет мы используем в программе моделирующей печатную мини–машину. Нажатие на клавишу выводит соответствующий символ, нажатие на кнопку мыши меняет текущую позицию, а клавиша с символом § заканчивает программу. Единственная трудность заключается в переходе на новую строку. Для упрощения, предположим что высота символа не превышает 12 пикселей.
Клавиша DEL, стирающая предыдущий символ, не обрабатывается этой программой.
Telecran небольшая игра рисования для тренировки координации. При помощи клавиш контроля точка на планшете может передвигаться по X и Y не отрывая пера от планшета. При помощи данной модели, мы проиллюстрируем взаимодействие программы и пользователя. Для этого, применим предыдущий скелет, а также некоторые клавиши клавиатуры для указания движения.
Определим тип state — запись в которой будет храниться размер планшета выраженный в количестве позиций по X и Y, текущая позиция пера, масштаб для просмотра, цвет которым рисует перо, цвет экрана и цвет для определения положения пера на экране.
Функция draw_point рисует точку по указанным координатам, масштабу и цвету.
Все функции инициализации, обработки событий и выхода из программы получают параметр соответствующий состоянию (state). Вот как определены первые четыре функции.
Функция t_init открывает окно и выводит перо на текущую позицию, t_end закрывает это окно и выводит сообщение, t_mouse и t_except ничего не делают. В этой программе действия мыши, так же как и исключения, не обрабатываются. Последние могут возникнуть во время запуска программы. Функция t_key — главная, она отвечает за обработку нажатий на клавиатуру.
Она закрашивает текущую точку цветом пера. В зависимости от переданого символа изменяет, если возможно, текущую позицию пера (символы '2','4','6','8'), очищает экран (символ 'c') или возбуждает исключение End (символ 'е'), затем выводит новое положение пера. Все остальные символы проигнорированы. Выбор клавиш для перемещение пера основывается на расположении клавиш малой цифровой клавиатуры.
И наконец, определим состояние, а также воспользуемся скелетом следующим образом:
Вызов функции slate выводит на экран окно и ожидает действий с клавиатуры. На рисунке 4.6 приведено изображение, реализованное этой программой.
Вернемся к нашему примеру с калькулятором, описанным в предыдущей главе о императивном программировании (??). Создадим для него графический интерфейс, облегчающий использование.
Интерфейс реализует кнопки (цифры и функции) и экран для просмотра результата. Кнопка может быть нажата либо с использованием мыши, либо с клавиатуры. На рисунке 4.7 изображен будущий интерфейс.
Здесь мы вновь воспользуемся функцией рисования блоков, описанной на странице ??. Определим следующий тип:
В нем хранится состояние калькулятора, список блоков для каждой кнопки и блок для вывода результата. Так как мы хотим построить легко изменяемый калькулятор, поэтому создание интерфейса параметризовано списком из ассоциаций:
С помощью этого описания мы создаем список блоков. У функции gen_boxes 5 параметров: описание (descr), число колонок (n), расстояние между кнопками (wsep), расстояние между текстом и границами блока (wsepint), размер окантовки (wbord). Она возвращает список блоков кнопок и блок для вывода результата. Для вычисления расположения элементов, воспользуемся функцией max_xy вычисляющей максимальные размеры списка пар целых чисел, а также функцию max_lbox вычисляющую максимальное положение списка блоков.
Ниже представлена главная функция gen_boxes, создающая интерфейс.
Мы хотим воспользоваться скелетом определенным на странице ??, для этого определим функции обработки клавиатуры и мыши. Функция обработки нажатий на клавиши клавиатуры очень проста; она передает символ, переведенный в тип key, функции калькулятора transition, затем выводит текст о состоянии калькулятора.
Обработка мышиных событий немного сложней. Необходимо удостоверится, что нажатие было произведено на одну из кнопок калькулятора. Для этого, определим функцию, которая проверяющую положение на принадлежность блоку.
Если функция f_mouse нашла блок, которому соответствуют координаты нажатия, она передает соответствующую кнопку функции транзиции, затем выводит результат. В противном случае эта функция ничего не делает.
Функция f_exc обрабатывает исключения которые могут возникнуть во время выполнения программы.
В случае деления на ноль, эта функция обнуляет состояние калькулятора и выводит сообщение об ошибке. Неправильная клавиша просто–напросто игнорируется. И наконец, исключение Key_off возбуждает исключение End выхода из цикла скелета программы.
При инициализации калькулятора необходимо вычислить размер окна. Следующая функция генерирует нужную графическую информацию, для этого она использует список кнопка-текст и возвращает размер главного окна.
Функция инициализации использует результат предыдущей функцию.
Функция выхода закрывает окно.
Функция go, параметризованная описанием, запускает цикл ожидания событий.
Вызов go descr_calc соответствует изображению 4.7.
В этой главе мы представили основы графического программирования и программирование событиями, используя библиотеку Graphics дистрибутива Objective CAML. Сначала мы рассмотрели базовые элементы (цвет, рисунок, заливка, текст и bitmap), затем изучили анимацию этих элементов. После введения механизма обработки событий библиотеки Graphics, мы увидели общий метод управления действиями пользователя используя программирование событиями. Для того чтобы улучшить интерактивность и предложить разработчику интерактивные графические компоненты была разработана библиотека, упрощающая создание графических интерфейсов, Awi. Это библиотека была использована при написании интерфейса императивного калькулятора.