Сортировка колонок в DBGrid

Многие профессиональные приложения отображают данные в полях табличной сетки и разрешают сортировать любую колонку, просто щелкая по ее заголовку. То, что здесь изложено – не наилучший путь для решения задачи, данная технология не что иное, как простая имитация такого поведения компонента.

Главное препятствие в решении задачи – сам DBGrid. Проблема в отсутствии событий OnClick или OnMouseDown, позволяющих реагировать на элементарные манипуляции с заголовком. Правда, существует событие OnDoubleClick, но для данной цели оно не слишком изящно. Нам нужно лишь создать заголовок, реагирующий на однократный щелчок мышью. Обратимся к компоненту THeaderControl.

Этот компонент, введенный в палитру компонентов еще в Delphi 2.0, обеспечивает необходимые нам функции. Главное достоинство – реакция компонента при щелчке по отдельным панелям. Панели также обеспечивают визуальное отображение подобно кнопке (могут вдавливаться и отжиматься). Нам необходимо «прицепить» THeaderControl к DBGrid. Вот как это сделать:

Во-первых, создайте новое приложение. Положите THeaderControl на форму. Он автоматически выровняется по верхнему краю формы. Затем поместите на форму DBGrid и присвойте свойству Align значение alClient. Затем добавьте компоненты TTable и TDataSource. В компоненте TTable присвойте свойству DatabaseName значение DBDEMOS, а свойству TableName значение EVENTS.DB. В TDataSource укажите в свойстве DataSet на компонент Table1, а в TDBGrid в свойстве DataSource на DataSource1. Если свойство Active компонента TTable равно True, выключите его (значение False).

Сделаем так, чтобы компонент THeaderControl выглядел похожим на заголовок компонента DBGrid. Произведем необходимые манипуляции в момент создания формы. Дважды щелкните на событии OnCreate формы и введите следующий код:

procedure TForm1.FormCreate(Sender: TObject);
var
  TheCap: String;
  TheWidth, a: Integer;
begin
  DBGrid1.Options := DBGrid1.Options - [dgTitles];
  HeaderControl1.Sections.Add;
  HeaderControl1.Sections.Items[0].Width := 12;
  Table1.Active := False;
  Table1.Exclusive := True;
  Table1.Active := True;
  for a := 1 to DBGrid1.Columns.Count do begin
    with DBGrid1.Columns.Items[a - 1] do begin
      TheCap := Title.Caption;
      TheWidth := Width;
    end;
    with HeaderControl1.Sections do begin
      Add;
      Items[a].Text := TheCap;
      Items[a].Width := TheWidth + 1;
      Items[a].MinWidth := TheWidth + 1;
      Items[a].MaxWidth := TheWidth + 1;
    end;
    try
      Table1.AddIndex(TheCap, TheCap, []);
    except
      HeaderControl1.Sections.Items[a].AllowClick := False;
    end;
  end;
  Table1.Active := False;
  Table1.Exclusive := False;
  Table1.Active := True;
end;
После того как THeaderControl заменил стандартный заголовок DBGrid, в первую очередь мы сбрасываем (устанавливаем в False) флаг dgTitles в свойстве Options компонента DBGrid. Затем мы добавляем колонку в HeaderControl и устанавливаем ее ширину равной 12. Это будет пустой колонкой, которая имеет ту же ширину, что и левая колонка статуса в DBGrid.

Затем нужно убедиться, что таблица открыта для эксклюзивного доступа (никакие другие пользователи работать с ней могут). Причина будет объяснена немного позже.

Теперь добавляем секции в HeaderControl. Для каждой добавленной колонки мы создаем в заголовке тот же текст, что и в соответствующей колонке DBGrid. В цикле мы проходим по всем колонкам DBGrid и повторяем текст заголовка колонки и его высоту. Мы также устанавливаем для HeaderControl значения свойств MinWidth и MaxWidth равным ширине соответствующей колонки в DBGrid. Это предохранит колонки от изменения их ширины. Для изменяющих размер колонок нужно дополнительное кодирование. Этот вопрос читателю предлагается решить самостоятельно.

Теперь самое интересное. Мы собираемся создать индекс для каждой колонки в DBGrid. Имя индекса будет таким же, как и название колонки. Данный код мы должны заключить в конструкцию try..finally, поскольку существуют некоторые поля, которые не могут быть проиндексированы (например, поля Blob и Memo). При попытке индексации этих полей генерируется исключительная ситуация. Мы перехватываем это исключение и не допускаем возможности щелчка в данной колонке. Это означает, что колонки, содержащие неиндексированные поля, не будут реагировать на щелчок мышью. Создание этих индексов объясненяет, почему таблица должна быть открыта в режиме эксклюзивного (монопольного) доступа. И в заключение мы закрываем таблицу, сбрасываем флаг эксклюзивности и снова делаем таблицу активной.

Последний шаг. При щелчке на HeaderControl нам необходимо включить правильный индекс таблицы. Создадим обработчик события OnSectionClick компонента HeaderControl как показано ниже:

procedure TForm1.HeaderControl1SectionClick(HeaderControl: THeaderControl;
                                            Section: THeaderSection);
begin
  Table1.IndexName := Section.Text;
end;
После щелчка в заголовке колонки значение свойства таблицы IndexName становится равным заголовку компонента HeaderControl.

Просто и красиво. Тем не менее, есть масса мест, требующих улучшения. Например, вторичный щелчок должен возобновлять порядок сортировки. Или возможность изменения размера самих колонок.

Улучшения
Здесь приведен код, улучшенный по сравнению с предыдущей версией «Совета», он заключается в использовании в качестве имени индекса имя поля вместо заголовка.

Это улучшает гибкость.

procedure TForm1.FormCreate(Sender: TObject);
var
  TheCap: String;
  TheFn: String;
  TheWidth: Integer;
  a: Integer;
begin
  Table1.Active := True;
  DBGrid1.Options := DBGrid1.Options - [DGTitles];
  HeaderControl1.Sections.Add;
  HeaderControl1.Sections.Items[0].Width := 12;
  for a := 1 to DBGrid1.Columns.Count do begin
    with DBGrid1.Columns.Items[a - 1] do begin
      TheFn := FieldName;
      TheCap := Title.Caption;
      TheWidth := Width;
    end;
    with Headercontrol1.Sections do begin
      Add;
      Items[a].Text     := TheCap;
      Items[a].Width    := TheWidth + 1;
      Items[a].MinWidth := TheWidth + 1;
      Items[a].MaxWidth := TheWidth + 1;
    end;
    try
{ Используем индексы с тем же именем, что и имя поля }
{ Пробуем задать имя индекса }
      (DataSource1.Dataset as TTable).IndexName := TheFn;
     except
       HeaderControl1.Sections.Items[a].AllowClick := False; { Индекс недоступен }
     end;
  end;
end; 
Используем свойство FieldName компонента DBGrid для задания индекса с тем же именем, что и имя поля.

procedure TfrmDoc.HeaderControl1SectionClick(HeaderControl: THeaderControl;
                                             Section: THeaderSection);
begin
  (DataSource1.Dataset as TTable).IndexName :=
         DBGrid1.Columns.Items[Section.Index - 1].FieldName;
end;
Примечание

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