Версия для печати


Grid с человеческим лицом
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=667

Андрей Пляко
дата публикации 19-09-2002 10:53

Grid с человеческим лицом

    Сетка (Grid) — очень удобный компонент для представления данных. К несчастью, внешний вид Borland'овских сеток крайне непригляден; да и работать с ними пользователю, подчас, неудобно. Я выделил два критерия, которым, на мой взгляд, должна удовлетворять хорошая сетка:

  1. Сетка должна "покрывать" весь компонент; компонент не должен быть шире представляемой им сетки. Другими словами, в сетке должна быть (хотя бы одна) растягиваемая колонка, ширина которой меняется при изменении размеров сетки (например, при resize'е формы).

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

Пример такой сетки представлен в проекте GridResize.

    Так как многие пользуются не стандартными сетками, а каким-то их модификациями, то вместо написания очередного компонента на базе TStringGrid, я решил написать специальный модуль (VCLRoutine) и продемонстрировать в проекте GridResize, как с его помощью можно сделать любую сетку хорошей. То есть, воспользовавшись модулем VCLRoutine вы можете привить своей любимой сетке либо способность растягиваться на всю доступную ширину, либо возможность по-умному изменять ширину колонки, либо и то и другое. Например, все это можно проделать с DataAware сетками; пример такой модификации стандартной сетки TDBGrid представлен в проекте GridDBResize. Оба демонстрационных проекта (под Delphi 5) расположены здесь.

    Кроме того, модуль VCLRoutine содержит еще несколько полезных процедур, которые, впрочем, никак не задействованы в проектах Grid[DB]Resize, но которые могут понравится и/или пригодится читателю. В меру возможностей код модуля снабжен пояснительными комментариями.

    В последующих двух подразделах приводится описание модуля VCLRoutine и дается комментарий по наиболее сложным кускам кода проекта GridResize.

Модуль VCLRoutine

Комментарии к проекту GridResize

    Процедура ResizeCustomGrid позволяет легко удовлетворить первому критерию хорошей сетки; достаточно вот так описать обработчик события OnResize формы:

procedure TGridForm.FormResize(Sender: TObject);
begin
  // Растягиваем последнюю колонку
  ResizeCustomGrid(MyGrid);
end;

Если бы сетка MyGrid "лежала" не на форме, а на панели (TPanel) или еще каком-нибудь контейнере, то мы бы делали вызов ResizeCustomGrid в обработчике события OnResize этого контейнера. С этим все ясно.

    А вот с задачей "по-умному изменять ширину колонок" придется повозиться. Когда пользователь нажмет левую клавишу мышки, то функция CanResizeCustomGridColumn позволит нам узнать начинает ли пользователь растягивать/сужать колонку, или же нет. К сожалению, событие OnMouseDown у сетки не возникает, если щелчок происходит на "шапке" сетки. А так как нас интересуют именно щелчки в области "шапки", то вместо использования события OnMouseDown нам придется напрямую обрабатывать Windows-сообщение WM_LBUTTONDOWN, которое возникает при нажатии пользователем левой клавиши мышки.

    Для этого нам надо будет "подменить" функцию WindowProc, отвечающую за обработку Windows-сообщений. Делается это так: в private секции формы заводится переменная типа TWndMethod (эта переменная будет хранить ссылку на "старую" WindowProc сетки) и описывается процедура, которая станет новой WindowProc:

TGridForm = class(TForm)
// [ ... Skiped ... ]
private { Private declarations }
// [ ... Skiped ... ]
  FWndOrigin: TWndMethod;  // WindowProc сетки MyGrid
  // Подложная WindowProc сетки MyGird, обрабатывает WM_LBUTTONDOWN
  procedure FGridDownMtd(var Msg: TMessage);
end;
Сама "подмена" осуществляется в обработчике OnCreate формы:
procedure TGridForm.FormCreate(Sender: TObject);
begin
// [ ... Skiped ... ]
  // Подмена WindowProc
  FWndOrigin := MyGrid.WindowProc;
  MyGrid.WindowProc := FGridDownMtd;
end;

Все, теперь обработку Windows-сообщений сетки осуществляет метод FGridDownMtd. Эта процедура имеет следующую структуру:

procedure TGridForm.FGridDownMtd(var Msg: TMessage);
begin
  if MSg.Msg = WM_LBUTTONDOWN then
    begin
// [ ... Skiped ... ]
    end;
  // Передаем сообщение изначальной WindowProc
  FWndOrigin(Msg);
end;

То есть наша процедура в обязательном порядке "доверяет" обработку Windows-сообщения старому обработчику, ссылку на который мы сохранили в FWndOrigin. Но если на обработку пришло сообщение WM_LBUTTONDOWN, то FGridDownMtd предварительно проделает некоторые дополнительные действия.

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

TGridForm = class(TForm)
// [ ... Skiped ... ]
private { Private declarations }
  FResizeColumn: Integer;  // Номер resize'уемой колонки
  FOriginSize: Integer;    // Ее изначальная ширина
  FResizeSum: Integer;     // Сумма ширин resize'уемой и смежной колонок
  FWndOrigin: TWndMethod;  // WindowProc сетки MyGrid
  // Подложная WindowProc сетки MyGird, обрабатывает WM_LBUTTONDOWN
  procedure FGridDownMtd(var Msg: TMessage);
end;
инициируем их при создании формы:
procedure TGridForm.FormCreate(Sender: TObject);
begin
  // Инициализация переменных
  FResizeColumn := -1;
  FResizeSum := 0;
  FOriginSize := 0;
  // Подмена WindowProc
  FWndOrigin := MyGrid.WindowProc;
  MyGrid.WindowProc := FGridDownMtd;
end;
и соответствующим образом опишем процедуру FGridDownMtd:
procedure TGridForm.FGridDownMtd(var Msg: TMessage);
begin
  if MSg.Msg = WM_LBUTTONDOWN then
    begin
     // Смотрим, какую колонку при этом будем растягивать
     FResizeColumn :=
       CanResizeCustomGridColumn(MyGrid, Msg.LParamLo, Msg.LParamHi);
     // Если какую-то, то запоминаем параметры изменяемых колонок
     if FResizeColumn>=0 then
        with MyGrid do
         begin
           FOriginSize := ColWidths[FResizeColumn];
           FResizeSum := FOriginSize;
           // Если risize'аем не последнюю колонку, то...
           if ColCount>FResizeColumn+1 then
              // ...прибавляем к FResizeSum ширину смежной (справа) колонки
              inc(FResizeSum, ColWidths[FResizeColumn+1]);
         end;
    end;
  // Передаем сообщение изначальной WindowProc
  FWndOrigin(Msg);
end;

    Осталось совсем простая задача: правильно отреагировать на отжатие клавиши мыши; для этого вполне походит событие OnMouseUp. В обработчике этого события нам надо так изменить ширину смежной колонки, чтобы FResizeSum (сумма растянутой и смежной колонок) осталось прежней. Добавим к этому возможность отменить действия пользователя, и в результате получим вот такой код:

procedure TGridForm.MyGridMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var NewWidth: Integer;
begin
  // Если мы перетаскиваем какую-нибудь колонку, то...
  if FResizeColumn>=0 then
     with Sender as TStringGrid do
      begin
       // Если это не последняя колонка, то
       if FResizeColumn < ColCount-1 then
         begin
          // Смотрим, какой размер должен быть у смежной колонки
          NewWidth := FResizeSum-ColWidths[FResizeColumn];
          // Если отрицательный
          if NewWidth<=0 then // то отменяем действие пользователя
               ColWidths[FResizeColumn] := FOriginSize
               // иначе, модифицируем ширину смежной колонки
          else ColWidths[FResizeColumn+1] := NewWidth;
         end;
       // На всякий случай выравниваем ширину последней колонки
       ResizeCustomGrid(Sender as TStringGrid);
      end;
  // Сбрасываем флаг перетаскиваемой колонки
  FResizeColumn := -1;
end;

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

Исходники проекта GridResize и GridDBResize (Zip-архив 11K)

Пляко Андрей
Специально для Королевства Delphi