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

Приложение "Родственные отношения"

< Лекция 3 || Лекция 4 || Лекция 5 >
Аннотация: Следующие четыре главы посвящены элементам управления. Создается приложение "Родственные отношения". Цель создания приложения — задействовать разнообразные элементы управления — списки, вкладки, деревья, переключатели, поля редактирования и др. Строятся и отображаются деревья предков и потомков членов семьи. Создается сводная таблица данных. Реализуется возможность добавления и удаления сведений. В настоящей главе создается база данных и основное окно — форма, содержащая список членов семьи. Для каждого члена семьи создается объект класса person, в котором размещаются сведения о нем из базы данных.
Ключевые слова: файл, базы данных, список, поле

База данных

Создадим проект relatives (MDI). Подготовим текстовый файл так, как описано ниже, содержащий факты базы данных, и поместим его в папку Exe, а также bmp- и txt-файлы и разместим их в папках images и descriptions, которые должны быть созданы в директории Exe проекта (см. ниже).

Подготовка текстовых и bmp-файлов

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

Создадим текстовый файл в директории Exe проекта, так чтобы он отображался в дереве проекта. Для этого выделим корень дерева проекта, с помощью команды (контекстного) меню New In New Package откроем диалоговое окно Create Project Item, выделим элемент Text File и заполним поля следующим образом:

  • Name: family;
  • Parent Directory: Exe.

Далее нужно нажать кнопку Create. В результате в директории Exe проекта будет создан файл family.txt. В него необходимо поместить факты базы данных, приведенные ниже.

clauses
    person(1, "Иван", "Петров", "м", 0, 0).
    person(2, "Анна", "Петрова", "ж", 0, 0).
    person(3, "Мария", "Иванова", "ж", 1, 2).
    person(4, "Павел", "Иванов", "м", 0, 3).
    person(5, "Петр", "Иванов", "м", 0, 3).
    person(6, "Елизавета", "Иванова", "ж", 0, 3).
    person(7, "Степан", "Иванов", "м", 5, 0).
    person(8, "Юлия", "Иванова", "ж", 0, 0).
    
    spouse(1, 2).
    spouse(4, 8).
    
    pict(1, "ivan1.bmp").
    pict(1, "ivan2.bmp").
    pict(2, "anna.bmp").
    pict(3, "maria1.bmp").
    pict(3, "maria2.bmp").
    pict(6, "elizabeth.bmp").
    
    descr(3, "maria.txt").
Листинг 4.1. База данных. Файл family.txt

Предикат person/6 хранит идентификатор человека (порядковый номер), его имя, фамилию, пол и идентификторы родителей — отца и матери. Если сведения о каком-либо родителе отсутствуют, то вместо идентификатора указывается значение 0. Предикат spouse/2 хранит идентификаторы супругов — мужа и жены. Предикат pict/2 хранит идентификатор человека и название bmp-файла, содержащего его изображение. Для одного человека изображений может быть несколько (см. листинг 4.1 пример 4.1). Следует подготовить указанные bmp-файлы с изображениями, создать в директории Exe проекта папку images и поместить их в эту папку (рис. 4.1 рис. 4.1).

Папки Exe\descriptions и Exe\images

Рис. 4.1. Папки Exe\descriptions и Exe\images

Предикат descr/2 используется для хранения сведений о жизнеописании человека (биографии). В фактах хранится идентификатор человека и имя текстового файла. В директории Exe проекта следует создать папку descriptions и поместить в него необходимые текстовые файлы (см. рис. 4.1 рис. 4.1).

Класс person

Для каждого человека из базы данных создается объект класса person, в котором размещается вся информация об этом человеке.

Создадим класс person с интерфейсом person. В интерфейсе person следует объявить свойства, а также предикаты legend и idLegend, которые используются для формирования надписей в списках и деревьях.

properties
    id : unsigned.
    name : string.
    surname : string.
    sex : string.
    idFather : unsigned.
    idMother : unsigned.
    children : unsigned*.
    spouses : unsigned*.
    pictures : string*.
    descriptions : string*.
    status : string.
    
predicates
    legend: () -> string.
    idLegend: () -> string.
Листинг 4.2. Объявление свойств и предикатов в интерфейсе person

В декларации класса person следует объявить конструктор.

constructors
    new: (unsigned Id).
Листинг 4.3. Объявление конструктора в декларации класса person

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

facts
    id : unsigned.
    name : string := "".
    surname : string := "".
    sex : string := "м".				              % dbrel::male
    idFather : unsigned := 0.
    idMother : unsigned := 0.
    children : unsigned* := [].
    spouses : unsigned* := [].
    pictures : string* := [].
    descriptions : string* := [].
    status : string := "просмотр".
    
clauses
    new(Id):-
        id := Id.
        
    legend() = string::format("% %", name, surname).
    
    idLegend() = string::format("%2. %", id, legend()).
Листинг 4.4. Определение предикатов в имплементации класса person

Взаимодействие с базой данных

Ниже создается класс dbrel, который обеспечивает взаимодействие с базой данных.

Создадим класс dbrel с интерфейсом dbrel. В интерфейсе dbrel следует объявить указанные ниже константы, свойства и предикаты.

constants
    images = "images".			        % назв. папки с изобр.       
    descriptions = "descriptions".	% назв. папки с заметками
    
constants
    male = "м".				              % мужской пол
    female = "ж".			              % женский пол
    
properties
    personList : person*. 
    selectedPerson : optional{person}.
    
predicates
    load: ().
    getPerson: (unsigned) -> person determ.
    getNewId: () -> unsigned.
Листинг 4.5. Объявления в интерфейсе dbrel

Свойство personList используется для хранения указателей на объекты класса person. Свойство selectedPerson используется для хранения указателя на объект выделенного члена семьи. Домен optional{T} определен в классе core следующим образом:

domains
    optional{T} = none(); some(T).

Предикат load загружает базу данных, предикат getPerson/1 по идентификатору возвращает указатель на объект класса person. Предикат getNewId возвращает идентификатор для нового человека, сведения о котором будут записываться в базу данных.

Следующее объявление свойства и предикатов необходимо поместить в декларацию класса dbrel.

properties
    db : optional{dbrel}.		          % указатель на объект текущей БД
    
constructors
    new: (string FileName).
    
predicates
    imageFolder: () -> string.
    descriptionsFolder: () -> string.
Листинг 4.6. Объявления в декларации класса dbrel

Свойство db используется для хранения указателя на объект текущей базы данных. Приложение предоставляет возможность одновременной работы с несколькими семьями. Конструктор new/1 создает по имени файла объект базы данных. Предикаты imageFolder и descriptionsFolder возвращают путь к папкам, содержащим изображения и описания.

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

В раздел open имплементации класса следует добавить имя класса stdio.

open core, stdio

facts
    filename : string.
    personList : person* := [].
    selectedPerson : optional{person} := none().
    maxId : unsigned := 0.
    
clauses
    new(FileName):-
        filename := FileName.
        
class facts
    db : optional{dbrel} := none().
    
facts - rel
    person: (unsigned, string, string, string, unsigned, unsigned).
    spouse: (unsigned IdHusband, unsigned IdWife).
    pict: (unsigned Id, string BmpFileName).
    descr: (unsigned Id, string TextFileName).
    
clauses
    load():-
        try file::consult(filename, rel)
        catch Error do
            writef("Error %. Unable to load the database from %\n",
                Error, filename)
        end try,
        setMaxId(),
        loadDb().
        
predicates
    setMaxId: ().
clauses
    setMaxId():-
        IdList = [Id || person(Id, _, _, _, _, _)],
        IdList <> [],
        !,
        maxId := list::maximum(IdList).
    setMaxId().
    
predicates
    loadDb: ().
clauses
    loadDb():-
        person(Id, Name, Surname, Sex, IdFather, IdMother),
            Person = person::new(Id),
            Person:name := Name,
            Person:surname := Surname,
            Person:sex := Sex,
            Person:idFather := IdFather,
            Person:idMother := IdMother,
            Person:children := getChildren(Id, Person:sex), 
            Person:spouses := [I || spouse(Id, I); spouse(I, Id)],
            Person:pictures := [BmpFile || pict(Id, BmpFile)],
            Person:descriptions := [TxtFile || descr(Id, TxtFile)],
            personList := [Person | personList],
        fail.
    loadDb().
    
predicates
    getChildren: (unsigned Id, string Sex) -> unsigned* ChildrenList.
clauses
    getChildren(Id, male) = [I || person(I, _, _, _, Id, _)]:- !.
    getChildren(Id, _) = [I || person(I, _, _, _, _, Id)].
clauses
    getPerson(Id) = Person:-
        Person in personList,
        Person:id = Id,
        !.
        
clauses
    getNewId() = maxId:-
        maxId := maxId + 1.
        
clauses
    imageFolder() = folder(images).
    descriptionsFolder() = folder(descriptions).
    
class predicates
    folder: (string Name) -> string Path.
clauses
    folder(Name) = string::concat(Path, Folder):-
        mainExe::getFileName(Path, _),
        Folder = string::format(@"%\", Name).
Листинг 4.7. Определение в имплементации класса dbrel

Предикат loadDb для каждого члена семьи, информация о котором помещена в базу данных, создает объект класса person и записывает в него все сведения, имеющиеся о нем в базе данных. Предикат getFileName/2 возвращает путь к exe-файлу проекта.

Форма просмотра общих сведений

Ниже создается окно просмотра общих сведений. Окно содержит список, в котором пересчисляются члены семьи, поле для просмотра изображений и ряд кнопок (рис. 4.2 рис. 4.2).

База данных "Family"

Рис. 4.2. База данных "Family"

Замечание.Пользователи версии Visual Prolog 7.x Commercial Edition могут не создавать окно pictControl, а вместо него использовать imageControl. Ниже показано, как это можно сделать.

Предварительно создадим окно pictControl, которое будет использоваться для отображения изображений (см. ниже).

Создание поля для просмотра изображений

Выделим корень дерева проекта, с помощью команды меню New in New Package откроем диалоговое окно Create Project Item, выделим в нем элемент Draw Control, в поле Name напишем название pictControl и нажмем кнопку Create. Затем закроем редактор окна.

В интерфейсе pictControl следует объявить предикаты drawPict/1 и clear. Первый предикат показывает изображение, второй очищает поле.

predicates
    drawPict: (string FileName).
    clear: ().
Листинг 4.8. Объявление предикатов в интерфейсе pictControl

Определение объявленных предикатов в имплементации класса pictControl приведено ниже.

facts
    pict : picture := erroneous.
    pictRct : rct := erroneous.
    
class facts 		      % база загруженных изображений
    picture: (string FileName, picture, rct).
    
clauses
    drawPict(FileName):-
        picture(FileName, Picture, PictRect),
        !,
        pict := Picture,
        pictRct := PictRect,
        invalidate().
    drawPict(FileName):-
        Picture = loadPicture(FileName),
        !,
        pict := Picture,
        vpi::pictGetSize(Picture, PictWidth, PictHeight, _),
        pictRct := rct(0, 0, PictWidth, PictHeight),
        assert(picture(FileName, pict, pictRct)),
        invalidate().
    drawPict(_FileName):-
        clear().
        
predicates
    loadPicture: (string FileName) -> picture determ.
clauses
    loadPicture(FileName) = Picture:-
        FullName = string::concat(dbrel::imageFolder(), FileName),
        try Picture = vpi::pictLoad(FullName)
        catch Error do
            stdio::writef("Error %. Unable to load the picture from %\n",
                Error, FullName),
            fail
        end try.
        
clauses
    clear():-
        pict := erroneous,
        invalidate().
Листинг 4.9. Построение изображения

Далее для окна pictControl следует добавить обработчики событий PaintResponder, SizeListener и EraseBackgroundResponder.

Предикат onPaint либо отображает изображение, либо выводит надпись "No picture".

clauses
    onPaint(_Source, Rectangle, GDI):-
        not(isErroneous(pict)),
        !,
        GDI:pictDraw(pict, Rectangle, pictRct, rop_SrcCopy).
    onPaint(_Source, Rectangle, GDI):-
        GDI:clear(color_MediumTurquoise),
        GDI:drawTextInRect(Rectangle, "NO PICTURE",
            [dtext_center, dtext_singleline, dtext_vcenter]).
Листинг 4.10. Определение предиката onPaint

Предикаты onSize и onEraseBackground определяются так же, как и ранее.

clauses
    onSize(_Source):-
        invalidate().
Листинг 4.11. Определение предиката onSize
clauses
    onEraseBackground(_Source, _GDI) = noEraseBackground.
Листинг 4.12. Определение предиката onEraseBackground

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

Создадим форму familyForm. В редакторе формы удалим из нее кнопки Cancel и Help. Ниже на форму добавляется ряд элементов управления и устанавливаются свойства для них с помощью окна свойств. Элемент управления следует выбрать на панели инструментов окна Controls и "перенести" на форму, а затем в таблице свойств изменить значения свойств, указанные ниже.

При выделении элемента управления в списке элементов, расположенном в верхней части окна свойств, или непосредственно на прототипе формы, в таблице свойств отображается список свойств этого элемента управления. Если на форме не выделен ни один из элементов управления или если в списке выделен элемент Form, то в таблице свойств отображаются свойства формы. На рис. 4.3 рис. 4.3 в верхнем списке окна свойств выделен элемент Form, поэтому в таблице свойств отображаются свойства окна.

Редактор формы familyForm

Рис. 4.3. Редактор формы familyForm

С помощью панели инструментов Controls добавим следующие элементы управления (см. рис. 4.3 рис. 4.3):

cписок (List Box), свойству UseTabStop установим значение True;

надпись (Static Text), для которой установим следующие свойства:

Representation: Fact Variable;
Name: status_ctl;
Text:<пустое поле>

пользовательский элемент управления (Custom Control); в окне Choose Class Name for Custom Control, которое вызывается автоматически (рис. 4.4 рис. 4.4), выберем имя класса pictControl (пользователи Commercial Edition вместо него могут указать imageControl), затем установим свойства:

Right Anchor: True;
Bottom Anchor: True;

пять кнопок (Push Button)

Name: view_ctl; Text: Открыть; Enabled: False;
Name: new_ctl; Text: Добавить;
Name: save_ctl; Text: Сохранить; Enabled: False;
Name: del_ctl; Text: Удалить; Enabled: False;
Name: table_ctl; Text: Таблица
Окно выбора класса для элемента управления

Рис. 4.4. Окно выбора класса для элемента управления

Для всех новых кнопок следует установить следующие свойства:

Left Anchor: False;
 Top Anchor: False;
   Right Anchor: True;
   Bottom Anchor: True.

После этого следует закрыть редактор формы.

Свойство Anchor (якорь) определяет, привязан ли элемент к краю контейнера (формы). Если его значение равно True, то изменение размеров формы не влияет на расстояние от элемента до границы формы. В окне familyForm кнопки привязываются к правой и нижней границам формы, расстояние от них до указанных границ при изменении размеров формы остается неизменным. Элемент pictControl привязан ко всем границам формы, поэтому при изменении размеров формы будет изменяться и его размер.

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

properties
    db : dbrel.
Листинг 4.13. Объявление свойства в интерфейсе familyForm

В декларации класса familyForm следует объявить свойство, которое используется для !указателя на объект текущей формы (оно необходимо при одновременной работе с несколькими формами класса familyForm).

properties
    familyForm : familyForm.
Листинг 4.14. Объявление свойства в декларации класса familyForm

В имплементации класса familyForm определим объявленные свойства и добавим предикаты legend и getSelectedPerson. Предикат legend/1 используется для формирования элемента списка. Предикат getSelectedPerson/1 для выделенного элемента списка возвращает указатель на объект класса person.

facts
    db : dbrel := erroneous.
    
class facts
    familyForm : familyForm := erroneous.
    
predicates
    legend: (person) -> string.
clauses
    legend(Person) = string::format("%2.  %-12\t%", 
        Person:id, Person:name, Person:surname).
predicates
    getSelectedPerson: () -> person determ.
clauses
    getSelectedPerson() = db:getPerson(Id):-
        Index = listbox_ctl:tryGetSelectedIndex(),
        Item = listbox_ctl:getAt(Index),
        string::frontToken(Item, Tok, _),
        Id = tryToTerm(unsigned, Tok).
Листинг 4.15. Определение в имплементации класса familyForm

Предикат tryGetSelectedIndex возвращает номер выделенного элемента списка (элементы нумеруются с нуля), предикат getAt/1 возвращает по номеру элемент списка.

Теперь в редакторе формы familyForm добавим обработчик событий ShowListener. При открытии окна формируется список членов семьи.

clauses
    onShow(_Source, _Data):-
        some(Db) = dbrel::db,
        !,
        db := Db,
        listbox_ctl:setTabStops([22]),
        listbox_ctl:addList([legend(Pers) || Pers in db:personList]).
    onShow(_Source, _Data).
Листинг 4.16. Определение предиката onShow

Далее добавим обработчик события выделения элемента списка SelectionChangedListener. Для этого в редакторе формы нужно выделить список, перейти на вкладку Events окна свойств и выбрать для обработчика события SelectionChangedListener элемент onListBoxSelectionChanged.

При выделении элемента списка (члена семьи) находится указатель на объект класса person, появляется изображение (или удаляется предыдущее), отображается статус — "просмотр" или "добавление". Статус "просмотр" имеют члены семьи, сведения о которых уже записаны в базу данных, остальные персоны получают статус "добавление" (см. главу 6) "Деревья. Сводная таблица" . Кроме этого, включаются (т. е. делаются доступными) кнопки "Открыть" и "Удалить", а также включается или выключается кнопка "Сохранить", в зависимости от статуса члена семьи ("просмотр" или "добавление"). Ниже приведено определение предиката.

predicates
    onListboxSelectionChanged :
        listControl::selectionChangedListener.
clauses
    onListboxSelectionChanged(_Source):-
        Person = getSelectedPerson(),
        !,
        if [FileName | _] = Person:pictures then
            pictControl_ctl:drawPict(FileName)
        else
            pictControl_ctl:clear()
        end if,
        status_ctl:setText(Person:status),
        view_ctl:setEnabled(true),
        del_ctl:setEnabled(true),
        save_ctl:setEnabled(toBoolean(Person:status <> "просмотр")).
    onListboxSelectionChanged(_Source).
Листинг 4.17. Определение предиката onListBoxSelectionChanged

Замечание. Пользователи Commercial Edition, использующие imageControl, должны заменить строку pictControl_ctl:drawPict(FileName) следующим кодом:

FullName = string::concat(dbrel::imageFolder(), FileName),
        imageControl_ctl:setImageFile(FullName)

Вместо pictControl_ctl:clear() следует написать

imageControl_ctl:setNoImage().

Сделаем включенным пункт меню File -> New главного окна приложения (см. п. "Основные элементы графического интерфейса пользователя" ), добавим обработчик события вызова данной команды меню (см. п. 1.1.3 "Основные элементы графического интерфейса пользователя" ) и определим его так, как показано ниже.

clauses
    onFileNew(_Source, _MenuTag):-
        mainExe::getFileName(StartPath, _Name),
        FileName = vpiCommonDialogs::getFileName(
            "*.txt", ["Текстовый файл", "*.txt"],
            "Открыть базу данных", [], StartPath, _),
        !,
        Db = dbrel::new(FileName),
        Db:load(),
        dbrel::db := some(Db),
        Form = familyForm::display(This),
        familyForm::familyForm := Form,
        Form:setText(fileName::getName(FileName)).
    onFileNew(_Source, _MenuTag).
Листинг 4.18. Определение предиката onFileNew

При выборе пункта меню File -> New открывается окно "Открыть базу данных" (рис. 4.5 рис. 4.5).

Окно "Открыть базу данных"

Рис. 4.5. Окно "Открыть базу данных"

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

Упражнения

4.1. Подготовьте еще одну базу данных с информацией о какой-либо семье или династии. Текстовый файл, содержащий факты базы данных, поместите в директорию Exe проекта.

4.2. Создайте bmp-файлы с изображениями членов семьи (см. упр. 4.1) и текстовые файлы, содержащие их жизнеописания. Поместите их в папки Exe\images и Exe\descriptions, соответственно.

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