Если мы хотим редактировать данные,
а не только просматривать

Редактируется, как правило, одна таблица, остальные же таблицы в лучшем случае поставляют для нее данные в виде ссылок.

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

Меняем :

qryPersons -> SQL:=
 a.id, /* ключевой столбец */
 a.feature_id,
 a.occupation_id,
 a.country_id,
 a.descr,
 a.sexual_potention ,
 a.photo,
 b.descr as country,
 c.descr as occupation,
 d.descr as feature,
 a.if_happy,
 a.dateofbirth

from persons a left outer join
 countries b on a.country_id=b.id left outer join
 occupations c on a.occupation_id=c.id left outer join
 features d on a.feature_id=d.id
order by a.id;

Щелкаем на "Test" -> должно быть "ОК" .

Итого - добавлены поля:

a.id ключ редактируемой таблицы ( persons )
a.feature_id ссылка на таблицу "features"
a.occupation_id ссылка на таблицу "occupations"
a.country_id ссылка на таблицу "countries"

Эти поля - целочисленные ( integer ), ибо почти 100% именно такими являются номера строк таблиц.

Отображать будем те же поля, что и ранее:

Непосредственно редактироваться не будут только поля :

Видоизменение программы просмотра под возможное редактирование

Выключаем запрос, так как меняется схема данных :

qryPersons.active:= false

и назначаем их persistent-полями типа "1" (ненастраиваемыми):

QryPersons->controller->fields.count->[...] -> открывается диалог, в котором - по очереди из незатененных ( доступных ) полей справа, выбираем "id" и "photo" и жмем кнопку со стрелкой влево (переносим поля из автосоздаваемых ( “fielddefs” ) в разряд постоянных – “fields” ), и закрываем диалог.

Остальные поля будут учавствовать в отображении и редактировании, поэтому для облегчения жизни ( чтобы работать с ними не в коде, а через дизайнер ) - сделаем их persistent-полями типа "2" ( визуально-настраиваемыми ) :

Роняем эти поля из палитры DBF на “grdPerson” , под названиями состветствующих столбцов таблицы :

Тип в DBF-палитре
Свойство "FieldName"
Свойство "name"
Поле в запросе ( для справки )
Tmsedtringfield
feature
fldFeature
d.descr as feature
Tmsedtringfield
occupation
fldOccupation
c.descr as occupation
Tmsedtringfield
country
fldCountry
b.descr as country
Tmsedtringfield
descr
fldName
a.descr
Tmsefloatfield
sexual_potention
fldSexPotention
a.sexual_potention
Tmsebooleanfield
if_happy
fldHappy
a.if_happy
Tmsedatefield
dateofbirth
fldDateOfBirth
a.dateofbirth
Tmselongintfield
feature_id
fldFeatureId
a.feature_id
Tmselongintfield
occupation_id
fldOccupationId
a.occupation_id
Tmselongintfield
country_id
fldCountryId
a.country_id

Свойство "Dataset:= qryPersons" у всех этих полей, поля нужно настраивать поочередно.

Также:

Примечания :

Теперь можно включить запрос :

qryPersons.active:= true

чтобы увидеть появившиеся данные в таблице. Если теперь запустить программу (через F9), то увидим, что она пока ничем не отличается от предыдущей прораммы просмотра. Но зато мы сейчас включили в программу точки редактирования - модифицировали запрос и создали persistent-поля.

Реализация редактирования

Каждую из записей можно :

  1. редактировать
  2. добавлять
  3. удалять

Инициировать эти действия будем :

  1. специальными кпонками
  2. popup-menu
  3. клавиатурными комбинациями
  4. Контексными клавишами

Раз у нас три способа инициирования, значит - опять понадобятся "taction". Уроним на таблицу три компонента "GUI -> taction" :

Name
onexecute
actEdit
editformshow
actAdd
addformshow
actDelete
deleterecord

Для реализации способа (1) - увеличим место внизу формы, уроним туда уроним три кнопки (Widget -> tbutton ), и увяжем их с "taction" :

Name
Action

Caption

btnEdit
actEdit
&Edit..
btnAdd
actAdd
&Add..
btnDelete
actDelete
&Delete..

Свойство 'anchor" всех трех кнопок установим [an_bottom:= true, an_right:= true, остальные - false], чтобы эти кнопки держались первоначального зазора с нижним краем формы при изменении ее размера.

Назначим новым кнопкам ранее принятый "кнопочный" стиль :

удерживая клавишу "Ctrl", выбираем все три кнопки "btnEdit", "btnAdd" и "btnDelete", далее идем в редактор свойств :

frame->[...]

видим, что у всех кнопок появился заметный бордюрчик, плюс все они кнопки окрасились с один цвет (светло-желтый, назначенный кнопкам "закрыть/отменить/завершить" ). Исправим это, приняв, что :

btnEdit :

  • template
    • colorclient:= cl_ltgreen

    выглядит темноватым, поэтому выберем не из палитры, а через диалог :

    • colorclient -> [...]
      • red:= 200
      • green:= 255
      • blue:= 200

btnAdd :

  • template
    • colorclient:= cl_ltblue

    тоже выглядит темноватым, поэтому выберем не из палитры, а через диалог :

    • colorclient -> [...]
      • red:= 200
      • green:= 200
      • blue:= 255

btnDelete :

Примечания :

Для реализации способа (2) - уроним на таблицу компонент :

Widget-> tpopupmenu

и скажем таблице "при клике правой кнопкой - вызывай это меню" :

grdPersons.popupmenu:= pupPersons

Для реализации способа (3) - просто назначим свойство "shortcut" объектов "taction" :

Способ (4) у нас уже реализован - см. буквы после "&" в свойствах "caption".

Специальная форма для редактирования и добавления

А теперь, по порядку: Так как имеющаяся таблица позволяет только просматривать данные, то для их изменения будем использовать отдельную форму = редактор записи.

File -> New -> Form -> Simple Form : сохранить файл под именен "editform" -> создается "editfo" типа "teditfo". "Editfo" нельзя переименовывать.

В нижнюю часть этой формы роняем две кнопки "Widgets->tbutton" :

Левая кнопка:

Правая кнопка:

Раз появились новые кнопки - значит, пора им назначить наш "кнопочный" стиль. Кнопка "btnCance", как кнопка отмены должна иметь светло-желтый цвет ( по-умолчанию для кнопок ), а кнопка "btnOk" ( кнопка готовности/подтверждения ) - пусть будет светло-цианового цвета :

btnOk :

btnCancel :

Примечания :

Непустые значения "modalresult" говорят о том, что щелчок на такой кнопке закрывает форму, после чего функция показа формы <Форма>.show(true) ( будет использована ниже по тексту ) вернет соответствующее значение "mr_ok" или "mr_cancel", которое затем можно использовать для принятия решения.

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

Эта форма должна показываться по команде на редактирование ( или добавление ) - в методах объектов "actEdit" и "actAdd" , находящихся на основной форме "mainfo":

Изменения в файле "main.pas"

procedure tmainfo.editformshow(const sender: TObject);
begin
 try
  with qryPersons do begin
   edit; // переводим "qryPerson" в режим редактирования текущей записи
   application.createform(teditfo,editfo); // модальный показ "editfo"
   // показать, что именно редактирование, а не добавление
   editfo.caption:= ' Editing a person => '+ fldName.asstring;

   case editfo.show(true) of
     mr_ok: begin // "editfo" закрыта кнопкой с кодом "mr_ok"
     // здесь будет код записи в БД + первыборки
     end else begin // "editfo" закрыта кнопкой с кодом, отличным от "mr_ok"
      cancel; // отказаться от подготовленных к записи в БД изменений
     end;
   end;

  end;
 finally
  editfo.free; // при любом исходе - удалить "editfo" из памяти
 end;
end;

// добавить метод через двойной клик в поле "actAdd->onexecute"
// Процедура добавления новой записи :
//
procedure tmainfo.addformshow(const sender: TObject);
begin

 try
  with qryPersons do begin
// вставить в редактируемую таблицу пустую строку,
// и тут же войти в режим ее редактирования
   append;
   application.createform(teditfo,editfo);
   editfo.caption:= ' Adding a new person';

   case editfo.show(true) of
    mr_ok: begin
     // здесь будет код записи в БД + первыборки
     end else begin
      cancel; // отказаться от добавления
     end;    
    end;

   end;
  finally
   editfo.free;
  end;

end;

 

Как видно, эти две процедуры отличаются только :

Удаление текущей записи не требует никаких дополнительных форм - достаточно обойтись выводом диалога для подтверждения решения на удаление. Поэтому - еще изменения в файле "main.pas" :

implementation

uses
  main_mfm,
  editform, // для доступа к "editfo"
  msewidgets // чтобы можно было использовать диалоги "showmessage", "askyesno", ..
;

...( процедуры редактирования и добавления )

procedure tmainfo.deleteprson(const sender: TObject);
begin
 if askyesno('Are you a nut ???','Deletion request',mr_no,200) then begin
  with qryPersons do begin
   delete;
  end;
 end;
end;

Способы непосредственного редактирования текущей записи

Сервис редактирования полей текущей записи базовой таблицы ("persons") обеспечивается следующими компонентами палитры "DB" :

  1. текcтовые поля ( вроде "descr" ) -> через компонент "tdbstringedit"
  2. логические поля ( вроде "if_happy" ) -> через компоненты "tdbbooleantextedit" л"tdbbooleanedit"
  3. целочисленные поля ( нет в нашей задаче ) -> через компонент "tdbintegeredit"
  4. поля для значений с плавающей точкой ( вроде "sexual_potention" ) -> через компонент "tdbstringedit"
  5. ссылки на строки в других таблицах ( вроде "country_id" ) -> "tdbenumeditlb" и "tenumeditlb"
  6. .. другие пока не рассматриваем ..

Примечание:

Когда следует использовать числовые поля вместо строковых ( ведь и числа, и буквы, по своей природе - символы ) ? Ответ - тогда, когда их значения могут быть использованы в арифметических выражениях.

Перечисленные компоненты напрямую меняют значение соответствующего поля строки БД. Но бывает и другая потребность - загрузить список значений из БД, но, после выбора в списке - не записывать выбранное значение обратно в БД, а использовать по другому - в процедуре фильтрации, и т.п. Пример такого компонента - "tenumeditlb". Это компонент у нас будет использован для сужения поиска страны по выбранному атрибуту "континент".

Компоненты "tdbenumeditlb" и "tenumeditlb" (как и все с именами типа "t..(datatype)lb" ) нуждаются в специальном компоненте для хранения списков значений - так называемых "lookup buffers". Если такой "buffer" заполняется из БД - это компонент "tdblookupbuffer", а если вручную - "tlookupbuffer" .
Очень важный сервис этих "буферов" - синхронизированность нескольких столбцов данных, даже разного типа, что позволяет выбрать, например, текстовый столбец для заполнения списка текстовых значений ( названий стран и т.п.), а в момент выбора в этом списке - зафиксировать соответствующий выбранному названию станы ключ строки в таблице стран, и т.п.

Data Module

Теперь надо решить - откуда брать данные для заполнения этих "буферов" ( список атрибутов ), Неплохо бы также предусмотреть возможность редактирования содержимого этих списков - для каждого из которых потребуется своя отдельная форма.
Итого, получается, что одни и те данные могут потребоваться в разных местах программы, а именно - в виде списков для редактирования данных о персоне (в таблице "persons" ), и в виде таблиц для редактирования самих списков. Поэтому эти списочные данные :

  1. удобнее сделать глобально-доступными;
  2. выбирать нужно один раз и в одном месте, чтобы изменения в результате редактирования тут же стали бы видны в местах просмотра.

Для этих целей идеально подходят так называемые "data modules".

File -> New -> Form -> Data Module : сохранить файл под именен "refsdatamodule" -> создается "refsdatamo" типа "trefsdatamo". "refdatamo" нельзя переименовывать.

Далее - набросать на форму дата-модуля следующие компоненты с палитры "DB" :

Список планет

tmsesqlquery (1) :

tmsedatasource (1) :

tdblookupbuffer (1) :

 
Список континентов

tmsesqlquery (2) :

 

Следующий код - для гарантии того, что данные о планетах будут доступны перед выборкой данных о континентах ( описания континентов содержат сссылки на планеты ) :

Установить "refsdatamodule.pas-> qryContinents -> beforeopen:= qrycontinentsbeforeopen", и описать код :

procedure trefsdatamo.qrycontinentsbeforeopen(DataSet: TDataSet);
   begin qryPlanets.active:= true;
end;

Это код нужен в том случае, если список планет может измениться во время использования списка континентов (предусмотрено редактирование планет).

tmsedatasource (2) :

 

tdblookupbuffer (2) :

 

Несколько слов о "lbufContinents" :

 
Список стран

tmsesqlquery (3) :

 

Следующий код - для гарантии того, что данные о континентах будут доступны перед выборкой данных о странах ( описания стран содержат сссылки на континенты ) :

Установить "refsdatamodule.pas-> qryCountries -> beforeopen:= qrycountriesbeforeopen", и описать код :

procedure trefsdatamo.qrycountriesbeforeopen(DataSet: TDataSet);
   begin qryContinents.active:= true;
end;

Этот код нужен в том случае, если список континентов может измениться во время использования списка стран ( если предусмотрено редактирование планет ).

Используемый как параметр тип "TDataSet" определен в файле "db.pp" из комплекта FPC, поэтому этот файл нужно добавить в "refsdatamodule.pas" -> interface -> uses :

interface
  uses
  msegui,mseclasses,mseforms,msesqldb,msedb,mselookupbuffer,
  db
;

Почему "interface" часть ? Потому что "refsdatamodule -> qryCountries -> beforeopen" добавила объявление "qrycountriesbeforeopen" сперва в interface-часть refdatamodule.pas

tmsedatasource (3) :

 

tdblookupbuffer (3) :

 

Несколько слов о "lbufCountries" :

 
Список профессий

tmsesqlquery (4) :

tmsedatasource (4) :

 

tdblookupbuffer (4) :

 

Список черт характера

tmsesqlquery (5) :

 

tmsedatasource (5) :

 

tdblookupbuffer (5) :

 

Примечания :

pfInKey -> нужно для автоматической записи ВСЕЙ отредактированной строки в БД; данный параметр говорит "данное поле несет в себе ключ ( или один из ключей строки), то есть однозначно ее идентифицирует" ; смысл - чтобы не испортить другую строку, нужно всегда знть, где находишься

pfInUpdate -> нужно для записи нового значения данного поля в момент автоматической записи всей строки, иначе изменения при редактировании будут проигнорированы

UsePrimaryKeyAsKey:=False -> приказ не пытаться автоматически определить поле с уникальным ключом, ведь мы сами указываем ключевое поле, см. pfInKey

integerfields.* -> поля с ключами и ссылками на ключи

textfields.* -> поля с текстовыми значениями, связанные с ключевыми полями

"... order by id" во всех запросах - чтобы гарантировать прежний порядок записей после редактирования ( иначе сервер БД может дать порядок по времени изменения )

 

Готовый "refsdatamo" выглядит следующим образом :

 

Теперь нужно решить, в какой момент создавать этот "datamodule" во время работы готового приложения, иначе размещенные на нем компоненты будут недоступны. Налицо два варианта :

  1. сразу при запуске приложения - проще, но может без необходимости отъесть память, если не планируется никакого редактирования
  2. по мере необходимости - при редактировании данных, использующем его компоненты, при этом подразумевается, что этот "datamodule" будет удаляться сразу, как только станет ненужным.

Мы будем использовать вариант (2) как более "продвинутый".

Сразу видно, что "datamodule" должен создаваться для целей редактирования/добавления с использованием формы "editfo", то есть непосредственно перед созданием "editfo"

Установить "editform.pas->editfo->oncreate:= editfocreated", и описать код :

procedure teditfo.editfocreated(const sender: TObject);
begin
  application.createdatamodule(trefsdatamo, refsdatamo);
end;

и удаляться из памяти сразу вслед за освобождением формы :

Установить "editform.pas->editfo->ondestroy:= editfodestroyed", и описать код :

procedure teditfo.editfodestroyed(const sender: TObject);
begin
  refsdatamo.free;
end;

Не забудьте включить "refsdatamodule" в "editform.pas" -> implementation -> uses :

uses
  editform_mfm,
  refsdatamodule
;

Если все "qry(*).Active:=true" отработали нормально - возвращаемся на "editfo".

Компоненты для редактирования полей

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

tdbstringedit (1)

  • Name:= seName
  • frame
    • caption = 'Name'
    • captionpos = cp_left
    • captiondist = 15
  • datasource = mainfo.dsPersons
  • datafield = 'descr'

tenumeditlb (1)

tenumeditlb (2)

 

tdbenumeditlb (1)

  • Name:= cbCountries
  • frame
    • caption = 'Country'
    • captionpos = cp_left
    • captiondist = 15
  • ondataentered = countryentered
  • datafield = 'country_id'
  • datasource = mainfo.dsPersons
  • dropdown
    • lookupbuffer = refsdatamo.lbufCountries
    • optionslb = [olb_copyitems]
    • keyfieldno: = 0 // refsdatamo.lbufCountries's integerfields[0]
    • cols
      • item 0
        • fieldno:= 0 // refsdatamo.lbufCountries's textfields[0]
    • onfilter = countriesfilter

tdbenumeditlb (2)

tdbenumeditlb (3)

 

Примечания :

 

tdbrealedit (1)

Примечания :

  • "formatdisp" ( показ ) и "formatedit" ( подсветка ввода ) на самом деле - "##.##", что, согласно синтаксису функции "Format" , означает "число с плавающей точкой не более более "2-х цифр до" и "2-х цифр после" десятичного разделителя, с принудительным округлением". То есть количество "#" должно быть удвоено - потому что одинарный символ "#" в языке "Pascal" начинает код символа ( вроде кода #32, обозначающего пробел ) .

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

Widget->tlabel (1) :

  • Name:= lblSexPotentionHint
  • Caption:= (0..100) %

 

 

tdbcalendardatetimedit (1)

Примечания :

  • "cdeDateOfBirth" позволяет как вводтить дату напрямую, так и выбирать из выпадающего календаря
  • "formatdisp" и "formatedit" оставляем пуcтыми, чтобы опять-таки при внутренних преобразованиях использовался "ShortDateFormat" (см. выше )
  • значения дат допускают неточное ( сокращенное ) соответствие формату - и день, и месяц, и год могут быть введены (1..2) цифрами, для чего во время внутренного преобразования будут применены некоторые допущения, вплоть до очень удобных упрощений вроде :
    • 20 -> 20-е число текущего месяца
    • 20.1 -> 20-е января текущего года
    • 20.1.6 -> 20 января 2006 года
  • вследствие особенностей внутреннего представления дат в "FreePascal" версий до 2.4.х, минимальное корректное значение даты равно 30.12.1899;
    начиная с версий 2.4.0-rc1, минимальная дата ограничивается не вынужденным лимитом, а автопереводом сокращенного формата ( см. выше, например - к значению года меньше 100 автоматически прибавляется 2000 ) , и равна 01.01.0100
  • если допускается пустое значение даты - оставьте свойство "min" также пустым
  • свойство "мах" не должно оставаться пустым - иначе любая дата будет считаться неверной
  • пробел в поле ввода даты, после нажатия "Enter" ,превращается в текущую дату
  • для вывода календаря без помощи мыши - используейте комбинацию "Shift+Enter "

Чтобы пользователь знал правильный формат ввода даты ( если старое значение пустое и потому негде подсмотреть ) , покажем ему этот формат.
Уроним справа от cdeDateOfBirth :

Widget->tlabel (2) :

  • Name:= lblDateFormatHint

Примечания :

  • Так как, в отличие от "lblSexPotentionHint", невозможно прописать ( run-time ) значение константы "ShortDateFormat" в "lblDateFormatHint.caption" используя редактор свойств ( design-time ) , придется это сделать в коде при открытии формы "editfo" - будет сделано позже, вместе с другими операциями

 


tdbbooleantextedit (1)

Несколько слов о "beHappy" :

  • это - особый тип компонента для отображений значений типа "да/нет" плюс пустое значение для "неизвестно"; в нашем случае такие неизвестные значения допускаются, поэтому использование другого компонента - с только 2-мя состояниями ( "tdbboleanedit" ) было бы некорректно
  • свойства text_(false/true) позволяют переопределить текст значений по своем усмотрению
    • есть еще один способ подмены текста булевых( логических ) значений в ячейках таблиц типа "tgbstringgrid", как наша таблица "grdPersons" - он контроллируется настройками поля БД, с которым связан данный компонент, в данном случае "mainfo.dsPersons->if_happy":
      • переходим на форму "mainfo" (файл "main.pas" ) , выбираем это поле ( "fldHappy" ) , и прописываем, через ";", в "DisplayValues" более понятную нам пару - путь даже "Еще как :);Увы :(" , чтобы этот ект выглядел одинаково и на "grdPersons", и на форме редактирования "editfo" ;
        может быть, придется чуть увеличить размер последнего столбца "grdPersons"
        [ для "неизвестно" такой подмены не предусмотрено ]

 

Примечания :

Общий вид формы "editfo" с компонентами :

Теперь можно запустить нашу программу и протестировать форму "edotfo", она будет выглядеть следующим образом :

Теперь, если быть внимательным, можно заметить одну особенность - изменения артибутов "Name", "Sex.potention" и "Happy ?" тут же отражаются также и в таблице просмотра ( "grdPersons" ) , в отличие от остальных атрибутов. Почему ? Потому что эти значения этих атрибутов напрямую связаны с полями выборки, формирующей "grdPersons" - "qryPersons", и поэтому редактируются также напрямую. Атрибуты же "Country" & "Occupation" & "Feature" редактируются через замену ссылок на ключи других таблиц, поэтому, делая выбор в списке "Country", мы меняем значение поля "country_id" выборки "qryPersons", но не поля "country", отображаемого в "grdPersons". Чтобы изменить "country" синхронно с "country_id", нужен код :

editform->cbCountries ->ondataentered:= countryentered :

procedure teditfo.countryentered(const sender: TObject);
begin
   mainfo.fldCountry.value:= cbCountries.text;
end;

editform->cbOccupations ->ondataentered:= occupationentered :

procedure teditfo.occupationentered(const sender: TObject);
begin
   mainfo.fldOccupation.value:= cbOccupations.text;
end;

editform->cbFeatures ->ondataentered:= featureentered :

procedure teditfo.featureentered(const sender: TObject);
begin
   mainfo.fldFeature.value:= cbFeatures.text;
end;

То есть напрямую корректируется выборка "qryPersons", а уже по ней перерисовывается связанная таблица "grdPersons".

Не забудьте вставить "main" в секцию "uses" файла "editform.pas" , чтобы видеть "main" :

implementation

uses
  editform_mfm,
  refsdatamodule,
  main
;

Примечание:

Далее, мы видим списки "Planets" & "Continents", выбор в которых нигде не отражается. И вообще - зачем они нужны ? В качестве фильтров - чтобы сузить выбор стран, ведь стран в галактике может быть необозримое множество !
Ни в таблице "persons", ни в самой выборке "qryPesrons" нет данных ни о планетах, ни о континентах - только ключи таблицы стран. Зато в таблице стран "countries" есть ссылки на ключи таблицы континентов "continents", а таблице континетов - есть ссылки на на ключи таблицы планет "planets". Вот и используем эту цепочку ключей :

editform->cbPlanets ->onsetvalue:= planetchanged :

procedure teditfo.planetchanged(const sender:TObject; var avalue:Integer;var accept:Boolean);
begin
// если выбрана реально другая планета
 if avalue <> (sender as tenumeditlb).value then begin
  cbContinents.value:= -1; // отменить выбор в списке континентов

// и страну теперь нужно выбирать по-новой
  mainfo.fldCountry.clear; // автоматически отменит выбор в списке стран
// (сервис компонента TDB ENUMEDITLB)
  mainfo.fldCountryId.clear;
 end;
end;

editform->cbPlanets ->onsetvalue:= planetchanged :

procedure teditfo.continentchanged( const sender:TObject; var avalue:Integer;var accept:Boolean );
begin
// если выбран реально другой континент
 if avalue <> (sender as tenumeditlb).value then begin
  mainfo.fldCountry.clear; // страну теперь нужно выбирать по-новой
  mainfo.fldCountryId.clear;
 end;
end;

Примечания :

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

procedure teditfo.editfocreate(const sender: TObject);
var
 int1: integer;
begin
 application.createdatamodule(trefsdatamo, refsdatamo);

// начальная синхронизация континента по стране
 if refsdatamo.lbufCountries.findphys(0,integer(cbCountries.value),int1) then begin
  cbContinents.value:= refsdatamo.lbufCountries.integervaluephys(1,int1);
 end;

// и синхронизация планеты по синхронизированному континенту
 if refsdatamo.lbufContinents.findphys(0,integer(cbContinents.value),int1) then begin
  cbPlanets.value:= refsdatamo.lbufContinents.integervaluephys(1,int1);
 end;
// показать правильный формат даты
 lblDateFormatHint.caption:= '( ' + uppercase(ShortDateFormat) + ' )';

end;

Примечания :

Теперь непосредственно сама фильтрация. Выбирая планету и/или континент, мы будем сужать список стран в списке для выбора. Для фильтрации удобно применить свойство "onfilter" компонентов с выпадающими списками :

editform => cbContinents -> dropdown -> onfilter:= continentsfilter :

procedure teditfo.continentsfilter(const sender: tcustomlookupbuffer;
  const physindex: Integer; var valid: Boolean);
begin

 valid:=
  (cbPlanets.value = -1)
 or
  (sender.integervalue[1,physindex] = cbPlanets.value);

end;

editform => cbCountries -> dropdown -> onfilter:= countriesfilter :

procedure teditfo.countriesfilter(const sender: tcustomlookupbuffer;
  const physindex: Integer; var valid: Boolean);
begin

 if cbPlanets.value = -1 then begin
  if cbContinents.value = -1 then begin
   valid:= true;
  end else begin
   valid:= sender.integervalue[1,physindex] = cbContinents.value;
  end;
 end else begin
  if cbContinents.value = -1 then begin
   valid:= false;
  end else begin
   valid:= sender.integervalue[1,physindex] = cbContinents.value;
  end;
 end;

end;

Примечания :

Конечно, не забудьте добавить модуль "mselookupbuffer", описывающий "tcustomlookupbuffer", в "uses"-секцию "editform" :

interface

uses
 msegui,mseclasses,mseforms,msesimplewidgets,msedbedit,
 mselookupbuffer;

Теперь, запустив приложение, мы можем редактировать, добавлять и удалять записи из таблицы "persons".

Важно предотвратить попытки записи в БД заведомо некорректных значений, иначе программа может аварийно завершиться. Пример - поля БД с атрибутами "NOT NULL", в случае нашего проекта - "descr"-поля всех таблиц. Поэтому запретим оставлять пустые значения в компонентах, связанных с полями "descr" :

editform->seName ->optionsedit -> oe_notnull:= true

Теперь, при закрытии формы "editfo", этот компонент будет проверен на предмет пустой строки.

Примечания:

Далее, атрибут "Sex.potention" измеряется в процентах - потому должен быть в границах (0..100) % . Также покажем, какой десятичный разделитель должен использоваться в текущей локали. Обеспечим это :

editform ->reSexPotention->oncheckalue:= sexpotentioncheckvalue :

procedure teditfo.sexpotentioncheckvalue(const sender: tdataedit;
  const quiet: Boolean; var accept: Boolean);
var
 f1: double;
begin
 try
  // сделать тестовое значение доступым для арифметических операциий
  // - числовым
  f1:= StrToFloat(sender.text);
  if (f1 < 0) or (f1 > 100) then begin // затем проверить диапазон
   // если выход за границы - отказать
   accept:= false;
   // и ругнуться
   showmessage('Percent of people sexual potention should be ' +
   'in range 0..100','Invalid Input',150);
  end;
 except
  on EConvertError do begin // чтобы не вылететь на неверном формате
   // пустое значение - разрешить
   if sender.text <> '' then
    // но про неверный формат - сказать
    accept:= false;
    showmessage('Percent value 0..100% step 0'+
     DecimalSeparator +
     '01 expected here','Invalid Input',150);
   end;
  end;
 end;
end;

Пояснения к коду :

Да, не забудьте подкорректировать секции "uses" файла "editform" :

interface
uses
  msegui,mseclasses,mseforms,msesimplewidgets,msedbedit,
  mselookupbuffer,
  msedataedits; // импорт "tdataedit"
// ..

implementation
uses
  editform_mfm,
  refsdatamodule,
  main,
  sysutils, // импорт "StrToFloat"
  msewidgets // импорт "ShowMessage"
;

Осталось одно "но" - перезапустив пограмму, мы убедимся, что наша работа пропала даром - опять видим "старые" данные. А все потому, что наши изменения делались не в БД, а во временном буфере редактирования. Поэтому переходим к следующему этапу.

Фиксация изменений, добавлений и удалений в БД

Добавим сюда же возможность откатить измеениния до их фиксациив БД.

Компонент "TBufDataSet" предоставляет следующий сервис по обмену между буфером редактирования и БД :

 

В нашем случае данные в таблице "grdPersons" получены из много-табличного запроса, поэтому по-любому будем использовать способ (2) .

mainfo -> qryPersons

делаем Active:=false, чтобы избежать кое-какой ругани

Уронить на "mainfo", где-нибудь под компонент "trans", с палитры "DBF" :

DBF->tmselongintfield :

  • Name:= fldPersonId
  • DataSet:= qryPersons
  • FieldName:= id

Примечание :

  • "fldPersonId" как отдельный компонент имеет смысл при многократном обращении к значению поля. Более привычный способ "qryPersons.fieldbyname('id')" здесь оказывается менее эффективным, так как подразумевает поиск поля по имени среди списка полей при каждом обращении

и возвращаем "qryPersons.Active:=true"

и описываем код "personsupdate" :

procedure tmainfo.personsupdate(
 const sender: tmsesqlquery;
 const updatekind: TUpdateKind;
 var asql: AnsiString;
 var done: Boolean);
begin
 with qryPersons do begin
  case updatekind of // анализируется тип обновления
   ukModify: begin // изменение текущей записи
    asql:= 'update persons set '+
     'descr=' + fldName.assql +
     ',country_id=' + fldCountryId.assql +
     ',feature_id=' + fldFeatureId.assql +
     ',occupation_id=' + fldOccupationId.assql +
     ',sexual_potention=' + fldSexPotention.assql +
     ',if_happy=' + fldHappy.assql +
     ',dateofbirth=' + fldDateOfBirth.assql +
     ' where id='+ fldPersonId.assql + ';';
   end;
   ukInsert: begin // добавление новой записи
    asql:= 'insert into persons (' +
     'id' +
     ',descr' +
     ',country_id' +
     ',feature_id' +
     ',occupation_id' +
     ',sexual_potention' +
     ',if_happy' +
     ',dateofbirth' +
     ') values (' +
     'nextval('+ #39 + 'person_id_seq' + #39 + ')' +
     ',' + fldName.assql +
     ',' + fldCountryId.assql +
     ',' + fldFeatureId.assql +
     ',' + fldOccupationId.assql +
     ',' + fldSexPotention.assql +
     ',' + fldHappy.assql +
     ',' + fldDateOfBirth.assql +
     ')';
     writeln(asql);
   end;
   ukDelete: begin // удаление текущей записи
    asql:= 'delete from persons where id=' + fldPersonId.assql;
   end;
  end;
 end;
end;

То есть видим формирование обычной SQL-команды. Чтобы эта команда реально выполнилась, нужно всего лишь вызвать метод "qryPersons.ApplyUpdates" - который сформирует и выполнит такие же команды последовательно для всех "нуждающихся" записей.

Но и это нет все ! Все SQL-команды, начиная с первичной выборки данных, выполняются в режиме неявной транзакции. Поэтому, чтобы зафиксировать данные в БД, нужно эту транзакцию подтвердить ( "Commit" ). Далее, после подтверждения транзакции, первичная выборка сбрасывается и связанные с ней компоненты очищаются (что 100% правильно - ведь теперь содержимое БД может не совпадать со "старыми" значениями компонентов ) . Никаких проблем - нужно просто переустановить свойство "Active:= true" у сброшенных выборок, в нашем случае - у "qryPersons".

Еще один побочный эффект - сброс так называемых закладок ( "bookmark" ) , поэтому после повторной загрузки данных номер текущей строки также будет сброшен. Поэтому, если мы хотим восстановить местонахождение после обновления данных - нужно сделать это в коде (через специальную переменную для хранения ключа текущей записи таблицы "persons", вместе с кодом сохранения и воостановления этого ключа ).

Примечание:

Добавим эту переменную в секцию "implementation" файла "main.pas"

var
 prevkey: integer ; // переменная для хранения ключа текущей строки "qryPersons"

В коде ( обновление + перевыборка ) данных делаются через добавление вызовов "ApplyUpdates", "Commit" и "Active:=true" в процедуры "editformshow", "addformshow" и "deleterecord" в файле "main", и эти процедуры принимают следующий вид :

editformshow :

procedure tmainfo.editformshow (const sender: TObject);
begin
 try
  with qryPersons do begin
   // запомнить позицию на случай отказа от изменений
   prevkey:= fldPersonId.asinteger;
    edit; // войти в режим редактирования текущей записи
    application.createform(teditfo,editfo);
    editfo.caption:= ' Editing a person => '+ fldName.asstring;
    case editfo.show(true) of
     mr_ok: begin // если форма редактирования закрыта кнопкой "Ok", то
      applyupdates; // сформировать и выполнить SQL-команду на обновление,
      trans.commit; // подтвердить эту SQL-команду,
      active:= true; // перечитать обновленное содержимое БД
      locate(prevkey, fldPersonId); // и вернуться на прежнюю позицию
     end else begin
      cancel; // отказаться от записи в БД
     end;
    end;
  end;
  finally
   editfo.free;
  end;
end;

Примечания :

  • "editfo.caption:= ' Editing a person => '+ fldName.asstring;" - показывает детали по форме ( изменение данных и по какой персоне )

 

addformshow :

procedure tmainfo.addformshow(const sender: TObject);
begin
 try
  with qryPersons do begin
   // запомнить позицию на случай отказа от изменений
   prevkey:= fldPersonId.asinteger;
   // создать новую запись, сделать ее текущей
   // и войти в режим ее радактирования
   append;
   application.createform(teditfo,editfo);
   editfo.caption:= ' Adding a new person';

   case editfo.show(true) of
    mr_ok: begin // если форма редактирования закрыта кнопкой "Ok", то
     applyupdates; // сформировать и выполнить SQL-команду на добавление,
     trans.commit; // подтвердить эту SQL-команду,
     active:= true; // перечитать обновленное содержимое БД
     last; // и встать на добавленную запись
    end else begin // если же кнопкой "Cancel", то
     cancel; // отказаться от записи в БД
     locate(prevkey, fldPersonId); // и вернуться на предыдущую позицию
    end;
   end;

  end;
 finally
  editfo.free;
 end;
end;

Примечания :

  • "editfo.caption:= ' Adding a new person';" - показывает, что данная форма - не для редактирования, а именно для добавления новой песоналии

 

deleterecord :

procedure tmainfo.deleterecord(const sender: TObject);
var
 recnum: integer;
begin
 if askyesno('Are you a nut ???','Deletion request',mr_no,200) then begin
  with qryPersons do begin
   recnum:= recno;
   delete; // пометить текущую запись на удаление
   applyupdates; // сформировать и выполнить SQL-команду на удаление,
   trans.commit; // подтвердить эту SQL-команду,
   active:= true; // перечитать обновленное содержимое БД
   if recno > 0 then
     recno:= recnum - 1; // и встать на запись, последннюю перед удаленной
 end;
end;

Обратите внимание, что в этой процедуре вместо ключа строки ( prevkey ) используется значение "qryPersons.RecNo" - номер строки в таблице ( фиксируемый во внутренней переменной "recnum". Почему ? Потому что поле "id' после удаления записи уже не будет содержать искомого значения.


Обратите также внимание на ( необязательные ) замены, связанные с введением компонента "fldPersonId" :

Все - теперь можно запустить программу, она полностью функциональна.

Некоторые мелкие штрихи :

Все, теперь мы можем просматривать и редактировать данные о галактических персоналиях - их именах, местожительстве, чертах характера и т.д.
Но что делать, если в нашей галатике открыта новая планета, и на ней тоже обнаружены разумные существа ? Или какая-то злобная цивилизация распылила одну из наших планет на атомы ? Или некие революционеры захватили власть и первым делом переименовали свою планету ? Или в нашей галактике наконец-то изжиты некие омеризительные черты характера ?
Правильно, нужно иметь сервис по редактированию планет ( заодно - континетов и стран на этих планетах ) . Аналогично касается черт характера и родов деятельности.

Поэтому переходим на следующий этап - просмотр и редактирование справочных таблиц.