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

Деревья. Сводная таблица

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

Отображение деревьев

Для создания деревьев можно использовать два подхода — строить дерево в виде терма или использовать объектную модель (последнее возможно только для пользователей

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

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

Выделим папку ).

(рис 6.1) Изображения для терминальной вершины: (a) обычной; (b) выделенной

Аналогично следует создать изображения (.

Построение дерева в виде терма

Откроем редактор страницы ).

(рис 6.2) Редактор страницы treePage

Кроме этого, добавим пользовательский элемент управления (). Пользователям Personal Edition следует сразу создать еще один элемент управления (Custom Control) для отображения дерева потомков (Class: treeViewControl; Name: descendtree_ctl).

Закроем редактор страницы и выберем команду компиляции пакета treePage.pack.

В раздел open имплементации класса treePage слeдует добавить имена классов resourceIdentifiers и treeViewControl:

open core, vpiDomains, resourceIdentifiers, treeViewControl

Далее необходимо изменить определение конструктора new/0 и добавить объявление констант.

clauses
    new():-
        userControlSupport::new(),
        generatedInitialize(),
        %
        db := familyForm::familyForm:db.
facts
    db : dbrel.
constants
    closed : bitmapId = resid(idb_closed).
    closedSel : bitmapId = resid(idb_closedsel).
    opened : bitmapId = resid(idb_opened).
    openedSel : bitmapId = resid(idb_openedsel).
    terminal : bitmapId = resid(idb_terminal).
    terminalSel : bitmapId = resid(idb_terminalsel).
    ancestors = 0.
    descendants = 1.

Добавим обработчик событий ShowListener. Ниже приведено его определение.

clauses
    onShow(_Source, _Data):-
        some(Person) = db:selectedPerson,
        !,
        AncTree = createTree(Person:id, ancestors),
        anctree_ctl:insertTree(wcc_null, sorted(), AncTree),
        DescTree = createTree(Person:id, descendants),    % для PE
        descendtree_ctl:insertTree(wcc_null, sorted(), DescTree).
    onShow(_Source, _Data).

Предикат insertTree/3 размещает дерево в окне treeViewControl. Параметр sorted означает, что поддеревья упорядочиваются по надписям, помещенным в корнях этих поддеревьев.

Предикат createTree/2 строит дерево. Дерево строится в виде терма — шестиместной структуры с функтором tree (см. ниже). Первый аргумент хранит идентификатор, второй — список свойств узла (например, он может быть развернутым — expanded), третий и четвертый — изображения для невыделенных и выделенных вершин, пятый — текст (надпись) и шестой — список поддеревьев.

predicates
    createTree: (unsigned, positive AncOrDesc) -> tree.
clauses
    createTree(Id, Type) =
        tree(uncheckedConvert(itemId, Id), [], Pict, PictSel, 
            Person:legend(),
            list::map(ChildList, {(I) = createTree(I, Type)})):-
        Person = getPerson(Id),
        ChildList = childList(Person, Type),
        nodeBitmap(toBoolean([] = ChildList), true, Pict, PictSel).
predicates
    getPerson: (unsigned Id) -> person.
clauses
    getPerson(Id) = Person:-
        Person = db:getPerson(Id),
        !.
    getPerson(_) = _:-
        exception::raise_error().
predicates
    childList: (person, positive Type) -> unsigned* IdList.
clauses
    childList(Person, descendants) = Person:children:- !.
    childList(Person, _) = 
        list::filter([Person:idFather, Person:idMother], {(I):- I > 0}).
predicates
    nodeBitmap: (boolean IsTerminal, boolean IsNotExpanded, 
        bitmapId NewBmp [out], bitmapId NewSelBmp [out]).
clauses
    nodeBitmap(true, _, terminal, terminalSel):- !.
    nodeBitmap(_, true, closed, closedSel):- !.
    nodeBitmap(_, _, opened, openedSel).

Предикат nodeBitmap/4 возвращает изображения для вершины (невыделенной и выделенной).

Сделаем так, чтобы при открывании или закрывании узла дерева менялось изображение. В редакторе страницы treePage выделим в списке окна Properties элемент anctree_ctl и добавим обработчик событий ExpandBeginResponder. Ниже приведено его определение.

predicates
    onAnctreeExpandBegin : treeViewControl::expandBeginResponder.
clauses
    onAnctreeExpandBegin(Source, ItemId, IsExpanded) =
            treeViewControl::acceptExpand:-
        Source:getItem(ItemId, Text, StateList, Bmp, _SelBmp),
        nodeBitmap(toBoolean(terminal = Bmp), IsExpanded, 
            NewBmp, NewSelBmp),
        Source:setItem(ItemId, Text, StateList, NewBmp, NewSelBmp),
        Source:unselect().

Далее (в .

(рис 6.3) Деревья предков и потомков

Объектная модель дерева

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

  • Name: desctree_ctl;
  • @Node: treeNode_std;
  • Border: True.
  • Изменим определение конструктора new/0 так, как показано ниже.

    clauses
        new():-
            userControlSupport::new(),
            generatedInitialize(),
            %
            db := familyForm::familyForm:db,
            descTreeControl_ctl:imageList := imageList::new(16, 16),
            closedIdx := desctree_ctl:imageList:addResource(idb_closed),
            closedSelIdx :=
                desctree_ctl:imageList:addResource(idb_closedsel),
            terminalIdx := 
                desctree_ctl:imageList:addResource(idb_terminal),
            terminalSelIdx :=
                desctree_ctl:imageList:addResource(idb_terminalsel),
            if some(Person) = db:selectedPerson then
                Root = treeNode_std::new(Person:legend()),
                Root:bitmapIdx := closedIdx,
                Root:selectedBitmapIdx := closedSelIdx,
                getTree(Root, Person:id, descendants), 
                TreeModel = treeModel_std::new(),
                TreeModel:addTree(Root),
                desctree_ctl:model := TreeModel,
                desctree_ctl:nodeRenderer := TreeModel:nodeRenderer
            end if.
    

    Ниже приведено определение предиката getTree/3, который строит дерево, а также объявление фактов-переменных.

    facts
        closedIdx : integer := 0.
        closedSelIdx : integer := 0.
        terminalIdx : integer := 0.
        terminalSelIdx : integer := 0.
    predicates
        getTree: (treeNode_std, unsigned, positive Type).
    clauses
        getTree(ParentNode, Id, Type):-
            Person = getPerson(Id),
            List = childList(Person, Type),
            Idx = if [] = List then terminalIdx else closedIdx end if,
            SelIdx = if []= List then terminalSelIdx else closedSelIdx end if,
            ParentNode:bitmapIdx := Idx,
            ParentNode:selectedBitmapIdx := SelIdx,
            list::forAll(List, {(IdChild):-
                Child = getPerson(IdChild),
                Node = treeNode_std::new(ParentNode, Child:legend()),
                getTree(Node, IdChild, Type)}).
    

    При выделении вершины будем изменять цвет надписи. Добавим для элемента управления treeControl обработчики событий SelectBeginResponder и SelectEndListener.

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

    predicates
        onDesctreeSelectBegin :
            treeControl{treeNode_std}::selectBeginResponder.
    clauses
        onDesctreeSelectBegin(_Source, _Unselected, Selected) =
                treeControl{treeNode_std}::acceptSelect:-
            Selected = treeControl{treeNode_std}::node(Node),
            !,
            Node:textColor := color_DarkBlue,
            desctree_ctl:refresh(Node).
        onDesctreeSelectBegin(_Source, _Unselected, _Selected) =
            treeControl{treeNode_std}::acceptSelect.
    

    Далее приведено определение предиката onDesctreeSelectEnd.

    predicates
        onDesctreeSelectEnd : 
            treeControl{treeNode_std}::selectEndListener.
    clauses
        onDesctreeSelectEnd(_Source, Unselected, _Selected):-
            Unselected = treeControl{treeNode_std}::node(Node),
            !,
            Node:textColor := color_Black,
            desctree_ctl:refresh(Node).
        onDesctreeSelectEnd(_Source, _Unselected, _Selected).
    

    Результат показан справа на рис. 6.3 рис 6.3.

    Отображение данных в виде списка

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

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

    Cоздадим форму listViewForm и поместим в нее следующие элементы управления (рис. 6.4 рис 6.4):

    пользовательский элемент управления (Custom Control):

    Class: listViewControl;
     Right Anchor: True, Bottom Anchor: True;

    поле редактирования (Edit Control):

    Text: <пустое поле>
    Top Anchor: False, Right Anchor: True, Bottom Anchor: True;
    VScroll: True, AutoVScroll: True, AutoHScroll: False.
    (рис 6.4) Редактор формы listViewForm

    Закроем редактор окна и выберем команду компиляции пакета listViewForm.pack.

    Затем в пакете listViewForm создадим изображения (Bitmap) male (idb_male) и female (idb_female) размером 16 х 16.

    В раздел open имплементации класса listViewForm добавим имена классов resourceIdentifiers и listViewControl:

    open core, vpiDomains, resourceIdentifiers, listViewControl
    

    После этого изменим определение конструктора, а также определим факты-переменные.

    clauses
        new(Parent):-
            formWindow::new(Parent),
            generatedInitialize(),
            %
            db := familyForm::familyForm:db,
            ImageList = imageList::new(16, 16),
            maleIdx := ImageList:addResource(idb_male),
            femaleIdx := ImageList:addResource(idb_female),
            listViewControl_ctl:imageList := ImageList.
            
    facts
        db : dbrel.
        maleIdx : integer := 0.
        femaleIdx : integer := 0.
    

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

    predicates
        getLegend: (unsigned Id) -> string Name.
    clauses
        getLegend(Id) = Person:legend():-
            Person = db:getPerson(Id),
            !.
        getLegend(_Id) = "".
    predicates
        getPict: (person) -> integer Idx.
    clauses
        getPict(Person) = maleIdx:-
            dbrel::male = Person:sex,
            !.
        getPict(_Person) = femaleIdx.
    predicates
        getSpouses: (person) -> string.
    clauses
        getSpouses(P) = string::concatWithDelimiter(
            [S:legend() || Id in P:spouses, S = db:getPerson(Id)], ", ").
    

    Предикат getLegend/1 возвращает строку, состоящую из имени и фамилии родителя, либо пустую строку, если сведения о родителе отсутствуют в базе данных. Предикат getPict/1 возвращает изображение, которое отображается в строке таблицы. Предикат getSpouses/1 собирает строку, состоящую из имен и фамилий супругов.

    Создание таблицы

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

    (рис 6.5) Окно listViewForm

    Добавим в редакторе формы listViewForm обработчик событий ShowListener. Его определение приведено ниже.

    clauses
        onShow(_Source, _Data):-
            ColumnList = [
                column("Имя", 100, alignLeft),
                column("Фамилия", 80, alignLeft),
                column("Пол", 50, alignCenter),
                column("Отец", 100, alignLeft),
                column("Мать", 110, alignLeft),
                column("Супруги", 120, alignLeft)
            ],
            Comparator = {(X, Y) = compare(X:id, Y:id)},
            PersonList = list::sortBy(Comparator, db:personList),
            ItemList = [
                item(
                    uncheckedConvert(itemId, Person:id), 
                    Person:name, Pict, [], 
                    [Person:surname, Person:sex, Father, Mother, Spouses])
                ||
                    Person in PersonList,
                    Pict = getPict(Person),
                    Father = getLegend(Person:idFather),
                    Mother = getLegend(Person:idMother),
                    Spouses = getSpouses(Person)
            ],
            listViewControl_ctl:insertColumnList(1, ColumnList),
            listViewControl_ctl:insertItemList(ItemList),
            listViewControl_ctl:setLVType(lvs_report),
            listViewControl_ctl:setStyle([lvs_singlesel]).
    

    Список ColumnList содержит параметры столбцов таблицы — название, ширину столбца и тип выравнивания элементов в столбце.

    Строка таблицы хранится в виде терма item/5. Первый аргумент содержит идентификатор, второй — значение в первом столбце, третий — изображение, четвертый — список свойств, пятый — список значений в остальных столбцах. Если изображение не требуется, то значение параметра Pict можно положить равным (– 1).

    Форма listViewForm открывается при нажатии на кнопку "Таблица" окна familyForm. В редакторе формы familyForm добавим обработчик события нажатия на эту кнопку. Ниже приведено его определение.

    clauses
        onTableClick(_Source) = button::defaultAction:-
            familyForm := This,
            _ = listViewForm::display(getParent()).
    

    События выделения строки или столбца

    Добавим операцию сортировки списка по столбцу при выделении столбца. Для этого следует открыть редактор формы listViewForm, выделить элемент listViewControl и добавить обработчик события ColumnClickListener. Он определяется ниже.

    predicates
        onListViewControlColumnClick : 
            listViewControl::columnClickListener.
    clauses
        onListViewControlColumnClick(S, ColumnNumber):-
            S:sortItems(listViewControl::ascending(), ColumnNumber).
    

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

    При выделении строки таблицы, описание о члене семьи из файла, который указан в базе данных (первым), должно отобразиться в поле редактирования (см. рис. 6.5 рис 6.5). Если файл с описанием отсутствует в базе данных, то в поле редактирования должно отобразиться имя человека. В редакторе формы listViewForm добавим для списка обработчик событий SelectBeginResponder.

    predicates
        onListViewControlSelectBegin :
            listViewControl::selectBeginResponder.
    clauses
        onListViewControlSelectBegin(S, ItemId, true) = 
                listViewControl::acceptSelect:-
            item(_, Name, _, _, [Surname | _]) = S:getItem(ItemId),
            Id = uncheckedConvert(unsigned, ItemId),
            Person = db:getPerson(Id),
            !,
            Text = getDescription(Person, Name, Surname),
            edit_ctl:setText(Text).
        onListViewControlSelectBegin(_, _, _) = acceptSelect:-
            edit_ctl:setText("").
    

    Ниже приведено определение предиката getDescription/3.

    predicates
        getDescription: (person, string Name, string Surname) -> string.
    clauses
        getDescription(Person, _, _) = Text:-
            [FileName | _] = Person:descriptions,
            FilePath = dbrel::descriptionsFolder(),
            FullName = fileName::setPath(FileName, FilePath),
            file::existFile(FullName),
            try Text = file::readString(FullName, _)
            catch _ do
                fail
            end try,
            !.
        getDescription(_, Name, S) = string::concat(Name, " ", S).
    

    Способы отображения списка

    Существует четыре способа отображения списка — таблица (см. выше), список, значки и плитка (рис. 6.6 рис 6.6).

    (рис 6.6) Способы отображения списка: (a) список; (b) значки; (c) плитка

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

    В папке listViewForm создадим меню listViewPopUpMenu с пунктами:

  • Таблица (id_report);
  • Список (id_list);
  • Значки (id_icon);
  • Плитка (id_smallicon).
  • После этого добавим обработчик событий MouseRightClickListener для списка и обработчик событий MenuItemListener для формы. Ниже приведено определение предиката onListViewControlMouseRightClick.

    predicates
        onListViewControlMouseRightClick :
            listViewControl::mouseRightClickListener.
    clauses
        onListViewControlMouseRightClick(_Source, Point):-
            Menu = vpi::menuGetRes(id_listviewpopupmenu),
            menuPopUp(Menu, Point, align_left).
    

    Далее определяется предикат onMenuItem.

    predicates
        onMenuItem : window::menuItemListener.
    clauses
        onMenuItem(_Source, id_report):- !,
            listViewControl_ctl:setLVType(lvs_report).
        onMenuItem(_Source, id_list):- !,
            listViewControl_ctl:setLVType(lvs_list).
        onMenuItem(_Source, id_icon):- !,
            listViewControl_ctl:setLVType(lvs_icon).
        onMenuItem(_Source, id_smallicon):- !,
            listViewControl_ctl:setLVType(lvs_smallicon).
        onMenuItem(_Source, _MenuTAG).
    

    Высплывающее меню можно создать непосредственно в окне списка. Для этого нужно изменить определение предиката ):

    onListViewControlMouseRightClick(_Source, Point):-
            Menu = vpi::menuGetRes(id_listviewpopupmenu),
            listViewControl_ctl:menuPopUp(Menu, Point, align_left).
    

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

    Упражнения

    6.1. Создайте отдельное окно для просмотра изображения выделенного члена семьи. Оно должно открываться с помощью двойного щелчка по изображению на форме familyForm и на форме tabForm.

    6.2. Создайте окно для поиска ответов на запросы к базе данных на языке, близком к естественному языку. Для вывода ответа на запрос используйте список (listBox или listViewControl).

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