Разноцветный DBGrid

=== Вступление ===
Представленный материал - это пример проекта, который реализует некоторые возможности по "раскраске" компонента TDBGrid.
Проект составлен исключительно из учебных целей для публикации на сервере "Королевство Дельфи" и самостоятельной ценности не несет.
Аналогичный пример выложен на сайте www.citforum.ru/programming/ , по содержанию предлагаемый проект не повторяет его и если сложить все эти публикации, то вопрос "как раскрасить Grid" можно считать исчерпанным.
=== Общие вопросы проекта ===
Создание примера "Разноцветный Grid" продиктовано тем, что вопрос о том, как "раскрасить" строки grid'а в зависимости от каких-либо условий, является очень популярным на Круглом столе. И судя по тому, что ответ "обрабатывайте событие OnDrawColumnCell" многих не вдохновляет, тема остается открытой.
Хочется внести ясность в этот, довольно занятный, вопрос. Может после этой статьи кому-то станет легче ориентироваться в такого рода проблемах.
Итак, наш проект называется DBGridColor, в нем мы поставим несколько задач и решим их, очень надеюсь, раз и навсегда.
* Задача первая - как поменять цвет фона и цвет шрифта в строках TDBGrid. То есть как его "раскрасить" в зависимости от содержимого строк.
* Задача вторая - как поместить в поле TDBGrid вместо текста какой-либо объект (картинку или другой control).
* Задача третья - как поместить в поле TDBGrid'а произвольный текст вместо содержимого текущего поля.
* Задача четвертая - как раскрасить строки Grid'а вне зависимости от его содержимого, а просто через запись, как делается во многих отчетах для облегчения читаемости. То есть просто нарисуем полосатое окно.
* Задача пятая - нам нужно выделить несколько строк, вернее выделить их должен пользователь, а нам нужно запомнить и отобразить это выделение. Пользоваться свойством MultiSelect самого компонента мы не будем. Оно не во всех случаях годится - стоит случайно кликнуть на другой записи и все наше выделение погибнет, что не есть хорошо.
Пример и сама статья рассчитаны на начинающих программистов, так что некоторые вещи могут показаться само собой разумеющимися.
Окно программы содержит сам DBGrid, с которым мы и будем экспериментировать и три крыжика, которые переключают режимы работы с Grid'ом. Так как сам проект исключительно учебный и все описываемые задачи не приходится решать СРАЗУ в одном проекте, то эти крыжики и будут регулировать наши эксперименты. Они определяют три режима:
# Полосатое окно
# Выделение цветом
# Комментировать длину
В зависимости от состояния этих крыжиков в в проекте обрабатывается решение той или иной задачи. Это сделано только для того, чтобы не создавать для каждого примера свой проект. Подробнее смотрите исходный код.
Код, который будет приводиться в статье, несколько отличается от того, что вы увидите в проекте.
=== Раскрашиваем строку ===
Практически все действия, которые мы должны произвести, вмешиваются в процедуру прорисовки DBGrid'ом самого себя. Сделать это можно, обрабатывая событие TDBGrid.OnDrawColumnCell.
Возникает это событие тогда, когда ячейка TDBGrid нуждается в перерисовке. Подробнее об этом вы можете прочесть в help'е. Кстати, именно там, первой строкой написано, что обрабатывая это событие, вы можете обеспечить собственную прорисовку ячейки.
В обработчике этого события нам доступны:
* сам DBGrid (это не что иное, как Sender),
* текущая колонка - та, ячейка из которой сейчас перерисовывается (DataCol и Column),
* прямоугольник Rect, который содержит эту ячейку
* и ее состояние State ( выделена, в фокусе и т.д.)
Самым стандартным советом изменить цвет шрифта и фона в строке является следующий код:
procedure TForm1.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect;
 DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
 if {какое-то условие} then
 begin
 with DBGrid1.Canvas do 
 begin
 Brush.Color := clGreen;
 Font.Color := clWhite;
 FillRect(Rect);
 TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text);
 end;
 end;
end;
Что, собственно, мы делаем? По условию назначаем для канвы нашего DBGrid'а цвет заливки DBGrid1.Canvas.Brush.Color и цвет шрифта DBGrid1.Canvas.Font.Color. После чего заливаем ячейку и выводим в нее текст. Работает как часы!
;Маленькое лирическое отступление:
:По правилам хорошего тона программирования не следует обращаться к объекту по его имени, если он доступен по ссылке. То есть: обращаться к канве грида как DBGrid1.Canvas внутри обработчика его же события некорректно. Это не ошибка, но это не совсем правильно. Оценить разницу вы сможете в том случае, если у вас несколько Grid'ов с одним обработчиком или вам вдруг понадобилось переименовать его... Обратитесь к канве как TDBGrid(Sender).Canvas и это всегда будет канва того компонента, который сейчас перерисовывается, как бы он не назывался.
Код работает, строки перерисовываются, разве можно добавить что-то еще?
Можно. Как говорилось в одной известной рекламе, есть способ лучше!
Предложенный код подходит только для тех случаев, если у вас нет специального форматирования выводимого текста в полях таблицы, если у вас нет выравнивания по правому краю или центру и т.д.
Потому что методом TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text); вы рисуете в определенных координатах текст, который содержится в текущей строке текущего поля и больше ничего... Все внутреннее форматирование будет потеряно. Для его восстановления вам будет необходимо проверять, какое установлено выравниваение и т.д., самим пересчитывать координаты вывода текста в прямоугольнике. В общем, это не сложная задача, но только зачем делать то, что прекрасно умеет делать сам DBGrid ?
Дадим ему Ценные Указания и пусть рисует, а уж это он умеет делать хорошо!
 
procedure TformColorGrid.DBGridDrawColumnCell(Sender: TObject; const Rect: TRect;
 DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
 // Даем DBGrid'у ЦУ 
 if {какое-то условие} then
 begin
 TDBGrid(Sender).Canvas.Brush.Color := clGreen;
 TDBGrid(Sender).Canvas.Font.Color := clWhite;
 end;
 // если строка была выделена, оставляем "подсвеченные" цвета 
 if gdSelected in State then
 begin
 TDBGrid(Sender).Canvas.Brush.Color := clHighLight;
 TDBGrid(Sender).Canvas.Font.Color := clHighLightText;
 end;
 // А теперь пусть он рисует сам !
 TDBGrid(Sender).DefaultDrawColumnCell(Rect, DataCol, Column, State);
end;
Поначалу даем DBGrid'у Ценные Указания, то есть назначаем его канве нужные свойства. Проверка if gdSelected in State необходима, чтобы отследить выделение позиции курсора, иначе выделенная сейчас строка будет перекрашена нами. Это не только неверно, но и некрасиво. Попробуйте убрать эту проверку и сами в этом убедитесь.
И, наконец, последний штрих - вызов метода TDBGrid.DefaultDrawColumnCell, который заставляет DBGrid отрисовать указанную ячейку со всеми настройками DBGrid'а, но только с нашими цветами на канве!
И все! И никаких проблем с выравниванием текста в строках, все что нужно рисовать, компонент рисует сам, а мы только "подсказали" ему цвет.
Если нужно перекрасить не всю строку, а только отдельную ячейку, то в проверку вашего условия необходимо включить проверку текущего поля таблицы, имя которого лежит в Column.FieldName.
Результаты работы аналогичного кода можно увидеть на рис.1 и рис.2 (раскрашена вся строка). На рис.3 раскрашена конкретная ячейка и шрифт меняет стиль (добавляется Bold).
=== Картинка и CheckBox в поле TDBGrid ===
На канву DBGrid'а можно выводить не только текст, но рисовать другие объекты.
Часто для поля, которое принимает логическое значение, хочется видеть стандартный компонент TCheckBox. Чтобы поместить его туда , воспользуемся все тем же событием перерисовки ячейки. В наш код для TformColorGrid.DBGridDrawColumnCell нужно добавить еще кусочек. Теперь нам понадобиться определить, какая именно колонка рисуется и выбрать соответствующее действие :
 
procedure TformColorGrid.DBGridDrawColumnCell(Sender: TObject; const Rect: TRect;
 DataCol: Integer; Column: TColumn; State: TGridDrawState);
var
 Style: Integer; 
begin
 if {рисуется колонка, в которую мы хотим поместить TCheckBox} then
 begin
 if {значение поля TRUE} then
 Style := DFCS_CHECKED
 else
 Style := DFCS_BUTTONCHECK;
 end;
 DrawFrameControl(TDBGrid(Sender).Canvas.Handle, Rect, DFC_BUTTON, Style);
end;
Функция DrawFrameControl рисует на канве в определенном прямоугольнике стандартный windows-control, тип и состояние которого определяется передаваемыми параметрами.
BOOL DrawFrameControl(
 HDC hdc, 	// handle to device context
 LPRECT lprc,	// pointer to bounding rectangle
 UINT uType,	// frame-control type
 UINT uState	// frame-control state
 ); 
Подробнее о том, что еще может рисовать эта функция, смотрите help по Windows API (или MSDN или win32.hlp в поставке Delphi).
Результаты работы этого кода смотрите на Рис. 1. Строки grid'а содержат в первой колонке CheckBox'ы и выделенные строки отмечены крыжиками. Работает такая конструкция достаточно естественно.
Рис. 1
Поместить в поле DBGrid'а картинку тоже не составляет особого труда.
Картинку удобнее хранить в ImageList. Особенно если их несколько для разных случаев.
И вновь наше любимое событие...
 
procedure TformColorGrid.DBGridDrawColumnCell(Sender: TObject; const Rect: TRect;
 DataCol: Integer; Column: TColumn; State: TGridDrawState);
var
 ImageIndex: Integer; 
begin
 if // рисуется колонка, в которую мы хотим поместить картинку 
 // и выполняется условие для рисования картинки
 then
 begin
 ImageIndex := 1; { получим значение номера картинки в ImageList }
 // А теперь пусть ImageList нарисует ее на канве DBGrid'а 
 ImageList.Draw(TDBGrid(Sender).Canvas, Rect.Left, Rect.Top, ImageIndex);
 end;
end;
На рис.2 показано, как картинки выводятся для каждой строки определенных полей и по некоторому условию.
Рис. 2
"Почему здесь используется метод рисования ImageList, а не метод канвы DBGrid'а ?", спросите вы. Причин несколько, во-первых не надо "вытаскивать" из списка ImageList сам рисунок, чтобы подставить его как параметр в метод TCanvas.Draw, во-вторых, такой способ работает достаточно быстро и сохраняет свойство прозрачности картинки.
Если кому-то не нравится, можете попробовать оба эти способа и выбрать тот, который покажется более естественным.
Когда вы рисуете картинку таким способом в ячейке DBGrid'а, которая содержит длинный текст, то он будет "просвечиваться" по краям ячейки. Что, в общем-то, совершенно естественно, так как мы просто рисуем поверх поля. Чтобы устранить эту неприятность можно перед вызовом ImageList.Draw залить ячейку цветом фона.
Например так:
TDBGrid(Sender).Canvas.FillRect(Rect);
Но это не единственный способ! Те, кто не любит экзотику, следующий абзац могут не читать. :о)
Есть такой класс TField, у него есть интересное событие TField.OnGetText. Происходит это событие тогда, когда требуется обновить содержимое текущей ячейки на экране. Ясно, что это событие возникает гораздо реже, чем перерисовка ячейки DBGrid. Сомневающимся предлагаю поэкспериментировать.
В обработчике этого события нам доступны само поле Sender, текст Text, который будет выводиться в этом поле и признак DisplayText.
Читая help, видим, что переопределив этот обработчик мы полностью контролируем что именно будет выводиться в поле и запрещаем автоматический вывод его значения из таблицы. Таким образом, просто переопределив пустой обработчик этого события, мы запрещаем выводить текст в поле, в котором собираемся рисовать картинку !
procedure TformColorGrid.TableBioGraphicGetText(Sender: TField;
 var Text: String; DisplayText: Boolean);
begin
end;
Вот и все. Канву можно не беспокоить.
Если вы хотите одновременно раскрашивать строки и помещать в них другие объекты, помните, что важно не перепутать порядок следования вызовов DrawFrameControl, ImageList.Draw и DefaultDrawColumnCell. Иначе получится несколько не то...
Каким именно должен быть этот порядок, решите сами :о).
=== Подмена текста или "Какой длины рыба?" ===
Довольно часто возникает задача подменить значение в поле DBGrid'а другим текстом. Например в таблице хранится числовое значение, которое пользователю мало о чем говорит и нужно заменить его чем-то более естественным.
По аналогии со всеми выше приведенными примерами хочется вернуться к событию TDBGrid.OnDrawColumnCell. Кажется чего проще, нарисуем на канве нужный нам текст и все готово!
Действительно, такой вариант среди советов по этой теме самый стандартный, но ... не самый лучший.
Рис. 3
На рис.3 в поле "Длина (см)" вместо числового значения (см. рис.1 и рис.2) приведен текстовый вариант осмысления длины рыбы. :о)
Итак, нам нужно вместо числа сантиметров написать, длинная эта рыба или короткая. Вспомним только что упомянутое событие TField.OnGetText - это как раз то, что нам нужно! Просто и, главное, быстро! Не нужно перерисовывать канву ячейки при каждом передвижении курсора по DBGrid'у.
procedure TformColorGrid.TableBioLengthcmGetText(Sender: TField;
 var Text: String; DisplayText: Boolean);
begin
 if Sender.AsFloat > 100 then
 Text := 'Длинная рыба'
 else
 Text := 'Короткая рыба';
end;
На мой взгляд так намного изящнее :о)
Вот, собственно, и все на тему подмены текста.
Для любознательных предлагается поэкспериментировать с этим событием при одновременном использовании раскраски шрифта в строке и разных значениях свойства TDBGrid.DefaultDrawing.
=== Полосатое окно ===
Нужно раскрасить DBGrid вне зависимости от данных в строках, а просто через одну, как часто делается в отчетах для повышения читаемости большого количества строк. Эта задача очень легко решается для локальных баз данных но, к сожалению, для SQL-СУБД такое же решение применить невозможно.
Важно понять, что сам компонент TDBGrid не хранит данных и ничего не знает о текущих строках своего DataSource, он их только отображает на форме.
Воспользуемся свойством RecordNo для класса TDataSet, оно отдает номер текущей записи в наборе данных (работает только для локальных СУБД, в остальных случаях равно -1 ).
Алгоритм раскраски очень прост: каждую нечетную строку перекрашиваем в нужный нам цвет, а четную строку оставляем такой, какая она есть. Для этого мы, как уже делали неоднократно, переопределяем событие TDBGrid.OnDrawColumnCell.
procedure TformColorGrid.DBGridDrawColumnCell(Sender: TObject; const Rect: TRect;
 DataCol: Integer; Column: TColumn; State: TGridDrawState);
begin
 // Красим нечетные строки
 if (TDBGrid(Sender).DataSource.DataSet.RecNo mod 2) = 1 then
 TDBGrid(Sender).Canvas.Brush.Color := RGB($CC, $CC, $99);
 // Восстанавливаем выделение текущей позиции курсора
 if gdSelected in State then
 begin
 TDBGrid(Sender).Canvas.Brush.Color := clHighLight;
 TDBGrid(Sender).Canvas.Font.Color := clHighLightText;
 end;
 // Просим GRID перерисоваться самому
 TDBGrid(Sender).DefaultDrawColumnCell(Rect, DataCol, Column, State);
end;
Результат работы этого кода виден на Рис.1
Итак, все строки аккуратно раскрашены через одну - задача решена.
Как уже отмечалось, такой вариант годится исключительно для локальных таблиц, для которых легко получить номер текущей строки в наборе данных.
Для SQL-баз можно, например, добавить вычисляемое поле, которое и будет содержать значение номера строки по порядку ( что никакого отношения к физическому номеру строки не будет иметь). То есть необходимо привязываться к данным, так как сам DBGrid ничего не знает о текущем состоянии своих строк.
Для малого количества записей такой способ годится, для больших наборов данных еще и перебирать их все с тем, чтобы добавить фиктивный номер, не очень удачное решение. Но другого способа нет, если я ошибаюсь, поправьте меня - с удовольствием приму исправления.
=== Как отметить несколько строк или "Не MultiSelect'ом единым..." ===
Пользователь желает выделить несколько записей в таблице, чтобы потом производить с ними какие-либо действия. Совершенно неважно, что он потом с ними будет делать, печатать или удалять, важно то, что нам их надо как-то запомнить и доходчиво отобразить.
Один из вариантов решения этой задачи - создаем список ListSelect: TList; В этот список будем заносить идентификатор отмеченной записи. Ясно, что это должно быть какое-то уникальное значение, в нашем случае используем поле Species No, содержащее специфический номер записи.
Так как сам факт выделения записи имеет смысл только в текущем сеансе работы с программой, то в нашей таблице нет специального поля, в котором можно было бы хранить результат этого выделения. В проекте создается вычисляемое поле с названием Check, в нем мы ничего не будем хранить, оно необходимо не для этого, а для отрисовки в нем контрола TCheckBox.
Добавлять записи в список "выбранных" будем по клику мышки на CheckBox'е в текущей строке, что на самом деле соответствует клику на компоненте DBGrid в нашем поле Check. При клике на строке, котрой еще нет в списке, она туда помещается, если строка там уже есть, они удаляется из списка. Имитация стандартного поведения переключателя True/False.
Переопределяем событие TDBGrid.OnCellClick:
procedure TformColorGrid.DBGridCellClick(Column: TColumn);
var
 Value: Integer;
begin
 // Определяем, нужная ли это колонка 	
 if CompareText(Column.Field.FieldName, 'Check') = 0 then
 begin
 Value := Column.Field.DataSet.FieldByName('Species No').AsInteger;
 // Проверяем, если ли уже такой номер в списке 
 // и если нет - добавляем , а если есть - удаляем его из списка.
 if ListSelect.Count > 0 then
 begin
 if ListSelect.IndexOf(Pointer(Value)) >= 0 then
 ListSelect.Delete(ListSelect.IndexOf(Pointer(Value)))
 else
 ListSelect.Add(Pointer(Value));
 end
 else
 begin
 ListSelect.Add(Pointer(Value));
 end;
 end;
 // Обновляем grid и отображаем количество выбранных записей
 Column.Grid.Refresh;
 ShowStatusBar;
end;
Теперь у нас есть информация о том, какие записи выделены. Осталось только это отобразить. В каждой строке вместо поля Check нужно нарисовать стандартный CheckBox и поставить в нем крыжик, если эта запись входит в список ListSelect. Для этого немного модифицируем уже известный нам код:
procedure TformColorGrid.DBGridDrawColumnCell(Sender: TObject; const Rect: TRect;
 DataCol: Integer; Column: TColumn; State: TGridDrawState);
var
 Style: Integer; 
begin
 if СompareText(Column.FieldName, 'Check') = 0 then
 begin
 if ListSelect.IndexOf(Pointer(TDBGrid(Sender).DataSource.DataSet.FieldByName('Species No').AsInteger)) >= 0 then
 Style := DFCS_CHECKED
 else
 Style := DFCS_BUTTONCHECK;
 end;
 DrawFrameControl(TDBGrid(Sender).Canvas.Handle, Rect, DFC_BUTTON, Style);
end;
Довольно просто не правда ли? Теперь по этому списку можно спокойно обрабатывать выделенные записи, в отличие от использовании свойства TDBGrid.MultiSelect, выделенные строки не перестанут быть таковыми при следующем случайном клике мышкой.
Результаты выделения строк показы на рис.1 (с помощью CheckBox) и на рис.2 (строки выделены цветом и специальной картинкой)
=== Разные мелочи ===
Код, приводимый в тексте несколько отличается от того, что есть в самом проекте, который вы скачаете. Дело в том, что проект реализует сразу все указанные возможности, а в статье мне хотелось не смешивать слишком много в одном месте, дабы не запутать тех, кто только начинает сам разбираться. Поэтому в проекте проводятся более сложные проверки при обработке события перерисовки ячейки.
Примечания к коду проекта:
* Функция ShowStatusBar выводит в статусной строке количество выделенных строк.
* Тип TColumnAction помогает аккуратно разобраться между типами колонок, для которых применяются разные методы отрисовки.
* Функция GetColumnAction нужна для получения типа колонки по ее названию.
* Компоненты CheckLarge, cbTypeSelect и cbLine нужны для переключения между режимами работы проекта.
Если не ограничивать эти режимы, то при одновременном использовании всех этих способов "выделить" что-то в DBGrid'е будет выглядеть просто ужасно.
Кстати, рис.4 можно считать иллюстрацией того, как не надо делать. Но это уже скорее вопрос проектирования пользовательского интерфейса и к нашей теме не имеет прямого отношения.
Рис. 4
Есть еще маленькое замечание: в проекте используется ограничение на уменьшение размеров формы. Несмотря на то, что начиная с Delphi 4 такое ограничение очень легко выполняется с помощью свойства Constraints для формы, в примере приводится способ перехвата системного сообщения WM_GETMINMAXINFO.
Просто он работает более красиво, чем Constraints. Изменения размера окна не просто игнорируются, а именно запрещаются, то есть нельзя даже попробовать заехать за ограничения.
Ну это уже на любителя, так... всякие мелочи :о)
В архиве содержатся файлы проекта и exe-файл. Проект ориентирован на стандартную демонстрационную базу данных Paradox, поставляемую с Delphi, которая находится в каталоге $(DELPHI)\Borland Shared\Data.
Проект настроен на алиас DBDEMOS, работа идет с таблицей biolife.db
Откомпилирован в Delphi 5
К материалу прилагаются файлы:
* Файлы проекта + exe (337 K)
* Файлы проекта (14 K)
Author: Елена Филиппова
Source: http://www.delphikingdom.com/asp/viewitem.asp?catalogid=168
Category:Базы данных
Category:DBGrid
Category:Учебники