Версия для печати
Grid с человеческим лицом
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=667Андрей Пляко
дата публикации 19-09-2002 10:53Grid с человеческим лицом Сетка (Grid) — очень удобный компонент для представления данных. К несчастью, внешний вид Borland'овских сеток крайне непригляден; да и работать с ними пользователю, подчас, неудобно. Я выделил два критерия, которым, на мой взгляд, должна удовлетворять хорошая сетка:
Пример такой сетки представлен в проекте GridResize.
Сетка должна "покрывать" весь компонент; компонент не должен быть шире представляемой им сетки. Другими словами, в сетке должна быть (хотя бы одна) растягиваемая колонка, ширина которой меняется при изменении размеров сетки (например, при resize'е формы).
При изменении пользователем ширины колонки, должна соответствующим образом меняться ширина смежной с ней колонки, так чтобы создавалось впечатление, будто пользователь просто передвинул "границу" между двумя колонками.
Так как многие пользуются не стандартными сетками, а каким-то их модификациями, то вместо написания очередного компонента на базе
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;Вот мы и получили хорошую сетку! Разумеется, если мы предполагаем пользоваться такой сеткой во многих проектах; то имеет смысл сделать собственный компонент, применив продемонстрированную выше технику.
Исходники проекта GridResize и GridDBResize (Zip-архив 11K)
Пляко Андрей
Специально для Королевства Delphi