Фильтрация данных в Delphi
Хочу рассказать о нескольких полезных способах фильтрации данных, которые можно применить, чтобы достигнуть эффекта отсеивания данных, по мере ввода искомой строки. На пример в таблице есть столбец "Фамилия", пользователь вводит в поле поиска последовательно буковки и данные фильтруются(отсеиваются) - пользователь ввел первую буковку и выводятся все фамилии, начинающиеся с первой буквы, потом только те которые начинаются с первой и второй и т.д. пока не найдет , что то полезное или вообще ничего не найдет. Т.е. фильтрация происходит не по полному совпадению, а по части введенной пользователем строки. Так же будут приведены способы фильтрации по нескольким полям.
Немного теории.
Основное преимущество фильтров по сравнению с поиском состоит в том, что применять фильтр можно без всяких подготовительных действий над набором данных, так как фильтры не используют индексы. Один минус - фильтрация выполняется медленнее чем поиск основанный на индексах.
Есть два варианта с помощью которых можно задать условие на фильтрацию. Можно использовать их по отдельности или оба сразу.
Первый - строковое значение в свойстве Filter.
Второй - это условие описанное в обработчике события OnFilterRecord.
Начинается фильтрация как только свойство Filtered, установлено в True.
Свойство Filter.
Сначала опишу возможности которые предоставляет свойство Filter, а во второй части главы будет рассказано как можно использовать событие OnFilterRecord. Именно свойство Filter нужно использовать в первую очередь, т.к. это дает значительный выигрыш в производительности при обработке больших по объему наборов данных (по сравнению с событием OnFilterRecord). Особенно это заметно при большом количестве записей в таблице (~свыше 100.000), ну и конечно многое зависит от мощности машины. При малом объеме обрабатываемых данных, разница в скорости не заметна.
Ну что ж приступим. Допустим, есть таблица с полями Фамилия, Имя, Отчество. Нужно фильтровать данные по одному из полей (на выбор), т.е. пользователь должен иметь возможность выбрать по какому из полей фильтровать данные. Для тех кто в танке термины "поле" и "столбец" - обозначают одно и тоже.
Задача стоит такая отсеивать данные по мере ввода искомой строки в Edit.
Кидаем на форму компоненты Combobox и Edit. Combobox будет использоваться для выбора столбца, а в Edit будет вводится искомое слово. Заполняем свойство Items в Combobox именами столбцов. Ставим свойство Style у Combobox равным csDropDownList (чтоб что попало не вводили). Затем в обработчике события Edit1Change пишем следующее:
procedure TForm1.Edit1Change(Sender: TObject);
begin
if Length(Edit1.Text) > 0 then
begin
ADOTable1.Filtered:=false;
ADOTable1.Filter:=Combobox1.Text + ' LIKE ' + #39 + Edit1.Text + '%' + #39;
ADOTable1.Filtered:=true;
end
else ADOTable1.Filtered:=false;
end;
Жмем F9 - вуаля. Как видите, все довольно просто. Код простой и понятный. На всякий случай перевожу на русский - если Edit не пустой (при пустом вылетит ошибка), отключаем фильтрацию, формируем строку фильтра, запускаем фильтрацию, если Edit пустой - отключаем фильтрацию.
В самом простом случае, если не нужно давать возможность выбора определенного столбца, а фильтровать по одному конкретному полю(например "ФАМИЛИЯ"), строка фильтра выглядела бы следующим образом:
ADOTable1Fil.Filter:='ФАМИЛИЯ LIKE '+ #39 + Edit1.Text + '%' + #39;
Строка-условие фильтра означает следующее - выбрать те записи из столбца 'ФАМИЛИЯ', которые начинаются с тех же символов, что и набраны в Edit1.Text.
Особое внимание обратите на пробелы - "потеряете" пробел - не будет работать.
Ключевое слово LIKE позволяет по заданному шаблону сравнивать строки. При этом нужно знать следующее:
символ '_' (подчеркивание) – заменяет любой одиночный символ,
символ '%' (процент) – заменяет любую последовательность из символов.
Знак #39 - означает номер символа ' (одинарная кавычка) в кодовой таблице ASCII. Дело в том, что значение на фильтрацию нужно указывать в одинарных кавычках, а так как одинарные кавычки используются в Delphi для ограничения строк, то чтобы внутри строки поставить одинарную кавычку, её нужно поставить дважды. Конструкция #39 + Edit1.Text + '%' + #39 идентична '''' + Edit1.Text + '%' + ''''. Неправда ли первый вариант как-то попонятней. Можно также использовать спец. функцию QuotedStr, которая возвращает строку окаймленную одинарными кавычками.
В общем следующие три варианта формирования строки абсолютно одинаково работают и какой вариант проще использовать, решайте сами.
ADOTable1.Filter:='ФАМИЛИЯ LIKE '+ '''' + Edit1.Text + '%' + '''';
ADOTable1.Filter:='ФАМИЛИЯ LIKE '+ QuotedStr(edit1.Text+ '%');
ADOTable1Fil.Filter:='ФАМИЛИЯ LIKE '+ #39 + Edit1.Text + '%' + #39;
Идем дальше. Если, например, нужно искать любое вхождение искомой строки в записях, а не начиная с первого символа, то строка фильтра выглядела бы следующим образом:
ADOTable1.Filter:='ФАМИЛИЯ LIKE ' + #39 + '%' + Edit1.Text + '%' + #39;
Заметили '%' перед Edit1.Text? Переводится так - выбрать те записи из столбца 'ФАМИЛИЯ', в которых есть последовательность символов, которые набраны в Edit1.Text.
Все это прекрасно, но часто нужно фильтровать по нескольким столбцам. Тут применяется немного др. конструкция.
Добавим еще два компонента Edit на форму. Combobox использовать не будем, его можно удалить, т.к. у нас каждое поле (Edit) будет отвечать за ввод информации по определенному столбцу. Еще добавим кнопочку, она будет запускать фильтрацию. Казалось бы следующая конструкция должна корректно работать:
procedure TForm1.Button1Click(Sender: TObject);
begin
ADOTable1.Filtered:=false;
ADOTable1.Filter:='ФАМИЛИЯ LIKE '+ #39 + Edit2.Text + '%' + #39 +' AND ' + 'ИМЯ LIKE '+ #39 + Edit3.Text + '%' + #39 + ' AND ' + 'ОТЧЕСТВО LIKE ' + #39 + Edit4.Text + '%' + #39;
ADOTable1.Filtered:=true;
end;
Она и работает, но только в том случае, если данные введены во все три поля. Если одно из полей ввода(Edit) пустое - вылетает ошибка "Аргументы имеют неверный тип".
Впрочем, есть способ с этим бороться. Сначала код:
procedure TForm1.Button1Click(Sender: TObject);
var filtr, // формируемая строка фильтра
add: string;
begin
ADOTable1.filtered:=false;
filtr:='';
if length(edit2.text) > 0 then
filtr:= 'ФАМИЛИЯ LIKE '+ #39 + Edit2.Text + '%' + #39;
if length(edit3.text) > 0 then
begin
if length(filtr) > 0 then add:= ' and ' else add:='';
filtr:=filtr + add + 'ИМЯ LIKE '+ #39 + Edit3.Text + '%' + #39;
end;
if length(edit4.text) > 0 then
begin
if length(filtr) > 0 then add:= ' and ' else add:='';
filtr:=filtr + add + 'ОТЧЕСТВО LIKE '+ #39 + Edit4.Text + '%' + #39;
end;
if length(filtr) > 0 then
begin
ADOTable1.Filter:= filtr;
ADOTable1.filtered:=true;
end
else Showmessage('Все поля пусты!');
end;
Смысл такой, нужно провести проверку, пустое поле ввода или нет, а затем уже формировать строку фильтра. Причем начиная со второго поля ввода нужно проверять и строку фильтра. Это нужно для последующего правильного формирования строки.
Например если поле Edit2(Фамилия) пустое, то соответственно и строка фильтра будет пуста(filtr:='') , а, например, поле Edit3(ИМЯ) заполнено, это значит строка фильтра должна иметь вид 'ИМЯ LIKE ' + #39 + Edit3.Text + '%' + #39 ,
а не ' and ' + 'ИМЯ LIKE '+ #39 + Edit3.Text + '%' + #39 , (т.е. без ' and ' вначале).
Следующий фрагмент кода это и выполняет:
.....
if length(edit3.text) > 0 then
begin
if length(filtr) > 0 then add:= ' and ' else add:='';
filtr:=filtr + add + 'ИМЯ LIKE '+ #39 + Edit3.Text + '%' + #39;
end;
.....
Опять перевожу - если Edit3 не пустое, то проверяем строку фильтра(filtr) если она не пуста, то прибавляем к сторке фильтра ' and ' + 'ИМЯ LIKE '+ #39 + Edit3.Text + '%' + #39, если пуста, то просто 'ИМЯ LIKE '+ #39 + Edit3.Text + '%' + #39.
Все. В конце присваиваем сформированную строку(filtr) свойству ADOTable1.Filter и запускаем фильтрацию.
.....
if length(filtr) > 0 then
begin
ADOTable1.Filter:= filtr;
ADOTable1.filtered:=true;
end
else Showmessage('Все поля пусты!');
.....
Соответственно, если на этом этапе filtr='', значит ни одно из полей не было заполнено, о чем и выводим соответствующую надпись.
Объяснение немного запутанное, но стоит внимательно посмотреть на код и все станет ясно. Забегая вперед, скажу, что если использовать событие OnFilterRecord, то необходимость в проверке наличия символов в поле ввода отпадает(т.е. его удобней и проще использовать), но при этом падает скорость фильтрации. Впрочем это заметно только при обработке больших по объему наборов данных. Напоследок скажу, кроме операции and можно использовать и другие логические операции or, not, xor, при формирования строки-условия фильтрации по нескольким столбцам, хотя такая необходимость возникает достаточно редко.
Здесь можно скачатьрабочий пример фильтрации на основе свойства Filter.
Событие OnFilterRecord.
Разберемся с событием OnFilterRecod.
Посмотрим на заголовок процедуры
procedure TForm1.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean);
DataSet - имя фильтруемого набора данных. Accept имеет тип Boolean, а это значит может принимать два значения true - принять запись, false - отклонить. Кто не дружит с английским перевожу: accept -принимать, брать, соглашаться.
Принцип такой - в обработчике OnFilterRecord последовательно перебираются все записи определенного поля(полей) таблицы данных на предмет соответствия условию фильтрации.
Для наглядности несколько примеров:
Напомню еще раз, что Accept - переменная булевского типа, а это значит можно применять логические операторы(=, <>, <, >, <=, >=, and, or, not) для присвоения ей некоторого значения.
Допустим, в таблице есть поля ГОД и МЕСЯЦ которые имеют целочисленный тип и содержат год и месяц рождения кого либо.
TForm1.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean);
begin
Accept:= (DataSet['ГОД'] > 1973);
end;
Где то в программе
…
Table1.Filtered:=true;
…
В результате в DBGrid отобразятся записи, у которых значение поля ГОД больше 1973.
Если написать в обработчике такую строку:
Accept:= (DataSet['ГОД'] > 1973) and ((DataSet['МЕСЯЦ'] > 5) and (DataSet['МЕСЯЦ'] < 9));
то отобразятся записи тех людей, которые родились после 1973 год летом.
По-моему все понятно - DataSet['ИМЯ_СТОЛБЦА'] "вытягивает" очередное значение записи указанного столбца, а дальше Accept:= некоторое_логическое_условие;
Ради эксперимента поставьте Accept:=true; и выведутся все записи, если Accept:=false; - ни одной.
Если по какой-то причине Вы хотите обратится к DataSet не по имени столбца, а по его номеру, то можно использовать конструкцию DataSet.Fields[НОМЕР_СТОЛБЦА].AsString вместо DataSet['ИМЯ_СТОЛБЦА'].
С теорией покончено, переходим к практике.
Допустим, в таблице есть поле ФАМИЛИЯ (тип строка) и есть поле ввода искомой строки - Edit1.
Нам нужно, чтобы при вводе очередного символа происходила фильтрация (сначала по первому символу, потом по первому и второму и т.д.). Другими словами, нужно определить некоторое условие в OnFilterRecord, а в обработчике события OnEdit1Change запустить фильтрацию - Table1. Filtered:= true;.
Итак начнем ваять OnFilterRecord.
procedure TForm1.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean);
var FieldVal:string;
begin
FieldVal := DataSet['ФАМИЛИЯ'];
Accept := copy(FieldVal, 1, length(edit1.text)) = edit1.Text;
end;
FieldVal - значение очередной записи в заданном столбце, в нашем случае в столбце ФАМИЛИЯ.
Дальше так - функция copy копирует часть строки переменной "FieldVal" начиная с первой позиции "1" на длину искомой строки "length(edit1.text)", потом идет сравнение строки полученной с помощью функции copy и строки введенной в edit1.Text. Если строки совпадают, то запись принимается, если нет - не принимается.
Если фильтрация должна происходить без учета регистра букв, то обработчик должен иметь следующий вид:
FieldVal := DataSet['ФАМИЛИЯ'];
Accept := copy(AnsiUpperCase(FieldVal), 1, length(edit1.text)) = AnsiUpperCase(edit1.Text);
Осталось только активизировать наш обработчик, как уже говорилось выше в событии OnEdit1Change примерно так:
procedure TForm1.Edit1Change(Sender: TObject);
begin
table1.Filtered:=false;
table1.Filtered:=true;
end;
Вот и все. Почти. Есть еще один способ "посимвольной" фильтрации, но он работает немного не так как предыдущий пример. Сначала приведу код:
procedure TForm1.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean);
begin
accept:=pos(Edit1.Text, DataSet['ФАМИЛИЯ'])<>0);
end;
Функция Pos осуществляет поиск вхождения подстроки в строку, если подстрока найдена то возвращается позиция вхождения указанной последовательности символов в указанную строку, но это нам не интересно, интересно то, что если в указанной строке нет заданной последовательности символов, то функция возвращает ноль. То есть если в любом месте строки содержащей фамилию (а не только начиная с первой буквы, в этом и отличие) будет найдена искомая комбинация символов, то запись принимается.
Если фильтрация должна происходить без учета регистра букв, то строка должна иметь следующий вид:
accept:=pos(AnsiUpperCase(Edit1.Text), AnsiUpperCase(DataSet['ФАМИЛИЯ']))<>0;
Да, еще иногда нужно знать, сколько записей находится в отфильтрованном DataSet, для этого можно использовать свойство RecordCount. Оно возвращает текущее число записей в наборе данных. Например можно применить следующую конструкцию:
Label1.Caption := IntToStr(table1.RecordCount);
В данном случае ее можно разместить в обработчике события OnEdit1Change сразу после активизации процедуры фильтрации.
Здесь можно скачать рабочий пример фильтрации на основе события OnFilterRecord.
PS. Каждый из способов имеет свои преимущества и недостатки. Свойство Filter дает ощутимый выигрыш в скорости обработки данных, а событие OnFilterRecord намного "гибче" и удобней в использовании. Применяйте свойство Filter в тех случаях если можно обойтись без использования OnFilterRecord.
Если же Вы применяете оба способа, то стоит помнить, что обработчик события дополняет, а не замещает свойство Filter, т.е если включена фильтрация и св-во фильтр содержит значение фильтра, то обработчик события OnFilterRecord и фильтр(Filter) связаны логическим отношением "AND" (выполняются оба).
Автор: Губарев Михаил
Источник: delphibd.sk6.ru
Добавить закладку на материал:
|