DataGridView. Новый контрол в составе Framework 2.0. Часть 2.

Обзор архитектуры, свойств и возможностей. Практика использования.

Автор: Щербунов Нейл
Источник: RSDN Magazine #2-2006
Опубликовано: 06.06.2005
Версия текста: 1.0
"Заморозка" строк и колонок. Переупорядочение колонок.
Заголовочные ячейки
Развитое управление редактированием
Размещение в ячейке пользовательского UserControl
Постановка задачи и предварительные операции
Создание редактора объекта
Создание специализированного типа ячейки и колонки
Использование готового решения
Стили ячеек
Для чего нужны стили ячеек
Применение и наследование стилей
Формирование стиля
Уровни заголовочных ячеек
Уровни обычных ячеек
Продвинутое управление стилями ячеек
Заключение aka FAQ

Код к статье

"Заморозка" строк и колонок. Переупорядочение колонок.

Об этом новой способности DataGridView уже упоминалось вскользь в первой части статьи. Теперь я более подробно опишу, что это такое, и как может быть использовано.

Вообще способность "замораживать" или, если хотите, фиксировать некоторую часть отображаемого контента отличает довольно продвинутые редакторы электронных таблиц вроде Excel. Именно они позволяют сделать так, что часть колонок и/или строк всегда будет оставаться на дисплее. Трудно переоценить удобство подобной возможности для пользователя. Действительно, при работе с довольно "широкой" таблицей (а фиксируют, прежде всего, именно столбцы) обычно первые 2-3 столбца содержат идентифицирующую информацию по данной записи вроде "код продукта"-"наименование продукта". Следующие колонки описывают нюансы конкретного продукта – цена, запас на складе, сколько заказано и т.п. Ясно, что самое удобное – отображать все колонки на экране с целью избежать горизонтального скроллинга. К сожалению, очень часто это физически нереально даже на самых больших мониторах. Колонок слишком много, и содержимое каждой тоже весьма внушительно (по длине строки). Поэтому скроллинг неизбежен. Но, сдвигаясь вправо, чтобы увидеть колонку "цена за упаковку", мы теряем возможность понять – а о цене какого именно продукта речь-то идет? Ведь название продукта "ушло" влево, за край монитора! Вот тут фиксация колонки с названием продукта становится чрезвычайно актуальной. Аналогичные сценарии бывают и со строками – мы не хотим, чтобы первые 2-3-4 строки "уходили" вверх при вертикальном скроллинге, т.к. они являются определяющими, и нам желательно постоянно их видеть для сопоставления с прочими строками.

DataGridView, конечно, не является полноценной электронной таблицей, но кое-чему он у последних научился. В частности – «научился» замораживать свои строки и колонки. Причем с программной точки зрения это делается элементарно. Достаточно выставить свойство DataGridViewColumn.Frozen (тип bool; по умолчанию false) любой колонки в true, и она сама, а также все колонки левее неё фиксируются в клиентской части grid-а. У DataGridViewRow есть абсолютно аналогичное по названию и характеристикам свойство, которое, будучи выставленным в true, фиксирует данную строку и все строчки выше неё. Можно также выставить оба свойства в true одновременно. Это, как легко вообразить, помимо эффектов, присущих каждому из этих свойств в отдельности, создаст в левом верхнем углу грида область, нечувствительную к скроллингу. Фиксированные и обычные элементы составляют две группы. В гриде всегда представлено максимум по одной группе каждого вида. Окончание одного вида означает начало второго. Заметьте, что если свойство DataGridView.AllowUserToOrderColumns (тип bool; по умолчанию false) выставить в true, то пользователь прямо во время работы сможет менять порядок следования колонок путем перетаскивания мышкой их заголовков. Но даже при значении этого свойства true подобное изменение возможно только в пределах той группы, к которой колонка принадлежала в момент включения механизма фиксации. Перетянуть колонку в другую группу нельзя. Так что, как видите, все действительно очень несложно, а удобство может быть громадным. Единственный сопутствующий момент: неплохо бы дать пользователю "почувствовать" границу между "замороженными" элементами и обычными. Решение, лежащее на поверхности – обозначить подобную границу более "жирной" чертой. Если учесть, что любая колонка имеет свойство DataGridViewColumn.DividerWidth (тип int), а любая строчка - DataGridViewRow.DividerHeight (тип int), то и это вспомогательное решение реализуется одной строкой:

_dgv.Columns["productNameDataGridViewTextBoxColumn"].Frozen = true;
_dgv.Columns["productNameDataGridViewTextBoxColumn"].DividerWidth = 3;
_dgv.Rows[1].Frozen = true;
_dgv.Rows[1].DividerHeight = 4;

На рисунке 1 зафиксированы первые две колонки и первые две строки. При этом фиксированные колонки отделяет от обычных граница толщиной 3 пиксела, а фиксированные строчки – граница в 4 пиксела:


Рисунок 1.

Уделю пару абзацев более подробному описанию процесса изменения порядка следования колонок. В данном случае интерес представляют два свойства:Index и DisplayIndex. Оба этих свойства имеют тип int. Первое из них доступно только для чтения. Index получает свое значение в момент внесения колонки в коллекцию grid-а, и после этого его значение строго неизменно, что бы ни случилось. Второе свойство изначально, в момент отображения грида, равно по значению первому. Но его можно изменить программно или путем перетаскивания колонки мышью, если свойство DataGridView.AllowUserToOrderColumns выставлено в true, по умолчанию это не так (рисунок 2).


Рисунок 2.

Что же происходит при изменении DisplayIndex? Значение DisplayIndex-а меняется сразу у всех колонок, втянутых в процесс реорганизации. Связано это с тем, что значения DisplayIndex всех колонок в совокупности всегда образуют строгую последовательность: 0, 1, 2, 3, 4 и т.д., без пропусков или перестановок. Ну а само назначение этого свойства элементарно – узнать/установить визуальное (а не логическое, в отличие от Index) местоположение колонки среди прочих.

Заголовочные ячейки

Grid имеет заголовки строк и столбцов – RowHeaders и ColumnHeaders, соответственно. Каждый такой заголовок представляет из себя все ту же обычную ячейку, т.е. потомка класса DataGridViewCell. Заголовок строки является объектом типа DataGridViewRowHeaderCell и по умолчанию умеет помечать текущую строку треугольничком, редактируемую – карандашом, новую – звездочкой. Кроме того, в нем также вполне возможно отображение текста, хотя это и не часто встречается на практике. Заголовок же колонки будет иметь тип DataGridViewColumnHeaderCell и, напротив, почти всегда будет содержать тот или иной текст. Традиционно этот текст описывает содержимое данной колонки. Также ячейка подобного типа будет успешно показывать треугольничек, если колонка поддерживает сортировку, и она задействована, причем направление вершины треугольничка должно символизировать направление сортировки. Характерно, что в событиях, которые могут генерироваться как заголовочными, так и обычными ячейками первые всегда будут помечаться индексом -1. Например, общее событие DataGridView.CellPainting генерируется, когда ячейка (абсолютно любая) готовится к отрисовке. Если подписаться на данное событие, то в обработчик придет экземпляр объекта типа DataGridViewCellPaintingEventArgs, а в нем уже до боли знакомые свойства ColumnIndex и RowIndex. Так вот, когда будет требоваться отрисовка очередной ячейки-заголовка колонки, первое будет возвращать -1. При готовности к отрисовке ячейки типа DataGridViewRowHeaderCell то же значение вернет второе свойство. Но ведь это значит, что объект-ячейку заголовка строки нельзя извлечь традиционным способом, через коллекцию строк grid-а, а затем через коллекцию ячеек в ней:

        _dgv.Rows[1].Cells[-1].Value = "Row_Headers_1"; //так неверно!

т.к. индекс коллекции не может быть отрицательным. Именно поэтому были введено специальное свойство – HeaderCell. Такое свойство имеет и DataGridViewColumn и DataGridViewRow. Только в первом случае оно будет типа DataGridViewColumnHeaderCell, а во втором – DataGridViewRowHeaderCell. Вот через это-то свойство и можно "достучаться" до любой ячейки-заголовка:

        _dgv.Rows[1].HeaderCell.Value = "Row_Headers_1"; //а так правильно...

Содержимое ячейки определяет свойство Value. Как вы понимаете, поскольку свойство HeaderCell доступно не только для чтения, можете создать наследника стандартного типа-заголовка, реализовать нужную функциональность и внешний вид, создать экземпляр этого типа и присвоить его свойству HeaderCell. Т.е. фактически внешний вид заголовков ограничен только вашей фантазией и временем, оставшимся до сдачи проекта заказчику.

К этому моменту мы умеем обращаться абсолютно к любой ячейке grid-а за исключением одной. Той, что находится левее всех колонок и выше всех строк, т.е. по индексу -1/-1. Да-да, не удивляйтесь, grid – это ячейки, ячейки и ничего кроме. Так что там тоже находится ячейка, правда, уже типа DataGridViewTopLeftHeaderCell, наследника DataGridViewColumnHeaderCell. Как же получить доступ к этой ячейке? Кому она принадлежит – строкам или колонкам? Вопрос по зубодробительности сравним с "что главнее – яйцо или курица"? :) В MS справедливо рассудили, что главнее по любому фермер :)) поэтому доступ к этой ячейке производится напрямую из grid-а, через свойство DataGridView.TopLeftHeaderCell:

        _dgv.TopLeftHeaderCell.Value = "Sales / Person";

Такое присвоение может быть неплохим способом для пометки всей таблицы, для описания общей сути отображаемых данных и т.п.:


Рисунок 3.

Опять же не забывайте, что это обычная ячейка. Посмотрев на рисунок 3, вы можете убедиться, что для этой ячейки, как и для любой другой, можно задать свой, индивидуальный шрифт. Также, при желании, можно было бы окрасить ее в произвольный цвет, выровнять ее содержимое по правому (а не по левому) краю и т.д. Для нее, как и для всех прочих, вызывается DataGridView.CellPainting, позволяющий отрисовывать ее как угодно. Ну и, разумеется, стандартная схема "унаследуй от DataGridViewTopLeftHeaderCell и создай свою функциональность" также остается к вашим услугам.

Развитое управление редактированием

Давайте еще раз вспомним, как происходит редактирование ячейки. Во-первых, ячейка в принципе должна допускать изменение своего состояния. Из шести встроенных типов ячеек три отпадают сразу: ячейки со ссылками, кнопками и картинками вообще не позволяют пользователю редактировать себя. Оставшиеся три позволяют так или иначе это делать, хотя ячейка типа CheckBox ведет себя несколько иначе, как уже говорилось в первой части статьи.

Два оставшихся типа ячеек действительно привлекают для работы control-ы TextBox и ComboBox. После редактирования, если последнее завершилось чем угодно, кроме нажатия на ESC, генерируется событие DataGridView.CellParsing. Из переданного в обработчик этого события объекта типа DataGridViewCellParsingEventArgs можно узнать координаты (ColumnIndex и RowIndex) ячейки, инициировавшей событие. В нем же можно получить "сырое", только что введенное пользователем значение и конвертировать его сообразно обстоятельствам, чтобы оно наилучшим образом подошло соответствующей ячейке источника данных. Если же редактирование закончилось нажатием на ESC, то данное событие не генерируется, а ячейка возвращается к исходному состоянию. Все сказанное о событии CellParsing в равной степени касается всех трех редактируемых ячеек. В этом смысле CheckBox-ячейка ничем не лучше и не хуже двух своих коллег. Но на самом деле в этом разделе я хотел бы сфокусироваться не на этом событии, а на самом процессе редактирования.

А происходят при этом вещи довольно занятные. При входе в режим редактирования генерируется другое событие, DataGridView.EditingControlShowing.

Что же интересного можно узнать из переданного в обработчик данного события объекта типа DataGridViewEditingControlShowingEventArgs? Не так уж много. Всего у него 2 свойства. Свойство CellStyle (тип DataGridViewCellStyle) позволяет задать или считать стиль редактируемой ячейки. Второе свойство, Control (типа Control, доступно только для чтения), возвращает ссылку на control, используемый для редактирования ячейки. Понятно, что для текстовой ячейки реальным типом объекта будет DataGridViewTextBoxEditingControl, а для ComboBox-ячейки – DataGridViewComboBoxEditingControl.

Чтобы показать, чем может быть полезно событие DataGridView.EditingControlShowing и свойство DataGridViewEditingControlShowingEventArgs.Control, рассмотрим пусть не очень жизненную, но формально корректную задачу.

Допустим, у нас есть Grid, содержащий 3 колонки: текстовую, выпадающий список и переключатель. ComboBox-ячейки содержат списки из нескольких элементов. Нужно, чтобы в процессе выбора из списка текущий (не окончательный!) выбор пользователя отображался в текстовой ячейке слева от активной ComboBox-ячейки. Сделать это можно, подписавшись на событие ComboBox-а SelectedIndexChanged. Но как на него подписаться? Во время разработки об этом не может быть и речи – в этот момент нужный нам ComboBox неизвестен, ибо он создается по требованию (на лету) самим grid-ом. Значит, надо перехватить момент создания этого ComboBox-а, точнее, момент сразу после такого создания, чтобы получить ссылку на него. Так, а EditingControlShowing для чего?! Попробуем:

      public partial class ComboEdit : Form
{
  public ComboEdit()
  {
    InitializeComponent();
  }

  privatevoid ComboEdit_Load(object sender, EventArgs e)
  {
    ((DataGridViewComboBoxColumn)_dgv.Columns[1])
      .Items.AddRange(newstring[]
      {
        "One", "Two", "Three", "Four"
      });
    _dgv.Rows.Add(4);
    _dgv.EditingControlShowing += _dgv_EditingControlShowing;
  }

  privatevoid _dgv_EditingControlShowing(
    object sender, DataGridViewEditingControlShowingEventArgs e)
  {
    if (_dgv.CurrentCellAddress.X == 1)
    {
      DataGridViewComboBoxEditingControl combo = 
       (DataGridViewComboBoxEditingControl)e.Control;
      combo.SelectedIndexChanged += ComboBox_SelectedIndexChanged;
    }
  }

  void ComboBox_SelectedIndexChanged(object sender, EventArgs e)
  {
    _dgv.Rows[_dgv.CurrentCellAddress.Y].Cells[0].Value =
      _dgv.CurrentCell.EditedFormattedValue.ToString();
  }
} 

В момент загрузки формы помещаем во все ComboBox-ячейки список из четырех элементов и добавляем четыре же строчки в сам grid. Подписываемся на событие EditingControlShowing. Его обработчик – _dgv_EditingControlShowing – вызывается при каждой попытке пользователя распахнуть выпадающий список или произвести в нем выбор курсорными клавишами. В нем мы первым делом определяем, что редактирование началось для ячейки первой (по индексу) колонки. Не забывайте, тот же обработчик будет вызван и для редактируемой текстовой ячейки, но в этом случае _dgv.CurrentCellAddress.X будет равен нулю, и мы просто пропускаем его. Если же редактированию подвергается именно первая колонка – получаем control, его осуществляющий. В данном случае это наследник System.Windows.Forms.ComboBox и, разумеется, в нем есть столь желанное нами событие SelectedIndexChanged. Подписываемся на него. Его обработчик – ComboBox_SelectedIndexChanged – будет вызван при каждой смене текущего выбора пользователя. Сам код элементарен – узнаем строку, которой принадлежит редактируемый ComboBox, и в соответствующую ему текстовую ячейку записываем значение, сообщенное нам цепочкой свойств CurrentCell и EditedFormattedValue. Первое из них возвращает объект типа DataGridViewCell – саму активную ячейку. В рассматриваемом сценарии это всегда будет одна из ComboBox-ячеек. Второе возвращает объект типа object – текущее значение ячейки, вне зависимости от того, что оно является транзитным и может быть отброшено по ESC. Именно к этому значению мы столь упорно и шли:


Рисунок 4.

Итак, наличие события EditingControlShowing позволяет динамически подключаться к событиям редактирующего control-а и, таким образом, реагировать буквально на каждое действие пользователя, будь то ввод очередного символа, нажатие произвольной клавиши, щелчок мышью и т. п.

Размещение в ячейке пользовательского UserControl

В предыдущей части статьи я обещал продемонстрировать возможность создания собственного типа редактируемой ячейки. Что ж – пора выполнить обещание. Нас ждет интересная (хотя довольно объёмная) тема – изготовление собственного control-а и внедрение его в ячейку DataGridViewCell.

Поскольку данная задача подразумевает изрядное количество кода, было создано два проекта: PassportEditControl и PassportEditControl_Test. Первый – библиотека, содержащая новый control и всю его "инфраструктуру", необходимую для успешного размещения в ячейке. Второй – Windows Application с одной тестовой формой, содержащей один DataGridView, просто для оценки работоспособности первого решения. Код обоих проектов прилагается к статье.

Постановка задачи и предварительные операции

Начну с описания сценария. Пусть нужно написать приложение для ведения базы личных данных. Каждая персона характеризуется тремя присущими только ей свойствами: именем, фамилией и паспортными данными. С первыми двумя все ясно – обычные строки. С последним параметром ситуация интереснее. Допустим объект "паспорт" сам характеризуется тремя свойствами:

Для хранения первых двух свойств подойдут строки, для третьего – поле типа DateTime. Для редактирования же подобного объекта идеально подошли бы ComboBox, TextBox, DateTimePicker. При попытке перенести все это в grid-мир мы видим, что с первыми двумя элементами проблем нет, DataGridViewComboBoxCell и DataGridViewTextBoxCell соответственно к нашим услугам. С последним сложнее. У нас нет ячейки типа DataGridViewDateTimePickerCell (хотя в MSDN есть готовый пример по созданию как раз CalendarCell, аналога DataGridViewDateTimePickerCell). Для интересующихся подобным "облегченным" вариантом работы приведу название статьи: "How to: Host Controls in Windows Forms DataGridView Cells". Учитывая все вышесказанное, вырисовывается следующая схема редактируемого grid-а: пара текстовых колонок (имя/фамилия), выпадающий список (серия), еще одна текстовая (номер) и "почти готовая" календарная ячейка (дата выдачи). В этом подходе нет ничего плохого, хотя можно отметить и некоторые недостатки:

ПРИМЕЧАНИЕ

Справедливости ради отметим, что последний недостаток и при описываемом раздельном редактировании в пяти колонках все же обходится. Для этого наш объект "паспорт" должен будет реализовать интерфейс IEditableObject. Это позволит отложить решение о принятии или откате внесенных изменений на момент ввода информации в последнюю колонку, т.е. осуществить требуемую "глобальную" проверку. Тем не менее, мы пойдем другим путем.

Отметим, что использование TextBox для ввода номера – нормальное, но не идеальное решение. Из-за ограничений возможных значений этого поля гораздо лучше подойдет MaskedTextBox – новый control в составе FW2.0. Он позволит отсекать явно "нелегальный" ввод прямо по месту, без ожидания глобальной проверки. Но тогда нам нужна еще одна специальная колонка типа DataGridViewMaskedTextBoxCell. Напрашивается вывод: создать свой собственный UserControl с тремя control-ами и затем поместить его в ячейку. Таким образом, будущий grid видится состоящим всего из трех ячеек: пары текстовых и одной "паспортной".

Переходим к практике. Создадим проект типа WindowsControlLibrary и назовем его PassportEditControl. Переименуем в окне Solution Explorer имеющийся по умолчанию control в PassportEdit. В результате мы получим класс, унаследованный от UserControl.

Пока оставим этот класс как есть, и начнем работу с создания центрального объекта, вокруг которого все и будет крутиться. Оформим его в виде public-класса Passport. Он содержит три private-поля (пару типа string и одно – типа DateTime). Доступ к ним производится через public-свойства тех же типов, причем setter-ы первых двух полей проверяют корректность предлагаемых значений в соответствии с наложенными ранее ограничениями. У класса также имеются два public-конструктора. Один, с тремя параметрами, просто инициализирует ими поля. Второй же, без параметров, инициализирует поля значениями, принятыми по умолчанию. Собственно, ядро решения готово. Но надо помнить одну важную вещь. Мы создадим свой "паспорт-редактор" и научим grid правильно работать с нашим объектом. Но все это будет происходить в фазе редактирования. В фазе же отображения grid захочет показывать наш объект совершенно самостоятельно. Для нашего сценария вполне подойдет отображение в этом режиме некоторой информативной строки. Именно поэтому нашу "спец-ячейку" мы унаследуем от DataGridViewTextBoxCell. Нас вполне устраивает поведение этой ячейки в фазе отображения, нам не нравятся ее способности при редактировании. Вот именно этот момент мы и изменим. Здесь также возможны варианты. Я выбрал путь изготовления собственного конвертора, тем более что готовится он буквально за 5 минут. Наследуем класс PassportConverter от TypeConverter и переопределяем всего один метод – ConvertTo(). В нем нам будут передавать объект-паспорт, мы будем "красиво" раскладывать его свойства в форматированной строке и возвращать последнюю. А как управляться с ней, DataGridViewTextBoxCell и сама знает. Разумеется, после создания своего конвертора надо не забыть сообщить об этом всем интересующимся путем "навешивания" на центральный класс (Passport) атрибута:

[TypeConverter(typeof(PassportConverter))]

Создание редактора объекта

Теперь возвращаемся к классу-редактору PassportEdit. Переключаемся в дизайнер и прямо в нем изображаем нечто вроде показанного на рисунке 5.


Рисунок 5.

Средний control является MaskedTextBox control-ом, а не обычным текстовым окном. С целью избежать перегрузки примера кодом, не будем проводить никакой проверки серии и года выдачи. Ограничимся тем, что заставим пользователя вводить в поле "номер" ровно шесть цифр (иначе не будем выпускать фокус за пределы этого поля). Подобное поведение реализуется несложной настройкой control-а MaskedTextBox. Для интересующихся нюансами такой настройки могу рекомендовать свою предыдущую статью, Исследование WinForms 2.0 (beta 2). Там вопрос работы с маскирующим вводом освещен достаточно подробно.

Как будет проистекать процесс редактирования в будущем гриде? Вот пользователь оказывается на (пока не существующей) паспорт-ячейке и нажимает F2. Что произойдет? Будет вызван специальный метод (мы его рассмотрим в следующем подразделе) и ему будет сообщено, что содержит данная ячейка в данный момент в своем свойстве Value. Там может быть или null, или уже существующий объект-паспорт. Мы из этого метода будем вызывать метод разрабатываемого класса PassportEdit, который называется SetupControls(Passport passport). Он получает на вход как раз содержимое ячейки до редактирования. Только вместо null будем передавать фиктивный (или некий ”начальный”, с датой выдачи 1 Января 1970г.) паспорт. Метод SetupControls инициализирует редактируемые control-ы значениями полей переданного объекта Passport. Таким образом, если ячейка уже содержала паспорт серии "55", то при нажатии F2 в выпадающем списке будет выделено именно это значение. Чтобы впоследствии можно было отказаться от изменений, мы будем редактировать не непосредственно переданный объект, а его временную копию. Эта копия создается в методе SetupControls и помещается в переменную _tempPassport.

Каждый из трех подчиненных control-ов при любом изменении своего содержимого вызывает метод OnValueChanged(). В нем, во-первых, данные, введенные пользователем, помещаются во временный объект. Во-вторых, производится оповещение grid-а, что временный объект подвергся изменениям. Делается это элементарно – вызовом метода NotifyCurrentCellDirty(true). Этот указывает, что были произведены изменения, и grid должен учитывать это при окончании редактирования ячейки.

Но это еще была "надводная часть айсберга". Мы еще не говорили об интерфейсе IDataGridViewEditingControl. А без реализации последнего у нас не будет ни малейшего шанса поместить наш control в какую-либо ячейку. Что же, приступаем. Вот описание этого интерфейса:

        public
        interface IDataGridViewEditingControl
{
  DataGridView EditingControlDataGridView { get; set; }
  object EditingControlFormattedValue { get; set; }
  int EditingControlRowIndex { get; set; }
  bool EditingControlValueChanged { get; set; }
  Cursor EditingPanelCursor { get; }
  bool RepositionEditingControlOnValueChange { get; }

  void ApplyCellStyleToEditingControl(
    DataGridViewCellStyle dataGridViewCellStyle);
  bool EditingControlWantsInputKey(
    Keys keyData, bool dataGridViewWantsInputKey);
  object GetEditingControlFormattedValue(
    DataGridViewDataErrorContexts context);
  void PrepareEditingControlForEdit(bool selectAll);
}

В таблице 1 приведено описание его методов и свойств.

Член интерфейса IDataGridViewEditingControl Замечания о назначении и реализации
EditingControlDataGridView Позволяет установить или считать ссылку на DataGridView. Значение этого свойства задает grid в момент входа ячейки в фазу редактирования. Через него родительский grid дает ссылку на самого себя. Именно благодаря этому у нас появляется возможность вызова DataGridView.NotifyCurrentCellDirty в нашем "паспорт-редакторе".
EditingControlFormattedValue Свойство чрезвычайной важности. Зачем нужен setter, неясно, за все время моих исследований не был вызван ни кем, ни разу. Напротив, getter вызывается при малейших изменениях в редактируемом объекте (очевидно, для отображения значения свойства на экране – прим. ред.). Дело в том, что любая ячейка содержит сразу 2 значения: абсолютное (в свойстве Value) и форматированное (в свойстве FormattedValue). Причем, как было показано выше, мало того, что эти свойства могут возвращать различные значения, так еще и тип возвращаемого значения может отличаться. EditingControlFormattedValue возвращает как раз форматированное значение. Данное свойство должно быть согласовано со свойством ячейки FormattedValueType. Обычно ячейка возвращает через Value объект типа object, а через FormattedValue тип string. Я решил, что в нашем случае они оба будут возвращать тип Passport. Поэтому анализируемое свойство интерфейса просто возвращает временный объект, на который ссылается _tempPassport.
EditingControlRowIndex Значение свойства задается grid-ом в момент входа ячейки в фазу редактирования и сообщает редактору индекс редактируемой строки.
EditingControlValueChanged Если значение ячейки изменено, данное свойство должно возвращать true.
EditingPanelCursor При входе в фазу редактирования в ячейку сначала помещается панель (экземпляр класса System.Windows.Forms.Panel), занимающая всю доступную площадь ячейки. Форму курсора над этой панелью и определяет данное свойство. Однако обычно всю площадь панели занимает редактирующий control. Так что на практике значение данного свойства особого смысла не имеет.
RepositionEditingControlOnValueChange Значение true говорит grid-у о необходимости изменения позиционирования ячейки при изменении ее содержимого. Например, если бы мы создавали свою реализацию DataGridViewTextBoxCell и хотели, чтобы при достижении правой границы ячейки текст переносился на следующую строчку, можно было бы вернуть в нужный момент true в этом свойстве.
ApplyCellStyleToEditingControl Этот метод вызывается grid-ом в момент перехода ячейки в фазу редактирования и дает возможность использовать стиль текущей ячейки в редактирующем control-е. Кстати, здесь же мы проделываем еще одну важную вещь: устанавливаем свойство MinimumSize нашего редактора равным его реальным размерам. Но на самом деле нас интересует только высота. Как будет показано ниже, мы на время редактирования пытаемся увеличить высоту ячейки для более корректного отображения в ней редактирующих control-ов. При этом родительский grid пытается не ячейку подогнать под control, а, напротив, – control под ячейку, и тем самым срезает пару пикселов снизу у каждого из редактирующих control-ов. Устанавливая MinimumSize, мы заявляем, что скорректировать (в сторону увеличения) все же придется именно высоту строки, т.к. мы решительно против попытки уменьшить высоту нашего редактора (как редакторы, полностью поддерживаем! – прим.ред.). Дальнейшее развитие этой темы см. в описании метода InitializeEditingControl в следующей таблице.
EditingControlWantsInputKey Возвращая из этого метода true, мы говорим, что хотим использовать данное сочетание клавишей.
GetEditingControlFormattedValue Фактически дублирует поведение свойства EditingControlFormattedValue. Неудивительно поэтому, что характерная реализация данного метода состоит из одной строчки - return EditingControlFormattedValue.
PrepareEditingControlForEdit Метод вызывается grid-ом в момент входа ячейки в фазу редактирования и дает возможность подготовить редактор к процессу редактирования.
Таблица 1.

Еще раз отмечу, что все свойства/методы из таблицы выше могут быть вызваны только в фазе редактирования. В обычном режиме объекта типа PassportEdit просто не существует, а поэтому и никакие его члены не вызываются.

Ну вот – готово. Наш редактор готов "прописаться" в ячейке grid-а. Но и саму ячейку необходимо подготовить. Этим и займемся.

Создание специализированного типа ячейки и колонки

На очереди – создание специальной ячейки, использующей для редактирования данных control PassportEdit. Объявим новый класс DataGridViewPassportCell, наследника DataGridViewTextBoxCell. Если в свойстве Value данной ячейки не содержится объекта типа Passport, то она должна в фазе отображения показывать строку "Паспортные данные неизвестны". В таблице 2 перечислены унаследованные от DataGridViewTextBoxCell члены, которые нужно переопределить.

Члены класса DataGridViewTextBoxCell Замечания о назначении и реализации
EditType
тип – Type, только-для-чтения
Тип редактора, создаваемого для редактирования значения данного типа, в нашем случае typeof(PassportEdit).
ValueType
тип – Type, только-для-чтения
Тип значения, хранящегося данной ячейкой. Поскольку мы создаем спец-ячейку только для объекта Passport, то и хранить она будет только объекты такого типа. Поэтому возвращаем typeof(Passport).
FormattedValueType
тип – Type, только-для-чтения
Тип форматированного значения данной ячейки. Вообще, по идее, мы должны были бы возвращать здесь typeof(string), в силу того, что наша базовая DataGridViewTextBoxCell в фазе отображения умеет отрисовывать именно такие данные. А картинки, к примеру, не умеет. Равно как и объекты типа Passport. Но мы решаем взять часть работы на себя и предоставить ей уже готовый для отображения объект – см. метод GetFormattedValue чуть ниже. Поэтому мы можем вернуть абсолютно то же значение, что и предыдущее свойство, и ни о чем более не беспокоиться.
DefaultNewRowValue
тип – object, только-для-чтения
Возвращает значение, отображаемое в новой строке. В этом свойстве мы возвращаем уже упоминавшуюся строку "Паспортные данные неизвестны".
InitializeEditingControl
тип возвращаемого значения – void
Вызывается grid-ом в момент перехода ячейки в фазу редактирования. Назначение этого метода – подготовить редактор к процессу редактирования. В процессе подготовки к редактированию производится коррекция высоты строки так, чтобы редактирующий control при отображении в ней не обрезался. Далее получаем объект типа Passport, хранящийся в ячейке, и передаем его методу SetupControls().
DetachEditingControl
тип возвращаемого значения – void
Вызывается grid-ом в момент выхода ячейки из фазы редактирования. Позволяет редактору "хлопнуть дверью" напоследок. :) В данном случае мы восстанавливаем высоту строки, изменённую предыдущим методом.
GetFormattedValue
тип возвращаемого значения – object
В этом методе можно возвратить непосредственно объект Passport, если таковой существует, и строку "Паспортные данные неизвестны", если значение равно null.
Таблица 2.

Теперь создадим колонку DataGridViewPassportColumn. Опять же заметим, что мы могли бы свободно создать нужную колонку, просто создав экземпляр типа DataGridViewColumn через перегруженный конструктор последнего, принимающий объект типа DataGridViewCell, т.е. можно было бы написать нечто вроде:

        new DataGridViewColumn(new DataGridViewPassportCell())

Это бы установило свойство CellTemplate новой колонки в нужное нам значение. В первой части статьи мы уже разбирали важность этого свойства и причины пагубности описанного подхода. И это еще без учета того, что описанный сценарий лишает новый тип колонки design time-поддержки. А вот в случае создания колонки собственного типа дизайнер её распознает, что мы вскоре и увидим.

Создадим public-класс с именем DataGridViewPassportColumn и унаследуем его от DataGridViewColumn. Какие из членов базового класса требуется переопределить? На этот раз это единственное свойство CellTemplate. В конструкторе без параметров наш класс вызывает конструктор базового класса, передавая ему в качестве параметра шаблон ячейки (экземпляр класса DataGridViewPassportCell).

Использование готового решения

К этому моменту у нас готов основной проект – PassportEditControl. В нем находится целых пять public-классов:

Попробуем провести "полевые испытания" разработанного решения. Создаем новый проект типа Windows Application и называем его PassportEditControl_Test. В его References добавляем ссылку на библиотеку PassportEditControl.dll (результат компиляции предыдущего проекта). Кидаем на форму grid. Через smart tag последнего вызываем диалоговое окно 'Add Columns'. В нем, прежде всего, добавляем пару обычных текстовых колонок, в которых будут отображаться имя и фамилия, и колонку типа DataGridViewPassportColumn. Когда мы добавили ссылку на сборку PassportEditControl.dll, дизайнер увидел определенный в ней тип колонки (DataGridViewPassportColumn) и теперь мы можем работать с ней, как со встроенной колонкой.


Рисунок 6.

Схема таблицы определена. Идем в обработчик события загрузки формы и в нем добавляем в grid 2 пустых строчки. В этих строчках колонка DataGridViewPassportColumn будет содержать null. Затем добавляем заполненную строчку и еще одну пустую. Далее для большего удобства работы и исключения горизонтального скроллинга устанавливаем ширину последней колонки по ширине нашего редактора, а ширину всей формы – по сумме ширин всех колонок grid-а.

На форме также присутствует кнопка, отображающая содержимое последней колонки текущей строки.

Запускаем тестовый проект. Как и следовало ожидать, лишь одна строка отображает паспортные данные. Все прочие показывают "Паспортные данные неизвестны...". Попробуем отредактировать, например, первую (по индексу) строку. Вводим имя и фамилию, и переходим к редактированию ячейки паспортных данных. Убеждаемся, что столь желанный нами редактор на месте, выглядит аккуратно и функционирует как швейцарские часы (рисунок 7).


Рисунок 7.

Проверяем, что при входе в фазу редактирования все три редактирующих control-а инициализируются значениями из текущего объекта Passport. Удостоверяемся, что добавление новых записей также вопросов не вызывает. Наконец, для контроля, щелкая по кнопке, убеждаемся, что все вводимые нами данные действительно попадают в ячейку. Задача решена.

Стили ячеек

Последний относительно большой раздел статьи будет посвящён именно этому вопросу – стилям ячеек.

Для чего нужны стили ячеек

Основная задача, решаемая стилями ячеек – обеспечить богатое визуальное форматирование ячеек, в то же время не заставляя разработчика заниматься каждой ячейкой индивидуально.

Для начала отметим, что стиль ячейки представляется классом DataGridViewCellStyle. Поскольку в этой части статьи речь на 95% будет крутиться вокруг именно этого класса, то договоримся, что до конца этого раздела, если при описании очередного свойства явно не указано иное, то такое свойство принимает или возвращает объект именно этого типа. Рассмотрим подробнее свойства этого класса. Назначение ряда свойств очевидно:

Свойство WrapMode (типа DataGridViewTriState) определяет, будет ли переноситься либо обрезаться текст, не вмещающийся в одну строку. Свойство Padding (типа Padding) задаёт отступ от краев ячейки (рисунок 8).


Рисунок 8.

Здесь представлена одна и та же ячейка, причем момент снятия скриншота специально был выбран в такое время, когда эта ячейка является выбранной и текущей, т.е. по её периметру идет такой штрихпунктирный прямоугольник, показывающий, что фокус находится именно на этой ячейке. Здесь этот прямоугольник для наглядности подсвечен красным. А разница в скриншотах в том, что левый сделан, когда свойство Padding показанной ячейки установлено в нулевые размеры по всем четырем сторонам. Кстати, по умолчанию это именно так. Правый же сделан для случая 10 пиксельного отступа со всех четырех сторон.

Свойство Alignment (тип DataGridViewContentAlignment) позволяет задать выравнивание содержимого ячейки относительно ее границ. Мы можем прижать содержимое к разным сторонам ячейки или выровнять его по центру. Свойство NullValue (тип object) позволяет задать отображаемое значение ячейки, чьё свойство Value возвращает null либо DBNull.Value. По умолчанию для таких ячеек показывается string.Empty. Ячейка с пустой строкой по умолчанию выглядят так же, как содержащая null. Чтобы различать эти значения, присвойте этому свойству, к примеру, строку из трех "решеток", вот так - "###". Вплотную к рассмотренному примыкает и свойство DataSourceNullValue (тип object). Нужно оно вот зачем: допустим мы работаем в режиме привязки к источнику и, соответственно, данная ячейка грида имеет соответствующую ей ячейку в нижележащем источнике данных. Значение этого свойства будет отправлено источнику данных при помещении пользователем в ячейку null-значения. По умолчанию значение этого свойства – DBNull.Value.

СОВЕТ

Для ввода значения null в текущую ячейку можно нажать Ctrl+0 или ввести значение только что рассмотренного свойства NullValue.

Еще одно свойство класса DataGridViewCellStyle – Format (тип string). Оно задает правила форматирования текстового содержимого ячейки. По умолчанию - string.Empty, т.е. никакого форматирования не производится. Форматирующие коды описаны в MSDN в статье "Formatting Types". К примеру "C" будет отображать число как валюту, а "d" отобразит дату в данной ячейке в коротком формате.

СОВЕТ

Обратите внимание, что форматирование распространяется на тот тип данных, для которого оно предназначено. Например, если форматирование предназначено для преобразования целых чисел в строку (например, код форматирования "X" предназначен для преобразования числа в шестнадцатеричную строку), то работать оно будет только если в ячейке лежит целочисленное значение.

Следующее свойство (кстати, имеющее непосредственное отношение к предыдущему) FormatProvider (тип IFormatProvider). Данное свойство определяет, как будут форматироваться регионально-зависимые значения ячеек. По умолчанию этому свойству присваивается значение свойства CultureInfo.CurrentUICulture, так что форматирование идет в соответствиями с текущими региональными установками. Естественно, вы можете задать значение этого свойства самостоятельно. К примеру, следующие строки кода:

        _dgv.Rows[4].Cells[3].Style.Format = "C";
_dgv.Rows[4].Cells[3].Style.FormatProvider = 
  new System.Globalization.CultureInfo("de-DE");

заставят ячейку с указанными координатами выглядеть вот так:


Рисунок 9.

Код форматирования "C" задаёт отображение содержимого ячейки как национальной валюты.

Как было отмечено выше, свойства NullValue, DataSourceNullValue, FormatProvider имеют по умолчанию значения, отличные от null. Для того, чтобы узнать, не изменилось ли (по сравнению со значением по умолчанию) значение какого-либо из этих свойств, служат свойства IsNullValueDefault, IsDataSourceNullValueDefault и IsFormatProviderDefault (все три – типа bool, доступны только для чтения). Назначение каждого, полагаю, очевидно из названия свойства.

Применение и наследование стилей

Рассмотрим на примере применение стилей ячеек. Для демонстрации работы со стилями был написан проект CellStyles (код прилагается к статье). Форма этого проекта содержит grid с двумя колонками и четырьмя строками. Из множества характеристик стиля ячейки в примере будет использоваться цвет фона ячейки (DataGridViewCellStyle.BackColor).

Начнем с концепции. Группы ячеек, использующих один стиль, могут включать все ячейки какой-либо строки или колонки, все ячейки с конкретными значениями, или же все ячейки control-а. Поскольку эти группы пересекаются, каждая ячейка может получать информацию о стиле из нескольких источников. Например, можно заставить все ячейки control-а использовать один шрифт, но только ячейки в колонках, содержащих валюту, будут использовать формат currency, и только цвет текста ячеек с отрицательными значениями будет красным.

Формирование стиля

Внешний вид конкретной ячейки определяется свойством InheritedStyle. Его значение формируется путем последовательного слияния значений иерархии свойств типа DataGridViewCellStyle. В этой иерархии довольно много уровней, и каждый из этих уровней отвечает за ячейки, сгруппированные по тому или иному признаку. Иерархия этих уровней различна для заголовочных и обычных ячеек.

Ниже приводятся иерархии свойств для заголовочных и обычных ячеек, упорядоченные по порядку применения их при слиянии. Чем выше стоит свойство в перечислении, тем раньше применяется его значение.

В стиле может быть задана только часть атрибутов. При этом каждый следующий применяемый стиль может задать или переопределить значения тех атрибутов, которые в нем указаны. Таким образом, стили, идущие ниже, имеют приоритет перед стилями, идущими выше.

У стиля высшего уровня DataGridView.DefaultCellStyle для всех компонентов значения гарантированно заданы. Например, цвет фона ячейки он задает как SystemColors.Window, перенос текста на другую строку отключен, содержимое ячейки выравнивается по центру левой кромки ячейки, цвет текста определяется значением свойства ForeColor grid-а и т.д.


Рисунок 10.

Уровни заголовочных ячеек

Свойство DataGridView.ColumnHeadersDefaultCellStyle определяет стиль для всех заголовочных ячеек всех колонок.

grid.ColumnHeadersDefaultCellStyle.BackColor = ...

Свойство DataGridView.RowHeadersDefaultCellStyle определяет стиль для всех заголовочных ячеек всех строк.

grid.RowHeadersDefaultCellStyle.BackColor = ...

Свойство DataGridViewCell.Style определяет стиль для конкретной заголовочной ячейки колонки или строки.

grid.Columns[1].HeaderCell.Style.BackColor = ... // заголовок 1-й колонки
grid.Rows[1].HeaderCell.Style.BackColor    = ... // заголовок 1-й строки
ПРЕДУПРЕЖДЕНИЕ

По умолчанию в WinForms-приложении перед созданием и активацией первой формы содержится вызов метода Application.EnableVisualStyles(). Этот метод включает поддержку визуальных стилей (visual styles) ОС. В этом случае те аспекты внешнего вида заголовочных ячеек, что определены визуальным стилем ОС, не могут быть изменены. К примеру, если визуальные стили включены, то бесполезно пытаться изменять фон ячейки или цвет текста заголовка с помощью свойств RowHeadersDefaultCellStyle или ColumnHeadersDefaultCellStyle. В то же время шрифт заголовка, к примеру, изменить можно. Если вы предпочтете не использовать визуальные стили, вам будут доступны все аспекты отображения заголовочных ячеек.

Уровни обычных ячеек

Свойство DataGridView.DefaultCellStyle определяет стиль для всех обычных ячеек.

grid.DefaultCellStyle.BackColor = ...

Свойство DataGridViewColumn.DefaultCellStyle определяет стиль для всех обычных ячеек данной колонки.

grid.Columns[1].DefaultCellStyle.BackColor = ...

Свойство DataGridView.RowsDefaultCellStyle определяет стиль для всех обычных ячеек.

grid.RowsDefaultCellStyle.BackColor = ...

Свойство DataGridView.AlternatingRowsDefaultCellStyle определяет стиль для всех обычных ячеек нечетных строк.

        grid.AlternatingRowsDefaultCellStyle.BackColor = ...

Свойство DataGridViewRow.DefaultCellStyle определяет стиль для всех обычных ячеек данной строки.

        grid.Rows[1].DefaultCellStyle.BackColor = ...

Свойство DataGridViewCell.Style определяет стиль для одной конкретной ячейки.

grid.Rows[1].Cells[1].Style.BackColor = ...
ПРЕДУПРЕЖДЕНИЕ

В MSDN утверждается, что DataGridView.DefaultCellStyle воздействует на абсолютно все ячейки, включая и заголовочные. Практические эксперименты опровергают это утверждение, однозначно показывая воздействие данного свойства только на обычные ячейки. Т.е. правильная формулировка будет: DataGridView.DefaultCellStyle воздействует на все ячейки, исключая заголовочные.

Нетрудно заметить, что описания свойств DataGridView.DefaultCellStyle и DataGridView.RowsDefaultCellStyle идентичны. Разница состоит только в последовательности их применения. Значение DataGridView.DefaultCellStyle может быть переопределено значением DataGridViewColumn.DefaultCellStyle, а вот значение DataGridView.RowsDefaultCellStyle имеет более высокий приоритет, чем DataGridViewColumn.DefaultCellStyle.

Все вышеописанное вы можете "пощупать" в готовом демо-проекте CellStyles. Как уже говорилось, он работает только с одним атрибутом стиля – цветом фона ячейки (DataGridViewCellStyle.BackColor). Каждая из кнопок основной формы устанавливает фон ячеек для того или иного региона. Цвет фона кнопки соответствует цвету фона ячеек, задаваемому этой кнопкой, а надписи указывают свойство и регион действия. В нижней части формы отображается цвет фона для ячейки с координатами [1,1], который отслеживается параллельно в двух местах – в свойстве DataGridViewCell.Style и свойстве DataGridViewCell.InheritedStyle указанной ячейки. Это позволяет лучше понять, значение какого свойства меняется при нажатии каждой из кнопок, а также взаимное влияние этих двух свойств. Отдельный блок кнопок служит для работы с заголовочными ячейками.


Рисунок 11.

Коснемся такого вопроса – а можем ли мы отлавливать моменты изменения стиля ячейки? Да, и для этого предусмотрены целых 2 типа событий. Первый тип представлен одним событием, CellStyleContentChanged. Второй представляет собой целую "пачку" однотипных событий. Вот имена этих событий и свойства, изменение которых ведет к возникновению соответствующего события:

Обработчики этих событий получают ссылки на колонку, строку или ячейку, затрагиваемую изменением стиля. Вероятно, вы удивитесь – к чему же тогда первое событие? Дело в том, что стиль ячейки можно изменять по-разному. Вот два примера изменения стиля конкретной ячейки с координатами [3,1]:

        // Изменили КОМПОНЕНТ стиля. 
// Само значение свойства DataGridViewCell.Style не изменилось.
_dgv.Rows[1].Cells[3].Style.BackColor = Color.Goldenrod; 

и

DataGridViewCellStyle myStyle = new DataGridViewCellStyle();
myStyle.BackColor = Color.DarkOrange;
////....дальнейшая настройка myStyle//
//изменили непосредственно свойство DataGridViewCell.Style 
_dgv.Rows[1].Cells[3].Style = myStyle; 

Код принципиально разный, а результат один – стиль ячейки [3,1] стал иным. Причем в аналогичной манере можно менять стиль колонки, строки, всего grid-а. Вот событие CellStyleContentChanged и позволяет отловить изменение стиля первым способом, когда меняется всего лишь одно из свойств объекта типа DataGridViewCellStyle, возвращенного любым из свойств. "Пачка" рассмотренных событий ответственна за перехват изменений вторым способом. Причем изменение стиля вторым путем не приводит генерации первого события. Чтобы гарантированно отследить изменения стиля любого из элементов grid-а, можно подписаться на оба этих события одновременно.

Продвинутое управление стилями ячеек

Ну хорошо, скажете вы. Теперь можно поверить, что нам не составит труда раскрасить таблицу во все цвета радуги. Но что если нам потребуется всегда подсвечивать вон ту колонку голубым, но по пятницам желтым? Более жизненный пример – ячейки одной и той же колонки выводят целые числа. Допустим, нужно подсвечивать положительные – зеленым, отрицательные – красным, а нулевые выделять жирным шрифтом с наклоном. Такое возможно? Да. Это называется динамическим управлением стилем ячейки.

В первой части статьи уже говорилось о событии DataGridView.CellFormatting, но там речь шла о форматировании самого значения ячейки, переданного в обработчик этого события. Напомню, что это значение хранится в свойстве Value (тип object) объекта типа DataGridViewCellFormattingEventArgs, который передается вторым аргументом в обработчик этого события. Теперь мы поговорим о визуальном форматировании. Концептуально здесь все достаточно просто: сперва вычисляется окончательный стиль данной ячейки и помещается в свойство DataGridViewCell.InheritedStyle. В последний момент grid вызывает анализируемое событие, и передает InheritedStyle обработчику этого события в свойстве CellStyle (тип DataGridViewCellStyle) объекта типа DataGridViewCellFormattingEventArgs. Это позволяет исправить стиль ячейки перед отрисовкой.

Рассмотрим, как это все происходит на практике. Есть колонка с именем "Num" с индексом 3, которая содержит числовые значения. Подписываемся на событие DataGridView.CellFormatting, и в его обработчике делаем следующее:

        void _dgv_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
  if (col != e.ColumnIndex)
    return;

  int val = (int)e.Value;

  if (val > 0)
    e.CellStyle.BackColor = Color.LightGreen;
  elseif(val < 0)
  {
    e.CellStyle.BackColor = Color.IndianRed;
    e.CellStyle.Alignment = DataGridViewContentAlignment.MiddleRight;
  }
  else
  {
    e.CellStyle.Font = new Font(e.CellStyle.Font.Name, 14,
      FontStyle.Bold|FontStyle.Italic);
    e.CellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
  }
}

А вот и результат (к заданным выше условиям добавлено выравнивание для отрицательных и нулевых чисел):


Рисунок 12.

Допустим, что в обработчике загрузки этой формы у нас есть строка:

        // цвет фона для всей колонки "Num"

        _dgv.Columns[3].DefaultCellStyle.BackColor = Color.LightBlue; 

И точно такой же обработчик события DataGridView.CellFormatting, как приведенный выше. Что мы увидим после запуска такого приложения? Если вы сказали "точно такой же скриншот, как и последний, только две ячейки со значением 0 будут фоново подсвечены бледно-голубым", то я вас поздравляю – в стилях вы разобрались.

Отметим также, что динамическое управление стилями может потребоваться для реагирования на действия пользователя. К примеру, события DataGridView.CellMouseEnter и DataGridView.CellMouseLeave генерируются при входе курсора мыши в ячейку и выходе из нее. Допустим, нам нужно, чтобы фон ячейки [1,1] изменялся при попадании на нее курсора мыши, а при выходе курсора – возвращался к стандартному. С помощью стилей это делается элементарно:

        private
        void _dgv_CellMouseEnter(object sender, DataGridViewCellEventArgs e)
{
  if(e.ColumnIndex == 1 && e.RowIndex == 1)
    _dgv.Rows[1].Cells[1].Style.BackColor = Color.ForestGreen;
}

privatevoid _dgv_CellMouseLeave(object sender, DataGridViewCellEventArgs e)
{
  if(e.ColumnIndex == 1 && e.RowIndex == 1)
    _dgv.Rows[1].Cells[1].Style.BackColor = Color.White;
}

В таком же духе можно реагировать на щелчок по ячейке, на ввод определенного ключевого значения в текстовую ячейку, на изменение текущей строки и т.д.

Заключение aka FAQ

Как я писал во введении, к данной точке повествования автор убежден – DataGridView на самом деле заслуживает отдельной книги. Наивно полагать, что даже самая большая статья в состоянии охватить все нюансы и особенности этого мега-контрола. Я разобрал самые важные, с моей точки зрения, вещи, относящиеся к DataGridView. За бортом повествования остались контекстные меню и tooltip-ы ячеек, общие строки (shared rows) и их влияние на эффективность расходования памяти, и еще много, много всего. Назвать поименно все 153 свойства и 187 событий ни в какой статье просто не реально. Это задача сухой и строгой документации вроде MSDN.

Я хотел бы завершить этот обзор самодельным FAQ, написанным "по мотивам" вопросов о DataGridView, задаваемых в наших и англоязычных форумах и новостных группах. Но прежде чем перейти к этому, хочу, как обычно, заверить, что любые замечания, поправки, возражения и просто предложения по изложенному материалу будут с благодарностью приняты и тщательно проанализированы. И спасибо, что были с нами на протяжении всей статьи. :)

Q. Хочу, что бы прямо во время исполнения grid начал работать с другим источником данных. С этой целью меняю ему значения свойств DataSource и/или DataMember. Почему колонки старой таблицы не заменяются колонками новой?

A. Проверяйте свойство DataGridView.AutoGenerateColumns. Для описываемого сценария оно должно быть выставлено в true.

Q. Как передать фокус ввода некоторой ячейке?

A. Для этого используется свойство DataGridView.CurrentCell.

Q. Как сделать так, чтобы некоторая ячейка стала первой видимой (располагалась в левом верхнем углу grid-а)?

A. Для этого используется свойство DataGridView.FirstDisplayedCell.

Q. У меня есть бизнес-объект – класс Player. Я создаю типизированную коллекцию таких объектов и добавляю в нее ряд записей:

players = new BindingList<Player>();
players.Add(new Player("Larry", 10));
players.Add(new Player("Barry", 11));
players.Add(new Player("Harry", 12));
players.Add(new Player("Mary", 13));

После чего привязываю grid к коллекции:

      _dgv.DataSource = players;

Все отображается верно. Но теперь по нажатию кнопки я программно меняю, допустим, во второй записи свойство Name моего бизнес-объекта с "Barry" на "Teddy". Указанное изменение никак не влияет на содержимое грида, он продолжает показывать старое имя "Barry". Как быть?

A. В вашем бизнес-классе Player реализуйте интерфейс INotifyPropertyChanged. Grid автоматически подпишется на единственный член данного интерфейса, событие PropertyChanged. Вашей задачей будет лишь генерировать данное событие при любых попытках изменить те свойства, которые вы хотели бы видеть автоматически обновляемыми.

Q. Пишу программу мат. анализа, приходится работать с матрицами. Представляю их в виде двумерных массивов вида

      class Matrix
{
  publicstaticdouble[,] arr = newdouble[,] 
  {
    {1.0, 1.5, 2.0, 2.5, 3.0, 3.5},
    {2.0, 2.5, 2.0, 2.5, 4.0, 4.5},
    {5.0, 5.5, 8.0, 8.5, 3.0, 9.5}
  };
}

и пытаюсь напрямую связать grid с массивом:

BindingSource bs = new BindingSource();
bs.DataSource = Matrix.arr;
_dgv.DataSource = bs;

Получаю исключение с сообщением, что массив не одномерен. В качестве эксперимента делаю его одномерным:

      public
      static
      double[] arr = newdouble[] { 2.0, 2.5, 2.0, 2.5, 4.0, 4.5 };

Исключение пропадает, но grid показывается пустым. Как же связывать grid с массивами вообще и многомерными в особенности?

A. Связывание с массивом "в лоб" невозможно. Дело в том, что grid рассматривает каждый элемент массива как отдельный объект (читай класс) и пытается обнаружить его публичные свойства, чтобы подключиться к ним. А какие же публичные свойства у double? С многомерными массивами все еще хуже. А выход заключается в том, чтобы ввести искусственные свойства и возврать коллекцию объектов с этими свойствами. Каждое из искусственных свойств возвращает один из элементов массива. В вашем примере массив представляет что-то похожее на набор координат по трем осям. Т.е. логически мы хотим видеть grid с 3-я колонками (оси) и множеством строк (координаты). Кроме того, нужно сообщить BindingSource-у, что создаваемый класс поддерживает итерацию сквозь коллекцию. И, разумеется, привязываться уже надо к классу, а не к массиву.

Q. Как задать порядок вывода колонок при подключении к источнику данных?

A. При автогенерации колонок в момент привязки грида к любому объекту порядок добавления колонок произволен. Поэтому после привязки нужно задать DisplayIndex для колонок grid-а:

BindingSource bs = new BindingSource();
bs.DataSource = new Matrix();
_dgv.DataSource = bs;
_dgv.Columns["X"].DisplayIndex = 0;
_dgv.Columns["Y"].DisplayIndex = 1;
_dgv.Columns["Z"].DisplayIndex = 2;

Q. Как "вписывается" редактирующий control в обычную ячейку понятно, а можно ли то же самое сделать с заголовочной? В частности, мне бы хотелось видеть такое поведение: пользователь делает двойной щелчок по заголовку столбца (сортировка отключена) и на его месте возникает обычное окно ввода. Пользователь вводит некое значение. Если после этого нажат Esc, то введенное значение просто отбрасывается и возникает обычный заголовок. Если же нажат Enter, то все то же самое, но еще производится попытка нахождения среди значений данного столбца того, что эквивалентно введенному пользователем. При успешном поиске искомая ячейка выбирается в качестве текущей.

A. Совсем "по-честному" описанное поведение не реализуется. По своему дизайну заголовочные ячейки не хотят и не принимают фокус ввода и, уж конечно, не предназначены для хостинга других контролов. Но можно схитрить – накладывать (временно) обычный текстбокс прямо поверх ячейки и убирать его по нажатию ESC / ENTER. Пример реализации этой идеи см. в проекте RenameInPlace_ColumnHeader (код приложен к статье).

Q. Я хочу запрещать пользователю выделять некоторые строки, базируясь на определенных условиях. При этом редактирование ячеек этих строк должно дозволяться. Это возможно?

A. Вполне. Создайте наследника DataGridView и перекройте защищенный метод SetSelectedRowCore:

      public
      class MyGrid : DataGridView
{
  protectedoverridevoid SetSelectedRowCore(int rowIndex, bool selected)
  {
    if ((rowIndex == 2 || rowIndex == 3) && selected)
      return;
    elsebase.SetSelectedRowCore(rowIndex, selected);
  }
}

Теперь строки 2 и 3 стали недоступны для выбора. Указанный метод работает и в режиме выбора DataGridViewSelectionMode.RowHeaderSelect и в режиме DataGridViewSelectionMode.FullRowSelect.

Q. Строкой кода

      _dgv.Columns[0].DefaultCellStyle.WrapMode = DataGridViewTriState.True;

я разрешил перенос длинных данных в ячейках нулевой колонки на следующую строку. Действительно, если я программно присваиваю длинную строку одной из ячеек, она автоматом разбивается на несколько строк. Но если данные вводит пользователь – как он может принудительно задать переход к следующей строке?

A. Если перенос строк для данной ячейки включен (в стиле ячейки свойство WrapMode выставлено в true) то переход на следующую строку в пределах одной ячейки осуществляется комбинацией клавиш Shift+Enter (в фазе редактирования).

Q. Хотелось бы сохранять расположение колонок при выходе из приложения или закрытии окна и восстанавливать его при открытии. По умолчанию grid этого не делает. Можно это как-то поправить?

A. Можно, причем вариантов масса. Один из:

Дважды щелкаем по файлу Settings.settings в Solution Explorer и добавляем настройку с некоторым именем (в данном примере с именем «ColsOrder») в список. В колонке «Type» настройки нужно указать «System.Int32[]» (в выпадающем списке типов выбрать пункт «Browse...» и в появившемся диалоге вписать указанный тип). В колонке «Scope» указать – «User».

Далее пишем две функции сохранения и восстановления порядка колонок (вместо DefNamespace нужно подставить имя пространства имен используемого по умолчанию в данном проекте):

      public
      static
      void LoadColumnsOrder(DataGridView grid, string settingsEntry)
{
  int[] displayIndexes = (int[])
    DefNamespace.Properties.Settings.Default[settingsEntry];

  if (displayIndexes == null || displayIndexes.Length != grid.Columns.Count)
    return;

  for (int i = 0; i < grid.Columns.Count; i++)
    grid.Columns[i].DisplayIndex = displayIndexes[i];
}

publicstaticvoid SaveColumnsOrder(DataGridView grid, string settingsEntry)
{
  int[] displayIndexes = newint[grid.Columns.Count];

  for (int i = 0; i < grid.Columns.Count; i++)
    displayIndexes[i] = grid.Columns[i].DisplayIndex;

  DefNamespace.Properties.Settings.Default[settingsEntry] = displayIndexes;
}

Вызываем первую при загрузке формы:

      private
      void Form1_Load(object sender, EventArgs e)
{
  // "ColsOrder" – это имя настройки в файле Settings.settings
  LoadColumnsOrder(dataGridView1, "ColsOrder");
}

А вторую – при закрытии:

      private
      void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
  SaveColumnsOrder(dataGridView1, "ColsOrder");
  WindowsApplication1.Properties.Settings.Default.Save();
}

Q. Вторая колонка grid-а содержит в каждой из ячеек довольно длинный текст. Как сделать так, чтобы при входе в режим редактирования любой ячейки второй колонки выпадал многострочное текстовое окно, в котором бы и происходило редактирование?

A. В готовом примере DataGridView_PopUpEdit (код приложен к статье) такое текстовое окно встраивается в объект ToolStripDropDown, который ниспадает из выбранной ячейки второй колонки. Вход в режим редактирования осуществляется как обычно. Выход – ESC. Обратите внимание, что в данном случае ESC означает "закрыть ToolStripDropDown и принять исправленный текст". Если вы хотите более традиционное (для мира DataGridView) поведение "закрыть ToolStripDropDown и игнорировать изменения" поправьте код анонимного делегата в вызове метода _dgv.BeginInvoke.


Эта статья опубликована в журнале RSDN Magazine #2-2006. Информацию о журнале можно найти здесь