среда, 22 февраля 2012 г.

Урок 30. Поиск и фильтрация

"Давненько я не брал в руки шашек...", - говорил Гоголевский Ноздрёв.

Давненько я ничего не писал в моем блоге. Не буду приносить никаких извинений и писать оправдательных фраз. Достаточно одного слова: "Работа..." А посему - за дело...

Вряд ли какая-то программа обходится без алгоритма поиска чего либо. А интерфейс, предоставляющий пользователю возможность работы с цифрами, тем более не может обойтись без таких полезных функций как поиск и фильтрация записей. Я утверждаю это со 100% уверенностью как пользователь собственных творений. А причина тому проста: чаще всего оператору не нужен полный набор всех записей. Как правило, его интересует ограниченное количество строк или вообще одна конкретная запись.

Если кто-то из Вас, мои дорогие читатели, работал с подобными программами, то мог сталкиваться с таким удобством: щелкнешь по заголовку какой либо колонки в таблице, и порядок сортировки строк таблицы изменился... Волшебство? Нет, уверяю Вас. Предлагаю снабдить сетку на главной форме подобными функциями. Это возможно, так как grid на форме main из библиотеки EhLib.

Не помню, описывал я ранее или нет, но для удобства пользователя (а об этом в очередной раз призываю не забывать никогда!) нужно установить такой порядок сортировки записей в главной форме при ее старте, чтобы самые свежие строки оказались вверху. Много для этого не нужно, укажите лишь в инспекторе объектов для ADOTableMain имя поля, по которому должна произойти сортировка, не забыв при этом указать обратный порядок (DESC):





Затем нужно обратиться к свойствам сетки:


SortLocal=True (по умолчанию стоит False).

Затем в событии onShow формы написать следующий обработчик:


procedure TMainFrm.FormShow(Sender: TObject);

Var
  i: Integer;
begin

  // Настройка заголовков колонок
  For i:=0 to DbGridEh1.Columns.Count-1 do begin
    DbGridEh1.Columns[i].Title.TitleButton := TRUE;                  //заголовок колонки становится "кнопкой"
  end;

// Настройка свойств сетки (добавляются свойства)
  DbGridEh1.OptionsEh := DbGridEh1.OptionsEh + [dghAutoSortMarking]; //автоматическая сортировка
  DbGridEh1.OptionsEh := DbGridEh1.OptionsEh + [dghMultiSortMarking]; //сортировка по нескольким колонкам

end;

А для клика по заголовку сетки создать обработчик вида:


procedure TMainFrm.DBGridEh1TitleBtnClick(Sender: TObject; ACol: Integer;

  Column: TColumnEh);
begin

If DBGridEh1.Columns[ACol].Title.SortMarker=smDownEh
then
  ADOTableMain.IndexFieldNames:=DBGridEh1.Columns[ACol].FieldName
else
  ADOTableMain.IndexFieldNames:=DBGridEh1.Columns[ACol].FieldName+' DESC';

end;,

здесь сначала проверяется, какой маркер сортировки установлен в колонке с номером ACol (напомню, что нумерация ведется от нуля), а затем назначается индекс по этой колонке для сортировки в прямом или обратном (DESC) порядке.

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




связанная с тем, что эти поля в сетке не простые, а с подстановкой (на экране отображается содержимое поля D_Name, а на самом деле ключевую роль (KeyField) выполняет поле с именем D). Решений у этой задачки несколько, но я полагаю, что останавливаться на этом не стоит.

Ну, а чтобы все работало без ошибок пока Вы думаете, исключите обработку этих полей, написав в начале последней процедуры:

If (DBGridEh1.Columns[ACol].FieldName='D_Name') OR (DBGridEh1.Columns[ACol].FieldName='K_Name')
then
  exit;

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

Для этого понадобится текстовое поле, где будет вводиться искомое значение и кнопка. Назовем их FindEdit и BitBtnFind.

Напишите следующий обработчик события нажатия кнопки:

procedure TMainFrm.BitBtnFindClick(Sender: TObject);
begin

  // Поиск первого вхождения
  ADOTableMain.First;
  While not ADOTableMain.Eof do
  begin
    If Copy(ADOTableMain.FieldByName(DBGridEh1.Columns[DBGridEh1.SelectedIndex].FieldName).AsString,1,Length(FindEdit.Text))=FindEdit.Text
    then
      Break;
    ADOTableMain.Next;
  end;

  // Сообщение при неудачном поиске
  If ADOTableMain.eof
  then
  begin
    MyMessenger.MessageString:='Попробуйте: '
                              +CR
                              +'1. Изменить искомое слово.'
                              +CR
                              +'2. Проверить регистр.'
                              +CR
                              +'3. Установить курсор в одно из полей <Куда> или <Откуда>';
    MyMessenger.TitleString:='Ничего не найдено';
    MyMessenger.MessageType:=mtInformation;
    MyMessenger.Buttons:=[mbOK];

    MyMessenger.ShowMessage;

    // Установка фокуса в поле ввода
    If FindEdit.CanFocus
    then
      FindEdit.SetFocus;

  end;

end;

Самое интересное на мой взгляд место в этом коде - функция Copy, которая выделяет подстроку в поле, начиная с первой позиции. Длина этой подстроки берется равной длине введенного в поле поиска значения.

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

Нужно ли при всем этом "изобретать" кнопку "Найти следующее..."? Думаю: "Нет!" Гораздо полезнее окажется кнопка ,устанавливающая фильтр по найденному значению. Т.е. все похожие записи будут представлены в таблице, а другие - временно скрыты.

Приступим к организации фильтра.

Прежде всего, создайте экшн в категории Data под данное действие (на предыдущем шаге создавать Action особого смысла не было, так как выполняемое по кнопке действие вряд ли имеет смысл подкладывать под какой-то другой элемент управления или пункт меню - действие связано с содержимым поля ввода и не работает само по себе).


Обработчик события onExecute напишите следующий:

procedure TMainFrm.Data_FilterExecute(Sender: TObject);
begin

  // Фильтр
  If MySelect.MySel_Field='MyDate'
  then
    NewFilter:='('+OldFilter+') AND (MyDate='+ADOTableMain.FieldByName('MyDate').AsString+')';

  If MySelect.MySel_Field='D_Name'
  then
    NewFilter:='('+OldFilter+') AND (D='+ADOTableMain.FieldByName('D').AsString+')';

  If MySelect.MySel_Field='K_Name'
  then
    NewFilter:='('+OldFilter+') AND (K='+ADOTableMain.FieldByName('K').AsString+')';

  If MySelect.MySel_Field='Summa'
  then
    NewFilter:='('+OldFilter+') AND (Summa='+ADOTableMain.FieldByName('Summa').AsString+')';

  ADOTableMain.Filtered:=False;
  ADOTableMain.Filter:=NewFilter;
  ADOTableMain.Filtered:=True;

  StatusBar1.Panels[0].Text:='Записей (фильтр): '+IntToStr(ADOTableMain.RecordCount);

end;

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

Установите этот Action под соответствующий пункт меню.
Тестирование будет состоять в следующем: 

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

Но, вот беда: как вернуть все записи? Как снять фильтр? Как отказаться от него?

Понадобится еще одно действие в категории Data вот с таким обработчиком события "выполнить":

procedure TMainFrm.Data_NoFilterExecute(Sender: TObject);
begin

  // Снятие фильтра
  ADOTableMain.Filtered:=False;
  ADOTableMain.Filter:=OldFilter;
  ADOTableMain.Filtered:=True;
  StatusBarUpdate;

end;

Для отмены действия вновь наложенного фильтра назначьте это действие пункту меню или кнопке.

На этом на сегодня все.

Жду Ваших вопросов.



Комментариев нет:

Отправить комментарий