За окном прохладный ноябрь, поэтому речь в статье пойдёт не об освежающем в летнюю жару напитке, а о механизме фильтрации в проекте Data Keeper, в результате применения которого мы должны получить отфильтрованные данные. Разумеется, они будут светлыми только в том случае, если вы будете использовать светлую тему оформления приложения.
Структура данных
Нам понадобится всего два новых поля:
- cproperty.is_filter – флаг, определяющий, нужно ли использовать данное свойство для построения фильтра
- class.is_ShowFilterPanel – флаг, включающий/отключающий отображение панели фильтра.
efmCProperty
Для нового поля потребуется компонент на форме редактирования свойства класса. Не забываем добавить его в список компонентов для сохранения данных в настройке кнопки btnSave.

Скрипты
Сначала я хотел переделать процедуру efmObject_OnShow() таким образом, чтобы использовать её как для генерации формы редактирования параметров объекта, так и для генерации панели фильтрации. Очевидно, что у них много общего: запрос на выборку данных, генерация компонентов редактирования. Однако, в перспективе могут возникнуть существенные различия. Например, при фильтрации данных по дате обычно используют не одну дату, а период; для чекбокса нужно учитывать три состояния и так далее. Если поместить всю логику в одну процедуру то она станет очень сложной в отладке. Поэтому я создал PrepareFilterPanel(), которая отвечает только за панель фильтрации.
procedure PrepareFilterPanel(APanel:TdbPanel; AIDClass:integer );
// создание элементов фильтрации
// APanel - панель для размещения элементов
// AIDClass - класс, для которого строится фильтр
var
tmpForm: TForm;
tmpSQL: string;
tmpDataSet: TDataSet;
tmpLineTop: integer;
tmpCount: integer;
tmpLabel: TdbLabel;
tmpEdit: TdbEdit;
tmpPanel: TdbPanel;
tmpImageOff: TdbImage;
tmpImageOn: TdbImage;
tmpImageNull: TdbImage;
tmpButton: TdbButton;
tmpComboBox: TdbComboBox;
tmpParentList:string;
tmpChildList:string;
tmpParent: TdbPanel;
i: integer;
tmpControlID: integer;
tmpDataEdit : TdbDateTimePicker;
begin
if APanel.Tag <> AIDClass then
begin
APanel.Tag := AIDClass;
CForm(APanel,tmpForm);
tmpParent := APanel;
// скрыть все компоненты
for i := tmpParent.ControlCount - 1 downto 0 do
begin
APanel.Controls[i].Visible := False;
end;
tmpCount := 0;
// создать компоненты, которые нужны для фильтрации
GetChildAndParent( AIDClass, tmpParentList, tmpChildList ); // список родительских узлов
tmpSQL := 'SELECT cproperty.is_detail, cproperty.id_ptype, cproperty.id, cproperty.name, cproperty.is_name, class.name as cname, class.id_uicontrol, class.id as ClassID '+
'FROM cproperty LEFT JOIN class ON class.id = cproperty.id_class1 WHERE id_class in ('+tmpParentList+') and cproperty.is_filter = 1 ORDER BY orderNum ';
tmpLineTop := 0;
SQLQuery(tmpSQL,tmpDataSet);
while not tmpDataSet.EOF do
begin
tmpControlID := tmpDataSet.FieldByName('id_uicontrol').asInteger;
// метка
FindC(tmpForm,'labData_'+intToStr(tmpCount),tmpLabel,False);
if tmpLabel = nil then
tmpLabel := TdbLabel.Create( tmpForm );
with tmpLabel do
begin
visible := True;
parent := tmpParent;
Font.Size := ScaleFontSize;
top := tmpLineTop;
left := Scale(8);
name := 'labData_'+intToStr(tmpCount);
Caption := tmpDataSet.FieldByName('name').asString;
end;
// поле ввода текста
FindC(tmpForm,'edtData_'+intToStr(tmpCount),tmpEdit,False);
if tmpEdit = nil then
tmpEdit := TdbEdit.Create( tmpForm );
with tmpEdit do // Tag -> cproperty.id ;
begin
visible := True;
name := 'edtData_'+intToStr(tmpCount);
parent := tmpParent;
Font.Size := ScaleFontSize;
top := tmpLineTop + tmpLabel.Height;
left := Scale(8);
width := APanel.width - Left*2; // по ширине панели с отступом
// заполняем данными
tag := tmpDataSet.FieldByName('id').asInteger; //
dbTable := tmpDataSet.FieldByName('id_ptype').asString;
dbField := '';
tagString := '';
text := '';
//
dbCurrency := False;
NumbersOnly := False;
if tmpControlID = 3 then // целое число
begin
dbCurrency := True;
dbAccuracy := 0;
Alignment := taLeftJustify;
end;
if tmpControlID = 4 then // вещественное число
begin
NumbersOnly := True;
end;
onChange := 'FilterEdit_OnChange';
Font.Style := 0;
tmpEdit.ReadOnly := False;
end;
// переключатель
if tmpControlID = 7 then
begin
tmpEdit.Width := trunc(tmpEdit.Height * 1.1); // Scale(32);
FindC(tmpForm,'panData_'+intToStr(tmpCount),tmpPanel,False);
if tmpPanel = nil then
tmpPanel := TdbPanel.Create(tmpForm);
with tmpPanel do
begin
parent := tmpParent;
name := 'panData_'+intToStr(tmpCount);
caption := '';
top := tmpEdit.Top;
left := tmpEdit.Left;
width := tmpEdit.width;
height := tmpEdit.height;
bevelWidth := 0;
color := tmpParent.color;
visible := True;
end;
//
FindC(tmpForm,'imgDataOn_'+intToStr(tmpCount),tmpImageOn,False);
if tmpImageOn = nil then
tmpImageOn := TdbImage.Create(tmpForm);
with tmpImageOn do
begin
parent := tmpPanel;
name := 'imgDataOn_'+intToStr(tmpCount);
align := alClient;
center := True;
stretch := True;
picture.LoadFromFile( ExtractFilePath(Application.ExeName) + 'images\toggle\toggle_on.png' );
onClick := 'Toggle_Off';
end;
//
FindC(tmpForm,'imgDataOff_'+intToStr(tmpCount),tmpImageOff,False);
if tmpImageOff = nil then
tmpImageOff := TdbImage.Create(tmpForm);
with tmpImageOff do
begin
parent := tmpPanel;
name := 'imgDataOff_'+intToStr(tmpCount);
align := alClient;
center := True;
stretch := True;
picture.LoadFromFile( ExtractFilePath(Application.ExeName) + 'images\toggle\toggle_off.png' );
onClick := 'Toggle_Null';
end;
//
FindC(tmpForm,'imgDataNull_'+intToStr(tmpCount),tmpImageNull,False);
if tmpImageNull = nil then
tmpImageNull := TdbImage.Create(tmpForm);
with tmpImageNull do
begin
parent := tmpPanel;
name := 'imgDataNull_'+intToStr(tmpCount);
align := alClient;
center := True;
stretch := True;
picture.LoadFromFile( ExtractFilePath(Application.ExeName) + 'images\toggle\toggle_indeterminate.png' );
onClick := 'Toggle_On';
end;
// переключатель в нейтральное положение
tmpImageNull.Visible := True;
tmpImageOff.Visible := False;
tmpImageOn.Visible := False;
end;
if tmpControlID = 6 then // справочник значений - кнопка
begin
FindC(tmpForm,'btnData_'+intToStr(tmpCount),tmpButton,False);
if tmpButton = nil then
begin
tmpButton := TdbButton.Create( tmpForm );
end;
with tmpButton do
begin
visible := True;
name := 'btnData_'+intToStr(tmpCount);
parent := tmpParent;
Font.Size := Scale(13);
Caption := chr(8981);
imageAlignment := iaCenter;
Top := tmpEdit.Top - 1;
Height := tmpEdit.Height + 2;
Width := Height + 2 ;
Left := tmpEdit.Left + tmpEdit.Width - Width;
onClick := 'efmObject_btnData_OnClick';
tag := tmpDataSet.FieldByName('ClassID').asInteger;
//
tmpEdit.Width := tmpEdit.Width - Width;
tmpEdit.ReadOnly := True;
end;
end;
if tmpControlID = 2 then // выпадающий список
begin
//
FindC(tmpForm,'cmbData_'+intToStr(tmpCount),tmpComboBox,False);
if tmpComboBox = nil then
tmpComboBox := TdbComboBox.Create( tmpForm );
with tmpComboBox do
begin
visible := True;
name := 'cmbData_'+intToStr(tmpCount);
parent := tmpParent;
Font.Size := tmpEdit.Font.Size;
top := tmpEdit.Top;
left := tmpEdit.Left;
width := tmpEdit.width;
dbSQL := 'SELECT object.id, oproperty.value_s FROM object LEFT JOIN oproperty ON oproperty.id_object = object.id WHERE oproperty.id_cproperty = ( SELECT id FROM cproperty WHERE cproperty.is_name = 1 ) AND object.id_class = '+tmpDataSet.FieldByName('ClassID').asString+' ORDER BY 2';
dbUpdate;
enabled := True;
onChange := 'efmObject_cmbEdit_OnChange';
Font.Style :=0;
end;
end;
//
if tmpControlID = 5 then // дата
begin
FindC(tmpForm,'dtpData_'+intToStr(tmpCount),tmpDataEdit,False);
if tmpDataEdit = nil then
begin
tmpDataEdit := TdbDateTimePicker.Create( tmpForm );
AssignEvents(tmpDataEdit);
end;
with tmpDataEdit do
begin
visible := True;
name := 'dtpData_'+intToStr(tmpCount);
parent := tmpParent;
Font.Size := tmpEdit.Font.Size;
top := tmpEdit.Top;
left := tmpEdit.Left;
width := tmpEdit.width;
ShowCheckBox := True;
DateTime := Now();
Checked := False;
//
dbOnChange := 'efmObject_dtpEdit_OnChange';
Font.Style :=0;
end;
end;
inc(tmpCount);
tmpLineTop := tmpLineTop + tmpEdit.Height*2;
tmpDataSet.Next;
end;
tmpDataSet.Free;
APanel.Visible := (tmpCount <> 0); // если ничего нет на панели, скрыть её
end;
end;
Code language: Delphi (delphi)
Обработчики для комбобокса и поля ввода даты я решил использовать те же, что и для формы редактирования, немного доработав их таким образом, чтобы данные сразу попадали в связанные с ними поля редактирования текста:
procedure efmObject_cmbEdit_OnChange (Sender: TObject);
// выбор нового значения в выпадающем списке
var
tmpEdit: TdbEdit;
tmpForm: TForm;
begin
CForm(Sender,tmpForm);
FindC(tmpForm,'edt'+DeleteClassName(TdbComboBox(Sender).Name),tmpEdit);
TdbComboBox(Sender).Font.Style := fsBold;
// установить флаг обновления данных
tmpEdit.Text := TdbComboBox(Sender).Text;
tmpEdit.Font.Style := fsBold;
tmpEdit.dbField := IntToStr( TdbComboBox(Sender).dbItemID );
end;
procedure efmObject_dtpEdit_OnChange (Sender: TObject);
// выбор нового значения даты
var
tmpEdit: TdbEdit;
tmpForm: TForm;
tmpDate:TdbDateTimePicker;
begin
tmpDate:= TdbDateTimePicker(Sender);
CForm(Sender,tmpForm);
FindC(tmpForm,'edt'+DeleteClassName(TdbDateTimePicker(Sender).Name),tmpEdit);
TdbDateTimePicker(Sender).Font.Style := fsBold;
// установить флаг обновления данных
tmpEdit.Font.Style := fsBold;
if tmpDate.Checked then
tmpEdit.Text := DateToStr( tmpDate.DateTime )
else
tmpEdit.Text := '';
end;
Code language: Delphi (delphi)
Главное отличие работы компонентов, находящихся на форме фильтра от тех, что расположены форме редактирования, в том, что при изменении значения сразу вызывается обновление списка объектов. За это отвечает процедура FilterEdit_OnChange().
procedure FilterEdit_OnChange( Sender: TObject);
// обработчик изменний в поле редактирования текста фильтра
var
tmpForm:TForm;
begin
// если будет работать быстро, то использовать, нет - отдельная кнопка должна быть
CForm(Sender,tmpForm);
Form_UpdateData(tmpForm);
end;
Code language: Delphi (delphi)
Данная процедура содержит философское высказывание об ожидаемой производительности, поэтому я решил добавить на панель инструментов главной формы кнопку, по которой можно обновлять содержимое активной таблицы.
procedure DTF_btnUpdateOnClick(Sender: TObject; var Cancel: boolean);
// редактирование записи
var
tmpForm: TForm;
begin
if (ActiveGrid <> nil) then
begin
CForm(ActiveGrid,tmpForm);
Form_UpdateData(tmpForm);
end
else
ShowHint( TControl(Sender), 'Выберите таблицу' );
end;
Code language: Delphi (delphi)
Волшебство
Напомню, что у формы отображения списка объектов frmObject есть ключевая процедура frmObject_btnUpdate_OnClick() – обработчик нажатия кнопки btnUpdate. В ней мы добавляем вызов построителя фильтра (строка 16). А чтобы фильтр перестраивался только при смене класса, будем сохранять текущий класс в свойстве Tag (см. PrepareFilterPanel() строки 26-28). Затем в строке 19 вызывается процедура btnPrepareOject(), которая готовит отображаемые данные.
procedure frmObject_btnUpdate_OnClick (Sender: TObject; var Cancel: boolean);
// обновить отображение
var
tmpIDClass: integer;
tmpButton: TdbButton;
tmpDataSet: TDataSet;
tmpSQL: string;
i: integer;
tmpTabSheet: TdbTabSheet;
Splitter:TSplitter;
begin
frmObject.labIDClass.Caption := 'Change'; // блокируем срабатывание frmObject_tgrMain_OnColumnResize
// строится запрос на выборку данных - все объекты указанного класса
// получаем ID класса из дерева класса
tmpIDClass := Form_GetDataViewer( GetFormByName('dtfClass_Tree') ).dbItemId;
PrepareFilterPanel( frmObject.panFilter, tmpIDClass ); // настроить панель фильтра
frmObject.labIDClass.Tag := tmpIDClass; // запоминаем класс
tmpButton := TdbButton(Sender);
btnPrepareOject( tmpButton, tmpIDClass);
// убираем детализацию
for i:= frmObject.pgcDetails.PageCount - 1 downto 0 do
begin
frmObject.pgcDetails.Pages[i].Free;
end;
FindC(frmObject,frmObject.panDetails.TagString,Splitter);
Splitter.Visible := False;
frmObject.panDetails.Visible := False;
// определить, есть ли детализация, и создать вкладки-кнопки
tmpSQL := 'SELECT class.id, class.name FROM cproperty LEFT JOIN class ON class.id = cproperty.id_class WHERE cproperty.is_detail = 1 AND cproperty.id_class1 = '+IntToStr( tmpIDClass );
SQLQuery(tmpSQL,tmpDataSet);
while not tmpDataSet.EOF do
begin
tmpTabSheet := TdbTabSheet.Create( frmObject );
with tmpTabSheet do
begin
PageControl := frmObject.pgcDetails;
// Name := '';
Caption := tmpDataSet.FieldByName('name').asString;
Tag := tmpDataSet.FieldByName('id').asInteger;
// TagString := '';
end;
tmpDataSet.Next;
end;
if frmObject.pgcDetails.PageCount > 0 then
begin
frmObject.panDetails.Visible := True;
Splitter.Visible := True;
frmObject_Detail.btnUpdate.Click;
end;
end;
Code language: Delphi (delphi)
Доработок в btnPrepareOject() немного, добавлена логика для формирования условия фильтра по данным, в строках 81-86.
procedure btnPrepareOject( AButton: TdbButton; AIDClass: integer; ACheckBox:Boolean = False; ADetail: boolean = False);
// генерация табличного предстваления класса
// AButton - кнопка для генерации запроса
// AIDClass - класс, для которого делается запрос
// ACheckBox - нужна ли колонка с чеерами
// ADetail - нужна ли фильтрация по мастеру
var
tmpParentList: string;
tmpChildList: string;
tmpDataSet: TDataSet;
tmpSQL: string;
tmpFields : string;
tmpJoins: string;
tmpCount: integer;
tmpCaptions: string;
tmpTableAlias: string;
tmpOrderBy: string;
tmpIDMasterClass: integer;
tmpIDMasterObject: integer;
tmpFilter: string;
tmpVisible: boolean;
tmpForm:TForm;
tmpPanel:TdbPanel;
tmpFilterValue: string;
tmpFiltered : boolean;
begin
CForm(AButton,tmpForm);
FindC(tmpForm,'panFilter',tmpPanel,False);
tmpFiltered := (tmpPanel <> nil) and (tmpPanel.Visible);
tmpFilter := '';
if ADetail then
begin
tmpIDMasterClass := frmObject.labIDClass.Tag;
tmpIDMasterObject := frmObject.tgrMain.dbItemID;
end;
GetChildAndParent( AIDClass, tmpParentList, tmpChildList); // получить список ID родителей и детей
tmpSQL := 'SELECT * FROM cproperty WHERE id_class in ('+tmpParentList+') ORDER BY orderNum ';
SQLQuery(tmpSQL,tmpDataSet);
tmpFields := 'object.id';
if ACheckBox then
tmpFields := tmpFields + ',"$checkbox"';
tmpJoins := '';
tmpCaptions := 'delete_col'; // не отображать
tmpOrderBy := '2';
if ACheckBox then
begin
tmpCaptions := tmpCaptions + ',#';
tmpOrderBy := '3';
end;
tmpCount := 0;
//
while not tmpDataSet.EOF do
begin
tmpVisible := tmpDataSet.FieldByName('visible').asInteger = 1;
tmpTableAlias := 'OP_'+IntToStr(tmpCount);
//
if tmpVisible then
begin
if tmpFields <> '' then
tmpFields := tmpFields + ', ';
tmpFields := tmpFields + tmpTableAlias+'.value_s ';
end;
//
tmpJoins := tmpJoins + 'LEFT JOIN oproperty '+tmpTableAlias+' ON '+tmpTableAlias+'.id_object = object.id AND '+tmpTableAlias+'.id_cproperty = '+tmpDataSet.FieldByName('id').asString+' AND '+tmpTableAlias+'.orderNum is NULL '+CR ;
//
if tmpVisible then
begin
if tmpCaptions <> '' then
tmpCaptions := tmpCaptions + ', ';
tmpCaptions := tmpCaptions + tmpDataSet.FieldByName('name').asString;
end;
//
if tmpDataSet.FieldByName('is_name').asInteger = 1 then
AButton.Tag := tmpCount;
//
if ADetail and (tmpDataSet.FieldByName('id_class1').asInteger = tmpIDMasterClass) then
begin
tmpFilter := tmpFilter + tmpTableAlias+'.id_object1 = '+IntToStr(tmpIDMasterObject)+' AND ';
end;
//
if tmpFiltered and (tmpDataSet.FieldByName('is_filter').asInteger = 1) then
begin
tmpFilterValue := GetTextFilterValue( tmpPanel, tmpDataSet.FieldByName('id').asInteger );
if tmpFilterValue <> '' then
tmpFilter := tmpFilter + tmpTableAlias+'.value_s LIKE "%'+tmpFilterValue+'%" AND ';
end;
inc(tmpCount);
tmpDataSet.Next;
end;
tmpDataSet.Free;
if tmpCount = 0 then
tmpOrderBy := '1';
tmpSQL := 'SELECT '+tmpFields+CR+' FROM object '+tmpJoins+CR+' WHERE '+tmpFilter+' object.id_class in ('+tmpChildList+') ORDER BY '+tmpOrderBy;
AButton.dbSQL := tmpSQL;
AButton.dbListFieldsNames := tmpCaptions;
end;
Code language: Delphi (delphi)
Для извлечения конкретных значений для фильтра добавлена функция GetTextFilterValue(), принцип поиска основан на том, что в свойстве Tag поля редактирования хранится значение CProperty.ID – идентификатор свойства класса. Как только находится совпадение, текст из поля редактирования возвращается в вызывающую процедуру. Дополнительная проверка видимости связана с механизмом “рециклинга” компонентов на панели во избежание ошибок, связанных с изменением настроек панели фильтрации в процессе работы приложения.
function GetTextFilterValue( APanel:TdbPanel; ACPropertyID:integer ):string;
// извлечение строкового значения фильтра
var
i:integer;
tmpEdit:TdbEdit;
begin
Result := '';
for i := APanel.ControlCount - 1 downto 0 do
begin
if APanel.Controls[i] is TdbEdit then
begin
tmpEdit:=TdbEdit(APanel.Controls[i]);
if tmpEdit.Visible and (tmpEdit.Tag = ACPropertyID) then
begin
Result := tmpEdit.Text;
Break;
end;
end;
end;
end;
Code language: PHP (php)
Результат






Список ToDo уменьшился на 1 пункт, но всё ещё выглядит внушительным:
- Удаление детализации при удалении записи-мастера
- Выравнивание по правому краю для данных числовых типов
- Интервальное дерево классов
- Поиск объектов по заданному значению свойства (нескольких свойств)
- Настройка видимости колонок в табличном представлении без использования наследования
- Макросы для автоматического заполнения полей на клиенте
- Вычисляемые поля для табличного представления
- Создание произвольных представлений (использования аналога SQL для ООП).
- Флаг обязательности заполнения свойства объекта (аналог обязательного поля)
- Флаг уникальности свойства объекта класса (аналог уникальности значения поля)
- Флаг глобальной уникальности свойства объекта (или свойство с генерацией GUID)
- Расширение базовых типов данных (время, да/нет, изображения/файлы и др.)
- Добавление контроля для типов данных (аналог доменов) – диапазон значений для чисел и дат, длина текста для строк и т.д.
- Ручная настройка положения и размера компонентов на форме редактирования классов.
- Добавление новых ссылочных значений непосредственно из формы редактирования объекта.
- Масштабирование форм: пользователь задает масштаб отображения форм и их содержимого.
Строго говоря, не обязательно все это делать в прототипе (на My Visual Database), в какой-то момент нужно будет принять решение о реализации всего этого на Delphi в мультиплатформенном проекте Windows/Android. Но прежде, пожалуй, стоит потестировать производительность, чтобы четко понимать ограничения принятой архитектуры данных.
Ссылки
- Data Keeper 1.6 – исходники проекта