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

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

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

База данных

Создадим проект 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").
    

    Предикат ). Следует подготовить указанные bmp-файлы с изображениями, создать в директории ).

    (рис 4.1) Папки Exe\descriptions и Exe\images

    Предикат ).

    Класс 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.
    

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

    constructors
        new: (unsigned Id).
    

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

    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()).
    

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

    Ниже создается класс 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.
    

    Свойство 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.
    

    Свойство 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).
    

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

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

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

    (рис 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: ().
    

    Определение объявленных предикатов в имплементации класса 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().
    

    Далее для окна 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]).
    

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

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

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

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

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

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

    С помощью панели инструментов ):

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

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

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

    пользовательский элемент управления (), выберем имя класса 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.
    

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

    properties
        familyForm : 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).
    

    Предикат 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).
    

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

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

    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).
    

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

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

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

    imageControl_ctl:setNoImage().
    

    Сделаем включенным пункт меню ), добавим обработчик события вызова данной команды меню (см. п. 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.5) Окно "Открыть базу данных"

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

    Упражнения

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

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

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