Опубликован: 08.08.2015 | Доступ: свободный | Студентов: 300 / 41 | Длительность: 09:22:00
Лекция 1:

Основные элементы графического интерфейса пользователя

Лекция 1 || Лекция 2 >
Аннотация: Создаются простейшие приложения с графическим интерфейсом пользователя в интегрированной среде разработки Visual Prolog. Рассматриваются основные средства рисования. В приложении drawing строится золотой прямоугольник и приближенная золотая спираль. В последующих двух главах в него добавляются новые модули.

Введение

Настоящее учебное пособие является продолжением пособия "Логическое программирование. Часть 1. Основы программирования на языке Visual Prolog", которое посвящено основным теоретическим положениям логического программирования и основам программирования на современной версии языка Visual Prolog. Вторая часть посвящена разработке приложений с графическим интерфейсом пользователя в интегрированной среде разработки системы программирования Visual Prolog. Цель пособия — формирование практических умений по использованию логического программирования в практической деятельности, получение навыков создания приложений на языке Visual Prolog.

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

Язык Visual Prolog является объектно-ориентированным. В пособии при создании приложений с графическим интерфейсом пользователя объектно-ориентированное программирование сочетается с логическим и функциональным.

Версия системы Visual Prolog Personal Edition свободно доступна на сайте разработчика www.visual-prolog.com. При установке системы рекомендуется сразу установить набор примеров Visual Prolog Examples.

Основные операции рисования

Создаются простейшие приложения с графическим интерфейсом пользователя в интегрированной среде разработки Visual Prolog. Рассматриваются основные средства рисования.

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

Ключевые слова: графический интерфейс, приложение, окно, редактор ресурса, свойство, событие, рисование, золотое сечение, градиентная заливка

Приложение "Hello, World"

Интегрированная среда разработки (IDEIntegrated Development Environment) Visual Prolog содержит все необходимые средства для создания приложений с графическим интерфейсом пользователя (GUIGraphical User Interface). Она включает компилятор, отладчик, текстовый и графический редакторы, редакторы ресурсов, средства для быстрой навигации по проекту и средства автодополнения (IntelliSense), средства автоматизации сборки и автоматической генерации кода и др.

Создание приложения

Вид приложения устанавливается при создании проекта: исполняемое или DLL (DLL можно создавать только в CECommercial Edition), с графическим интерфейсом пользователя (MDI или SDI) или консольное.

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

  • Project Name: hello;
  • Project Kind: MDI.

Мультидокументный интерфейс (MDIMultiple Document Interface) — это способ организации оконного интерфейса, в котором дочерние окна располагаются внутри одного окна.

Диалоговое окно New Project

увеличить изображение
Рис. 1.1. Диалоговое окно New Project

Нажмем кнопку Finish. Для компиляции проекта и запуска приложения используется команда меню Build -> Execute (или кнопка E панели инструментов). После запуска открывается приложение hello (рис. 1.2 рис. 1.2).

Главное окно приложения hello

Рис. 1.2. Главное окно приложения hello

Созданное приложение содержит главное меню, панель инструментов, строку состояния, окно сообщений и окно About ("О программе").

Включение пункта меню

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

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

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

Сделаем так, чтобы при выборе пункта меню File -> New открывалось диалоговое окно Note (окно сообщений). Для этого потребуется его включить — открыть к нему доступ.

Пункт меню File -> New (a) выключенный; (b) включенный

Рис. 1.3. Пункт меню File -> New (a) выключенный; (b) включенный

В папке TaskWindow дерева проекта найдем элемент TaskMenu.mnu и выделим его (рис. 1.4 рис. 1.4).

Дерево проекта hello. Элемент TaskMenu.mnu

Рис. 1.4. Дерево проекта hello. Элемент TaskMenu.mnu

С помощью правой кнопки мыши открывается контекстное меню, команды которого содержат операции, которые можно выполнять над выделенным элементом. С помощью двойного клика мыши открывается его редактор.

Итак, с помощью двойного клика мыши по элементу TaskMenu.mnu, или с помощью пункта Open контекстного меню, откроем редактор главного меню (рис. 1.5 рис. 1.5). В нем следует выделить подпункт New пункта меню File и снять флажок из поля Disabled (выключен).

Редактор меню

Рис. 1.5. Редактор меню

В поле Accelerator указана "горячая" клавиша (F7). Идентификатор ресурса пункта меню (id_file_new) указывается в поле Item.

Далее редактор меню нужно закрыть и сохранить сделанные изменения.

Компоненты GUI хранятся в виде отдельных файлов ресурсов. После их создания или изменения интегрированная среда разработки Visual Prolog автоматически компилирует их и связывает. Запустим приложение и убедимся, что пункт меню File -> New стал включенным (рис. 1.3 (b) рис. 1.3).

Событие выбора пункта меню

Добавим обработчик события выбора пункта меню File -> New. Для этого выделим элемент TaskWindow.win дерева проекта и, с помощью двойного клика левой кнопкой мыши, откроем окно Dialog and Window Expert (рис. 1.6 рис. 1.6).

Эксперт главного окна

Рис. 1.6. Эксперт главного окна

Последовательно раскроем узлы Menu, TaskMenu и id_file, выделим элемент id_file_new и нажмем кнопку Add (см. рис. 1.6 рис. 1.6). В результате в имплементации класса taskWindow (файл taskWindow.pro) появится автоматически сгенерированный код:

predicates
    onFileNew : window::menuItemListener.
clauses
    onFileNew(_Source, _MenuTag).

Напомним, что файлы с расширением i содержат интерфейсы классов, с расширением cl — декларации классов, с расширением pro — имплементации классов.

Предикат onFileNew — это обработчик события выбора команды меню File -> New. В его определение нужно поместить вызов окна сообщений — диалогового окна Note:

predicates
    onFileNew : window::menuItemListener.
clauses
    onFileNew(_Source, _MenuTag):-
        vpiCommonDialogs::note("Hello", "Hello, World!").

Класс vpiCommonDialogs содержит предикаты note, ask, error, getColor, getFileName и др., с помощью которых открываются стандартные диалоговые окна (рис. 1.7 рис. 1.7).

Автодополнение. Список предикатов создания стандартных окон

Рис. 1.7. Автодополнение. Список предикатов создания стандартных окон

Запустим приложение. При выборе команды меню File -> New главного окна приложения или при нажатии клавиши F7 появится модальное диалоговое окно Note (рис. 1.8 рис. 1.8).

Приложение hello

Рис. 1.8. Приложение hello

Базовым классом окон является класс window. Интерфейс window наследуют все виды окон — формы, диалоговые окна, элементы управления и др. Создадим простейшую форму и диалоговое окно. Изменим определение предиката onFileNew следующим образом:

onFileNew(_Source, _MenuTag):-
        Form = formWindow::new(This),
        Form:setText("Hello!"),
        Form:show().

Конструктор new/1 создает объект класса formWindow. Переменная Form хранит указатель на объект формы. Переменная This хранит указатель на объект текущего класса, в данном случае, класса taskWindow. Главное окно приложения является родительским окном формы. Предикат setText/1 помещает текст в строку заголовка окна. Предикат show визуализирует окно. По умолчанию автоматически устанавливаются остальные свойства окна (размер, положение и др.).

После запуска откроется пустая форма, в строке заголовка которой будет выведено: "Hello!" (рис. 1.9 (a) рис. 1.9). Можно изменять размеры окна, сворачивать его и разворачивать, а также открыть несколько таких окон.

(a) Форма; (b) диалоговое окно

Рис. 1.9. (a) Форма; (b) диалоговое окно

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

В дальнейшем окна создаются с помощью диалогового окна Create Project Item, а их основные свойства устанавливаются в визуальном редакторе.

Операции рисования

Создадим проект под названием drawing (MDI). Он будет использоваться в трех первых главах для построения изображений.

Изменение атрибутов главного окна приложения

По умолчанию в строке заголовка главного окна приложения пишется имя проекта (см. рис. 1.8 рис. 1.8). Изменить надпись в строке заголовка, а также установить другие атрибуты главного окна приложения можно с помощью окна Window Attributes ( рис. 1.10). Для того чтобы открыть это окно, следует выделить элемент TaskWindow.win дерева проекта, открыть контекстное меню с помощью правой кнопки мыши и выбрать в нем пункт Attributes.

Изменим текст, содержащийся в в поле Title. Кроме этого, установим флажок в поле Maximized (см. рис. 1.10). В результате главное окно приложения будет автоматически развернуто во весь экран.

Окно атрибутов главного окна приложения

Рис. 1.10. Окно атрибутов главного окна приложения

Установить указанные атрибуты главного окна приложения можно и программным образом, с помощью предикатов setText/1 и setState/1. Достаточно изменить определение конструктора new в файле taskWindow.pro следующим образом:

  
new() :-
        applicationWindow::new(),
        generatedInitialize(),
        setText("Тестовый проект"),
        setState([wsf_Maximized]).
Модификация главного меню

Изменим главное меню приложения. Для этого, как и ранее, с помощью элемента TaskMenu.mnu дерева проекта откроем редактор меню (рис. 1.11 рис. 1.11).

Изменение главного меню приложения

Рис. 1.11. Изменение главного меню приложения

Внесем в меню следующие изменения:

  • удалим пункт Edit: выделим его и нажмем клавишу Delete;
  • вставим пункт Test: выделим пункт File, выберем пункт New всплывающего меню (или нажмем кнопку New Item панели инструментов окна) и напишем название Test;
  • выделим пункт Test, выберем пункт New SubItem всплывающего меню и введем название rectangle;
  • выделим пункт rectangle, выберем пункт New всплывающего меню и напишем название gradient (это элементы одного уровня, поэтому используется команда New, а не NewSubItem).

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

Создание формы

Создадим форму. Для этого выделим корень дерева проекта (узел drawing) и выберем команду New In New Package всплывающего меню. В диалоговом окне Create Project Item выделим элемент Form и в поле Name напишем: drawForm (рис. 1.12 рис. 1.12).

Создание элемента проекта

Рис. 1.12. Создание элемента проекта

Нажмем кнопку Create. Откроется редактор окна ( рис. 1.13). Редактор окна включает прототип (графический редактор) формы, окна Controls и Layout, которые используются для размещения в окне элементов управления, а также окно свойств Properties. Окно свойств содержит две вкладки. Вкладка Properties содержит таблицу свойств и используется для установки атрибутов окна. Вкладка Events содержит таблицу событий и используется для добавления предикатов обработки событий.

Редактор окна

Рис. 1.13. Редактор окна

По умолчанию форма содержит кнопки OK, Cancel и Help. Их следует удалить. Для того чтобы удалить кнопку, нужно ее выделить и нажать клавишу Delete.

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

Добавим обработчик события выбора пункта меню Тест -> Золотой прямоугольник. Для этого откроем эксперт окон с помощью элемента TaskWindow.win дерева проекта, найдем элемент id_test_rectangle, выделим его и нажмем кнопку Add (см. рис. 1.6 рис. 1.6). Определение предиката onTestRectangle следует изменить так, как показано ниже.

predicates
    onTestRectangle : window::menuItemListener.
clauses
    onTestRectangle(_Source, _MenuTag):-
        _ = drawForm::display(This).
Листинг 1.1. Определение предиката onTestRectangle

Предикат display/1 определяется следующим образом (см. файл drawForm.pro):

display(Parent) = Form :-
        Form = new(Parent),
        Form:show().

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

Скомпилируем и запустим приложение. При запросе компилятора о добавлении директивы include (рис. 1.14 рис. 1.14) следует нажать кнопку Add или Add All:

Добавление директивы include

Рис. 1.14. Добавление директивы include

При выборе пункта меню Тест -> Золотой прямоугольник открывается пустая форма.

Создание изображения

Изображение в окне строит обработчик событий PaintResponder — предикат, который принадлежит предикатному домену paintResponder. По умолчанию ему дается имя onPaint.

Операционная система Windows непрерывно отслеживает события и посылает сообщения программе, что приводит к вызову предикатов обработки событий. В системе GUI используются обработчики событий двух видов — приемники (listener) и ответчики (responder). Обработчик события первого вида принимает сообщение, запускает обработку события некоторым стандартным способом и не возвращает никакого ответа. Обработчик второго вида возвращает некоторый ответ, который учитывается системой GUI. Примером первого вида обработчиков событий является обработчик события вызова команды меню, который уже использовался ранее (он принадлежит предикатному домену menuItemListener). Обработчик PaintResponder является примером обработчиков событий второго вида.

Для всех окон, кроме главного окна приложения (taskWindow), обработчики событий добавляются в редакторе окна с помощью таблицы событий, которая находится на вкладке Events окна свойств. Для главного окна приложения используется эксперт окон (см. рис. 1.6 рис. 1.6). Добавить предикат обработки событий можно и программным образом, с помощью соответствующих предикатов (например, addMenuItemListener).

Добавим предикат обработки события Paint. Событие Paint объявляет клиентскую область окна или ее часть недействительной и требующей перерисовки. Оно возникает, например, когда форма только создана или когда она перекрывается другим окном. Для этого откроем редактор окна с помощью элемента drawForm.frm дерева проекта, перейдем на вкладку Events окна свойств, найдем в списке обработчиков событий элемент PaintResponder и выберем в правом столбце той же строки элемент onPaint (рис. 1.15 рис. 1.15).

Таблица событий

Рис. 1.15. Таблица событий

Закроем редактор формы. Автоматически в имплементации класса drawForm появится код

  
predicates
    onPaint : window::paintResponder.
clauses
    onPaint(_Source, _Rectangle, _GDI).

Первый аргумент предиката onPaint хранит указатель на объект окна, из которого поступило сообщение, второй — прямоугольную область, подлежащую перерисовке (в ней строится изображение), третий — указатель на объект класса windowGDI1GDI является сокращением от Graphic Device Interface (интерфейс графического устройства). VPI означает Visual Programming Interface (интерфейс визуального программирования). (интерфейса графического устройства), который выполняет операции рисования в окне.

Все рисование в окне должно производиться с помощью предиката onPaint. Для начала просто изменим цвет фона:

  
onPaint(_Source, _Rectangle, GDI):-
        GDI:clear(color_LightSeaGreen). 

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

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

  
onPaint(_Source, _Rectangle, GDI):-
    GDI:clear(color_LightSeaGreen),
    getClientSize(W, H),
    GDI:setPen(pen(1, ps_Solid, color_DarkBlue)),
    GDI:setBrush(brush(pat_Solid, color_DarkBlue)),
    Rct = rct(W div 4, H div 4, 3*(W div 4), 3*(H div 4)),
    GDI:drawRect(Rct),
    GDI:setPen(pen(2, ps_Solid, color_White)),
    GDI:drawArc(Rct,pnt(3*(W div 4), H div 4), pnt(W div 4, H div 4)). 

Результат показан на рис. 1.16 (a) рис. 1.16.

Прямоугольник и дуга эллипса

Рис. 1.16. Прямоугольник и дуга эллипса

Предикат getClientSize/2 возвращает размеры сторон клиентской области окна — ширину и высоту. Ширина прямоугольника — это длина его горизонтальной стороны, а высота — вертикальной. Предикат setPen/1 устанавливает параметры пера, предикат setBrush/1 — параметры заливки, предикат drawRect/1 строит прямоугольник, предикат drawArc/3 проводит дугу эллипса.

Домены термов, представляющих инструменты рисования, принадлежат классу vpiDomains2VPI означает Visual Programming Interface (интерфейс визуального программирования).. Термы, описывающие параметры пера, заливки, точки и прямоугольника, принадлежат доменам pen, brush, pnt и rct, соответственно.

Точка описывается двумя целыми координатами (integer) в системе координат окна в виде pnt(X, Y). Началом координат окна является левый верхний угол клиентской области окна, ось абсцисс направлена вправо, ось ординат — вниз, единицей измерения является пиксель.

Предикат drawLine/2 проводит отрезок, соединяющий две точки. Например, добавим в определение предиката onPaint строку кода (в конец правила):

drawLine(pnt(0, H div 2), pnt(W, H div 2)).

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

Прямоугольник представляется термом rct(X1, Y1, X2, Y2), где (X1, Y1) и (X2, Y2) — координаты произвольной пары противолежащих вершин. В данном случае указаны координаты левой верхней вершины и правой нижней вершины (см. код).

Ширина прямоугольника, построенного выше, равна половине ширины окна, высота — половине высоты окна. Соответственно, площадь прямоугольника составляет четверть от площади клиентской области окна.

Предикату drawArc передаются три параметра: прямоугольник, описанный вокруг эллипса, частью которого является дуга, и две точки. Эти точки являются пересечениями ограничивающих дугу лучей, которые выходят из центра прямоугольника, с его границей (рис. 1.16 (b) рис. 1.16). Точки указываются в направлении против часовой стрелки, как это выглядит для пользователя; в системе координат окна — по часовой стрелке. Поэтому вторым аргументом предиката drawArc является точка P, а третьим — точка R (см. код).

Если изменить размеры окна с помощью мыши, то изображение исказится, так как перерисовка выполняется по-минимому. Поэтому в редакторе окна следует добавить обработчик события изменения размеров окна SizeListener (см. рис. 1.15 рис. 1.15) и определить его так, как показано ниже.

  
predicates
    onSize : window::sizeListener.
clauses
    onSize(_Source):-
        invalidate().
Листинг 1.2. Определение предиката onSize

Предикат invalidate/0 помечает всю клиентскую область окна как недействительную и, вследствие этого, требующую перерисовки. В результате инициируется событие Paint, что затем приводит к вызову предиката onPaint.

Упражнение. Постройте изображение, приведенное на рис. 1.16 (b) рис. 1.16.

Построение золотого прямоугольника

Далее создается изображение, показанное на рис. 1.17 рис. 1.17.

Золотой прямоугольник и приближенная золотая спираль

Рис. 1.17. Золотой прямоугольник и приближенная золотая спираль

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

\phi=\frac{1+\sqrt{5}}{2}

Этот коэффициент будет использоваться в разных классах, поэтому для его хранения создадим отдельный класс. Для этого выделим корень дерева проекта и выберем команду (всплывающего) меню New in New Package (или используем сочетание клавиш Ctrl+N). В появившемся окне Create Project Item следует выбрать элемент Class, вписать в поле Name имя класса goldenRatio, убрать флажок из поля Create Interface и нажать кнопку Create. В декларацию класса (файл goldenRatio.cl) следует поместить объявление свойства.

  
properties
    value : real.
Листинг 1.3. Объявление коэффициента золотого сечения

В имплементацию (файл goldenRatio.pro) нужно добавить его определение.

 
class facts
    value : real := (1 + math::sqrt(5)) / 2.
Листинг 1.4. Определение коэффициента золотого сечения

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

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

    open core, vpiDomains

predicates
    goldenRectangle: (integer W, integer H) -> rct Rectangle.
Листинг 1.5. Объявление предиката построения прямоугольника

В раздел open декларации класса geom (файл geom.cl), а также его имплементации (файл geom.pro) следует добавить имя класса vpiDomains. Определение предиката приведено ниже.

    open core, vpiDomains
    
clauses
    goldenRectangle(Width, Height) = rct(X1, Y1, X2, Y2):-
        X1 = math::round(Width / 8),
        X2 = math::round(7 * Width / 8),
        Hr = (X2 - X1)/goldenRatio::value,  % высота прямоугольника
        Y1 = math::round((Height - Hr) / 2),
        Y2 = math::round(Y1 + Hr).
Листинг 1.6. Определение предиката построения прямоугольника

Напоминаем, что модуль нужно скомпилировать.

Вернемся к имплементации класса drawForm (файл drawForm.pro).

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

constants
    bgColor : color = color_LightSeaGreen.
    lineColor : color = color_Black.
    brushColor : color = color_MidnightBlue.
    arcColor : color = color_White.
    rctPenWidth = 5.
    arcPenWidth = 2.
    maxIterNum = 11.
    
facts
    iterNum : positive := 7. % количество итераций
    
predicates
    outerRectangle: (rct) -> rct.
clauses
    outerRectangle(rct(X1, Y1, X2, Y2)) =
        rct(X1 - rctPenWidth, Y1 - rctPenWidth,
            X2 + rctPenWidth, Y2 + rctPenWidth).
Листинг 1.7. Параметры для построения изображения

Определение предиката onPaint следует изменить так, как показано ниже.

clauses
    onPaint(_Source, _Rectangle, GDI):-
        getClientSize(Width, Height),
        GoldenRectangle = geom::goldenRectangle(Width, Height),
        GDI:clear(bgColor),
        GDI:setPen(pen(rctPenWidth, ps_Solid, lineColor)),
        GDI:setBrush(brush(pat_Solid, brushColor)),
        GDI:drawRect(outerRectangle(GoldenRectangle)). 
Листинг 1.8. Построение прямоугольника

Построенное изображение показано на рис. 1.18 рис. 1.18.

Золотой прямоугольник

Рис. 1.18. Золотой прямоугольник
Построение кривой

Спираль строится итеративным образом. Рассмотрим золотой прямоугольник ABCD, приведенный на рис. 1.19 рис. 1.19. Для его сторон выполняется соотношение

|AB|=\phi|AD|,

где $\phi$ — коэффициент золотого сечения.

Построение спирали

Рис. 1.19. Построение спирали

На первой итерации проводится дуга DE окружности с центром в точке G и радиусом AD, где E и G — точки, лежащие на сторонах AB и CD, соответственно, такие что прямоугольник AEGD является квадратом. Далее рассуждения повторяются для прямоугольника BCGE, который также является золотым.

Пусть точки A и D имеют координаты $(x_{1},y_{1})$ и $(x_{2},y_{2})$, соответственно. Найдем координаты точек E и G. Имеем: $\overline{AD}=(x_{2}-x_{1},y_{2}-y_{1})$. Вектор $\overline{AE}$ перпендикулярен вектору $\overline{AD}$, длины этих векторов совпадают, поэтому вектор $\overline{AE}$, с точностью до знака, равен вектору $$(y_{1}-y_{2},x_{2}-x_{1})$$, т. е. $\overline{AE}=k(y_{1}-y_{2},x_{2}-x_{1})$, где k = 1 или k = – 1. Далее, ориентация системы векторов $\overline{AE}$, $\overline{AD}$ совпадает с ориентацией ортов координатных осей, поэтому определитель матрицы, составленной из координат этих векторов, должен быть положителен:

\begin{vmatrix}
k(y_{1}-y_{2})\quad x_{2}-x_{1}\\
k(x_{2}-x_{1})\quad y_{2}-y_{1}\\
\end{vmatrix}=-k(y_{1}-y_{2})^{2}-k(x_{2}-x_{1})^{2}>0.

Поэтому k = – 1. Следовательно,

\overline{AE}=(y_{2}-y_{1},x_{1}-x_{2})

Обозначим через O начало координат. Для координат точек E и G имеем:

\overline{OE}=\overline{OA}+\overline{AE}=(x_{1}+y_{2}-y_{1},y_{1}+x_{1}-x_{2})
\overline{OG}=\overline{OD}+\overline{AE}=(x_{2}+y_{2}-y_{1},y_{2}+x_{1}-x_{2})

Дуга ED является четвертью окружности, вписанной в квадрат AKFL (см. рис. 1.19 рис. 1.19). Вершину F этого квадрата, противоположную вершине A, можно найти следующим образом:

\overline{OF}=\overline{OA}+2\overline{AE}+2\overline{AD}=(x_{1}+2(y_{2}-y_{1})+2(x_{2}-x_{1}),y_{1}+2(x_{1}-x_{2})+2(y_{2}-y_{1}))

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

predicates
    createSpiral: (positive N, rct GoldenRectangle) ->
        tuple{pnt From, pnt To, rct Rectangle}* ArcList.
Листинг 1.9. Объявление предиката построения кривой

Ниже приведено определение предиката (файл geom.pro).

clauses
   createSpiral(N, rct(X1, Y1, X2, Y2)) = spiral(N, 
        [pnt(X1, Y1), pnt(X2, Y1), pnt(X2, Y2), pnt(X1, Y2)]).
        
class predicates
    spiral: (positive, pnt*) -> tuple{pnt From, pnt To, rct}*.
clauses
    spiral(0, _) = []:- !.
    spiral(N, RectVList) = [Arc | spiral(N - 1, NextRectVList)]:-
        step(RectVList, Arc, NextRectVList).
        
class predicates
    step: (pnt* VList, tuple{pnt From, pnt To, rct Rect} [out],
        pnt* [out] NextVList).
clauses
    step(VL, tuple(E, pnt(X2, Y2), rct(X1, Y1, Xf, Yf)), [B, C, G, E]):-
        VL == [pnt(X1, Y1), B, C, pnt(X2, Y2)],  % A, B, C, D
        E = pnt(X1 + Y2 - Y1, Y1 + X1 - X2),
        G = pnt(X2 + Y2 - Y1, Y2 + X1 - X2),
        Xf = X1 + 2 * (X2 - X1) + 2 * (Y2 - Y1),
        Yf = Y1 + 2 * (Y2 - Y1) + 2 * (X1 - X2).
Листинг 1.10. Определение предиката построения кривой

Предикат step/3 выполняет одну итерацию. Он возвращает тройку параметров, определяющих дугу окружности, и следующий золотой прямоугольник.

Определение предиката onPaint (файл drawForm.pro) следует изменить так, как показано ниже.

clauses
    onPaint(_Source, _Rectangle, GDI):-
        getClientSize(W, H),
        GoldenRectangle = geom::goldenRectangle(W, H),
        GDI:clear(bgColor),
        GDI:setPen(pen(rctPenWidth, ps_Solid, lineColor)),
        GDI:setBrush(brush(pat_Solid, brushColor)),
        GDI:drawRect(outerRectangle(GoldenRectangle)),
        GDI:setPen(pen(arcPenWidth, ps_Solid, arcColor)),
        ArcList = geom::createSpiral(iterNum, GoldenRectangle),
        list::forAll(ArcList, 
            {(tuple(From, To, Rect)):- GDI:drawArc(Rect, From, To)}).
Листинг 1.11. Построение кривой
Изменение количества итераций

Реализуем возможность изменения количества итераций в работающем приложении. Пользователь сможет с помощью правой кнопки мыши вызвать диалоговое окно GetString и указать новое количество итераций (рис. 1.20 рис. 1.20).

Изменение количества итераций

Рис. 1.20. Изменение количества итераций

После нажатия на кнопку OK кривая будет перерисована.

В редакторе окна drawForm добавим обработчик события MouseDownListener, которое возникает, когда кнопка мыши нажата. Ниже приведено его определение.

predicates
    onMouseDown : window::mouseDownListener.
clauses
    onMouseDown(_Source, _Point, _, mouse_button_right):-
        Title = "Количество итераций",
        Prompt = string::format("Введите целое число в пределах"
            "\nот 0 до % включительно", maxIterNum),
        InitString = toString(iterNum),
        Str = vpiCommonDialogs::getString(Title, Prompt, InitString),
        N = tryToTerm(positive, Str),
        N <= maxIterNum,
        !,
        iterNum := N,
        invalidate().
    onMouseDown(_Source, _Point, _ShiftControlAlt, _Button).
Листинг 1.12. Изменение количества итераций

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

Использование GDI+

Ниже для построения аналогичного изображения используются предикаты GDI+. Изображение кривой сглаживается. Применяется градиентная заливка.

Создание формы

Аналогично тому, как это делалось ранее, в проекте drawing следует создать форму drawPlusForm. В редакторе формы нужно изменить заголовок окна и удалить все кнопки. Далее следует добавить обработчик события выбора пункта меню Тест -> Градиент и определить его следующим образом:

  
clauses
    onTestGradient(_Source, _MenuTag):-
        _ = drawPlusForm::display(This).

В редакторе окна drawPlusForm необходимо добавить обработчики событий PaintResponder, SizeListener и DestroyListener. Событие Destroy возникает при уничтожении объекта.

Обработчик события изменения размеров окна onSize определяется так же, как и ранее:

onSize(_Source):-
        invalidate().
Операции startUp и shutDown

Подсистема Windows GDI+ — это улучшенный вариант интерфейса Windows GDI для представления графических объектов и передачи их на устройства отображения. Перед ее использованием следует вызывать предикат startUp, а по завершении — предикат shutDown. Предикат startUp инициализирует библиотеку GDI+, предикат shutDown очищает используемые ресурсы.

Откроем файл drawPlusForm.pro. В раздел open добавим имя класса gdiPlus_native:

core, vpiDomains, gdiPlus_native

Изменим определение конструктора new/1 и определим факт-переменную token так, как показано ниже.

clauses
    new(Parent):-
        formWindow::new(Parent),
        generatedInitialize(),
        %
        token := gdiplus::startUp().
        
facts
    token : unsigned := erroneous.
Листинг 1.13. Изменение определения конструктора

Ниже приведено определение предиката onDestroy.

predicates
    onDestroy : window::destroyListener.
clauses
    onDestroy(_Source):-
        gdiplus::shutDown(token),
        token := erroneous.
Листинг 1.14. Определение предиката onDestroy
Построение прямоугольника и спирали

Создадим класс geomPlus (следует убрать флажок из поля Create Interface), в котором определим предикаты построения золотого прямоугольника и спирали.

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

При построении нашей кривой на каждом четвертом шаге положение прямоугольника и дуги окружности повторяется, поэтому расчеты достаточно сделать для первых четырех шагов (см. рис. 1.19 рис. 1.19).

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

Определение начального угла для построения дуги

Рис. 1.21. Определение начального угла для построения дуги

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

Ниже приведено объявление предикатов построения золотого прямоугольника и спирали (файл geomPlus.cl) и их определение (файл geomPlus.pro).

Предикат goldenRectangle/6 возвращает координаты левой верхней вершины прямоугольника, его ширину и высоту. Предикат createSpiral/5 возвращает список четверок параметров. Четверка содержит координаты левого верхнего угла квадрата, в котором проводится дуга окружности, длину его стороны, а также величину начального угла.

predicates
    goldenRectangle: (integer W, integer H, integer Xr [out], 
        integer Yr [out], integer Wr [out], integer Hr [out]).
        
predicates
    createSpiral: (positive, integer X, integer Y, integer Wr, integer Hr)
    -> tuple{integer FromX, integer FromY, integer Side, real Angle}*.
Листинг 1.15. Объявление предикатов построения фигур
    open core, vpiDomains

clauses
    goldenRectangle(Width, Height, X, Y, Wr, Hr):-
        Wrect = 3 * Width / 4,			% ширина в real
        Hrect = Wrect / goldenRatio::value,	% высота в real
        Wr = math::round(Wrect), 		% ширина в integer
        Hr = math::round(Hrect), 		% высота в integer
        X = math::round(Width / 8),
        Y = math::round((Height - Hrect) / 2).
clauses

    createSpiral(N, X, Y, Wr, Hr) = spiral(N, 0, [pnt(X, Y), 
        pnt(X + Wr, Y), pnt(X + Wr, Y + Hr), pnt(X, Y + Hr)], 180).
        
class predicates
    spiral: (positive N, positive Counter, pnt* VL, integer InitAngle)
        -> tuple{integer X, integer Y, integer Side, real Angle}* ArcList.
clauses
    spiral(N, I, VL, Angle) = 
        [Arc | spiral(N, I + 1, NextVL, (Angle + 90) mod 360)]:-
            I <= N,
            !,
            step(I, VL, Angle, Arc, NextVL).
    getSpiral(_, _, _, _) = [].
    
class predicates
    step: (positive, pnt*, integer, 
        tuple{integer, integer, integer, real} [out], pnt* [out]).
clauses
    step(I, VL, Angle, tuple(X, Y, Side, Angle), [B, C, G, E]):-
        VL == [pnt(X1, Y1), B, C, pnt(X2, Y2)],  % A, B, C, D
        E = pnt(X1 + Y2 - Y1, Y1 + X1 - X2),
        G = pnt(X2 + Y2 - Y1, Y2 + X1 - X2),
        Side = 2 * (math::abs(X2 - X1) + math::abs(Y2 - Y1)),
        R = I mod 4,
        K1 = if R > 0, R < 3 then 1 else 0 end if,
        X = X1 - K1 * Side,
        K2 = if R > 1 then 1 else 0 end if,
        Y = Y1 - K2 * Side.
Листинг 1.16. Определение предикатов построения фигур
Построение изображения

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

constants
    lineColor : unsigned = black.
    arcColor : unsigned = white.
    bgColor1 : unsigned = lightcyan.
    bgColor2 : unsigned = lightseagreen.
    rctColor1 : unsigned = lightskyblue.
    rctColor2 : unsigned = midnightblue.
    unit = unitPixel.
    rctPenWidth = 5.
    arcPenWidth = 2.
    sweepAngle = 90.			% угол раствора дуги
    
facts
    iterNum : positive := 10.
Листинг 1.17. Определение параметров построения изображений

Ниже приведено определение обработчика onPaint.

clauses
    onPaint(_Source, _Rectangle, GDI):-
        getClientSize(Width, Height),
        HDC = GDI:getNativeGraphicContext(IsReleaseNeeded),
        Graphics = graphics::createFromHDC(HDC),
        createDrawing(Graphics, Width, Height),
        GDI:releaseNativeGraphicContext(HDC, IsReleaseNeeded).
Листинг 1.18. Определение предиката onPaint

Предикат getNativeGraphicContext/1 возвращает дескриптор контекста устройства, который предоставляет система Windows для рисования в клиентской области окна. Он передается в объект класса graphics, который выполняет рисование в окне. Этот объект создается предикатом createFromHDC/1. Предикат releaseNativeGraphicContext освобождает контекст устройства для использования другими приложениями, если второй его аргумент равен true; в противном случае он ничего не делает.

Изображение строит предикат createDrawing/3. Градиентная заливка получается посредством смешивания двух цветов в одном из четырех направлений — вертикальном, горизонтальном, прямом диагональном или обратном диагональном (см. листинг 1.19 пример 1.19).

predicates
    createDrawing: (graphics, integer Width, integer Height).
clauses
    createDrawing(Graphics, Width, Height):-
    % Установление режима сглаживания линий
        Graphics:smoothingMode := smoothingModeAntiAlias,
        
    % Перья для проведения линий
        LinePen = pen::createColor(
            color::create(lineColor), rctPenWidth, unit),
        ArcPen = pen::createColor(
            color::create(arcColor), arcPenWidth, unit),
            
    % Фон
        Gradient = linearGradientBrush::createLineBrushFromRectI(
                gdiplus::rectI(0, 0, Width, Height),
                color::create(bgColor1),
                color::create(bgColor2),
                linearGradientModeVertical),
        Graphics:fillRectangleI(Gradient, 0, 0, Width, Height),
        
    % Прямоугольник
        geomPlus::goldenRectangle(Width, Height, X, Y, Wr, Hr),
        
        Graphics:drawRectangleI(LinePen, X - 3, Y - 3, Wr + 6, Hr + 6),
        GradientR = linearGradientBrush::createLineBrushFromRectI(
                gdiplus::rectI(X, Y, Wr, Hr),
                color::create(rctColor1),
                color::create(rctColor2),
                linearGradientModeForwardDiagonal),
         Graphics:fillRectangleI(GradientR, X - 1, Y - 1, Wr + 2, Hr +2),
         
    % Кривая
        ArcList = geomPlus::createSpiral(iterNum, X, Y, Wr, Hr),
        try list::forAll(ArcList, {(tuple(Xa, Ya, Side, StartAngle)):- 
                Graphics:drawArcI(
                    ArcPen, Xa, Ya, Side, Side, StartAngle, sweepAngle)})
        catch _ do
            succeed()
        end try.
Листинг 1.19. Построение изображения

Результат показан на рис. 1.22 рис. 1.22. Спираль изображается сглаженной, в отличие от кривой, которая строится в окне drawForm. Как и ранее, кривая может "ломаться" из-за погрешности вычислений. Ее вид можно восстановить с помощью изменения размеров окна.

Градиентная заливка. Сглаживание линий

Рис. 1.22. Градиентная заливка. Сглаживание линий
Избавление изображения от мерцания

При изменении размеров окна изображение мерцает. Для того чтобы избавиться от этого, выполним два действия. Прежде всего, в редакторе окна drawPlusForm добавим обработчик событий EraseBackgroundResponder и определим его так, как показано ниже. Событие EraseBackground возникает, когда требуется обновить фон.

predicates
    onEraseBackground : window::eraseBackgroundResponder.
clauses
    onEraseBackground(_Source, _GDI) = noEraseBackground.
Листинг 1.20. Определение предиката onEraseBackground

Кроме этого, для построения изображения используем холст. Создадим объект класса pictureCanvas и осуществим рисование в нем. Аргументами конструктора new/2 являются ширина и высота рисунка. Предикат getPicture закрывает объект для рисования и возвращает рисунок.

Интерфейс pictureCanvas поддерживает интерфейс windowGDI, поэтому создание изображений на холсте выполняется теми же операциями.

В данном случае достаточно изменить определение предиката onPaint следующим образом.

clauses
    onPaint(_Source, _Rectangle, GDI):-
        getClientSize(Width, Height),
        Canvas = pictureCanvas::new(Width, Height),
        HDC = Canvas:getNativeGraphicContext(IsReleaseNeeded),
        Graphics = graphics::createFromHDC(HDC),
        createDrawing(Graphics, Width, Height),
        Canvas:releaseNativeGraphicContext(HDC, IsReleaseNeeded),
        Picture = Canvas:getPicture(),
        GDI:pictDraw(Picture, pnt(0, 0), rop_SrcCopy).
Листинг 1.21. Определение предиката onPaint

Для первоначального знакомства с GUI см. также [10, [ 10 ] ]. Подробнее об интегрированной среде разработки Visual Prolog см. [13, [ 13 ] ].

Примеры построения изображений с помощью предикатов GDI+ приведены также в проекте GDI+ Example из собрания примеров Visual Prolog Examples.

Упражнения

1.1. Добавьте возможность изменения количества итераций во время работы приложения для окна drawPlusForm.

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

1.3. Используйте для проведения каждой дуги (четверти окружности) свой цвет.

1.4. Придумайте и постройте свой рисунок средствами

(a) windowGDI; (b) GDI+

(см. Help, GDI+ Example и MSDN).

Лекция 1 || Лекция 2 >
Алексей Роднин
Алексей Роднин
Россия
Роман Гаранин
Роман Гаранин
Беларусь, Брест