Grid с человеческим лицом
Автор: Пляко Андрей
Специально для Королевства Delphi
Сетка (Grid) — очень удобный компонент для представления данных. К несчастью, внешний вид Borland'овских сеток крайне непригляден; да и работать с ними пользователю, подчас, неудобно. Я выделил два критерия, которым, на мой взгляд, должна удовлетворять хорошая сетка:
- Сетка должна "покрывать" весь компонент; компонент не должен быть шире представляемой им сетки. Другими словами, в сетке должна быть (хотя бы одна) растягиваемая колонка, ширина которой меняется при изменении размеров сетки (например, при resize'е формы).
- При изменении пользователем ширины колонки, должна соответствующим образом меняться ширина смежной с ней колонки, так чтобы создавалось впечатление, будто пользователь просто передвинул "границу" между двумя колонками.
Пример такой сетки представлен в проекте GridResize.
Так как многие пользуются не стандартными сетками, а каким-то их модификациями, то вместо написания очередного компонента на базе TStringGrid, я решил написать специальный модуль (VCLRoutine) и продемонстрировать в проекте GridResize, как с его помощью можно сделать любую сетку хорошей. То есть, воспользовавшись модулем VCLRoutine вы можете привить своей любимой сетке либо способность растягиваться на всю доступную ширину, либо возможность по-умному изменять ширину колонки, либо и то и другое. Например, все это можно проделать с DataAware сетками; пример такой модификации стандартной сетки TDBGrid представлен в проекте GridDBResize. Оба демонстрационных проекта (под Delphi 5) расположены здесь.
Кроме того, модуль VCLRoutine содержит еще несколько полезных процедур, которые, впрочем, никак не задействованы в проектах Grid[DB]Resize, но которые могут понравится и/или пригодится читателю. В меру возможностей код модуля снабжен пояснительными комментариями.
В последующих двух подразделах приводится описание модуля VCLRoutine и дается комментарий по наиболее сложным кускам кода проекта GridResize.
Модуль VCLRoutine
-
ResizeCustomGrid (aGrid: TCustomGrid;
SizeColumn: Integer=-1) Процедура изменяет ширину колонки с номером
SizeColumn так, чтобы сетка "покрывала" весь компонент. Если
SizeColumn не указан, то растягиваться будет последняя колонка
сетки. Если растягиваемая колонка не видна на экране, то ее ширина не
изменяется. Вызов этой функции логично производить при изменении ширины сетки;
например, в обработчике события OnResize формы.
-
ResizeCustomDBGrid (aGrid:
TCustomDBGrid; SizeColumn: Integer=-1) Это DB Aware аналог процедуры
ResizeCustomGrid ; в ней просто учитывается
возможное наличие "индикатора" dgIndicator in Options
-
ResizeStatusBar (aBar: TStatusBar;
DontResize: SetOfByte=[]) Процедура делает ширину всех панелей
TStatusBar одинаковыми; ширина панелей, номера которых указаны в множестве
DontResize остается прежней. Эту процедуру тоже логично вызывать из
обработчика события OnResize .
-
CanResizeCustomGridColumn (aGrid:
TCustomGrid; const x,y: Integer): Integer Эта функция позволяет
узнать, может ли пользователь вручную изменить ширину колонки, если курсор мышки
имеет координаты x,y . Функция возвращает номер колонки, размер
которой будет изменяться; функция возвращает -1 , если пользователь
не может изменить ширину никакой колонки. Пояснения по использованию этой
функции даны в следующем подразделе.
-
CanResizeCustomDBGridColumn (aGrid:
TCustomGrid; const x,y: Integer): Integer Это DB Aware аналог
процедуры CanResizeCustomGridColumn
-
SwapRows (aGrid: TStringGrid; const
Row1, Row2: Integer) Простенькая процедура; она меняет местами две
строки (Row1 и Row2 ) сетки TStringGrid
Комментарии к проекту 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;
|
Вот мы и получили хорошую сетку! Разумеется, если мы предполагаем пользоваться такой сеткой во многих проектах; то имеет смысл сделать собственный компонент, применив продемонстрированную выше технику.
|