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

Добавление и удаление сведений

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

Режим добавления

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

Для того чтобы добавить нового члена семьи в базу данных, необходимо нажать на кнопку "Добавить" окна ).

(рис 7.1) Режим добавления данных

Добавим обработчик события нажатия на кнопку "Добавить" окна familyForm. Ниже приведено его определение.

clauses
    onNewClick(_Source) = button::defaultAction:-
        db:selectedPerson := core::none(),
        familyForm := This,
        Win = tabForm::display(getParent()),
        Win:setText("Добавление"),
        if Index = listbox_ctl:tryGetSelectedIndex() then
            listbox_ctl:selectAt(Index, false)
        end if.

Начальное состояние страницы информации

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

Определим предикат getList/1, который формирует список мужчин или женщин, предикат fillList/2, который заполняет выпадающий список списком элементов и выделяет первый элемент, а также предикат getSex/1, который возращает пол по состоянию переключателя "женский". В список родителей добавляется пустой элемент, который используется в случае, когда сведения о родителе отсутствуют в базе данных.

constants
    emptyItem = "< >".
    
predicates
    getList: (string Sex) -> string* ItemList.
clauses
    getList(Sex) = [Person:idLegend() || 
        Person in db:personList, Person:sex = Sex].
predicates
    fillList: (string* ItemList, listButton).
clauses
    fillList(ItemList, ListCtl):-
        ListCtl:addList(ItemList),
        ListCtl:addAt(list::length(ItemList), emptyItem),
        ListCtl:selectAt(0, true).
        
predicates
    getSex: (radioButton::state Female) -> string Sex.
clauses
    getSex(radioButton::checked) = dbrel::female:- !.
    getSex(_) = dbrel::male.

Изменим определение предиката onShow страницы infoPage так, как показано ниже.

clauses
    onShow(_Source, _Data):-			% режим просмотра
        some(Person) = db:selectedPerson,
        !,
        name_ctl:setText(Person:name),
        surname_ctl:setText(Person:surname),
        setSex(Person:sex),
        setParent(Person:idfather, father_ctl),
        setParent(Person:idmother, mother_ctl),
        setSpouses(Person),
        setPictures(Person),
        setEnable(Person:status).
    onShow(_Source, _Data):- 			% режим добавления
        FemaleList = getList(dbrel::female),
        fillList(FemaleList, mother_ctl),
        MaleList = getList(dbrel::male),
        fillList(MaleList, father_ctl),
        spouses_ctl:clearAll(),
        spouses_ctl:addList(FemaleList),
        setEnable("добавление").

Изменение состояния переключателя

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

Добавим обработчик событий StateChangedListener для переключателя "мужской".

Ниже приведено определение предиката onMaleStateChanged. Список супругов обновляется при изменении состояния переключателя.

predicates
    onMaleStateChanged : radioButton::stateChangedListener.
clauses
    onMaleStateChanged(_Source, _OldState, NewState):-
        spouses_ctl:clearAll(),
        spouses_ctl:addList(getList(getSex(NewState))).

Добавление и удаление изображений

Для нового члена семьи можно записать в базу данных сразу несколько изображений. Эти изображения должны быть заранее подготовлены. Файлы, содержащие изображения, копируются в папку images, которая находится в директории Exe, если они не были размещены в ней заранее. Файлы, расположенные в одной и той же папке, могут добавляться списком. Добавление изображений производится с помощью кнопки "…". Для удаления изображения предназначена кнопка "-". Добавим обработчики событий нажатия на эти кнопки.

Ниже определяются предикаты onBrowseClick и getFileList/3. При нажатия кнопки "…" откроется окно "Открыть файл", с помощью которого можно выбрать сразу несколько файлов (изначально открывается папка images), используя клавиши Shift или Ctrl.

clauses
    onBrowseClick(_Source) = button::defaultAction:-
        StartPath = dbrel::imageFolder(),
        FileName = vpiCommonDialogs::getFileName(
            "*.bmp", ["Файл bmp", "*.bmp"], "Выберите файлы",
            [dlgfn_multisel], StartPath, FileNameList),
        !,
        Path = fileName::getPath(FileName),
        FileList = getFileList(FileNameList, Path, StartPath),
        pictures := list::union(pictures, FileList),
        n := list::length(pictures),
        k := 0,
        pictures == [FirstFile | _],
        pictControl_ctl:drawPict(FirstFile),
        del_ctl:setEnabled(true),
        right_ctl:setEnabled(toBoolean(n > 1)),
        left_ctl:setEnabled(false).
    onBrowseClick(_Source) = button::defaultAction.
predicates
    getFileList: (string*, string Path, string StartPath) -> string*.
clauses
    getFileList(FileNameList, Path, StartPath) =
        [FileName || FullName in FileNameList,
                FileName = filename::getNameWithExtension(FullName),
                if not(string::equalIgnoreCase(Path, StartPath)),
                    ImageFile = fileName::setPath(FullName, StartPath),
                    not(file::existFile(ImageFile))
                then
                    file::copy(FullName, ImageFile)
                end if].

При нажатии кнопки "-" текущее изображение, которое отображается в окне, удаляется из списка изображений. Ниже определяется предикат onDelClick.

clauses
    onDelClick(_Source) = button::defaultAction:-
        n > 0,
        pictures := list::remove(pictures, list::nth(k, pictures)),
        !,
        n := n - 1,
        k := if n > 0, k < n then k elseif n > 0 then n - 1 else 0 end if,
        if n > 0 then
            pictControl_ctl:drawPict(list::nth(k, pictures))
        else
            pictControl_ctl:clear()
        end if,
        right_ctl:setEnabled(toBoolean((n > 1, k < n - 1))),
        left_ctl:setEnabled(toBoolean((n > 1, k > 0))),
        del_ctl:setEnabled(toBoolean(n > 0)).
    onDelClick(_Source) = button::defaultAction.

Добавление описания

Для любого члена семьи, нового или старого, в базу данных можно добавить заметку (жизнеописание). Для этого нужно ввести текст на странице descrPage в текстовое поле или вставить его из файла. После этого текст необходимо сохранить в текстовом файле в папке descriptions, с помощью нажатия на кнопку "Сохранить". Затем нужно нажать на кнопку "Записать". Позднее потребуется сохранить изменения в базе данных (см. ниже).

В редакторе страницы descrPage добавим обработчик события нажатия на кнопку "Записать". Ниже определяется предикат onSaveInDbClick.

clauses
    onSaveInDbClick(_Source) = button::defaultAction:-
        some(Person) = db:selectedPerson,
        filename <> "",
        Text = string::trim(sciLexer_ctl:text),
        Text <> "",
        FileName = fileName::getNameWithExtension(filename),
        not((P in db:personList, FileName in P:descriptions)),
        !,
        file::writeString(filename, Text, isUnicode),
        Person:descriptions := [Filename | Person:descriptions],
        db:addDescription(Person, Filename),
        stdio::write("Описание добавлено в базу данных.\n").
    onSaveInDbClick(_Source) = button::defaultAction.

Предикат addDescription/2 необходимо объявить в интерфейсе dbrel и определить в имплементации класса dbrel.

predicates
    addDescription: (person, string).
Листинг 7.10. Определение предиката в имплементации класса dbrel
clauses
    addDescription(Person, FileName):-
        Person:status <> "добавление",
        not(descr(Person:id, Filename)),
        !,
        assert(descr(Person:id, Filename)).
    addDescription(_, _).

Добавление сведений

Сведения о новом члене семьи, помещенные на страницу информации окна ).

(рис 7.2) Добавление новой персоны

Добавим обработчик события нажатия на кнопку Ok. Его определение приведено ниже.

clauses
    onOkClick(_Source) = button::defaultAction:-
        Person = infoPage:getNewData(),
        !,
        familyForm:addPerson(Person).
    onOkClick(_Source) = button::defaultAction.

После нажатия на кнопку Ok, создается объект класса person, в который записывается информация, считанная с первой страницы вкладок. Затем имя нового члена семьи сразу отображается в списке окна familyForm.

В интерфейс infoPage cледует добавить объявление предиката getNewData.

predicates
    getNewData: () -> person determ.

Ниже приведено определение этого предиката. Новый объект класса person создается только в том случае, если заполнены поля "Имя" и "Фамилия".

clauses
    getNewData() = Person:-
        Name = getName(name_ctl),
        Surname = getName(surname_ctl),
        !,
        Id = db:getNewId(),
        Person = person::new(Id),
        Person:name := Name,
        Person:surname := Surname,
        Person:sex := getSex(),
        Person:idFather := getParent(father_ctl, Id),
        Person:idMother := getParent(mother_ctl, Id),
        Person:spouses := getSpouses(),
        Person:pictures := pictures,
        Person:status := "добавление",
        db:personList := [Person | db:personList].
    getNewData() = _:-
        vpiCommonDialogs::error("Поля apos;Имяapos; и apos;Фамилияapos; "
            "обязательны для заполнения"),
        fail.
        
predicates
    getName: (editControl) -> string determ.
clauses
    getName(Ctl) = Name:-
        Name = string::trim(Ctl:getText()),
        Name <> "".
        
predicates
    getId: (string Item) -> unsigned Id determ.
clauses
    getId(Item) = Id:-
        string::frontToken(Item, Tok, _),
        Id = tryToTerm(unsigned, Tok).
        
predicates
    getSex: () -> string.
clauses
    getSex() = getSex(female_ctl:getRadioState()).
    
predicates
    getParent: (listButton, unsigned IdPerson) -> unsigned IdParent.
clauses
    getParent(Ctl, Id) = IdParent:-
        Index = Ctl:tryGetSelectedIndex(),
        Item = Ctl:getAt(Index),
        IdParent = getId(Item),
        Parent = db:getPerson(IdParent),
        !,
        Parent:children := [Id | Parent:children].
    getParent(_Ctl, _Id) = 0.
    
predicates
    getSpouses: () -> unsigned* IdList.
clauses
    getSpouses() =  [Id || Item in L, Id = getId(Item)]:-
        L = spouses_ctl:getSelectedItems().

Объявим предикат addPerson в интерфейсе familyForm.

predicates
    addPerson: (person).

Ниже приведено определение предиката addPerson в имплементации класса familyForm.

clauses
    addPerson(Person):-
        listbox_ctl:addAt(-1, legend(Person)). 

Первым аргументом предиката addAt/2 является индекс элемента списка. Если вместо него указывается значение (– 1), то элемент либо добавляется в конец списка, в случае если его элементы не сортируются автоматически (автоматическая сортировка устанавливается по умолчанию — см. таблицу свойств для списка), либо он вставляется в список так, чтобы тот оставался упорядоченным.

Запись в базу данных. Удаление сведений

Информация о новом члене семьи не записывается в базу данных автоматически. Операции записи и удаления этой информации выполняются с помощью кнопок окна familyForm.

Запись и удаление информации

Для того чтобы записать сведения о члене семьи в базу данных, следует выделить его имя в списке окна familyForm и затем нажать на кнопку "Сохранить". Если он имел статус "добавление", то информация о нем запишется в базу данных. Удаление информации о члене семьи, как уже записанной в базу данных, так и еще не записанной в нее, выполняется при нажатии на кнопку "Удалить". Добавим обработчики событий нажатия на эти кнопки.

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

clauses
    onSaveClick(_Source) = button::defaultAction:-
        Person = getSelectedPerson(),
        "добавление" = Person:status,
        !,
        db:savePerson(Person),
        status_ctl:setText(Person:status).
    onSaveClick(_Source) = button::defaultAction.

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

clauses
    onDelClick(_Source) = button::defaultAction:-
        Person = getSelectedPerson(),
        db:deletePerson(Person),
        Index = listbox_ctl:tryGetSelectedIndex(),
        !,
        listbox_ctl:delete(Index),
        status_ctl:setText(""),
        pictControl_ctl:clear(),
        view_ctl:setEnabled(false),
        del_ctl:setEnabled(false).
    onDelClick(_Source) = button::defaultAction.

Предикаты savePerson/1 и deletePerson/1 следует объявить в интерфейсе dbrel.

predicates
    savePerson: (person).
    deletePerson: (person).

Ниже приведено определение этих предикатов в имплементации класса dbrel. Предикат savePerson записывает информацию в базу данных. Предикат deletePerson удаляет информацию из базы данных, а также из объектов класса person.

clauses
    savePerson(Person):-
        assert(person(Person:id, Person:name, Person:surname, 
            Person:sex, Person:idfather, Person:idmother)),
        list::forAll(Person:spouses, {
                (I):- getSpouses(Person, I, IdH, IdW),
                    assert(spouse(IdH, IdW))}),
        list::forAll(Person:pictures, 
            {(BmpFile):- assert(pict(Person:id, BmpFile))}),
        list::forAll(Person:descriptions, 
            {(TxtFile):- assert(descr(Person:id, TxtFile))}),
        Person:status := "просмотр".
        
predicates
    getSpouses: (person, unsigned, unsigned [out], unsigned [out]).
clauses
    getSpouses(Person, IdW, Person:id, IdW):-
        male = Person:sex,
        !.
    getSpouses(Person, IdH, IdH, Person:id).
    
clauses
    deletePerson(Person):-
        removeFromDb(Person),
        removeFromPersonList(Person).
        
predicates
    removeFromDb: (person).
clauses
    removeFromDb(Person):-
        "добавление" = Person:status,
        !.
    removeFromDb(Person):-
        retractAll(person(Person:id, _, _, _, _, _)),
        retractAll(spouse(Person:id, _)),
        retractAll(spouse(_, Person:id)),
        retractAll(pict(Person:id, _)),
        retractAll(descr(Person:id, _)).
        
predicates
    removeFromPersonList: (person).
clauses
    removeFromPersonList(Person):-
        personList := list::remove(personList, Person),
        P in personList,
            P:spouses:= list::remove(P:spouses, Person:id),
            P:children := list::remove(P:children, Person:id),
            if P:idfather = Person:id then P:idfather := 0 end if,
            if P:idmother = Person:id then P:idmother := 0 end if,
        fail.
    removeFromPersonList(_Person).

Сохранение изменений в базе данных

Текущее состояние базы данных не сохраняется автоматически в файле. Для этого пользователь должен ответить положительно на вопрос программы о сохранении изменений в базе данных (см. ниже). Добавим для формы familyForm обработчик событий DestroyListener. Его определение приведено ниже.

predicates
    onDestroy : window::destroyListener.
clauses
    onDestroy(_Source):-
        0 = vpiCommonDialogs::ask("Сохранение изменений",
            "Сохранить изменения в базе данных?", ["Да", "Нет"]),
        !,
        db:copy(),
        db:save(),
        stdio::writef("Изменения сохранены в базе данных.\n").
    onDestroy(_Source).

После закрытия окна ).

(рис 7.3) Диалоговое окно Ask

Если пользователь нажимает кнопку "Да", то создается новый файл, в который копируется содержимое файла, хранящего факты базы данных до ее загрузки. Затем новое состояние базы данных сохраняется в старом файле. Предикат ask/3 возвращает номер выбранной кнопки. Кнопка "Да" имеет номер 0, кнопка "Нет" — номер 1 (см. код).

Предикаты copy и save следует объявить в интерфейсе dbrel.

predicates
    copy: ().
    save: ().

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

clauses
    copy():-
        Time = time::new(),
        S = Time:formatDateTime("ddMMyy", "HHmmss"),
        Name = fileName::getName(filename),
        NewName = string::concat(Name, S),
        NewFileName = fileName::setExtension(NewName, "dbp"),
        file::copy(filename, NewFileName).
        
    save():-
        file::save(filename, rel).

Имя файла, в который копируется старое состояние базы данных, генерируется автоматически с помощью приписывания к имени файла, в котором хранится база данных, текущих даты и времени. Ему присваивается расширение dbp.

Упражнения

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

7.2. Добавьте режим редактирования сведений о персоне. Страница информации формы tabForm должна открываться так же, как в режиме добавления, но поля, содержащие имя, фамилию, пол и изображения должны быть уже заполнены. В списках родителей должны быть выделены строки с именами родителей персоны или пустые строки, если сведения о родителях отсутствуют в базу данных. В списке супругов должны быть выделены имена супругов. После нажатия кнопки Ok, обновленная информация о персоне должна быть записана в объект класса person.

7.3. Измените определение обработчика событий нажатия на кнопку "Сохранить" так, чтобы в режиме редактирования персоны (см. упр. 7.2) из базы данных была удалена старая информация об этом человеке и была записана новая информация. Добавьте соответствующие предикаты в класс dbrel.

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