Многострочные заголовки
Рисование многострочных заголовков с использованием стандартного компонента TDBGrid.
При использовании стандартного компонента TDBGrid для рисования доступна только область данных колонок, изначально не включающая в себя фиксированные области TDBGrid, рисующиеся самим компонентом. Зная тот факт, что при событиях рисования доступна вся клиентская область окна, можно попробовать обмануть компонент и рисовать в другой области, чем та, которая передается процедуре рисования. Так как событие OnDrawCell вызывается для каждой ячейки Grid'а, а заголовки желательно рисовать один раз, заводим массив признаков нарисованных заголовков:
GridTitles : : array of Boolean;
Обработчик события OnDrawColumnCell выглядит достаточно просто:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); begin if not GridTitles[Column.Index] then DrawGridTitle(Column.Index); end;
Если заголовок колонки не нарисован, то нарисовать его. Процедура рисования должна определить координаты области заголовка и ее размеры и заново перерисовать эту область. Сама процедура оформлена как локальная, для того, чтобы не передавать параметры, переданные обработчику события. Для простоты заголовок делается двухстрочным, но ничего не мешает рисовать произвольное количество строк. RowCount объявлено константой и равно 2.
procedure DrawGridTitle(ColIndex : Integer); var Titles : array[1..RowCount] of String; ARect : TRect; { Собственно область заголовка } RH : Integer; { Высота области заголовка } BlankPos : Integer; { Позиция разбиения заголовка } begin BlankPos := Pos(' ', Column.Title.Caption); if BlankPos <> 0 then begin { Рисуем многострочный заголовок только для тех колонок, у которых есть пробел в названии. Заголовки остальных колонки DBGrid нарисует сам. } Titles[1] := Copy(Column.Title.Caption, 1, BlankPos-1); Titles[2] := Copy(Column.Title.Caption, BlankPos+1, Length(Column.Title.Caption) - BlankPos); RH := RectHeight(Rect); { В прямоугольнике Rect передаются координаты текущей ячейки, область для рисования заголовка можно получить, указывая в качестве вертикальной координаты 0. Высота области рисования сейчас равна высоте стандартной ячейки DBGrid, как раз на одну строку заголовка. } SetRect(ARect, Rect.Left, 0, Rect.Right, RH); InflateRect(ARect, -2, -2); { Поправка на окантовку Titles } Dec(RH, 2); { Смещение для отступа текста от края по вертикали } with DBGrid1.Canvas do begin Brush.Color := DBGrid1.FixedColor; FillRect(ARect); { Залить область заголовка, стерев все, что там нарисовано DBGrid'ом } { Рисование первой строки в заголовке } ARect.Bottom := RH; DrawText(Handle, PChar(Titles[1]), -1, ARect, DT_CENTER or DT_SINGLELINE); { Рисование второй строки в заголовке, предварительно сместив область рисования вниз на размер строки. } OffsetRect(ARect, 0, RH-2); DrawText(Handle, PChar(Titles[2]), -1, ARect,DT_CENTER or DT_SINGLELINE); end; end; GridTitles[ColIndex] := true; //Нарисовали заголовок для этой колонки end;
Высота любой строки любого наследника TCustomGrid определяется свойством RowHeights[номер строки]. Так как это свойство объявлено protected, для того, чтобы высота области заголовков DBGrid'а была большая, чем стандартная, используется обычный прием доступа к защищенным свойствам компонента, с описанием наследника от требуемого класса и повышением области видимости требуемого свойства:
type THackGrid = class(TCustomGrid) public property RowHeights; end;
Высоту области надо задать один раз, что и делается в обработчике события FormShow
procedure TForm1.FormShow(Sender: TObject); var .... H : Integer; { Определение необходимой высоты строки для многострочных заголовков } H := DbGrid1.Canvas.TextHeight('gW'); THackGrid(DBGrid1).RowHeights[0] := (H + 2) * RowCount; { RowCount принудительно объявлено 2 } end;
Результат работы:
После первого запуска программы обнаружен интересный эффект - при переключении на другое окно и обратном переключении на окно с Grid'ом многострочность заголовков пропадает. Аналогичным образом она пропадает при перемещении по гриду с помощью вертикального и горизонтального ScrollBar'ов. Для события переключения окна положение можно исправить, указав необходимость перерисовки заголовков в событии FormActivate, со ScrollBar'ами бороться придется подменой оконной процедуры DBGrid'а. Сделаем метод формы, сбрасывающий признаки рисования у всех заголовков:
procedure TForm1.InvalidateGridTitles; var I : Integer; begin for I:=0 to Pred(DBGrid1.Columns.Count) do GridTitles[I] := false; end;
И будем вызывать его каждый раз, когда потребуется полная перерисвока заголовков.
procedure TForm1.FormActivate(Sender: TObject); begin InvalidateGridTitles(); end;
И в подмененной оконной процедуре DBGrid'а:
procedure TForm1.GridWndProc(var Message: TMessage); begin case Message.Msg of WM_ERASEBKGND, WM_VSCROLL: InvalidateGridTitles(); WM_HSCROLL: begin InvalidateGridTitles(); // сожалению, приходится мириться с необходимостью перерисовки всего // DBGrid'а при горизонтальном скроллинге, иначе, все усилия по рисованию // многострочных заголовков пропадают :-( InvalidateRect(GridWnd, nil, true); end; end; with Message do Result := CallWindowProc(OldWndProc, GridWnd, Msg, wParam, lParam); end;
В первом варианте при обработке собщения WM_HSCROLL не был написан код для перерисовки всего окна DBGrid. Как я ни старался, победить ситуацию пропадания многострочных заголовков мне не удалось, поэтому и был добавлен код, принудительно перерисовывающий все окна DBGrid.
Рисование многострочных заголовков с использованием наследника компонента TDBGrid.
В отличие от рисования нестандартных заголовков при использовании стандартного компонента TDBGrid, в наследнике такое рисование выполняется проще, так как в компоненте есть виртуальный метод DrawCell, который вызывается для всех ячеек грида, а не только для содержащих данные. Рисование нестандартных заголовков в этом случае выполняется в перекрытом методе DrawCell в наследнике.
Кроме того, так как метод DrawCell вызывается гридом при любой его перерисовке, затрагивающей клиентскую область окна, нет нужды отслеживать, какие заголовки были нарисованы или обновлять все окно грида при скроллинге. Наше рисование будет вызвано только тогда, когда возникнет реальная необходимость в отрисовке области заголовков грида.
procedure THSDBGrid.DrawCell(ACol, ARow: Integer; ARect: TRect; State: TGridDrawState); var TitleText : String; { Полный заголовок } Titles : array of String; { Части заголовка }
{ Разбиение полного заголовка на части с возвращением числа получившихся частей } function SplitTitle : Integer; const TitleSeparator = ' '; { Можно этот символ вынести в published property } var CurPos, J: Integer; CurStr: string; begin SetLength(Titles, FTitleLines); { Определяем, сколько реально строк присутсвует в заголовке. Просто считается количество символов TitleSeparator } J := 0; CurStr:= TitleText; repeat CurPos:= Pos(TitleSeparator, CurStr); if (CurPos > 0) and (J < Pred(FTitleLines)) then begin Titles[J] := Copy(CurStr, 1, Pred(CurPos)); CurStr:= Copy(CurStr, CurPos+Length(TitleSeparator), Length(CurStr)-CurPos-Length(TitleSeparator)+1); Inc(J); end else begin Titles[J] := CurStr; if J >= Pred(FTitleLines) then { Не надо копировать больше, чем может вместить заголовок } Break; end; until CurPos=0; Result := J+1; end;
var DataCol, I, TitleParts : Integer; TextRect : TRect; LineHeight : Integer; begin if (dgTitles in Options) AND (gdFixed in State) AND (ARow = 0) AND (ACol <> 0) then begin { Должна быть нарисована ячейка заголовка } { Стандартное действие DBGrid } if csLoading in ComponentState then begin Canvas.Brush.Color := Color; Canvas.FillRect(ARect); Exit; end; DataCol := ACol; if dgIndicator in Options then Dec(DataCol); { Изменение размеров области заголовка под окантовку, если хочется сделать плоские заголовки, то InflateRect надо пропустить } if [dgRowLines, dgColLines] * Options = [dgRowLines, dgColLines] then InflateRect(ARect, -1, -1); TitleText := Columns[DataCol].Title.Caption; Canvas.Brush.Color := FixedColor; { Если захочется сделать прозрачный заголовок, то вызов FillRect надо будет пропустить } { Если будет желание рисовать фоновую картинку в области заголовка, то нарисовать ее можно здесь } Canvas.FillRect(ARect); { Теперь можно нарисовать собственно текст } Canvas.Font := Font; if FTitleLines = 1 then begin WriteText (Canvas, ARect, 1, 1, TitleText, Columns[DataCol].Title.Alignment); end else begin TitleParts := SplitTitle(); TextRect := ARect; LineHeight := RectHeight(ARect) DIV TitleParts; TextRect.Bottom := TextRect.Top + LineHeight; for I:=0 to Pred(TitleParts) do begin WriteText (Canvas, TextRect, 1, 0, Titles[I], Columns[DataCol].Title.Alignment); OffsetRect(TextRect, 0, LineHeight); end; end; { Окантовка ячейки заголовка, если хочется сделать плоские заголовки, то DrawEdge надо пропустить } if [dgRowLines, dgColLines] * Options = [dgRowLines, dgColLines] then begin InflateRect(ARect, 1, 1); DrawEdge(Canvas.Handle, ARect, BDR_RAISEDINNER, BF_BOTTOMRIGHT); DrawEdge(Canvas.Handle, ARect, BDR_RAISEDINNER, BF_TOPLEFT); end; DoDrawTitleCell (DataCol, Columns[DataCol], ARect); end else inherited; end;
Кроме того, появляется возможность вызывать пользовательское событие при рисовании области заголовков, причем после того, как заголовок уже нарисован самим компонентом.
Задание высоты заголовков в наследнике также выполняется проще, так как имеется доступ к защищенным свойствам родительского компонента.
procedure THSDBGrid.CalcTitleHeight; begin if dgTitles in Options then RowHeights[0] := (Canvas.TextHeight('gW') + 2) * FTitleLines; end;
Высоту области заголовка необходимо задавать один раз при создании окна грида и каждый раз, при изменении свойств грида, влияющих на его внешний вид. При создании окна и при изменении свойств грида вызываются виртуальные методы CreateWnd и LayoutChanged, в перекрытые версии которых добавлен вызов процедуры CalcTitleHeight.
1 2 3 4 5 6
8 8 8
| |