Аннотация
В этом документе демонстрируется, насколько легко использовать
Windows Forms-элемент управления DataGrid в Microsoft Visual Studio .NET
для простого вывода табличных данных на форме. Кроме того, объясняется,
как изменить поведение этого элемента по умолчанию, что позволяет
применять его с большей гибкостью.
Скачать файл с кодом примера datagridcustomization.exe можно с сайта
Microsoft Download Center.
Содержание
Краткий обзор
В данной статье объясняется, как настроить внешний вид и поведение
элемента управления DataGrid. Производный класс DataGridBoolColumn
предоставляет событие BoolValChanged, вызываемое всякий раз при
изменении значения флажка. Кроме того, в производном классе
DataGridTextBoxColumn создано событие CellFormatting, позволяющее
управлять свойствами Font, ForeColor и BackColor каждой ячейки.
DataGrid-событие CurrentCellChanged используется, чтобы переместить
фокус на любой столбец независимо от того, какой столбец строки
изначально щелкнул пользователь. Дополнительно показывается, как
динамически изменять внешний вид ячеек DataGrid в зависимости от
значений в ключевых ячейках. Под ключевой понимается ячейка, значение
которой определяет внешний вид других ячеек. Далее объясняется, как
заставить столбец с флажком реагировать на щелчок в ячейке изменением
своего состояния и как управлять порядком отображения и видимостью
столбцов DataGrid вне зависимости от DataSource сетки. Наконец,
поясняется, как с помощью элемента управления ToolTip в Windows Forms
выводить разные для каждой строки в DataGrid подсказки. Полученный в
результате DataGrid приведен рис. 1.
Рис. 1. Настроенный DataGrid
Введение
У этой статьи две цели. Первая - показать, насколько легко
реализовать простой вывод табличных данных с помощью DataGrid в Windows
Forms. Вторая - объяснить, как изменять поведение DataGrid по умолчанию,
что позволяет использовать его с большей гибкостью. Попутно
рассматривается, как создавать классы, которые можно включать в проекты
для обеспечения сходной функциональности в приложениях.
Если вам известно, как при помощи дизайнера вывести DataGrid,
связанный с DataTable, пропустите часть материала до раздела
Настройка сетки. Все примеры написаны на VB.NET. Однако исходный
код, который можно скачать по ранее указанной ссылке, включает проекты
на VB.NET и на C#.
Основы
Для начала создайте в Visual Studio .NET проект Windows Forms
Application, поместите на форму DataGrid и добавьте DataSource. Все это
делается с помощью дизайнера. Чтобы отобразить DataGrid, связанный с
источником данных ADO.NET, нужно всего одна строка кода. Еще одна строка
требуется, чтобы пользователи могли редактировать значения и сохранять
их в источнике данных. Далее предполагается, что примеры баз данных,
поставляемые с .NET Framework, установлены. Если это не так, установите
их, перейдя по следующей ссылке (модифицируйте ее в зависимости от того,
куда установлен Visual Studio .NET):
C:\Program Files\Microsoft Visual
Studio.NET\FrameworkSDK\Samples\StartSamples.htm
Следующая процедура описывает создание приложения, отображающего
таблицу Products из базы данных Northwind в DataGrid.
- В меню File выберите New, затем Projects и создайте новый проект
приложения Windows Forms. Назовите проект CustomDataGrid.
- Перетащите DataGrid из Toolbox на форму. Измените его размер
так, чтобы элемент управления занимал практически всю форму и
настройте его свойство Anchor на привязку ко всем четырем краям.
Полученная форма в Visual Studio приведена на рис. 2.
Рис. 2. Форма в Visual Studio
- Затем в меню View выберите Server Explorer Window. Под Data
Connections откройте узел Northwind из списка Tables. Перетащите
таблицу Products в рабочую область дизайнера. После этого в секции
Components в самом низу рабочей области должны появиться два
компонента, SqlConnection1 и SqlDataAdapter1, как показано на рис.
3.
Рис. 3. Форма с компонентами SqlConnection и SqlDataAdapter
- Щелкните правой кнопкой мыши компонент SqlDataAdapter1 и
выберите Generate DataSet. Появится диалоговое окно Generate DataSet
(рис. 4). Нажмите Enter и согласитесь с операцией по умолчанию, т.
е. с созданием типизированного набора данных и размещением его
экземпляра в секции Components.
Рис. 4. Создание DataSet
- В рабочей области дизайнера щелкните DataGrid и установите его
свойство DataSource равным DataSet11Products.
- Добавьте обработчик события Load формы, дважды щелкнув пустой
участок формы. В этот обработчик поместите единственную строку кода:
Me.SqlDataAdapter1.Fill(Me.DataSet11)
- Наконец, скомпилируйте и запустите проект. Должна появиться
сетка вроде приведенной на рис. 5.
Рис. 5. DataGrid, созданный с помощью дизайнера и одной строки
кода
Настройка сетки
1. Столбцы и порядок их вывода
В элементе управления DataGrid необходимо управлять как выводимыми
столбцами, так и порядком их вывода. Столбцы и их порядок в DataGrid,
создаваемом по умолчанию, определяются SQL-запросом, сформированным в
процессе создания
SqlDataAdapter. Из DataGrid, созданного по умолчанию, нужно удалить
SupplierID и CategoryID. Кроме того, требуется переместить столбец
Discontinued с последнего места на первое.
Можно вернуться назад и вручную изменить SQL-запрос, чтобы задать,
какие столбцы должны появиться в DataGrid и каков должен быть порядок их
следования. Но вместо этого я объясню, как добавить к DataGrid
DataGridTableStyle. Добавив DataGridTableStyle, списком столбцов и
порядком их следования можно управлять на основе стилей, включаемых в
DataGridTableStyle-набор GridColumnStyles. Стили, используемые
DataGrid, определяются в момент добавления DataGridTableStyle к набору
DataGrid.TableStyle. Если TableStyle.GridColumnStyle на этом этапе пуст,
создается используемый DataGrid набор ColumnStyles по умолчанию. Однако,
если DataGridColumns явно добавлены к DataGridTableStyle до добавления
DataGridTableStyle к набору DataGrid.TableStyles, в DataGrid
отображаются только столбцы, содержащиеся в указанном наборе
DataGridTableStyle.GridColumnStyles, причем порядок их появления
соответствует порядку, в котором они размещены в наборе.
Прежде чем изучать фрагменты кода, задающие столбцы и порядок их
следования, рассмотрим класс DataGridColumnStyle. Это абстрактный класс.
Обычно применяют либо класс DataGridTextBoxColumn, либо класс
DataGridBoolColumn (оба они производны от DataGridColumnStyle и
поставляются с .NET Framework). Основное предназначение этих классов -
управление внешним видом столбца в DataGrid. Например,
DataGridBoolColumn заставляет столбец выглядеть и функционировать
подобно флажку. Позже я создам от этой пары дополнительные производные
классы, позволяющие производить дальнейшие изменения внешнего вида и
поведения столбцов.
Соответствие между конкретным столбцом в DataTable и конкретным
объектом DataGridColumnStyle задается в свойстве DataGridColumnStyle.MappingName.
Это единственное обязательное свойство, которое необходимо задать при
создании DataGridColumnStyle. К другим интересным свойствам
DataGridColumnStyle относятся
Header,
ReadOnly и
Width.
DataGrid по умолчанию содержит следующие 10 столбцов в таком порядке:
ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel и Discontinued.
Измененный DataGrid включает только восемь столбцов в следующем порядке:
Discontinued, ProductID, ProductName, QuantityPerUnit, UnitPrice,
UnitsInStock, UnitsOnOrder и ReorderLevel.
Далее приведен измененный обработчик события Load формы, который
создает сначала DataGridTableStyle (шаг 1), затем - объекты
DataGridColumnStyle, добавляемые к набору GridColumnStyles (шаг 2), и,
наконец, добавляет DataGridTableStyle к свойству DataGrid.TableStyles
(шаг 3).
Private Sub Form1_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Me.SqlDataAdapter1.Fill(Me.DataSet11)
? Шаг 1: создать DataGridTableStyle
? и присвоить MappingName таблицу.
Dim tableStyle As New DataGridTableStyle()
tableStyle.MappingName = "Products"
? Шаг 2: создать DataGridColumnStyle для каждого столбца
? выводимого в сетке в порядке их отображения.
?Discontinued
Dim discontinuedCol As New DataGridBoolColumn()
discontinuedCol.MappingName = "Discontinued"
discontinuedCol.HeaderText = ""
discontinuedCol.Width = 30
? Отключить третье состояние флажка
discontinuedCol.AllowNull = False
tableStyle.GridColumnStyles.Add(discontinuedCol)
? Шаг 2: ProductID
Dim column As New DataGridTextBoxColumn()
column.MappingName = "ProductID"
column.HeaderText = "ID"
column.Width = 30
tableStyle.GridColumnStyles.Add(column)
? Шаг 2: ProductName
column = New DataGridTextBoxColumn()
column.MappingName = "ProductName"
column.HeaderText = "Name"
column.Width = 140
tableStyle.GridColumnStyles.Add(column)
? Шаг 2: QuantityPerUnit
column = New DataGridTextBoxColumn()
column.MappingName = "QuantityPerUnit"
column.HeaderText = "QuantityPerUnit"
tableStyle.GridColumnStyles.Add(column)
? Шаг 2: UnitPrice
column = New DataGridTextBoxColumn()
column.MappingName = "UnitPrice"
column.HeaderText = "UnitPrice"
tableStyle.GridColumnStyles.Add(column)
? Шаг 2: UnitsInStock
column = New DataGridTextBoxColumn()
column.MappingName = "UnitsInStock"
column.HeaderText = "UnitsInStock"
tableStyle.GridColumnStyles.Add(column)
? Шаг 2: UnitsOnOrder
column = New DataGridTextBoxColumn()
column.MappingName = "UnitsOnOrder"
column.HeaderText = "UnitsOnOrder"
tableStyle.GridColumnStyles.Add(column)
? Шаг 2: ReorderLevel
column = New DataGridTextBoxColumn()
column.MappingName = "ReorderLevel"
column.HeaderText = "ReorderLevel"
tableStyle.GridColumnStyles.Add(column)
? Шаг 3: добавить tablestyle к datagrid
Me.DataGrid1.TableStyles.Add(tableStyle)
End Sub
На рис. 6 показан DataGrid после добавления кода, приведенного выше.
Столбцы совпадают с ранее указанными, а Discontinued появляется первым.
Рис. 6. DataGrid с определенными столбцами в требуемом порядке
2. Изменение реакции на щелчок в строке со сведениями о товаре,
выпуск которого прекращен
Допустим, пользователь не должен редактировать данные о товаре,
выпуск которого прекращен (discontinued product), пока не сбросит флажок
Discontinued. То есть, если пользователь щелкнет любую ячейку в строке с
таким товаром, фокус переводится с этой ячейки на первый столбец -
Discontinued.
Чтобы создать такую функциональность, добавьте обработчик события
CurrentCellChanged. В этом обработчике, если значение столбца
Discontinued равно true, явно устанавливайте текущей ячейку в столбце
Discontinued. Вот пример кода:
Private Sub dataGrid1_CurrentCellChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles DataGrid1.CurrentCellChanged
? Если пользователь щелкнул в строке с товаром, выпуск
? которого прекращен, установить CurrentCell равным CheckBox
Dim discontinuedColumn As Integer = 0
Dim val As Object = Me.DataGrid1( _
Me.DataGrid1.CurrentRowIndex, _
discontinuedColumn)
Dim productDiscontinued As Boolean = CBool(val)
If productDiscontinued Then
Me.DataGrid1.CurrentCell = _
New DataGridCell( _
Me.DataGrid1.CurrentRowIndex, _
discontinuedColumn)
End If
End Sub
3. Флажок, реагирующий на единственный щелчок
Чтобы изменить значение флажка в DataGrid по умолчанию требуется два
щелчка. Первый переводит фокус ввода на ячейку, а второй изменяет
состояние флажка. В нашем примере мы сделаем так, чтобы значение флажка
изменялось при первом щелчке. Для этого надо захватывать событие
DataGrid.Click и проверять, находится ли курсор мыши в ячейке с флажком.
Если это первый щелчок после того, как ячейка стала текущей, следует
явно изменить булево значение в сетке. Чтобы отследить первый щелчок
после того, как ячейка стала текущей, добавьте к форме переменную и
присваивайте ей соответствующее значение в обработчике
CurrentCellChanged.
Далее приведены фрагменты кода, реализующее такое поведение. В
обработчике Click определяется позиция курсора мыши, которая передается
DataGrid-методу HitTest, чтобы выяснить, в какой ячейке произошел
щелчок. Задача - среагировать на первый щелчок в ячейке после изменения
текущей ячейки. Во избежание проблем, возникающих при щелчке на строке
AddNew, находящейся внизу DataGrid, проводится дополнительная проверка,
т. е. выясняется, является ли строка реальной (связанной с источником
данных). Для этого номер строки сравнивается со значением свойства Count
объекта BindingManagerBase.
Private afterCurrentCellChanged As Boolean = False
Private Sub dataGrid1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles DataGrid1.Click
Dim discontinuedColumn As Integer = 0
Dim pt As Point = Me.dataGrid1.PointToClient( _
Control.MousePosition)
Dim hti As DataGrid.HitTestInfo = _
Me.dataGrid1.HitTest(pt)
Dim bmb As BindingManagerBase = _
Me.BindingContext(Me.dataGrid1.DataSource, _
Me.dataGrid1.DataMember)
If afterCurrentCellChanged _
AndAlso hti.Row < bmb.Count _
AndAlso hti.Type = DataGrid.HitTestType.Cell _
AndAlso hti.Column = discontinuedColumn Then
Me.DataGrid1(hti.Row, discontinuedColumn) = _
Not CBool(Me.DataGrid1(hti.Row, _
discontinuedColumn))
End If
afterCurrentCellChanged = False
End Sub ?dataGrid1_Click
? Добавить строку к этому (существующему) обработчику
Private Sub dataGrid1_CurrentCellChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles DataGrid1.CurrentCellChanged
? Если щелчок произошел в строке с товаром, выпуск которого прекращен,
? сделать текущей ячейку с флажком
Dim discontinuedColumn As Integer = 0
Dim val As Object = Me.DataGrid1( _
Me.DataGrid1.CurrentRowIndex, _
discontinuedColumn)
Dim productDiscontinued As Boolean = CBool(val)
If productDiscontinued Then
Me.DataGrid1.CurrentCell = _
New DataGridCell( Me.DataGrid1.CurrentRowIndex, _
discontinuedColumn)
End If
? Добавьте эту строку
afterCurrentCellChanged = True
End Sub ?dataGrid1_CurrentCellChanged
4. Изменение свойств ячейки BackColor, ForeColor и Font
Для изменения способа отрисовки ячейки DataGrid нужно переопределить
метод Paint в производном классе DataGridTextBoxColumn. Когда элементу
DataGrid, с которым связан DataGridTableStyle, требуется нарисовать
ячейку, он вызывает метод Paint класса DataGridTextBoxColumn. Аргументы
вызываемого метода включают ForeBrush и BackBrush, управляющие цветом
ячейки. Таким образом, чтобы управлять цветами каждой отдельной ячейки,
можно переопределить виртуальный метод Paint класса
DataGridTextBoxColumn и изменять кисти для каждой рисуемой ячейки.
Суть в том, чтобы добавить событие к производному классу
DataGridTextBoxColumn. В производном методе Paint это событие
срабатывает, и все его приемники могут указать BackColor и ForeColor для
рисуемой ячейки. После того как это событие создано, для управления
цветом ячейки нужно лишь создать обработчик события. Кроме того,
специальные EventArgs обеспечивают приемник события информацией о
номерах строки и столбца, а также позволяют ему настроить свойства
цвета.
Создание класса аргументов события
В Solution Explorer дважды щелкните проект и добавьте новый класс
DataGridFormatCellEventArgs, производный от EventArgs. Далее приведен
список его открытых свойств с описанием их применения. Три первых
свойства передаются в обработчик события в виде аргумента; приемник
события устанавливает семь последних свойств, указывая, как рисовать
ячейку.
- Column (Integer) - номер столбца рисуемой ячейки.
- Row (Integer) - номер строки рисуемой ячейки.
- CurrentCellValue (Object) - текущее значение в ячейке.
- TextFont (Font) - шрифт для вывода текста в ячейке.
- BackBrush (Brush) - кисть, используемая для рисования фона
ячейки.
- ForeBrush (Brush) - кисть для рисования текста в ячейке.
- TextFontDispose (Boolean) - если равно true, вызывается
TextFont.Dispose.
- BackBrushDispose (Boolean) - если равно true, вызывается
BackBrush.Dispose.
- ForeBrushDispose (Boolean) - если равно true, вызывается
ForeBrush.Dispose.
- UseBaseClassDrawing (Boolean) - если равно true, вызывается
MyBase.Paint.
Dispose-свойства DataGridFormatCellEventArgs устанавливаются
приемником события в том случае, если переопределенная версия Paint
должна вызвать метод Dispose объектов, реализующих
IDisposable. Например, если каждый раз при отрисовке ячейки
динамически создается BackBrush, то по окончании отрисовки вызывайте
Dispose кисти из метода Paint. С другой стороны, если создается и
кэшируется единственная кисть, предоставляющая BackBrush для нескольких
ячеек, для нее вызывать Dispose не следует. Dispose-свойства позволяют
приемнику события сообщить переопределенному методу Paint, что делать с
вызовами Dispose.
Вот как должен выглядеть код класса. Перед определением класса
добавлен делегат, определяющий сигнатуру обработчика события, требуемую
событием SetCellFormat и определенную как часть производного класса
DataGridTextBoxColumn.
Public Delegate Sub FormatCellEventHandler( _
ByVal sender As Object, _
ByVal e As DataGridFormatCellEventArgs)
Public Class DataGridFormatCellEventArgs
Inherits EventArgs
Public Sub New(ByVal row As Integer, _
ByVal col As Integer, _
ByVal cellValue As Object)
End Sub ?New
End Class
Создание производного класса DataGridTextBoxColumn
Теперь нужно добавить код в производный класс DataGridTextBoxColumn.
Для этого щелкните проект правой кнопкой мыши в Solution Explorer и
добавьте класс FormattableTextBoxColumn. Чтобы добавить заглушку (stub)
для переопределенной версии Paint, выберите (Overrides) в раскрывающемся
списке в левой верхней части окна редактирования кода, а в аналогичном
списке в правой верхней части - второй метод Paint (с семью
аргументами). Соответствующий код показан ниже.
Примечание Заметьте,
что добавлено и объявление события SetCellFormat; при этом используется
делегат FormatCellEventHandler, определенный в файле предыдущего класса.
Public Class FormattableTextBoxColumn
Inherits DataGridTextBoxColumn
Public Event SetCellFormat As FormatCellEventHandler
Protected Overloads Overrides Sub Paint(………)
End Sub ?Paint
End Class
Реализация перегруженной версии Paint
Первое, что нужно сделать в перегруженной версии Paint, - вызвать
событие SetCellFormat. Именно через это событие его приемник указывает
формат текущей ячейки. Для этого следует создать
DataGridFormatCellEventArgs, инициализировать строку, столбец и текущее
значение, а затем вызвать событие с этим аргументом. Наконец, после
генерации события необходимо проанализировать аргумент события и
выяснить, какие форматирующие операции требуется выполнить.
Вызов события
Ниже приведен фрагмент кода, создающий аргумент и генерирующий
событие. Переменные source и rowNum передаются методу Paint.
Dim e As DataGridFormatCellEventArgs = Nothing
? Получить номер столбца
Dim col As Integer = _
Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me)
? Создать объект EventArgs
e = New DataGridFormatCellEventArgs( _
rowNum, col, _
Me.GetColumnValueAtRow([source], rowNum))
? Вызвать событие форматирования
RaiseEvent SetCellFormat(Me, e)
Цветовое оформление
Для задания цвета и фона ячейки проверьте свойства объекта
DataGridFormatCellEventArgs после того, как обработчик события вернул
управление. Две кисти (foreBrush и backBrush), используемые для
отрисовки ячейки, передаются как аргументы метода Paint. Для изменения
цвета и фона ячейки смените старые кисти на новые до вызова базовой
реализации метода Paint. Далее приведен фрагмент кода, иллюстрирующий
этот подход:
? допустим, мы вызываем BaseClass
Dim callBaseClass As Boolean = True
? Проверить кисти, возвращенные событием
If Not (e.BackBrush Is Nothing) Then
backBrush = e.BackBrush
End If
If Not (e.ForeBrush Is Nothing) Then
foreBrush = e.ForeBrush
End If
? Проверить свойство UseBaseClassDrawing
If Not e.UseBaseClassDrawing Then
callBaseClass = False
End If
If callBaseClass Then
MyBase.Paint(g, bounds, _
[source], rowNum, backBrush, _
foreBrush, alignToRight)
End If
В предыдущем фрагменте кисти настраивались на основе свойств объекта
DataGridFormatCellEventArgs. В зависимости от значения
UseBaseClassDrawing вызывается базовый класс. А вызов базового класса с
новыми кистями и изменяет цвет ячейки. Не забудьте, что этот метод
вызывается для каждой ячейки в столбце.
Обработка свойства TextFont
Изменение шрифта ячейки существенно усложняет перегруженную версию
Paint. Дело в том, что шрифт, используемый для вывода текста, - не
просто параметр, передаваемый как аргумент метода Paint. Напротив,
изначально шрифт извлекается из свойства DataGrid.Font. При этом
значение шрифта кэшируется и не считывается вновь для каждой ячейки.
Именно поэтому первое, что приходит в голову, - динамическое изменение
DataGrid.Font для каждой ячейки - не сработает. Поэтому, чтобы изменить
шрифт ячейки, необходимо самостоятельно отрисовывать строку. Для точной
имитации вывода стандартного DataGrid придется потрудиться. В примере
этому не уделяется чрезмерно много внимания - удовлетворимся похожестью
вместо полной идентичности. Вот фрагмент кода, выводящий строку новым
шрифтом.
? Если шрифт текста (TextFont) задан, вывести строку
If Not (e.TextFont Is Nothing) Then
Try
Dim charWidth As Integer = _
Fix(Math.Ceiling(g.MeasureString("c", _
e.TextFont, 20, _
StringFormat.GenericTypographic).Width))
Dim s As String = _
Me.GetColumnValueAtRow([source], _
rowNum).ToString()
Dim maxChars As Integer = _
Math.Min(s.Length, bounds.Width / charWidth)
Try
g.FillRectangle(backBrush, bounds)
g.DrawString(s.Substring(0, maxChars), _
e.TextFont, foreBrush, _
bounds.X, bounds.Y + 2)
Catch ex As Exception
Console.WriteLine(ex.Message.ToString())
End Try
Catch ? пустой catch
End Try
callBaseClass = False
End If
В первой части кода оценивается, сколько символов можно вывести с
учетом ширины текущей ячейки. Для этого ширина ячейки делится на средний
размер символов. Более точно количество символов можно вычислить,
последовательно вызывая MeasureString. При этом вы получите точное число
символов, но тогда вывод будет существенно отличаться от вывода
стандартного DataGrid. Способ, основанный на среднем размере символа,
гораздо быстрее, а его результаты намного ближе к стандартному DataGrid.
Получив число символов, мы рисуем фон и вызываем Graphics.DrawString
с передачей нужных шрифта и кисти. Наконец, флаг callBaseClass задается
так, чтобы метод Paint базового класса впоследствии не вызывался,
уничтожая все наши предыдущие усилия.
Применение нового класса FormattableTextBoxColumn
Чтобы использовать новый столбец, вернитесь в код формы и замените
все вхождения DataGridTextBoxColumn на FormattableTextBoxColumn. Теперь
для каждого столбца подключите приемник события SetCellFormat,
генерируемого производным классом. Можно указать свой обработчик для
каждого столбца. Но нам для окрашивания строк достаточно одного
обработчика для всех столбцов. Ниже приведен код для типичного столбца
после этих изменений.
? ProductName
column = New FormattableTextBoxColumn()
column.MappingName = "ProductName"
column.HeaderText = "Name"
column.Width = 140
AddHandler column.SetCellFormat, AddressOf FormatGridRow
tableStyle.GridColumnStyles.Add(column)
Обратите внимание, что теперь создается экземпляр производного
класса, FormattableTextBoxColumn. Кроме того, добавляется FormatGridRow
- обработчик для события SetCellFormat.
Обработчик события SetCellFormat
В обработчике SetCellFormat кисти задают фон и цвет текста в ячейке,
номера строки и столбца которой передаются в аргументах события. Если
ячейка находится в текущей строке сетки, используется один набор цветов
и шрифта, а если она находится в строке, булево значение Discontinued в
которой равно true, применяется другой набор цветов. Далее приведен
фрагмент кода, объявляющий и определяющий эти кэшируемые объекты GDI+.
? Переменные формы, кэширующие объекты GDI+
Private disabledBackBrush As Brush
Private disabledTextBrush As Brush
Private currentRowFont As Font
Private currentRowBackBrush As Brush
? В Form1_Load создаются и кэшируются объекты GDI+
Me.disabledBackBrush = _
New SolidBrush(SystemColors.InactiveCaptionText)
Me.disabledTextBrush = _
New SolidBrush(SystemColors.GrayText)
Me.currentRowFont = _
New Font(Me.DataGrid1.Font.Name, _
Me.DataGrid1.Font.Size, _
FontStyle.Bold)
Me.currentRowBackBrush = Brushes.DodgerBlue
В коде обработчика события настраиваются члены
DataGridFormatCellEventArgs, передаваемые для вывода конкретных строк в
том или ином формате. Строки со сведениями о товарах, выпуск которых
прекращен, выводятся серыми, а текущая строка выделяется на фоне
DodgerBlue полужирным шрифтом. Вот обработчик, реализующий эту логику:
Private Sub FormatGridRow(ByVal sender As Object, _
ByVal e As DataGridFormatCellEventArgs)
Dim discontinuedColumn As Integer = 0
? Установить свойства e в зависимости от e.Row и e.Col
Dim discontinued As Boolean = CBool( _
IIf(e.Column <> discontinuedColumn, _
Me.DataGrid1(e.Row, discontinuedColumn), _
e.CurrentCellValue))
? Строка со сведениями о товаре, выпуск которого прекращен?
If e.Column > discontinuedColumn AndAlso _
CBool(Me.DataGrid1(e.Row, discontinuedColumn)) Then
e.BackBrush = Me.disabledBackBrush
e.ForeBrush = Me.disabledTextBrush
? Текущая строка?
ElseIf e.Column > discontinuedColumn AndAlso _
e.Row = Me.DataGrid1.CurrentRowIndex Then
e.BackBrush = Me.currentRowBackBrush
e.TextFont = Me.currentRowFont
End If
End Sub
Помните, что на самом деле в наборе
DataGrid.TableStyle(0).GridColumnStyles существует несколько объектов
FormattableTextBoxColumn. По сути каждый столбец (за исключением
булевого) использует эти объекты. Кроме того, каждый из этих объектов
присоединяется к своему событию SetCellFormat. Однако все эти события
используют один и тот же обработчик FormatGridRow, приведенный ранее. То
есть все ячейки в строке (кроме булевых) форматируются по одним и тем же
правилам. Таким образом, устанавливаются стили строк, и все ячейки
строки имеют одинаковый формат несмотря на то, что каждая ячейка
форматируется отдельно.
Посмотрим, что получилось
Ниже показана сетка, которая получилась у нас к этому моменту.
Заметьте, что строки с установленными флажками отображаются серым, а
текущая строка выделяется полужирным шрифтом на фоне DodgerBlue.
Рис. 7. DataGrid с цветными строками
На первый взгляд все хорошо. Но есть маленькая проблема, которую надо
решить. Дело в том, что щелчок ранее помеченного первого столбца
приводит к сбросу флажка, но строка не перерисовывается. А строку нужно
обновлять при каждом изменении значения флажка. Чтобы решить эту
проблему, создадим класс, производный от DataGridBooleanColumn, и
добавим обработчик события, вызывающий событие всякий раз, когда булево
значение изменяется. Тогда мы получим возможность реагировать на
соответствующий щелчок и перерисовывать строку в соответствии с новым
значением.
5. Добавление столбца типа bool с событием ValueChanged
Аргументы события
Начнем с создания класса, производного от EventArgs и хранящего
необходимые нам сведения о событии BoolValueChanged, а именно: столбец и
строку изменяемой ячейки, а также новое булево значение. Нижк показан
класс BoolValueChangedEventArgs. В отсутствующей реализации
инициализируемые в конструкторе закрытые поля используются только как
копии открытых свойств. Кроме того, в начале определяется делегат для
этого события - BoolValueChangedEventHandler.
Public Delegate Sub BoolValueChangedEventHandler( _
ByVal sender As Object, _
ByVal e As BoolValueChangedEventArgs)
Public Class BoolValueChangedEventArgs
Inherits EventArgs
Public Sub New(ByVal row As Integer, _
ByVal col As Integer, _
ByVal val As Boolean)
. . .
End Sub ?New
Public Property Column() As Integer
. . .
End Property
Public Property Row() As Integer
. . .
End Property
Public ReadOnly Property BoolValue() As Boolean
. . .
End Property
End Class ?BoolValueChangedEventArgs
Производный класс DataGridBoolColumn
В производном классе ClickableBooleanColumn перегружаются три метода,
отвечающие за различные аспекты работы с булевым значением в ячейке.
Первый из них - Edit. Он вызывается, когда ячейка должна перейти в режим
редактирования. В перегруженной версии некоторые закрытые поля
настраиваются в соответствии с текущем булевым состоянием и текущим
режимом (редактирование). Второй - такой же метод Paint, что и ранее
использованный в FormattableTextBoxColumn. Здесь вместо изменения
внешнего вида ячейки отслеживаются изменения булевого значения и, если
оно изменилось, вызывается событие BoolValueChanged. Третий - метод
Commit. Он вызывается при завершении редактирования, когда значение
необходимо сохранить в источнике данных. В этом методе обнуляются поля,
заполняемые в перегруженной версии метода Edit.
Есть несколько технических задач, которые нужно решить корректно.
Булево значение может измениться в результате щелчка ячейки или нажатия
клавиши-пробела, когда фокус ввода находится в ячейке. Для отслеживания
этих событий используется вспомогательный метод ManageBoolValueChanging.
При изменении значения он вызывает событие BoolValueChanged. Чтобы
отследить щелчок, ManageBoolValueChanging проверяет общие (статические)
свойства Control.MousePosition и Control.MouseButtons. Однако проверка
KeyState для клавиши-пробела - операция проблематичная; здесь придется
прибегнуть к Interop-вызову Win32-функции GetKeyState.
Вот как выглядит код класса:
Public Class ClickableBooleanColumn
Inherits DataGridBoolColumn
? Событие изменения
Public Event BoolValueChanged _
As BoolValueChangedEventHandler
? Настройка переменных для отслеживания изменений
Protected Overloads Overrides Sub Edit(. . .)
Me.lockValue = True
Me.beingEdited = True
Me.saveRow = rowNum
Me.saveValue = CBool( _
MyBase.GetColumnValueAtRow( _
[source], rowNum))
MyBase.Edit(. . .)
End Sub ?Edit
? Перегружен для обработки события BoolChange
Protected Overloads Overrides Sub Paint(. . .)
Dim colNum As Integer = _
Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me)
? Используется для обработки изменения значения
ManageBoolValueChanging(rowNum, colNum)
MyBase.Paint(. . .)
End Sub ?Paint
? Отключить отслеживание изменений
Protected Overrides Function Commit(. . .) As Boolean
Me.lockValue = True
Me.beingEdited = False
Return MyBase.Commit(dataSource, rowNum)
End Function ?Commit
? Поля, необходимые для отслеживания значения
Private saveValue As Boolean = False
Private saveRow As Integer = -1
Private lockValue As Boolean = False
Private beingEdited As Boolean = False
Public Const VK_SPACE As Integer = 32
? Необходимо, чтобы пробел изменял булево значение
<System.Runtime.InteropServices.DllImport("user32.dll")> _
Shared Function GetKeyState( _
ByVal nVirtKey As Integer) As Short
End Function
? Вызвать событие при изменении значения
Private Sub ManageBoolValueChanging( _
ByVal rowNum As Integer, _
ByVal colNum As Integer)
Dim mousePos _
As Point = Me.DataGridTableStyle.DataGrid.PointToClient( _
Control.MousePosition)
Dim dg As DataGrid = Me.DataGridTableStyle.DataGrid
Dim isClickInCell As Boolean = _
Control.MouseButtons = MouseButtons.Left AndAlso _
dg.GetCellBounds(dg.CurrentCell).Contains(mousePos)
Dim changing As Boolean = _
dg.Focused AndAlso isClickInCell _
OrElse GetKeyState(VK_SPACE) < 0
? Или пробел
If Not lockValue AndAlso _
beingEdited AndAlso _
changing AndAlso _
saveRow = rowNum Then
saveValue = Not saveValue
lockValue = False
? Вызвать событие
Dim e As New BoolValueChangedEventArgs( _
rowNum, colNum, saveValue)
RaiseEvent BoolValueChanged(Me, e)
End If
If saveRow = rowNum Then
lockValue = False
End If
End Sub ?ManageBoolValueChanging
End Class
Использование ClickableBooleanColumn
Чтобы применять специальный стиль столбца, создайте его экземпляр при
настройке columnstyle для discontinuedCol. Также добавьте обработчик
нового события BoolValueChanged.
? Строка со сведениями о товаре, выпуск которого прекращен
Dim discontinuedCol As New ClickableBooleanColumn()
discontinuedCol.MappingName = "Discontinued"
discontinuedCol.HeaderText = ""
discontinuedCol.Width = 30
? Отключить поддержку третьего состояния флажка
discontinuedCol.AllowNull = False
AddHandler discontinuedCol.BoolValueChanged, _
AddressOf BoolCellClicked
tableStyle.GridColumnStyles.Add(discontinuedCol)
Обработчик события BoolValueChanged
В этом обработчике события инициируется перерисовка строки с учетом
нового значения в ячейке. При каждом изменении булева значения
обработчик события объявляет недействительным прямоугольник, связанный
со строкой. Чтобы перерисовать строку, используется вспомогательный
метод RefreshRow. В обработчике события сохранение булева значения
инициируется окончанием редактирования. Это позволяет методу
FormatGridRow получить правильное значение булевой ячейки. Без этого
вызова в индексированном DataGrid доступно только первоначальное
значение ячейки.
Private Sub BoolCellClicked(ByVal sender As Object, _
ByVal e As BoolValueChangedEventArgs)
Dim g As DataGrid = Me.DataGrid1
Dim discontinuedCol As New _
ClickableBooleanColumn = _
g.TableStyles(0).GridColumnStyles("Discontinued")
g.EndEdit(discontinuedCol, e.Row, False)
RefreshRow(e.Row)
g.BeginEdit(discontinuedCol, e.Row)
End Sub
? Инициирует перерисовку данной строки
Private Sub RefreshRow(ByVal row As Integer)
Dim r As Rectangle = _
Me.dataGrid1.GetCellBounds(row, 0)
r = New Rectangle(r.Right, r.Top, _
Me.dataGrid1.Width, r.Height)
Me.dataGrid1.Invalidate(r)
End Sub ?RefreshRow
Осталась последняя проблема, после решения которой ячейка будет
реагировать на щелчки как положено, а строка менять цвет. При щелчке на
неотмеченном флажке в строке, отличной от текущей, строка приобретает
цвет текущей вместо цвета, которым выделяются ячейки с товаром, выпуск
которого прекращен. Более того, похоже, что проблема связана с
обновлением, так как при перетаскивании сетки по экрану строка
перерисовывается так, как и должно быть, - серым цветом. Решить эту
проблему можно, инициируя перерисовку строки в существующем обработчике
dataGrid1_Click. В этом событии выполняется код, изменяющий булево
значение и обновляющий строку.
If afterCurrentCellChanged AndAlso _
hti.Row < bmb.Count AndAlso _
hti.Type = DataGrid.HitTestType.Cell AndAlso _
hti.Column = 0 Then
Me.DataGrid1(hti.Row, 0) = _
Not CBool(Me.DataGrid1(hti.Row, 0))
RefreshRow(hti.Row)
End If
6. Добавление всплывающих подсказок, содержание которых зависит от
конкретной строки
С помощью Windows Forms-класса ToolTip можно добавить всплывающие
подсказки ко всему DataGrid. Для этого перетащите компонент ToolTip на
форму и настройте свойство ToolTip, которое добавляется к каждому
элементу управления. Однако подобная реализация всплывающих подсказок не
позволяет изменять подсказку при перемещении от строки к строке (или от
ячейки к ячейке) в DataGrid. Наша задача - выводить подсказку, разную
для разных строк. Тем же способом можно было бы реализовать подсказки,
разные для разных ячеек или столбцов.
Начнем с добавления к классу формы двух полей: hitRow и ToolTip1. Эти
поля инициализируются в Form1_Load и динамически изменяются в
добавленном обработчике события dataGrid1_MouseMove. В MouseMove
проверяется, находится ли курсор мыши над новой строкой, и, если да, в
ToolTip записывается другой текст. См. следующий пример кода:
Private hitRow As Integer
Private toolTip1 As System.Windows.Forms.ToolTip
? В Form1_Load
Me.hitRow = -1
Me.toolTip1 = New ToolTip()
Me.toolTip1.InitialDelay = 300
AddHandler Me.DataGrid1.MouseMove, _
AddressOf dataGrid1_MouseMove
Private Sub dataGrid1_MouseMove( _
ByVal sender As Object, _
ByVal e As MouseEventArgs)
Dim g As DataGrid = Me.dataGrid1
Dim hti As DataGrid.HitTestInfo = _
g.HitTest(New Point(e.X, e.Y))
Dim bmb As BindingManagerBase = _
Me.BindingContext(g.DataSource, g.DataMember)
If hti.Row < bmb.Count AndAlso _
hti.Type = DataGrid.HitTestType.Cell AndAlso _
hti.Row <> hitRow Then
If Not (Me.toolTip1 Is Nothing) AndAlso _
Me.toolTip1.Active Then
Me.toolTip1.Active = False ? выключить
End If
Dim tipText As String = ""
If CBool(Me.DataGrid1(hti.Row, 0)) Then
tipText = Me.DataGrid1(hti.Row, 2).ToString() & _
" discontinued"
If tipText <> "" Then
? Новая строка
hitRow = hti.Row
Me.toolTip1.SetToolTip(Me.DataGrid1, tipText)
? Сделать подсказку активной, чтобы она могла показываться
Me.toolTip1.Active = True
Else
hitRow = -1
End If
Else
hitRow = -1
End If
End If
End Sub ?dataGrid1_MouseMove
Рис. 8. DataGrid с всплывающими подсказками, разными для разных
строк
Заключение
В конечном DataGrid для форматирования ячейки и отслеживания
изменений булевого значения используются два производных
DataGridColumnStyles. Форматирование осуществляется с помощью
переопределения метода Paint производного класса и вызова события,
позволяющего приемнику передавать информацию о форматировании в
зависимости от номеров строки и столбца. Производный класс, управляющий
булевыми переменными, также перегружает метод Paint и вызывает событие
при изменении значения на этом этапе. Кроме того, для решения этой
задачи нужно переопределить методы Edit и Commit.
Для управления поведением DataGrid используются и события уровня
формы. С помощью события DataGrid.CurrentCellChanged текущей ячейкой
становится конкретная ячейка в строке независимо от того, на какой
ячейке щелкнул пользователь. Благодаря событию DataGrid.Clicked столбец
CheckBox реагирует на первый щелчок; чтобы изменить его значение, второй
щелчок не требуется. Наконец, событие DataGrid.MouseMove используется
для динамического изменения всплывающих подсказок от строки к строке.
В материалах для скачивания (см. ссылку в начале статьи) содержатся
два примера. Первый, CustomGrid, включает проект VB.NET, фрагменты из
которого приведены в этой статье. Второй, CustomGridA, содержит два
проекта: один - на VB.NET, другой - на C#. Последний сложнее, чем
обсуждаемый здесь, но в основном реализует тот же подход.