Практика
Кастомизированный PageControl как аналог RadioGroup

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

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

:: MVP ::

:: RSS ::

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


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

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

Наиболее оптимальный для решения задачи компонент – PageControl, с возможностью многострочной организации вкладок и принципом работы, схожим с RadioGroup. Но небольшая ложка дегтя нашлась и тут. Вкладки в данной задаче совершенно ни к чему. В самом деле, не создавать же DBGrid на каждой вкладки (в данном случае это излишняя ресурсоемкость), когда можно, и даже нужно, обойтись всего одном. Сначала была идея обрабатывать событие PageControlChange и перемещать DBGrid на активную вкладку путем изменения родителя с одновременной сменой фильтра. Решение вполне рабочее, но все еще не устраивало наличие вкладок. И вот, после не долгих раздумий было решено от них избавиться, превратив PageControl в подобие RadioGroup. Вот какой в результате получился код:

type
  TPageControl = class(Vcl.ComCtrls.TPageControl)
  protected
    procedure ConstrainedResize(var MinWidth, MinHeight, MaxWidth,
      MaxHeight: Integer); override;
    procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;
  end;

{...}

implementation

{ TPageControl }

procedure TPageControl.WMPaint(var Msg: TWMPaint);
const
{$J+}
  f: Boolean = False;
{$J-}
begin
   inherited;
   if not f then
   begin
      Perform(WM_SYSCOMMAND, SC_MINIMIZE, 0);
      Perform(WM_SYSCOMMAND, SC_RESTORE, 0);
      f := True;
   end;
end;

procedure TPageControl.ConstrainedResize(var MinWidth, MinHeight, MaxWidth,
  MaxHeight: Integer);
begin
   inherited;
   MinHeight := TabRect(0).Height * RowCount + 2;
   MaxHeight := TabRect(0).Height * RowCount + 2;
end;

Обработчик ConstrainedResize понятен, высота компонента ограничивается суммарной высотой заголовков вкладок, тем самым сами вкладки просто отсекаются. Немного интереснее с обработчиком WMPaint, он появился в результате того, что нужно было применить изменения к компоненту сразу после запуска приложения, и это оказалось единственным рабочим решением, которое удалось найти.

В качестве примера рассмотрим абстрактные бухгалтерские данные о приходах и расходах:

type
  TRec = record
    fId: Integer;
    fType: string[6];
    fDate: string;
    fValue: Double;
  end;

const
  Values: array[0..29] of TRec = (
    (fId:  1; fType: 'приход'; fDate: '01.01.2017'; fValue:  51234.25),
    (fId:  2; fType: 'приход'; fDate: '02.02.2017'; fValue: 236241.11),
    (fId:  3; fType: 'расход'; fDate: '05.02.2017'; fValue:   7785.5 ),
    (fId:  4; fType: 'расход'; fDate: '05.02.2017'; fValue:  14751.75),
    (fId:  5; fType: 'приход'; fDate: '08.02.2017'; fValue: 220541.07),
    (fId:  6; fType: 'расход'; fDate: '15.02.2017'; fValue:    886.59),
    (fId:  7; fType: 'приход'; fDate: '22.02.2017'; fValue:  55689.12),
    (fId:  8; fType: 'приход'; fDate: '28.02.2017'; fValue:  53247.36),
    (fId:  9; fType: 'расход'; fDate: '02.03.2017'; fValue:   4532.9 ),
    (fId: 10; fType: 'расход'; fDate: '03.03.2017'; fValue:  84521.03),
    (fId: 11; fType: 'приход'; fDate: '08.03.2017'; fValue: 154237.5 ),
    (fId: 12; fType: 'приход'; fDate: '17.03.2017'; fValue:  45122.32),
    (fId: 13; fType: 'приход'; fDate: '20.03.2017'; fValue: 225451.66),
    (fId: 14; fType: 'расход'; fDate: '22.03.2017'; fValue:  33556.11),
    (fId: 15; fType: 'расход'; fDate: '10.04.2017'; fValue:  55487.21),
    (fId: 16; fType: 'приход'; fDate: '11.04.2017'; fValue: 547783.12),
    (fId: 17; fType: 'приход'; fDate: '19.04.2017'; fValue:  15562.3 ),
    (fId: 18; fType: 'расход'; fDate: '22.04.2017'; fValue:  85462.32),
    (fId: 19; fType: 'расход'; fDate: '01.05.2017'; fValue:   4452.45),
    (fId: 20; fType: 'приход'; fDate: '11.05.2017'; fValue: 144325.1 ),
    (fId: 21; fType: 'расход'; fDate: '13.05.2017'; fValue:  45322.54),
    (fId: 22; fType: 'приход'; fDate: '19.05.2017'; fValue:  14895.99),
    (fId: 23; fType: 'приход'; fDate: '23.05.2017'; fValue:  54795.32),
    (fId: 24; fType: 'расход'; fDate: '29.05.2017'; fValue:   1255.5 ),
    (fId: 25; fType: 'приход'; fDate: '06.06.2017'; fValue:  77652.1 ),
    (fId: 26; fType: 'приход'; fDate: '13.06.2017'; fValue:  77892.12),
    (fId: 27; fType: 'расход'; fDate: '18.06.2017'; fValue:   7451.22),
    (fId: 28; fType: 'расход'; fDate: '24.06.2017'; fValue:  15486.33),
    (fId: 29; fType: 'приход'; fDate: '06.07.2017'; fValue: 150000   ),
    (fId: 30; fType: 'приход'; fDate: '17.07.2017'; fValue:  15323.6 )
  );

Заполним этими данными таблицу:

procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
  Tab: TTabSheet;
begin
   // Добавление полей в ClientDataSet
   ClientDataSet.Close;
   ClientDataSet.FieldDefs.Clear;

   with ClientDataSet.FieldDefs.AddFieldDef do
   begin
      DataType := ftInteger;
      Name := 'ID';
      Size := 0;
   end;

   with ClientDataSet.FieldDefs.AddFieldDef do
   begin
      DataType := ftString;
      Name := 'TYPE';
      Size := 6;
   end;

   with ClientDataSet.FieldDefs.AddFieldDef do
   begin
      DataType := ftDate;
      Name := 'DATE';
      Size := 0;
   end;

   with ClientDataSet.FieldDefs.AddFieldDef do
   begin
      DataType := ftFloat;
      Name := 'VALUE';
      Size := 0;
   end;

   ClientDataSet.CreateDataSet;

   // Заполнение ClientDataSet
   for i := Low(Values) to High(Values) do
   begin
      ClientDataSet.Append;
      ClientDataSet.FieldByName('ID').AsInteger  := Values[i].fId;
      ClientDataSet.FieldByName('TYPE').AsString := Values[i].fType;
      ClientDataSet.FieldByName('DATE').AsString := Values[i].fDate;
      ClientDataSet.FieldByName('VALUE').AsFloat := Values[i].fValue;
      ClientDataSet.Post;
   end;
end;

Фильтры будут следующими - отображать все данные, только приходы или только расходы:

type
  TFilter = record
    Caption: string;
    Filter: string;
  end;

const
  Filters: array[0..2] of TFilter = (
    (Caption: 'Все';    Filter: ''),
    (Caption: 'Приход'; Filter: '[TYPE]=''приход'''),
    (Caption: 'Расход'; Filter: '[TYPE]=''расход''')
  );

Теперь в событии PageControlChange остается просто поменять фильтр в DataSet. Результат - удобное представление данных по нескольким (заранее предопределенным) фильтрам в пользовательском интерфейсе. Из преимуществ:
  • экономит много (и без того ценного) места на форме;
  • сразу видны все возможные фильтры (при многострочной организации вкладок);
  • удобно переключаться между различными фильтрами.
procedure TForm1.PageControlChange(Sender: TObject);
begin
   ClientDataSet.Filtered := False;
   if Filters[PageControl.ActivePageIndex].Filter <> '' then
   begin
      ClientDataSet.Filter := Filters[PageControl.ActivePageIndex].Filter;
      ClientDataSet.Filtered := True;
   end;
   StatusBar.Panels[0].Text := 'Записей: ' +
      IntToStr(ClientDataSet.RecordCount);
end;

На этом все, удачи!

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


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