Previous Up Next

Chapter 4  Графический интерфейс

Введение

В этой главе представлена библиотека Graphics, она входит является частью дистрибутива языка Objective CAML. Эта библиотека одинаково работает в графических интерфейсах основных платформ: Windows, MacOS, Unix с оболочкой X-Windows. С помощью Graphics мы можем реализовать графические рисунки с текстом, картинками, управлять различными базовыми событиями как нажатие на кнопку мыши или клавиатуры.

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

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

План главы

В первом разделе мы объясним как пользоваться этой библиотекой на различных системах. Во втором изучим основы графического программирования: точка, рисунок, заполнение, цвет, bitmap. Для того чтобы проиллюстрировать эти концепции, в третьем разделе опишем и реализуем функции рисования “блоков” (boxes). В четвертом разделе увидим анимацию графических объектов и их взаимодействие с фоном экрана или другими анимационными объектами. В пятом разделе представлен стиль программирования событиями, а так же скелет любого графического приложения. И наконец, в последнем разделе мы воспользуемся библиотекой Graphics для реализации интерфейса калькулятора приведенного на стр. ??.

4.1  Использование библиотеки Graphics

Использование библиотеки зависит от операционной системы и способа компиляции. Здесь мы рассмотрим только программы используемые в интерактивном цикле Objective CAML. В операционных систем Windows и MacOS интерактивное рабочее окружение само загружает библиотеку. Для систем Unix необходимо создать новый toplevel1, который зависит от того где находится библиотека X11. Если она находится в одном из каталогов по умолчанию, где ищутся библиотеки C, то командная строка будет выглядеть так:

ocamlmktop -custom -o mytoplevel graphics.cma -cclib -lX11

Этим мы создаем команду mytoplevel с включенной библиотекой X11. Запуск этой комманды осуществляется так:

./mytoplevel

Если же библиотека расположена в другом каталоге, как в Linux, необходимо его явно указать:

ocamlmktop -custom -o montoplevel graphics.cma -cclib \ -L/usr/X11/lib -cclib -lX11

В этом примере файл libX11.a ищется в каталоге /usr/X11/lib.

Более подробный пример команды ocamlmktop приведен в главе 6.

4.2  Основные понятия

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

Экран bitmap можно рассматривать как прямоугольник точек, называемые пиксел от английского picture element. Они являются базовыми элементами изображения. Высота и ширина экрана называется разрешением основного bitmap. Размер этого bitmap зависит от памяти занимаемой пикселом. Для черно–белого изображения пиксел может занимать один бит. Для цветных или серых изображений размер пиксела зависит от числа оттенков ассоциированных пикселу. Для bitmap 320x640 пикселей по 256 цветов в каждом, нам понадобится 8 битов на каждый пиксел, соответственно размер видео–памяти должен быть 480*640 байтов = 307200 байтов ≃ 300 ko. Это разрешения до сих пор используется некоторыми программами в MS-DOS.

В следующем списке приведены базовые операции над bitmap из библиотеки Graphics:

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

4.3  Графический вывод

При графическом выводе следующие элементы являются базовыми: ориентир (начальная точка?) (reference point), графический контекст, цвет, изображение, заполнение замкнутых фигур, тексты и bitmap–ы.

4.3.1  Ориентир и графический контекст

Библиотека 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

4.3.2  Цвета

Цвет представлен тремя байтами, каждый из которых хранит яркость основного цвета в модели 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).

(* color == R * 256 * 256 + G * 256 + B *) # let from_rgb (c : Graphics.color) = let r = c / 65536 and g = c / 256 mod 256 and b = c mod 256 in (r,g,b) ;; val from_rgb : Graphics.color -> int * int * int = <fun> # let inv_color (c : Graphics.color) = let (r,g,b) = from_rgb c in Graphics.rgb (255-r) (255-g) (255-b) ;; val inv_color : Graphics.color -> Graphics.color = <fun>

Функция point_color, типа int -> int -> color, возвращает цвет точки, координаты которой указаны на входе.

4.3.3  Рисунок и заливка

Функция рисования выводит линию на экран, при этом для толщины и цвета используются текущие значения. Функция заливки закрашивает замкнутую форму текущим цветом. Различные функции рисования и заливки приведены в таблице 4.1.


рисунокзаполнениетип
plot int -> int -> unit
lineto int -> int -> unit
 fill_rectint -> int -> int -> int -> unit
 fill_poly( int * int) array -> unit
draw_arcfill_arcint -> int -> int -> int -> int -> unit
draw_ellipsefill_ellipseint -> int -> int -> int -> unit
draw_circlefill_circleint -> int -> int -> unit
Table 4.1: Функции рисования и заливки

Заметьте, что функция lineto имеет побочный эффект: она изменяет положение текущей точки.

Рисование многоугольников

В следующем примере, мы добавим некоторые функции рисования, которые не существуют в библиотеке. Многоугольник определяется вектором его вершин.

# let draw_rect x0 y0 w h = let (a,b) = Graphics.current_point() and x1 = x0+w and y1 = y0+h in Graphics.moveto x0 y0; Graphics.lineto x0 y1; Graphics.lineto x1 y1; Graphics.lineto x1 y0; Graphics.lineto x0 y0; Graphics.moveto a b;; val draw_rect : int -> int -> int -> int -> unit = <fun> # let draw_poly r = let (a,b) = Graphics.current_point () in let (x0,y0) = r.(0) in Graphics.moveto x0 y0; for i = 1 to (Array.length r)-1 do let (x,y) = r.(i) in Graphics.lineto x y done; Graphics.lineto x0 y0; Graphics.moveto a b;; val draw_poly : (int * int) array -> unit = <fun>

Обратите внимание на то что эти функции берут такие же аргументы что и существующие функции заливки. Так они не изменяют состояние текущей точки.

Модель художника

Следующий пример иллюстрирует сеть эстафетного кольца (token ring) (рис. 4.1). Каждый компьютер представлен в виде небольшого круга, все компьютеры соединены между собой и сеть образует кольцо. Текущее положение маркера в сети изображено черным кругом.

Функция net_points создает координаты всех компьютеров сети, эти данные будут храниться в векторе.

# let pi = 3.1415927;; val pi : float = 3.1415927 # let net_points (x,y) l n = let a = 2. *. pi /. (float n) in let rec aux (xa,ya) i = if i > n then [] else let na = (float i) *. a in let x1 = xa + (int_of_float ( cos(na) *. l)) and y1 = ya + (int_of_float ( sin(na) *. l)) in let np = (x1,y1) in np::(aux np (i+1)) in Array.of_list (aux (x,y) 1);; val net_points : int * int -> float -> int -> (int * int) array = <fun>

Функция draw_net выводит соединения, компьютеры и маркер.

# let draw_net (x,y) l n sc st = let r = net_points (x,y) l n in draw_poly r; let draw_machine (x,y) = Graphics.set_color Graphics.background; Graphics.fill_circle x y sc; Graphics.set_color Graphics.foreground; Graphics.draw_circle x y sc in Array.iter draw_machine r; Graphics.fill_circle x y st;; val draw_net : int * int -> float -> int -> int -> int -> unit = <fun>

При следующем вызове получим левую картинку на рисунке 4.1.

# draw_net (140,20) 60.0 10 10 3;; - : unit = () # save_screen "IMAGES/tokenring.caa";; - : unit = ()

Figure 4.1: Сеть эстафетное кольцо

Заметим что порядок вывода на экран важен — сначала соединения, а затем узлы. Изображение узлов сети скрывает (в тексте стирает, прим. пер.) часть отрезков соединений. Таким образом нет надобности вычислять точки пересечения отрезков и кругов. На правой картинке рисунка 4.1 результат обратного порядка вывода на экран, где мы видим пересечения отрезков внутри кругов, представляющих узлы.

4.3.4  Текст

Две следующие функции вывода текста на экран чрезвычайно просты: draw_char (с типом char -> unit) и draw_string (с типом string -> unit) выводят на экран один символ и строку соответственно. Вывод осуществляется на текущую позицию экрана, также эти обе функции учитывают значение текущего шрифта и его размер.


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

Вертикальный вывод текста

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

# let draw_string_v s = let (xi,yi) = Graphics.current_point() and l = String.length s and (_,h) = Graphics.text_size s in Graphics.draw_char s.[0]; for i=1 to l-1 do let (_,b) = Graphics.current_point() in Graphics.moveto xi (b-h); Graphics.draw_char s.[i] done; let (a,_) = Graphics.current_point() in Graphics.moveto a yi;; val draw_string_v : string -> unit = <fun>

Эта функция изменяет текущую позицию, после вывода позиция перемещается на расстояние равное ширине символа.

Следующая программа выводит легенду вдоль координатных осей (рис. 4.2).

Graphics.moveto 0 150 ; Graphics.lineto 300 150 ; Graphics.moveto 2 130 ; Graphics.draw_string "absciss" ; Graphics.moveto 150 0 ; Graphics.lineto 150 300 ; Graphics.moveto 135 280 ; draw_string_v "ordinate" ;; - : unit = ()

Figure 4.2: Легенда координатных осей

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

# let draw_text_v n l = let f s = let (a,b) = Graphics.current_point() in draw_string_v s ; Graphics.moveto (a+n) b in List.iter f l ;; val draw_text_v : int -> string list -> unit = <fun>

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

4.3.5  Bitmaps

Bitmap может быть представлен либо как матрица цветов (color array array), либо как значение абстрактного типа2 image из библиотеки Graphics. Имена и типы функций приведены в таблице 4.2.


fonctiontype
make_imagecolor array array -> image
dump_imageimage -> color array array
draw_imageimage -> int -> int -> unit
get_imageint -> int -> int -> int -> image
blit_imageimage -> int -> int -> unit
create_imageint -> int -> image
Table 4.2: Функции манипуляции bitmaps

Функции make_image и dump_image конвертируют из одного типа в другой — image и color array array. Функция draw_image выводит на экран bitmap начиная с координат его нижнего левого угла.

При помощи функции get_image мы можем захватить прямоугольную часть экрана и создать таким образом изображение, для этого необходимо указать нижнюю левую и правую верхнюю углы зоны захвата. Функция blit_image захватывает экран часть экрана и сохраняет изображение которое мы передали в виде аргумента (тип image), второй аргумент указывает нижний левый угол части экрана, которую мы желаем захватить. Размер захватываемой части зависит от размеров изображения, переданого в аргументе. Функция create_image инициализирует изображение, для этого необходимо указать его будущий размер. Позднее, это изображение мы можем изменить функцией blit_image.

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

Поляризация jussieu

3

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

# let inv_image i = let inv_vec = Array.map (fun c -> inv_color c) in let inv_mat = Array.map inv_vec in let matrice_inversee = inv_mat (Graphics.dump_image i) in Graphics.make_image matrice_inversee ;; val inv_image : Graphics.image -> Graphics.image = <fun>

На рисунке 4.3, картинка слева — начальное изображение и правое — новое, “освещенное солнцем”, после использование функции inv_image.

# let f_jussieu2 () = inv_image jussieu1;; val f_jussieu2 : unit -> Graphics.image = <fun>

Figure 4.3: Инверсия Jussieu

4.3.6  Рисование рельефных блоков

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

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



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



Реализация

Добавим новые свойства блока: толщина окантовки, тип вывода на экран (выпуклый, вогнутый или плоский), цвета окантовки и фона. Вся эта информация сгруппирована в записи.

# type relief = Top | Bot | Flat;; # type box_config = { x:int; y:int; w:int; h:int; bw:int; mutable r:relief; b1_col : Graphics.color; b2_col : Graphics.color; b_col : Graphics.color};;

Только поле r может быть изменено. Для вывода на экран мы используем функцию рисующую прямоугольник draw_rect, которую мы определили на ??.

Для удобства, определим функцию рисующую контур блока.

# let draw_box_outline bcf col = Graphics.set_color col; draw_rect bcf.x bcf.y bcf.w bcf.h;; val draw_box_outline : box_config -> Graphics.color -> unit = <fun>

Функция вывода на экран состоит из трех частей: рисование первой окантовки, затем второй и внутренней части блока.

# let draw_box bcf = let x1 = bcf.x and y1 = bcf.y in let x2 = x1+bcf.w and y2 = y1+bcf.h in let ix1 = x1+bcf.bw and ix2 = x2-bcf.bw and iy1 = y1+bcf.bw and iy2 = y2-bcf.bw in let border1 g = Graphics.set_color g; Graphics.fill_poly [| (x1,y1);(ix1,iy1);(ix2,iy1);(ix2,iy2);(x2,y2);(x2,y1) |] in let border2 g = Graphics.set_color g; Graphics.fill_poly [| (x1,y1);(ix1,iy1);(ix1,iy2);(ix2,iy2);(x2,y2);(x1,y2) |] in Graphics.set_color bcf.b_col; ( match bcf.r with Top -> Graphics.fill_rect ix1 iy1 (ix2-ix1) (iy2-iy1); border1 bcf.b1_col; border2 bcf.b2_col | Bot -> Graphics.fill_rect ix1 iy1 (ix2-ix1) (iy2-iy1); border1 bcf.b2_col; border2 bcf.b1_col | Flat -> Graphics.fill_rect x1 y1 bcf.w bcf.h ); draw_box_outline bcf Graphics.black;; val draw_box : box_config -> unit = <fun>

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

# let erase_box bcf = Graphics.set_color bcf.b_col; Graphics.fill_rect (bcf.x+bcf.bw) (bcf.y+bcf.bw) (bcf.w-(2*bcf.bw)) (bcf.h-(2*bcf.bw));; val erase_box : box_config -> unit = <fun>

И наконец, определим функцию выводящую текст выровненный слева, справа или по центру блока. Для описания позиции текста в блоке определим тип position.

# type position = Left | Center | Right;; type position = | Left | Center | Right # let draw_string_in_box pos str bcf col = let (w, h) = Graphics.text_size str in let ty = bcf.y + (bcf.h-h)/2 in ( match pos with Center -> Graphics.moveto (bcf.x + (bcf.w-w)/2) ty | Right -> let tx = bcf.x + bcf.w - w - bcf.bw - 1 in Graphics.moveto tx ty | Left -> let tx = bcf.x + bcf.bw + 1 in Graphics.moveto tx ty ); Graphics.set_color col; Graphics.draw_string str;; val draw_string_in_box : position -> string -> box_config -> Graphics.color -> unit = <fun>
Пример: рисование игры

Чтобы продемонстрировать использование блоков, выведем на экран поле игры “крестики–нолики”, как на рисунке 4.4. Для упрощения задачи, определим цвета для игры.

# let set_gray x = (Graphics.rgb x x x);; val set_gray : int -> Graphics.color = <fun> # let gray1= set_gray 100 and gray2= set_gray 170 and gray3= set_gray 240;; val gray1 : Graphics.color = 6579300 val gray2 : Graphics.color = 11184810 val gray3 : Graphics.color = 15790320

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

# let rec create_grid nb_col n sep b = if n < 0 then [] else let px = n mod nb_col and py = n / nb_col in let nx = b.x +sep + px*(b.w+sep) and ny = b.y +sep + py*(b.h+sep) in let b1 = {b with x=nx; y=ny} in b1::(create_grid nb_col (n-1) sep b);; val create_grid : int -> int -> int -> box_config -> box_config list = <fun>

Создадим список блоков.

# let vb = let b = {x=0; y=0; w=20;h=20; bw=2; b1_col=grey1; b2_col=grey3; b_col=grey2; r=Top} in Array.of_list (create_grid 5 24 2 b) ;; val vb : box_config array = [|{x=90; y=90; w=20; h=20; bw=2; r=Top; b1_col=6579300; b2_col=15790320; b_col=11184810}; {x=68; y=90; w=20; h=20; bw=2; r=Top; b1_col=6579300; b2_col=15790320; b_col=...}; ...|]

Изображение 4.4 соответствует результату следующих вызовов:

# Array.iter draw_box vb ; draw_string_in_box Center "X" vb.(5) Graphics.black ; draw_string_in_box Center "X" vb.(8) Graphics.black ; draw_string_in_box Center "O" vb.(12) Graphics.yellow ; draw_string_in_box Center "O" vb.(11) Graphics.yellow ;; - : unit = ()

Figure 4.4: Вывод блоков с текстом

4.4  Анимация

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

Перемещение объекта

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

Мы хотим перемещать прямоугольник в замкнутом пространстве. Объект перемещается с определенной скоростью в направлении X и Y. Если он дойдет до границы графического окна, объект отскочит он него на определенный угол отражения. Объект перемещается из одного положения в другое без перекрывания между новой и старой позицией. В зависимости от текущего положения (x,y), размера объекта (sx,sy) и его скорости (dx,dy) функция calc_pv вычисляет, учитывая границы окна, новое положение и новую скорость.

# let calc_pv (x,y) (sx,sy) (dx,dy) = let nx1 = x+dx and ny1 = y + dy and nx2 = x+sx+dx and ny2 = y+sy+dy and ndx = ref dx and ndy = ref dy in ( if (nx1 < 0) || (nx2 >= Graphics.size_x()) then ndx := -dx ); ( if (ny1 < 0) || (ny2 >= Graphics.size_y()) then ndy := -dy ); ((x+ !ndx, y+ !ndy), (!ndx, !ndy));; val calc_pv : int * int -> int * int -> int * int -> (int * int) * (int * int) = <fun>

Функция перемещая прямоугольник, указанный положением pos и размером size, n раз, полученная траектория учитывает скорость speed и границы окна. Путь перемещения, изображенный на рисунок 4.5, получен инверсией bitmap–а, который соответствует перемещенному прямоугольнику.

# let move_rect pos size speed n = let (x, y) = pos and (sx,sy) = size in let mem = ref (Graphics.get_image x y sx sy) in let rec move_aux x y speed n = if n = 0 then Graphics.moveto x y else let ((nx,ny),n_speed) = calc_pv (x,y) (sx,sy) speed and old_mem = !mem in mem := Graphics.get_image nx ny sx sy; Graphics.set_color Graphics.blue; Graphics.fill_rect nx ny sx sy; Graphics.draw_image (inv_image old_mem) x y; move_aux nx ny n_speed (n-1) in move_aux x y speed n;; val move_rect : int * int -> int * int -> int * int -> int -> unit = <fun>

Следующий код соответствует изображениям на рисунке 4.5, первое получено на красном фоне, второе — перемещением объекта по картинке Jussieu.

# let anim_rect () = Graphics.moveto 105 120; Graphics.set_color Graphics.white; Graphics.draw_string "Start"; move_rect (140,120) (8,8) (8,4) 150; let (x,y) = Graphics.current_point() in Graphics.moveto (x+13) y; Graphics.set_color Graphics.white; Graphics.draw_string "End";; val anim_rect : unit -> unit = <fun> # anim_rect();; - : unit = ()

Figure 4.5: Перемещение объекта

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

4.5  Обработка событий

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

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

4.5.1  Тип и функции событий

Существует следующая главная функция ожидания события: wait_next_event с типом list -> status.

Различные события определены типом сумма event:

type event = Button_down | Button_up | Key_pressed | Mouse_motion | Poll ;;

Первые четыре значения соответствуют нажатию и отпусканию кнопки мыши, нажатие на клавишу клавиатуры и перемещение мыши. Если конструктор Poll добавить в список событий, то ожидание не будет блокирующим. Функция возвращает значение типа status.

type status = { mouse_x : int; mouse_y : int; button : bool; keypressed : bool; key : char};;

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

Библиотека Graphics обрабатывает события на минимальном уровне, однако полученный код переносим на различные платформы: Windows, MacOS или X-Windows. Именно по этой причине данная библиотека не различает кнопки мыши. На компьютерах Mac существует всего одна кнопка. Другие события, как exposing a window или изменение размеров окна не доступны и оставлены на обработку библиотекой.

4.5.2  Скелет программы

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

# exception End;; exception End # let skel f_init f_end f_key f_mouse f_except = f_init (); try while true do try let s = Graphics.wait_next_event [Graphics.Button_down; Graphics.Key_pressed] in if s.Graphics.keypressed then f_key s.Graphics.key else if s.Graphics.button then f_mouse s.Graphics.mouse_x s.Graphics.mouse_y with End -> raise End | e -> f_except e done with End -> f_end ();; val skel : (unit -> 'a) -> (unit -> unit) -> (char -> unit) -> (int -> int -> unit) -> (exn -> unit) -> unit = <fun>

Этот скелет мы используем в программе моделирующей печатную мини–машину. Нажатие на клавишу выводит соответствующий символ, нажатие на кнопку мыши меняет текущую позицию, а клавиша с символом § заканчивает программу. Единственная трудность заключается в переходе на новую строку. Для упрощения, предположим что высота символа не превышает 12 пикселей.

# let next_line () = let (x,y) = Graphics.current_point() in if y>12 then Graphics.moveto 0 (y-12) else Graphics.moveto 0 y;; val next_line : unit -> unit = <fun> # let handle_char c = match c with '&' -> raise End | '\n' -> next_line () | '\r' -> next_line () | _ -> Graphics.draw_char c;; val handle_char : char -> unit = <fun> # let go () = skel (fun () -> Graphics.clear_graph (); Graphics.moveto 0 (Graphics.size_y() -12) ) (fun () -> Graphics.clear_graph()) handle_char (fun x y -> Graphics.moveto x y) (fun e -> ());; val go : unit -> unit = <fun>

Клавиша DEL, стирающая предыдущий символ, не обрабатывается этой программой.

4.5.3  Пример: telecran

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

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

# type state = {maxx:int; maxy:int; mutable x : int; mutable y :int; scale:int; bc : Graphics.color; fc: Graphics.color; pc : Graphics.color};;

Функция draw_point рисует точку по указанным координатам, масштабу и цвету.

# let draw_point x y s c = Graphics.set_color c; Graphics.fill_rect (s*x) (s*y) s s;; val draw_point : int -> int -> int -> Graphics.color -> unit = <fun>

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

# let t_init s () = Graphics.open_graph (" " ^ (string_of_int (s.scale*s.maxx)) ^ "x" ^ (string_of_int (s.scale*s.maxy))); Graphics.set_color s.bc; Graphics.fill_rect 0 0 (s.scale*s.maxx+1) (s.scale*s.maxy+1); draw_point s.x s.y s.scale s.pc;; val t_init : state -> unit -> unit = <fun> # let t_end s () = Graphics.close_graph(); print_string "Good bye..."; print_newline();; val t_end : 'a -> unit -> unit = <fun> # let t_mouse s x y = ();; val t_mouse : 'a -> 'b -> 'c -> unit = <fun> # let t_except s ex = ();; val t_except : 'a -> 'b -> unit = <fun>

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

# let t_key s c = draw_point s.x s.y s.scale s.fc; (match c with '8' -> if s.y < s.maxy then s.y <- s.y + 1; | '2' -> if s.y > 0 then s.y <- s.y - 1 | '4' -> if s.x > 0 then s.x <- s.x - 1 | '6' -> if s.x < s.maxx then s.x <- s.x + 1 | 'c' -> Graphics.set_color s.bc; Graphics.fill_rect 0 0 (s.scale*s.maxx+1) (s.scale*s.maxy+1); Graphics.clear_graph() | 'e' -> raise End | _ -> ()); draw_point s.x s.y s.scale s.pc;; val t_key : state -> char -> unit = <fun>

Она закрашивает текущую точку цветом пера. В зависимости от переданого символа изменяет, если возможно, текущую позицию пера (символы '2','4','6','8'), очищает экран (символ 'c') или возбуждает исключение End (символ 'е'), затем выводит новое положение пера. Все остальные символы проигнорированы. Выбор клавиш для перемещение пера основывается на расположении клавиш малой цифровой клавиатуры.

И наконец, определим состояние, а также воспользуемся скелетом следующим образом:

# let stel = {maxx=120; maxy=120; x=60; y=60; scale=4; bc=Graphics.rgb 130 130 130; fc=Graphics.black; pc=Graphics.red};; val stel : state = {maxx=120; maxy=120; x=60; y=60; scale=4; bc=8553090; fc=0; pc=16711680} # let slate () = skel (t_init stel) (t_end stel) (t_key stel) (t_mouse stel) (t_except stel);; val slate : unit -> unit = <fun>

Вызов функции slate выводит на экран окно и ожидает действий с клавиатуры. На рисунке 4.6 приведено изображение, реализованное этой программой.


Figure 4.6: Telecran

4.6  Графический калькулятор

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

Интерфейс реализует кнопки (цифры и функции) и экран для просмотра результата. Кнопка может быть нажата либо с использованием мыши, либо с клавиатуры. На рисунке 4.7 изображен будущий интерфейс.


Figure 4.7: Графический калькулятор

Здесь мы вновь воспользуемся функцией рисования блоков, описанной на странице ??. Определим следующий тип:

# type calc_state = { s : state; k : (box_config * key * string ) list; v : box_config } ;;

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

# let descr_calc = [ (Digit 0,"0"); (Digit 1,"1"); (Digit 2,"2"); (Equals, "="); (Digit 3,"3"); (Digit 4,"4"); (Digit 5,"5"); (Plus, "+"); (Digit 6,"6"); (Digit 7,"7"); (Digit 8,"8"); (Minus, "-"); (Digit 9,"9"); (Recall,"RCL"); (Div, "/"); (Times, "*"); (Off,"AC"); (Store, "STO"); (Clear,"CE/C") ] ;;
Создание блоков кнопок

С помощью этого описания мы создаем список блоков. У функции gen_boxes 5 параметров: описание (descr), число колонок (n), расстояние между кнопками (wsep), расстояние между текстом и границами блока (wsepint), размер окантовки (wbord). Она возвращает список блоков кнопок и блок для вывода результата. Для вычисления расположения элементов, воспользуемся функцией max_xy вычисляющей максимальные размеры списка пар целых чисел, а также функцию max_lbox вычисляющую максимальное положение списка блоков.

# let gen_xy vals comp o = List.fold_left (fun a (x,y) -> comp (fst a) x,comp (snd a) y) o vals ;; val gen_xy : ('a * 'a) list -> ('b -> 'a -> 'b) -> 'b * 'b -> 'b * 'b = <fun> # let max_xy vals = gen_xy vals max (min_int,min_int);; val max_xy : (int * int) list -> int * int = <fun> # let max_boxl l = let bmax (mx,my) b = max mx b.x, max my b.y in List.fold_left bmax (min_int,min_int) l ;; val max_boxl : box_config list -> int * int = <fun>

Ниже представлена главная функция gen_boxes, создающая интерфейс.

# let gen_boxes descr n wsep wsepint wbord = let l_l = List.length descr in let nb_lig = if l_l mod n = 0 then l_l / n else l_l / n + 1 in let ls = List.map (fun (x,y) -> Graphics.text_size y) descr in let sx,sy = max_xy ls in let sx,sy= sx+wsepint ,sy+wsepint in let r = ref [] in for i=0 to l_l-1 do let px = i mod n and py = i / n in let b = { x = wsep * (px+1) + (sx+2*wbord) * px ; y = wsep * (py+1) + (sy+2*wbord) * py ; w = sx; h = sy ; bw = wbord; r=Top; b1_col = gray1; b2_col = gray3; b_col =gray2} in r:= b::!r done; let mpx,mpy = max_boxl !r in let upx,upy = mpx+sx+wbord+wsep,mpy+sy+wbord+wsep in let (wa,ha) = Graphics.text_size " 0" in let v = { x=(upx-(wa+wsepint +wbord))/2 ; y= upy+ wsep; w=wa+wsepint; h = ha +wsepint; bw = wbord *2; r=Flat ; b1_col = gray1; b2_col = gray3; b_col =Graphics.black} in upx,(upy+wsep+ha+wsepint+wsep+2*wbord),v, List.map2 (fun b (x,y) -> b,x,y ) (List.rev !r) descr;; val gen_boxes : ('a * string) list -> int -> int -> int -> int -> int * int * box_config * (box_config * 'a * string) list = <fun>
Взаимодействие

Мы хотим воспользоваться скелетом определенным на странице ??, для этого определим функции обработки клавиатуры и мыши. Функция обработки нажатий на клавиши клавиатуры очень проста; она передает символ, переведенный в тип key, функции калькулятора transition, затем выводит текст о состоянии калькулятора.

# let f_key cs c = transition cs.s (translation c); erase_box cs.v; draw_string_in_box Right (string_of_int cs.s.vpr) cs.v Graphics.white ;; val f_key : calc_state -> char -> unit = <fun>

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

# let mem (x,y) (x0,y0,w,h) = (x >= x0) && (x< x0+w) && (y>=y0) && ( y<y0+h);; val mem : int * int -> int * int * int * int -> bool = <fun> # let f_mouse cs x y = try let b,t,s = List.find (fun (b,_,_) -> mem (x,y) (b.x+b.bw,b.y+b.bw,b.w,b.h)) cs.k in transition cs.s t; erase_box cs.v; draw_string_in_box Right (string_of_int cs.s.vpr ) cs.v Graphics.white with Not_found -> ();; val f_mouse : calc_state -> int -> int -> unit = <fun>

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

Функция f_exc обрабатывает исключения которые могут возникнуть во время выполнения программы.

# let f_exc cs ex = match ex with Division_by_zero -> transition cs.s Clear; erase_box cs.v; draw_string_in_box Right "Div 0" cs.v (Graphics.red) | Invalid_key -> () | Key_off -> raise End | _ -> raise ex;; val f_exc : calc_state -> exn -> unit = <fun>

В случае деления на ноль, эта функция обнуляет состояние калькулятора и выводит сообщение об ошибке. Неправильная клавиша просто–напросто игнорируется. И наконец, исключение Key_off возбуждает исключение End выхода из цикла скелета программы.

Инициализация и выход

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

# let create_e k = Graphics.close_graph (); Graphics.open_graph " 10x10"; let mx,my,v,lb = gen_boxes k 4 4 5 2 in let s = {lcd=0; lka = false; loa = Equals; vpr = 0; mem = 0} in mx,my,{s=s; k=lb;v=v};; val create_e : (key * string) list -> int * int * calc_state = <fun>

Функция инициализации использует результат предыдущей функцию.

# let f_init mx my cs () = Graphics.close_graph(); Graphics.open_graph (" "^(string_of_int mx)^"x"^(string_of_int my)); Graphics.set_color gray2; Graphics.fill_rect 0 0 (mx+1) (my+1); List.iter (fun (b,_,_) -> draw_box b) cs.k; List.iter (fun (b,_,s) -> draw_string_in_box Center s b Graphics.black) cs.k ; draw_box cs.v; erase_box cs.v; draw_string_in_box Right "hello" cs.v (Graphics.white);; val f_init : int -> int -> calc_state -> unit -> unit = <fun>

Функция выхода закрывает окно.

# let f_end e () = Graphics.close_graph();; val f_end : 'a -> unit -> unit = <fun>

Функция go, параметризованная описанием, запускает цикл ожидания событий.

# let go descr = let mx,my,e = create_e descr in skel (f_init mx my e) (f_end e) (f_key e) (f_mouse e) (f_exc e);; val go : (key * string) list -> unit = <fun>

Вызов go descr_calc соответствует изображению 4.7.

4.7  Резюме

В этой главе мы представили основы графического программирования и программирование событиями, используя библиотеку Graphics дистрибутива Objective CAML. Сначала мы рассмотрели базовые элементы (цвет, рисунок, заливка, текст и bitmap), затем изучили анимацию этих элементов. После введения механизма обработки событий библиотеки Graphics, мы увидели общий метод управления действиями пользователя используя программирование событиями. Для того чтобы улучшить интерактивность и предложить разработчику интерактивные графические компоненты была разработана библиотека, упрощающая создание графических интерфейсов, Awi. Это библиотека была использована при написании интерфейса императивного калькулятора.


1
в оригинале на английском, прим. пер.
2
абстрактным называется тип представление которого не известно. Обьявление подобных типов рассматривается в главе 13
3
Jussieu - здание парижского университета. Там в частности находится известная Лаборатория Информатики Париж-6 (lip6), где работают авторы книги, прим. пер.

Previous Up Next