Практика
Автоматизация настроек параметров фильтрации

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


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

Фильтрация – способ поиска подмножества данных в соответствии с заданными условиями. Количество условий в фильтре носит произвольный характер, и может варьироваться от однако, до нескольких десятков (с очень большими фильтрами работать неудобно, и в своей практике я их не встречал).

В приложении фильтр может выглядеть примерно так:


Логика работы такого фильтра очевидна - изменяя состояние того или иного чекбокса мы включаем или отключаем те или иные условия фильтрации. При этом для повышения комфортности пользователя при работе с таким фильтром, элементы интерфейса, связанные с тем или иным чекбоксом нужно переводить в состояние enabled/disabled, в зависимости от состояния соответствующего чекбокса. В итоге за визуальную часть работы такого фильтра отвечает примерно такой код:

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
   Edit1.Enabled := CheckBox1.Checked;
end;

procedure TForm1.CheckBox2Click(Sender: TObject);
begin
   Edit2.Enabled := CheckBox2.Checked;
end;

procedure TForm1.CheckBox3Click(Sender: TObject);
begin
   Edit3.Enabled := CheckBox3.Checked;
end;

procedure TForm1.CheckBox4Click(Sender: TObject);
begin
   CheckBox1.Enabled := CheckBox4.Checked;
end;

procedure TForm1.CheckBox5Click(Sender: TObject);
begin
   CheckBox2.Enabled := CheckBox5.Checked;
end;

procedure TForm1.CheckBox6Click(Sender: TObject);
begin
   DateTimePicker1.Enabled := CheckBox6.Checked;
end;

procedure TForm1.CheckBox7Click(Sender: TObject);
begin
   DateTimePicker2.Enabled := CheckBox7.Checked;
end;

procedure TForm1.CheckBox8Click(Sender: TObject);
begin
   DateTimePicker3.Enabled := CheckBox8.Checked;
end;

Всего 8 условий, а уже как громоздко! И ведь нужно не забыть о первоначальной ручной синхронизации в дизайнере между свойством Checked чекбокса и свойством Enabled соответствующего контрола. Конечно, можно сделать это и кодом, но тогда объем кода, который нужно поддерживать, раздуется еще больше... Ужас.

Конечно, выше приведенный код можно оптимизировать:

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
   case (Sender as TCheckBox).Tag of
      0: Edit1.Enabled := (Sender as TCheckBox).Checked;
      1: Edit2.Enabled := (Sender as TCheckBox).Checked;
      2: Edit3.Enabled := (Sender as TCheckBox).Checked;
      3: CheckBox1.Enabled := (Sender as TCheckBox).Checked;
      4: CheckBox2.Enabled := (Sender as TCheckBox).Checked;
      5: DateTimePicker1.Enabled := (Sender as TCheckBox).Checked;
      6: DateTimePicker2.Enabled := (Sender as TCheckBox).Checked;
      7: DateTimePicker3.Enabled := (Sender as TCheckBox).Checked;
   end;
end;

Уже лучше, но нужно не забывать правильно расставлять теги у чекбоксов. Но все усложняется, если фильтр имеет многоуровневую структуру.


А ведь таких форм десятки, кому захочется поддерживать столько никчемного кода?

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

Интерфейсная часть выглядит следующим образом:

type
  TCheckBox = class(Vcl.StdCtrls.TCheckBox)
  private
    [weak] FLinkedObject: TControl;
    FLinkedObjectsList: TList<TControl>;
    procedure SetLinkedObject(const Value: TControl);
    procedure NotifyEvent(Sender: TObject; const Item: TControl; Action: TCollectionNotification);
  protected
    procedure Click; override;
    procedure Toggle; override;
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property LinkedObject: TControl read FLinkedObject write SetLinkedObject;
    property LinkedObjectsList: TList<TControl> read FLinkedObjectsList;
  end;

Два свойства LinkedObject и LinkedObjectsList предназначены для связи нашего чекбокса с "контролируемыми" компонентами. Если чекбокс контролирует один компонент, его можно "положить" в свойство LinkedObject, если объектов несколько, тогда LinkedObjectsList вам в помощь. Хотя никто не мешает распределить "контролируемые" компоненты по двум свойствам сразу.

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

В момент связывания с чекбоксом компонент должен синхронизировать свое состояние (enabled/disabled), это происходит в событии NotifyEvent.

procedure TCheckBox.NotifyEvent(Sender: TObject; const Item: TControl;
  Action: TCollectionNotification);
begin
   if Action = cnAdded then
   begin
      Item.Enabled := Self.Checked;
      if Item is TCheckBox then
         (Item as TCheckBox).Click;
   end;
end;

Перегруженный метод Notification используется для корректного освобождения ссылок на объекты, которые по каким либо причинам прекратили свое существование во время работы приложения.

procedure TCheckBox.Notification(AComponent: TComponent; Operation: TOperation);
var
  i: Integer;
begin
   inherited;
   if (Operation = opRemove) and (not Application.Terminated) then
   begin
      if AComponent = FLinkedObject then
         FLinkedObject := nil;

      for i := FLinkedObjectsList.Count-1 downto 0 do
         if FLinkedObjectsList[i] = AComponent then
         begin
            FLinkedObjectsList.Delete(i);
            Break;
         end;
   end;
end;

Два перегруженных метода Click и Toggle реагируют на изменение состояния чекбокса.

procedure TCheckBox.Click;
var
  i: Integer;
begin
   inherited;

   if Assigned(FLinkedObject) then
   begin
      FLinkedObject.Enabled := Self.Checked and Self.Enabled;
      if FLinkedObject is TCheckBox then
         (FLinkedObject as TCheckBox).Click;
   end;

   for i := FLinkedObjectsList.Count-1 downto 0 do
   begin
      FLinkedObjectsList[i].Enabled := Self.Checked and Self.Enabled;
      if FLinkedObjectsList[i] is TCheckBox then
         (FLinkedObjectsList[i] as TCheckBox).Click;
   end;
end;

procedure TCheckBox.Toggle;
begin
   inherited;
   Click;
end;

Наличие в строке Self.Checked and Self.Enabled проверки на Enabled не случайно. Переводя чекбокс в состояние Checked = False, мы деактивируем "контролируемые" компоненты, среди которых также могут быть чекбоксы со своими "контролируемыми" компонентами (многоуровневая организация). При этом чекбокс может быть включен (Checked = True), но его "контролируемые" компоненты все равно должны быть отключены. Именно тут и играет определяющую роль and Self.Enabled.

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

procedure TForm1.FormCreate(Sender: TObject);
begin
   // Настройка фильтров идет от частного к общему
   // Сначала настройка вложенных CheckBox'ов:
   // "Edit", "Button", "ComboBox", "RadioButtons", "DateTimePicker"
   cbEdit.LinkedObject := Edit1;
   cbButton.LinkedObject := Button3;
   cbComboBox.LinkedObject := ComboBox1;
   cbDateTimePicker.LinkedObject := DateTimePicker1;

   cbRadioButtons.LinkedObjectsList.Add(RadioButton1);
   cbRadioButtons.LinkedObjectsList.Add(RadioButton2);
   cbRadioButtons.LinkedObjectsList.Add(RadioButton3);
   cbRadioButtons.LinkedObjectsList.Add(RadioButton4);

   // Затем настройка общего CheckBox'а:
   // "Включить фильтры"
   cbFilters.LinkedObjectsList.Add(cbEdit);
   cbFilters.LinkedObjectsList.Add(cbButton);
   cbFilters.LinkedObjectsList.Add(cbComboBox);
   cbFilters.LinkedObjectsList.Add(cbRadioButtons);
   cbFilters.LinkedObjectsList.Add(cbDateTimePicker);
end;

На этом все, удобного вам программирования!

.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна