Разработка приложений на языке Visual Prolog

Меню. События мыши

Показывать лекцию целиком

Создание панели инструментов

Подготовим к использованию новую форму в проекте ).

Для этого следует выполнить следующие действия:

  • создать новую форму paintForm и удалить из нее все кнопки;
  • создать новый подпункт Paint ("Рисование") пункта меню Тест;
  • добавить обработчик события выбора команды меню Тест -> Рисование.
  • Обработчик выбора команды меню определяется так же, как и ранее:

    predicates
        onTestPaint : window::menuItemListener.
    clauses
        onTestPaint(_Source, _MenuTag):-
            _ = paintForm::display(This).
    

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

    Создадим панель инструментов окна paintForm.

    Выделим папку paintForm дерева проекта и выберем пункт New In New Package (контекстного) меню. Выделение узла дерева проекта (папки) используется для того, чтобы новый узел оказался внутри этой папки.

    В окне ). В поле Style выберем элемент Left (слева). Затем нажмем кнопку OK.

    (рис 2.1) Окно атрибутов панели инструментов

    После этого откроется редактор панели инструментов (рис. 2.2 рис 2.2).

    (рис 2.2) Редактор панели инструментов

    Закроем редактор.

    Подготовка изображений

    Создадим изображения ().

    (рис 2.3) Окно атрибутов изображения

    Оставим параметры, установленные по умолчанию, и закроем это окно. Откроется графический редактор, показанный на рис. 2.4 рис 2.4.

    (рис 2.4) Графический редактор изображения

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

    Аналогично создаются растровые изображения (.

    (рис 2.5) Панель инструментов окна

    Размещение кнопок на панели инструментов

    Поместим кнопки на панель инструментов. Откроем редактор панели инструментов ). В окне ).

    (рис 2.6) Окно Controls

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

    (рис 2.7) Окно атрибутов кнопки

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

  • Constant: id_color;
  • Status Text: цвет;выберите цвет;
  • Bitmap Name: color.
  • В поле Constant указывается идентификатор команды меню, в поле Status Text — тексты подсказок. При наведении курсора на кнопку color появится подсказка (Tooltip) "цвет". Одновременно в строке состояния (Status Line) отобразится текст "выберите цвет".

    Аналогично создаются кнопки ).

    (рис 2.8) Создание кнопок в редакторе панели инструментов

    Далее следует закрыть редактор панели инструментов.

    Размещение панели инструментов в окне

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

    События мыши

    Идентификаторы всех ресурсов автоматически записываются в интерфейс resourceIdentifiers (файл resourceIdentifiers.i открывается из дерева проекта). В раздел open имплементации класса paintForm следует добавить имя интерфейса resourceIdentifiers:

    open core, vpiDomains, resourceIdentifiers
    

    Весь приведенный ниже код следует поместить в имплементацию класса paintForm.

    Инструментами создания изображения в нашем приложении являются "карандаш", "маркер" и "ластик". "Карандаш" ставит точки размером в один пиксель, маркер — крупные "точки" (квадраты) размером 10х10 пикселей, ластик имеет размер 20х20 пикселей. Координаты точек сохраняются в базе данных.

    Построение изображения

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

    domains
        dotType = dot; marker; eraser.
    constants
        dotsize = 5.			      % половина стороны крупной точки
        erasersize = 10. 		    % половина стороны ластика
        
    facts - dbpoints					                % база данных точек
        point: (pnt, dotType, color).
        
    facts
        switch : dotType := dot.			        % метка команды меню
        convexhull : pnt* := [].			        % вершины вып. обол.
        bgcolor : color := color_White.		    % цвет фона
        currentColor : color := color_Blue.		% текущий цвет
        
        isMouseDown : boolean := false. 	    % нажатие кнопки мыши 
    

    Добавим с помощью редактора формы paintForm обработчики событий SizeListener, PaintResponder и EraseBackgroundResponder. Предикаты onSize и onEraseBackground определяются так же, как и ранее.

    clauses
        onSize(_Source):-
            invalidate().
            
    clauses
         onEraseBackground(_Source, _GDI) = noEraseBackground.
    

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

    clauses
        onPaint(_Source, Rectangle, GDI):-
            true = isMouseDown,
            !,
            drawPoint(GDI, switch, Rectangle).
        onPaint(_Source, _Rectangle, GDI):-
            GDI:clear(bgcolor),
            point(Pnt, Type, Color),
                drawPoint(GDI, Type, Pnt, Color),
            fail.
        onPaint(_Source, _Rectangle, GDI):-
            convexhull <> [],
            !,
            GDI:setPen(pen(1, ps_Solid, color_Black)),
            GDI:setBrush(brush(pat_Hollow, color_Black)), 
            GDI:drawPolygon(convexhull).
        onPaint(_Source, _Rectangle, _GDI).
    

    Все рисование выполняется предикатом onPaint. Первое правило используется непосредственно в процессе рисования при нажатой левой кнопке мыши: обновляется только квадрат размером с "точку" или "ластик". Второе правило строит все изображение. Третье правило строит изображение выпуклой оболочки по списку вершин, который хранит факт-переменная convexhull: рисуется многоугольник с тонкой черной границей и прозрачной заливкой.

    Ниже приведено определение предикатов drawPoint/3 и drawPoint/4, а также вспомогательных предикатов.

    predicates
        drawPoint: (windowGDI, dotType, rct).
        drawPoint: (windowGDI, dotType, pnt, color).
    clauses
        drawPoint(GDI, dot, rct(X, Y, _, _)):- !,
            GDI:drawPixel(pnt(X, Y), currentColor).
        drawPoint(GDI, Type, Rectangle):- !,
            drawRect(GDI, Rectangle, color(Type)).
            
        drawPoint(GDI, dot, Pnt, Color):- !,
            GDI:drawPixel(Pnt, Color).
        drawPoint(GDI, Type, Pnt, Color):-
            drawRect(GDI, rectangle(Pnt, Type), Color).
            
    predicates
        drawRect: (windowGDI, rct, color).
    clauses
        drawRect(GDI, Rectangle, Color):-
            GDI:setPen(pen(1, ps_Solid, Color)),
            GDI:setBrush(brush(pat_Solid, Color)),
            GDI:drawRect(Rectangle).
            
    predicates
        rectangle: (pnt, dotType) -> rct.
        size: (dotType) -> integer.
        color: (dotType) -> integer.
    clauses
        rectangle(pnt(X, Y), dot) = rct(X, Y, X + 1, Y + 1):- !.
        rectangle(pnt(X, Y), Type) = rct(X - S, Y - S, X + S, Y + S):-
            S = size(Type).
            
        size(eraser) = erasersize:- !.
        size(_) = dotsize.
        
        color(eraser) = bgcolor:- !.
        color(_) = currentColor.
    

    Предикат drawPixel/2 закрашивает один пиксель.

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

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

    predicates
        onMenuItem : window::menuItemListener.
    clauses
        onMenuItem(_Source, id_color):- 		% выбор цвета
            NewColor = vpiCommonDialogs::getColor(currentColor),
            !,
            currentColor := NewColor.
        onMenuItem(_Source, id_clear):- !, 	% стирается рисунок
            retractFactDb(dbpoints),
            convexhull := [],
            invalidate().
        onMenuItem(_Source, id_point):- !,	% запоминается 
            switch := dot.				% команда меню
        onMenuItem(_Source, id_marker):- !,
            switch := marker.
        onMenuItem(_Source, id_eraser):- !,
            switch := eraser.
        onMenuItem(_Source, id_convexhull):- !,	 % строится
            convexhull := createConvexHull(),	% выпуклая оболочка
            invalidate().
        onMenuItem(_Source, _MenuTAG).
    

    После нажатия кнопки выбора цвета открывается диалоговое окно "Цвет" (рис. 2.9 рис 2.9). Факт-переменная currentColor хранит последнее выбранное значение цвета.

    (рис 2.9) Окно выбора цвета

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

    Обработка событий мыши

    В редакторе окна paintForm добавим обработчики событий мыши MouseDownListener, MouseMoveListener и MouseUpListener.

    После нажатия левой кнопки мыши факт-переменная isMouseDown принимает значение true. Далее запускается процедура обработки координат точки касания курсора, которая зависит от того, какая кнопка на панели инструментов была нажата последней (см. предикат drawing/2).

    predicates
        onMouseDown : window::mouseDownListener.
    clauses
        onMouseDown(_Source, Point, _ShiftControlAlt, _Button):-
            isMouseDown := true,
            convexhull := [],
            drawing(switch, Point).
    

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

    predicates
        onMouseMove : window::mouseMoveListener.
    clauses
        onMouseMove(_Source, Point, _ShiftControlAlt, _Button):-
            true = isMouseDown,
            !,
            drawing(switch, Point).
        onMouseMove(_Source, _Point, _ShiftControlAlt, _Button).
    

    После отпускания кнопки мыши факт-переменная isMouseDown принимает значение false.

    predicates
        onMouseUp : window::mouseUpListener.
    clauses
        onMouseUp(_Source, _Point, _ShiftControlAlt, _Button):-
            isMouseDown := false,
            invalidate().
    

    Определение предиката drawing/2 приведено ниже.

    predicates
        drawing: (dotType, pnt Point).
    clauses
        drawing(Type, Point):-
            Rectangle = rectangle(Point, Type),
            erasePoint(Type, Rectangle),
            addPoint(Type, Point),
            invalidate(Rectangle).
            
    predicates
        erasePoint: (dotType, rct).
        addPoint: (dotType, pnt).
    clauses
        erasePoint(eraser, Rectangle):-
            point(Dot, Type, _),
                pntInsideEraser(Rectangle, Dot, Type),
                retractAll(point(Dot, _, _)),
            fail.
        erasePoint(_, _).
        addPoint(eraser, _):- !.
        addPoint(Type, Point):-
            retractAll(point(Point, Type, _)),
            assert(point(Point, Type, currentColor)).
            
    predicates
        pntInsideEraser: (rct Rectangle, pnt Dot, dotType) determ.
    clauses
        pntInsideEraser(Rect, Dot, dot):- !,
            vpi::rectPntInside(Rect, Dot).
        pntInsideEraser(Rect, Dot, Type):-
            rct(A, B, C, D) = vpi::rectIntersect(rectangle(Dot, Type), Rect),
            (C - A) * (D - B) > 2 * dotsize^2.
    

    Предикат invalidate/1 помечает как недействительную прямоугольную область, в данном случае, размером с "точку" или "ластик", что приводит к ее перерисовке. Предикат rectPntInside/2 истинен, если точка находится внутри прямоугольника. Предикат rectIntersect/2 находит пересечение двух прямоугольников.

    Маленькая "точка" удаляется из базы данных, если она попадает под ластик. Удаление крупной "точки" производится в том случае, если ластик покрывает ее более чем наполовину.

    Упражнение. Создайте кнопку для выбора цвета фона рисунка и добавьте соответствующее правило для предиката onMenuItem.

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

    Выпуклая оболочка

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

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

    Для построения границы выпуклой оболочки используем алгоритм ГрэхемаКормен Т.Х., Лейзерсон Ч.И., Ривест Р.Л., Штайн К. Алгоритмы. Построение и анализ. – Изд. 2-e. – М.: Издательский дом "Вильямс", 2007. – 1296 с. – Глава 33 Сначала находится самая левая точка P множества X. Затем на множестве остальных точек вводится следующее отношение порядка. Если точки Q и R не лежат на одном и том же луче, выходящем из точки P, то полагается $$$Q\leq R$$$, если система векторов $$$\overline{PQ}$$$, $$$\overline{PR}$$$ имеет положительную ориентацию. Это означает, что они ориентированы так же, как и орты координатных осей, так что определитель матрицы, составленной из координат этих векторов, положителен. Для точек P(x, y), Q(a, b) и R(c, d) этот определитель находится следующим образом:

    $$\begin{vmatrix} a-x \quad c-x\\ b-y \quad d-y\\ \end{vmatrix}=(a-x)(d-y)-(c-x)(b-y).$$

    Если точки P, Q и R лежат на одной прямой, то полагается $$$Q\leq R$$$, если $$$|PQ|\leq |PR|$$$.

    Множество точек, отличных от ).

    (рис 2.10) Порядок на множестве точек:$$$Q\leq R\leq U\leq T$$$

    На следующем этапе алгоритма выполняется обход оставшегося множества точек, начиная с точки P. Используются список точек и стек, в который изначально помещаются точка P и затем первая из остальных точек. Напомним, что к этому моменту множество точек является упорядоченным, при этом из точек, лежащих на одном луче, оставлена одна, наиболее удаленная от P точка. Далее, пока не закончатся точки из списка, из него берется очередная точка K, а из стека последовательно берутся (но не удаляются) точка M и точка N. Если система векторов $$$\overline{MN}$$$, $$$\overline{MR}$$$ ориентирована положительно, то точка M удаляется из стека, и алгоритм продолжает работу для старого списка точек. В противном случае точка K перекладывается в стек, и алгоритм продолжает работу для оставшегося списка точек. Когда список окажется пустым, стек будет содержать множество вершин выпуклой оболочки.

    Например, рассмотрим множество точек, изображенное на рис. 2.10 рис 2.10.

    После сортировки из него удаляется точка ). Ориентация векторов $$$\overline{QP}$$$, $$$\overline{QS}$$$ отрицательна, поэтому точка ). Векторы $$$\overline{SQ}$$$, $$$\overline{SR}$$$ ориентированы положительно, поэтому точка ), поэтому точка R добавляется в стек, и так далее.

    (рис 2.11) Обход Грэхема

    Выпуклую оболочку множества крупных "точек" строит предикат createConvexHull.

    predicates
        createConvexHull: () -> pnt*.
    clauses
        createConvexHull() = graham::createConvexHull(PntList):-
            PntList = [Pnt || point(Pnt, marker, _)].
    

    Создадим класс graham (не порождающий объекты). В его декларации (файл graham.cl) объявим предикат createConvexHull.

    predicates
        createConvexHull: (vpiDomains::pnt*) -> vpiDomains::pnt*.
    

    Ниже приведена имплементация класса graham.

        open core, vpiDomains
    
    clauses
        createConvexHull(PntList) = ConvexHull:-
            [_, _| _] = PntList,
            !,
            LeftPoint = list::minimum(PntList),
            RestPntList = list::remove(PntList, LeftPoint),
            % сортировка множества точек
            Ord1 = {(Pnt1, Pnt2) = compare(true, LeftPoint, Pnt1, Pnt2)},
            SortedList = list::sortBy(Ord1, RestPntList),
            % удаление точек, лежащих на одном луче, кроме дальней
            Ord2 = {(Pnt1, Pnt2) = compare(false, LeftPoint, Pnt1, Pnt2)},
            List = list::removeConsecutiveDuplicatesBy(Ord2, SortedList),
            ConvexHull = graham([LeftPoint | List]).
        createConvexHull(_PntList) = [].
        
    % определение двух отношений порядка
    class predicates
        compare: (boolean, pnt, pnt, pnt) -> compareResult.
        compare: (integer Det) -> compareResult.
        det: (pnt, pnt, pnt) -> integer.
    clauses
        % точки, не лежащие на одном луче, сорт-ся в соотв. c det
        compare(_, LeftPnt, Pnt1, Pnt2) = compare(Det):-
            Det = det(LeftPnt, Pnt1, Pnt2),
            Det <> 0,
            !.
            
        % точки, лежащие на одном луче, различаются
        compare(true, pnt(X, Y), pnt(A, B), pnt(C, D)) = compare(
            % манхэттенское расстояние
            math::abs(A - X) + math::abs(B - Y),
            math::abs(C - X) + math::abs(D - Y)):- !.
        % точки, лежащие на одном луче, не различаются
        compare(_, _, _, _) = equal.
        
        compare(Det) = less:-
            Det > 0,
            !.
        compare(_) = greater.
        
        det(pnt(X, Y), pnt(A, B), pnt(C, D)) = 
            (A - X) * (D - Y) - (B - Y) * (C - X).
            
    % обход Грэхема
    class predicates
        graham: (pnt*) -> pnt*.
        graham: (pnt* List, pnt* Stack) -> pnt*.
    clauses
        graham([LeftPnt, Pnt | List]) = graham(List, [Pnt, LeftPnt]):- !.
        graham(List) = List.
        
        graham([K |List], [M, N |Stack]) = graham([K |List], [N |Stack]):-
            det(M, N, K) > 0,
            !.
        graham([K | List], Stack) = graham(List, [K | Stack]).
        graham([], Stack) = Stack.
    

    Напомним, что выпуклая оболочка строится при нажатии кнопки ).

    (рис 2.12) Выпуклая оболочка множества "крупных" точек

    Упражнение. Создайте на панели инструментов новую кнопку и добавьте процедуру сохранения рисунка в bmp-файле при нажатии на эту кнопку. Указание. Диалоговое окно "Сохранить как" вызывается следующим образом:

    FileName = vpiCommonDialogs::getFileName("*.bmp",
        ["Файл bmp", "*.bmp"], "Сохранить как", [dlgfn_Save], "", _).
    

    Получить рисунок из заданной прямоугольной области окна Rectangle и сохранить его в bmp-файле можно с помощью предикатов pictGetFromWin/2 и pictSave/2:

    Picture = vpi::pictGetFromWin(getVpiWindow(), Rectangle),
        vpi::pictSave(Picture, "mypicture.bmp").
    

    В данном случае необходимо учесть размеры панели инструментов. Прямоугольник TBRect, который занимает панель инструментов, возвращает предикат getRect/3:

    vpiToolbar::getRect(getVpiWindow(), vpiToolbar::tb_left, TBRect).
    

    Всплывающее меню и меню окна

    Добавим к окну paintForm всплывающее меню и меню окна.

    Создание всплывающего меню

    Создадим всплывающее меню. Для этого выделим папку paintForm дерева проекта и выберем команду всплывающего меню New In Existing Package. В окне Create Project Item выделим элемент Menu и назовем его paintPopUpMenu.

    Создадим меню, показанное на рис. 2.13 рис 2.13. Для пунктов меню важно выбрать идентификаторы, совпадающие с идентификаторами аналогичных кнопок панели инструментов (id_color, id_point, id_marker, id_eraser, id_clear и id_convexhull), чтобы использовать уже существующее определение предиката onMenuItem для обработки события выбора пункта контекстного меню.

    (рис 2.13) Окно редактора paintPopUpMenu

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

    onMouseDown(_Source, Point, _, mouse_button_right):- !,
            Menu = vpi::menuGetRes(id_paintpopupmenu),
            menuPopUp(Menu, Point, align_Left).
    

    Предикат .

    (рис 2.14) Всплывающее меню

    Создание меню окна

    Аналогично создадим меню окна и назовем его ).

    (рис 2.15) Окно редактора paintMenu

    Добавим меню в окно ).

    (рис 2.16) Меню окна paintForm

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

    Создадим новую кнопку и поместим ее на панель инструментов главного окна приложения. При нажатии на эту кнопку будет открываться окно paintForm.

    Подготовим изображение (bitmap) для кнопки размером 16 -> 16. С помощью элемента дерева проекта TaskWindow\Toolbars\ProjectToolbar.tb откроем редактор панели инструментов главного окна приложения. Добавим на панель кнопку так же, как это было описано ранее (Constant: id_paintForm). С помощью элемента taskWindow.win дерева проекта откроем эксперт окон, раскроем узлы Menu и ProjectToolbar, выделим элемент id_paintForm и добавим обработчик события нажатия на кнопку панели инструментов. Определим его так же, как и предикат onTestPaint (см. п. 2.1).

    Добавление строки состояния. Изменение пиктограммы

    Добавим строку состояния в окно ).

    (рис 2.17) Редактор строки состояния paintStatusLine

    Заполним поля в окне атрибутов текста так, как показано на рис. 2.18 рис 2.18:

  • Initial Text: <пустое поле>;
  • Text Width: 400;
  • Foreground Color: Green.
  • (рис 2.18) Редактор атрибутов текста

    Перед добавлением строки состояния в окно paintForm, создадим для этого окна новую иконку (пиктограмму). Выделим узел paintForm дерева проекта, откроем диалоговое окно Create Project Item, выделим элемент Icon и напишем в поле Name его название: paintIcon. Закроем появившееся окно атрибутов изображения. Создадим изображение в открывшемся графическом редакторе.

    Наконец, изменим определение конструктора new/1 в имплементации класса paintForm следующим образом:

    new(Parent):-
            formWindow::new(Parent),
            generatedInitialize(),
            %
            setIcon(idi_paintIcon),
            whenCreated({ :- paintStatusLine::create(getVpiWindow()) }).
    

    Строка состояния окна показана на рис. 2.19 рис 2.19.

    (рис 2.19) Строка состояния окна paintForm

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

    Включить пункт меню и добавить обработчик события выбора команды меню можно программным образом. Например, изменим в файле taskWindow.pro определение предиката onShow так, как показано ниже, а также добавим код, определяющий предикат myFileOpen:

    clauses
        onShow(_, _CreationData) :-
            _MessageForm = messageForm::display(This),
            menuEnable(resourceIdentifiers::id_file_open, true),
            menuSetText(resourceIdentifiers::id_file_open, "Открыть"),
            addMenuItemListener(myFileOpen).
            
    predicates
        myFileOpen : window::menuItemListener.
    clauses
        myFileOpen(_, resourceIdentifiers::id_file_open):- !,
            vpiCommonDialogs::note("Hello!").
        myFileOpen(_, _).
    

    В результате пункт меню File -> Open станет включенным, надпись изменится на "Открыть", и, при выборе данной команды меню, откроется диалоговое окно Note, в поле сообщения которого будет выведен текст "Hello!".

    Меню нетрудно создать динамически (см. определение домена dynMenu). Устанавливает меню предикат menuSet/1.

    Подробнее о редакторах ресурсов и, в частности, панели инструментов и пиктограмм, см. [14, ].

    Упражнения

    2.1. Добавьте команду удаления изображения выпуклой оболочки.

    2.2. Сделайте автоматической перерисовку выпуклой оболочки при изменении рисунка.

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

    2.4. Создайте инструменты для рисования графа. Вершины и ребра графа должны записываться в базу данных. База данных должна сохраняться в файле на диске и загружаться из него.

    Вернуться к учебному плану