Advanced
Border and Gridlines
With the DataGridView control, you can customize the appearance of the control's border and gridlines to improve the user experience.
You can modify the gridline color and the control border style in addition to the border styles for the cells within the control.
You can also apply different cell border styles for ordinary cells, row header cells, and column header cells.
The gridline color is used only with the Single, SingleHorizontal, and SingleVertical values of the DataGridViewCellBorderStyle enumeration and the Single value of the DataGridViewHeaderBorderStyle enumeration. The other values of these enumerations use colors specified by the operating system. Additionally, when visual styles are enabled on Windows XP and the Windows Server 2003 family through the System.Windows.Forms.Application.EnableVisualStyles method, the GridColor property value is not used.
Me.dataGridView1.GridColor = Color.BlueViolet
Me.dataGridView1.BorderStyle = BorderStyle.Fixed3D
With Me.dataGridView1
.CellBorderStyle = DataGridViewCellBorderStyle.None
.RowHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single
.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single
End With
Cell Styles
Each cell within the DataGridView control can have its own style, such as text format, background color, foreground color, and font. Typically, however, multiple cells will share particular style characteristics.
Groups of cells that share styles may include all cells within particular rows or columns, all cells that contain particular values, or all cells in the control. Because these groups overlap, each cell may get its styling information from more than one place. For example, you may want every cell in a DataGridView control to use the same font, but only cells in currency columns to use currency format, and only currency cells with negative numbers to use a red foreground color.
Multiple Select Checked ListBox
modAppSpecific
Public mobjDataTable As New System.Data.DataTable()
Public mobjDataView As System.Data.DataView
Public gdlgDialogName As frmDialog
Public giDataGridRowIndex As Integer
BET_DataGridName
Option Strict On
Public Class BET_DataGridWorksheets
Inherits System.Windows.Forms.DataGrid
Private Const msMODULENAME As String = "BET_DataGrid"
Private mobjCOMException As System.Runtime.InteropServices.COMException
Private mobjException As Exception
'***************************************************************************************
Protected Overrides Function ProcessCmdKey(ByRef msg As System.Windows.Forms.Message, _
ByVal keyData As System.Windows.Forms.Keys) _
As Boolean
Try
If clsError.ErrorFlag() = True Then Exit Function
clsDataGrid.MouseOrArrowKey = "ArrowKey"
Select Case msg.WParam.ToInt32
Case CType(System.Windows.Forms.Keys.Up, Integer)
Call CustomMouseUpEvent()
Case CType(System.Windows.Forms.Keys.Down, Integer)
Call CustomMouseDownEvent()
End Select
Call MyBase.ProcessCmdKey(msg, keyData)
Catch objCOMException As System.Runtime.InteropServices.COMException
mobjCOMException = objCOMException
Catch objException As Exception
mobjException = objException
Finally
If gbDEBUG = True Or _
((IsNothing(mobjCOMException) = False Or IsNothing(mobjException) = False)) Then
Call clsError.Handle("ProcessCmdKey", msMODULENAME, _
"respond to this event successfully.", _
mobjCOMException, mobjException)
End If
End Try
End Function
'***************************************************************************************
Private Sub CustomMouseUpEvent()
Try
If clsError.ErrorFlag() = True Then Exit Sub
Dim irowno As Integer
'the first row index in the datagrid
If giDataGridRowIndex = 0 Then Exit Sub
giDataGridRowIndex = giDataGridRowIndex - 1
Catch objCOMException As System.Runtime.InteropServices.COMException
mobjCOMException = objCOMException
Catch objException As Exception
mobjException = objException
Finally
If gbDEBUG = True Or _
((IsNothing(mobjCOMException) = False Or IsNothing(mobjException) = False)) Then
Call clsError.Handle("CustomMouseUpEvent", msMODULENAME, _
"respond to this event successfully.", _
mobjCOMException, mobjException)
End If
End Try
End Sub
'***************************************************************************************
Private Sub CustomMouseDownEvent()
Try
If clsError.ErrorFlag() = True Then Exit Sub
Dim irowno As Integer
'the last row indes in the datagrid
Call MsgBox("Number of rows : " & mobjDataView.Table.Rows.Count)
If giDataGridRowIndex = mobjDataView.Table.Rows.Count - 1 Then Exit Sub
giDataGridRowIndex = giDataGridRowIndex + 1
Catch objCOMException As System.Runtime.InteropServices.COMException
mobjCOMException = objCOMException
Catch objException As Exception
mobjException = objException
Finally
If gbDEBUG = True Or _
((IsNothing(mobjCOMException) = False Or IsNothing(mobjException) = False)) Then
Call clsError.Handle("CustomMouseDownEvent", msMODULENAME, _
"respond to this event successfully.", _
mobjCOMException, mobjException)
End If
End Try
End Sub
'***************************************************************************************
End Class
frmDialog
Private Sub frmSheetManager_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
Dim sacolumnsarray(,) As String
Dim isheetnumber As Integer
Dim sadataarray(,) As String
ReDim sacolumnsarray(1, 4)
sacolumnsarray(0, 0) = "CheckBox" 'Type
sacolumnsarray(0, 1) = "Include" 'Mapping Name
sacolumnsarray(0, 2) = "" 'Header Name
sacolumnsarray(0, 3) = "26" 'Width
sacolumnsarray(0, 4) = "Include" 'Registry Prefix
sacolumnsarray(1, 0) = "TextBox" 'Type
sacolumnsarray(1, 1) = "Name" 'Mapping Name
sacolumnsarray(1, 2) = "Name" 'Header Name
sacolumnsarray(1, 3) = "90" 'Width
sacolumnsarray(1, 4) = "Name" 'Registry Prefix
mobjDataTable = clsDataGrid.DataTableDefine("Worksheets", sacolumnsarray)
ReDim sadataarray(gApplication.Sheets.Count - 1, 1)
For isheetnumber = 1 To gApplication.Sheets.Count
objWorksjeet = CType(gApplication.Sheets(isheetnumber), Excel.Worksheet)
sadataarray(isheetnumber - 1, 0) = "TRUE"
sadataarray(isheetnumber - 1, 1) = objWorksheet.Name
Next isheetnumber
Call clsDataTable.InsertRowsFromArray(mobjDataTable.TableName, "Worksheets", sadataarray)
mobjDataView = New DataView(mobjDataTable)
mobjDataView.AllowNew = False
mobjDataView.AllowDelete = False
mobjDataView.AllowEdit = False
Me.dgrWorksheets.DataSource = mobjDataView
Call clsDataGrid.DataTableStyleDefine(mobjDataTable.TableName.ToString, _
Me.dgrWorksheets, _
sacolumnsarray)
End Sub
Private Sub dgrWorksheets_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles dgrWorksheets.Click
Try
If clsError.ErrorFlag() = True Then Exit Sub
Dim objPoint As System.Drawing.Point = _
Me.dgrWorksheets.PointToClient(System.Windows.Forms.Control.MousePosition)
Dim objDataGridHitTest As System.Windows.Forms.DataGrid.HitTestInfo = _
Me.dgrWorksheets.HitTest(objPoint)
Dim objBindingManagerBase As System.Windows.Forms.BindingManagerBase = _
Me.BindingContext(Me.dgrWorksheets.DataSource, Me.dgrWorksheets.DataMember)
If (clsDataGrid.CellChangedEvent = True) And _
(objDataGridHitTest.Row < objBindingManagerBase.Count) And _
(objDataGridHitTest.Type = System.Windows.Forms.DataGrid.HitTestType.Cell) Then
'this toggles the value of the checkbox
Me.dgrWorksheets(objDataGridHitTest.Row, 0) = _
Not CBool(Me.dgrWorksheets(objDataGridHitTest.Row, 0))
End If
Catch objCOMException As System.Runtime.InteropServices.COMException
mobjCOMException = objCOMException
Catch objException As Exception
mobjException = objException
Finally
If gbDEBUG = True Or _
((IsNothing(mobjCOMException) = False Or IsNothing(mobjException) = False)) Then
Call clsError.Handle("dgrWorksheets_Click", msMODULENAME, _
"respond to this event successfully.", _
mobjCOMException, mobjException)
End If
End Try
End Sub
'*************************************************************************************
Private Sub dgrWorksheets_CurrentCellChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles dgrWorksheets.CurrentCellChanged
Try
If clsError.ErrorFlag() = True Then Exit Sub
Dim itempdatagridrowindex As Integer
Dim irowno As Integer
If clsDataGrid.MouseOrArrowKey.Length = 0 Then
clsDataGrid.MouseOrArrowKey = "Mouse"
End If
Dim objPoint As System.Drawing.Point = _
Me.dgrWorksheets.PointToClient(System.Windows.Forms.Control.MousePosition)
Dim objDataGridHitTest As System.Windows.Forms.DataGrid.HitTestInfo = _
Me.dgrWorksheets.HitTest(objPoint)
If clsDataGrid.MouseOrArrowKey = "Mouse" Then
itempdatagridrowindex = objDataGridHitTest.Row()
Else
itempdatagridrowindex = -1
End If
If (itempdatagridrowindex > -1) Then
giDataGridRowIndex = itempdatagridrowindex
irowno = CInt(Me.dgrWorksheets(giDataGridRowIndex, 2))
End If
clsDataGrid.MouseOrArrowKey = ""
Catch objCOMException As System.Runtime.InteropServices.COMException
mobjCOMException = objCOMException
Catch objException As Exception
mobjException = objException
Finally
If gbDEBUG = True Or _
((IsNothing(mobjCOMException) = False Or IsNothing(mobjException) = False)) Then
Call clsError.Handle("dgrWorksheets_CurrentCellChanged", msMODULENAME, _
"respond to this event successfully.", _
mobjCOMException, mobjException)
End If
End Try
End Sub
#Region " Windows Form Designer generated code "
Change the control declaration to refer to the new custom control.
Friend WithEvents dgrWorksheets As BET_DataGridName
Me.dgrWorksheets = New BET_DataGridName()
Columns and Column Order
The correspondence between a particular column in a DataTable, and a particular DataGridColumnStyle object is made through the DataGridColumnStyle.MappingName property. This property is the one required property that is needed to be set when creating a DataGridColumnStyle. Other DataGridColumnStyle properties of interest include Header, ReadOnly and Width.
The following is a modified Form Load event handler that will create the DataGridTableStyle (step 1), create the DataGridColumnStyles and add them to the GridColumnStyles collection (step 2), and finally, add the DataGridTableStyle to the DataGrid.TableStyles property (step 3).
Private Sub MyForm_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Me.SqlDataAdapter1.Fill(Me.DataSet11)
Dim tableStyle As New DataGridTableStyle()
Dim discontinuedCol As New DataGridBoolColumn()
Dim column As New DataGridTextBoxColumn()
tableStyle = New DataGridTableStyle()
tableStyle.MappingName = "Products"
discontinuedCol.MappingName = "Discontinued"
discontinuedCol.HeaderText = ""
discontinuedCol.Width = 30
discontinuedCol.AllowNull = False
tableStyle.GridColumnStyles.Add(discontinuedCol)
column = New DataGridTextBoxColumn()
column.MappingName = "ProductID"
column.HeaderText = "ID"
column.Width = 30
tableStyle.GridColumnStyles.Add(column)
column = New DataGridTextBoxColumn()
column.MappingName = "ProductName"
column.HeaderText = "Name"
column.Width = 140
tableStyle.GridColumnStyles.Add(column)
'Add the tablestyle to the datagrid
Me.DataGrid1.TableStyles.Add(tableStyle)
End Sub
Change Click Behavior on a Discontinued Row
Move focus to a particular column when the user selects any cell the focus is moved to the first column
Add a handler for the CurrentCellChanged event. In that handler, if the value in the first column is true then set the current cell to the first column.
Private Sub dgrDataGrid_CurrentCellChanged(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles dgrDataGridCurrencies.CurrentCellChanged
Dim objCheckBoxValue As Object = Me.dgrDataGrid(Me.DataGrid1.CurrentRowIndex, 0)
Dim bCheckBoxValue As Boolean = CBool(objCheckBoxValue)
If (bCheckBoxValue = True) Then
Me.dgrDataGrid.CurrentCell = _
New System.Windows.Forms.DataGridCell(Me.dgrDataGrid.CurrentRowIndex, 0)
End If
End Sub
Changing Cell BackColor, ForeColor and Font
To customize the way a DataGrid cell is drawn, it is necessary to override the Paint method in a derived DataGridTextBoxColumn class. When the DataGrid with a DataGridTableStyle associated with it needs to paint a cell, it calls the Paint method of the DataGridTextBoxColumn. The arguments in this call include a ForeBrush and BackBrush that controls the colors used to draw the cell. Therefore, to control the colors used to paint a cell on a per cell basis, it is possible to override the virtual Paint method of the DataGridTextBoxColumn class, and change the brushes according to the particular cell being drawn.
The following strategy is to add an event to the derived DataGridTextBoxColumn. In the derived Paint method, this event is fired to allow listeners to provide the BackColor and ForeColor to be used for the cell being painted. Once this event is in place, it is only necessary to write an event handler to control the color of a grid cell. Also derived are special EventArgs to provide the event listener with the row index and column index values, and to allow the event listener to set the color properties.
Creating the Event Arguments Class
In Solution Explorer, right-click the project, and add a new class named DataGridFormatCellEventArgs, which is inherited from EventArgs. The following is a list of the public properties of this class and a note on how they are used. The first three properties are passed into the event handler through the argument; the last seven properties are set by the event listener to indicate how the cell should be drawn.
Column (Integer): The column number of the cell being painted.
Row (Integer): The row number of the cell being painted.
CurrentCellValue (Object): The current cell value.
TextFont (Font): The font to be used to draw text in the cell.
BackBrush (Brush): The brush used to paint the cell's background.
ForeBrush (Brush): The brush used to paint the text in the cell.
TextFontDispose (Boolean): True to call the TextFont.Dispose.
BackBrushDispose (Boolean): True to call the BackBrush.Dispose.
ForeBrushDispose (Boolean): True to call the ForeBrush.Dispose.
UseBaseClassDrawing (Boolean): True to call the MyBase.Paint.
The dispose properties included as part of the DataGridFormatCellEventArgs are set by the listener if the Paint override should call the Dispose method on the objects that implement IDisposable. For example, if dynamically creating a BackBrush every time a grid cell is drawn, use the Paint method to call Dispose on the brush after it is finished with its drawing. On the other hand, if a single brush is created and cached that will be used to provide the BackBrush for some cells, it is not desirable to have the Paint method to call Dispose on the cached brush. These dispose properties let the listener tell the Paint override how to handle the Dispose calls.
The following is how the actual class code should appear. Before the class definition, add the delegate that determines the signature of the event handler required for the SetCellFormat event that will be defined as part of the derived 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
Creating the Derived DataGridTextBoxColumn
The next step is to add the class code for the derived DataGridTextBoxColumn. This is done in the same way, right-clicking the project in Solution Explorer and adding a new class named FormattableTextBoxColumn. To add the stub for the Paint override, select (Overrides) in the left drop-down list on top of the editing window, and then select the second Paint method (the one with seven arguments) listed in the right drop-down list at the top of the editing window. The following is what the code will look like.
Note Notice the SetCellFormat event declaration is also added, using the event delegate FormatCellEventHandler that was defined in the previous class file.
Public Class FormattableTextBoxColumn
Inherits DataGridTextBoxColumn
Public Event SetCellFormat As FormatCellEventHandler
Protected Overloads Overrides Sub Paint(………)
End Sub 'Paint
End Class
Implementing the Paint Override
The first thing to do in the Paint override is to fire the SetCellFormat event. It is through this event that an event listener will provide the format preferences that are to be used with this cell. The steps are to create the DataGridFormatCellEventArgs, initializing the row, column and current value, and then raise the event with this argument. Finally, after the event has been raised, query the event argument to see what formatting actions need to be done.
Raising the Event
The following is the code snippet that creates the argument and fires the event. The variables source and rowNum are passed into the Paint method.
Dim e As DataGridFormatCellEventArgs = Nothing
'Get the column number
Dim col As Integer = _
Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me)
'Create the eventargs object
e = New DataGridFormatCellEventArgs( _
rowNum, col, _
Me.GetColumnValueAtRow([source], rowNum))
'fire the formatting event
RaiseEvent SetCellFormat(Me, e)
Color Formatting
Once the event has returned, inspect the properties of the DataGridFormatCellEventArgs object to find out about the colors in which the cell should appear. The two brushes, foreBrush and backBrush, used to draw the cell are passed as arguments to the Paint method. To change the cell colors, swap the old brushes for the new brushes before calling the Paint method's base class implementation. The following is a code snippet that illustrates this idea:
' assume we will call the baseclass
Dim callBaseClass As Boolean = True
' check the brushes returned from the event
If Not (e.BackBrush Is Nothing) Then
backBrush = e.BackBrush
End If
If Not (e.ForeBrush Is Nothing) Then
foreBrush = e.ForeBrush
End If
' check the UseBaseClassDrawing property
If Not e.UseBaseClassDrawing Then
callBaseClass = False
End If
If callBaseClass Then
MyBase.Paint(g, bounds, _
[source], rowNum, backBrush, _
foreBrush, alignToRight)
End If
The previous snippet sets the brushes depending upon the properties of the DataGridFormatCellEventArgs object. Depending upon the UseBaseClassDrawing value, it calls the base class. Calling the base class with the modified brushes is how to affect the cell color. Recall that this method will be called for each cell in a column as it is drawn.
Handling the TextFont Property
Trying to change the font used to draw in the cell adds a real complication to the Paint override. The reason is that the font used to draw the text is not a simple parameter passed as an argument in the Paint method. Instead, the font used to draw the cell text is originally gotten from the DataGrid.Font property. But this font is cached, and is not reset from the DataGrid.Font property for each cell. Thus, the first impulse, dynamically changing DataGrid.Font for each cell does not work. Therefore, to change the font of the cell, it is necessary to actually draw the string so that the font can be specified. Doing so adds significant work to try to mimic exactly the output of the standard DataGrid. The example will not go to extremes in this respect, but will be satisfied to get similar output, but not exactly the same output as with the unaltered DataGrid. The following is the code snippet that is used to implement drawing the string with a newly specified font.
'if TextFont set, then must call drawstring
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
System.Console.WriteLine(ex.Message.ToString())
End Try
Catch 'empty catch
End Try
callBaseClass = False
End If
The first part of the code approximates the number of characters that can be displayed in the current cell width. It does so by approximating an average character size, and dividing this value into the cell width. It would be possible to do this more rigorously—repeatedly calling MeasureString to get the exact number of characters that would fit—but doing so makes the output significantly different than what the default DataGrid produces. Using this average character size approach is much quicker, and gives results much closer to the default DataGrid.
After getting the number of characters, draw the background and call Graphics.DrawString passing it the font and brush desired for use. Finally, set the callBaseClass flag so the baseclass Paint method is not later called, undoing all of the previous work.
Using the New FormattableTextBoxColumn Class
To use the newly derived column, go back to the form code, and swap out the occurrences of DataGridTextBoxColumn with FormattableTextBoxColumn. Then for each column, wire up the listener for the SetCellFormat event exposed by the derived class. There exists the option of using a different event handler for each column. But the requirements (coloring rows) allow the use of the same handler for all the columns. The following is the code for the typical column after these changes:
'ProductName
column = New FormattableTextBoxColumn()
column.MappingName = "ProductName"
column.HeaderText = "Name"
column.Width = 140
AddHandler column.SetCellFormat, AddressOf FormatGridRow
tableStyle.GridColumnStyles.Add(column)
Notice that an instance of a derived class is now being created, FormattableTextBoxColumn. Also, a handler is added, FormatGridRow, for the SetCellFormat event.
The SetCellFormat Event Handler
In the SetCellFormat handler, the brushes used to set the forecolor and backcolor of the cell whose row and column indexes are passed in through the event arguments will be set. If the cell is on the current row of the grid, use one set of colors and font. If the cell is on a row that has the Discontinued Boolean value set to true, use another set of colors. The following is the code that declares and defines these cached GDI+ objects.
'form variables to cache GDI+ objects
Private disabledBackBrush As Brush
Private disabledTextBrush As Brush
Private currentRowFont As Font
Private currentRowBackBrush As Brush
' In Form1_Load create and cache some GDI+ objects.
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
In the code for the event handler, members of the DataGridFormatCellEventArgs value that are passed to display particular rows with special effects will be set. Discontinued rows will be grayed out, and the current row will be displayed with a DodgerBlue background using a bold font. The following is the handler that will implement these effects:
Private Sub FormatGridRow(ByVal sender As Object, _
ByVal e As DataGridFormatCellEventArgs)
Dim discontinuedColumn As Integer = 0
' Conditionally set properties in e depending upon e.Row and e.Col.
Dim discontinued As Boolean = CBool( _
IIf(e.Column <> discontinuedColumn, _
Me.DataGrid1(e.Row, discontinuedColumn), _
e.CurrentCellValue))
' Check if discontinued?
If e.Column > discontinuedColumn AndAlso _
CBool(Me.DataGrid1(e.Row, discontinuedColumn)) Then
e.BackBrush = Me.disabledBackBrush
e.ForeBrush = Me.disabledTextBrush
' current row?
ElseIf e.Column > discontinuedColumn AndAlso _
e.Row = Me.DataGrid1.CurrentRowIndex Then
e.BackBrush = Me.currentRowBackBrush
e.TextFont = Me.currentRowFont
End If
End Sub
Recall that there are actually several FormattableTextBoxColumn objects in the DataGrid.TableStyle(0).GridColumnStyles collection. In fact, every column, except the Boolean column, is using one of these objects. In addition, each of these objects has a SetCellFormat event, to which is being listened. However, each of these events is using the same handler, FormatGridRow, which was previously listed. Thus, the same formatting logic is being applied to every cell (except the Boolean cell) in a row. Therefore, the formatting is effectively setting row styles in that all cells in the row will show the same formatting behavior, even though the formatting is being done on a cell-by-cell basis.
The following is a picture of the grid at this point. Notice that the checked rows are grayed out, showing the item is discontinued, and the current row is DodgerBlue with a bold font.
The initial display looks pretty good. But there is a small problem that needs some effort to handle. The problem is that a click on a checked first column unchecks the cell properly, but does not redraw the row. This row needs to be refreshed anytime the check value changes. To handle this problem, it is necessary to derive from DataGridBooleanColumn and add an event handler that fires an event anytime the bool value changes. This will allow reaction to the changing click, and forces the row to be redrawn reflecting the new click value.
Adding a Boolean Column with a Value Changed
The Event Arguments
Start by adding a new class derived from EventArgs that holds the information the BoolValueChanged event will require. In particular, properties will be added that give the row and column of the cell being changed, as well as add properties to the new Boolean value. The following is the BoolValueChangedEventArgs class. The missing implementation uses private fields only to shadow the public properties with the fields being initialized in the constructor. The delegate will also be defined, BoolValueChangedEventHandler, for this event at the beginning of this file.
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
The Derived DataGridBoolColumn Class
Now for the derived class, ClickableBooleanColumn, three methods will be overridden to handle different aspects of monitoring the Boolean value in the cell. The first override is Edit. This method is called when a cell needs to go into edit mode. In the override, some private field members will be set to reflect the current Boolean state as well as the fact that the current mode is in edit mode. The second override is the same Paint method that was previously used in FormattableTextBoxColumn. Here, instead of trying to modify the cell appearance, this method is used to track the change in the Boolean value, and to raise the BoolValueChanged event if it does change. The third override is the Commit method. This method is called when the editing is complete, and the value should be committed to the datasource. This method is used to turn off the fields that were set in the Edit override.
There are some technical points that need to be handled. The Boolean value can change because of a click in the cell, or a pressed spacebar, while the cell has focus. The helper method, ManageBoolValueChanging, is used to monitor these possibilities. Furthermore, it raises the BoolValueChanged event if the value is changed. To monitor the mouse click, the ManageBoolValueChanging checks the Control.MousePosition and Control.MouseButtons shared (static) properties. However, checking the KeyState of the spacebar is problematic. Resort to an Interop call into Win32 API GetKeyState to handle this problem.
The following is the code for the class:
Public Class ClickableBooleanColumn
Inherits DataGridBoolColumn
'changed event
Public Event BoolValueChanged _
As BoolValueChangedEventHandler
'set variables to start tracking bool changes
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
'overridden to handle BoolChange event
Protected Overloads Overrides Sub Paint(. . .)
Dim colNum As Integer = _
Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me)
'used to handle the boolchanging
ManageBoolValueChanging(rowNum, colNum)
MyBase.Paint(. . .)
End Sub 'Paint
'turn off tracking bool changes
Protected Overrides Function Commit(. . .) As Boolean
Me.lockValue = True
Me.beingEdited = False
Return MyBase.Commit(dataSource, rowNum)
End Function 'Commit
'fields to track bool value
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
'needed to get the space bar changing of the bool value
<System.Runtime.InteropServices.DllImport("user32.dll")> _
Shared Function GetKeyState( _
ByVal nVirtKey As Integer) As Short
End Function
'fire the bool change event if the value changes
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
' or spacebar
If Not lockValue AndAlso _
beingEdited AndAlso _
changing AndAlso _
saveRow = rowNum Then
saveValue = Not saveValue
lockValue = False
'fire the event
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
Using the ClickableBooleanColumn
To use the special column style, create an instance of it when setting up the columnstyle for the discontinuedCol. A handler for the new BoolValueChanged event will also be added.
' Discontinued.
Dim discontinuedCol As New ClickableBooleanColumn()
discontinuedCol.MappingName = "Discontinued"
discontinuedCol.HeaderText = ""
discontinuedCol.Width = 30
' turn off tristate
discontinuedCol.AllowNull = False
AddHandler discontinuedCol.BoolValueChanged, _
AddressOf BoolCellClicked
tableStyle.GridColumnStyles.Add(discontinuedCol)
The BoolValueChanged Event Handler
In the event handler, the row must be forced to be redrawn using the new value of the Boolean cell. Anytime the Boolean value changes, the event handler invalidates the rectangle associated with a row to reflect the proper format. To force a redraw of a row, use a RefreshRow helper method. In the event handler, the Boolean value is forced to be stored by ending the edit mode. Doing so allows the FormatGridRow method to obtain the proper value of the Boolean cell. Without this call, only the initial value of the cell is available through the indexed 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
' Forces a repaint of given row.
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
There is one last problem to handle before clicking the check box to be certain that the cell behaves as expected with the row changing colors with each click. If a checkbox is clicked that is not checked and is not on the current row, then the click sets the row in the current row colors, instead of the discontinued colors. Furthermore, the problem appears strictly to be a refresh problem because dragging the grid on and off the display will cause the row to be redrawn using the expected discontinued format. This refresh problem can be handled by forcing the row to be refreshed during the existing dataGrid1_Click event handler. In that event, the code is executed to change the Boolean value, and also to refresh the row.
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
Adding Row-Dependent ToolTips
The Windows Forms ToolTip class can be used to add a ToolTip for an entire DataGrid. To do so, drag the ToolTip component onto the Form and set the ToolTip property that is added to every control on the form. However, implementing a ToolTip in this manner does not allow the tip to be varied when moving from row to row (or cell to cell) in the DataGrid. The goal is to display a ToolTip that can be varied from row to row. Using the same strategy, ToolTips could be implemented that vary from cell to cell or column to column.
Start by adding two fields to the Form class, hitRow and ToolTip1. These fields are initialized in Form1_Load, and are dynamically changed in the dataGrid1_MouseMove event handler that was added. In the MouseMove, check that the cursor is over a new row, and if so, reset the ToolTip with different text. See the following code example:
Private hitRow As Integer
Private toolTip1 As System.Windows.Forms.ToolTip
' In 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 'turn it off
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
'new hit row
hitRow = hti.Row
Me.toolTip1.SetToolTip(Me.DataGrid1, tipText)
' make it active so it can show
Me.toolTip1.Active = True
Else
hitRow = -1
End If
Else
hitRow = -1
End If
End If
End Sub 'dataGrid1_MouseMove
Conclusion
The final DataGrid uses two derived DataGridColumnStyles to handle cell formatting and monitor Boolean value changes. The formatting is accomplished by overriding the derived classes' Paint method, and raising an event to allow a listener to furnish format information based on a row and column index. The derived class that manages Boolean values also overrides the Paint method, firing an event when the value changes at this point; in addition, it requires Edit and Commit overrides to fully manage this task.
Also used are form level events to control DataGrid behavior. The DataGrid.CurrentCellChanged event was used to force the current cell to a particular cell in the row, no matter which cell in the row was clicked. The DataGrid.Clicked event was used to make the CheckBox column responds to the first click, instead of requiring two clicks to change its value. Finally, the DataGrid.MouseMove event was used to make ToolTips dynamic by changing from row to row.
There are two samples accompanying this article in the attached download (see link at the top). The first one, CustomGrid, contains the VB .NET project that served as the source for all the code snippets in this discussion. The second, CustomGridA, contains two projects, one VB .NET and the other C#. The C# project is more involved than the one used in this discussion, but generally follows the same flow.
The KeyUp, KeyPress and KeyDown events of the DataGrid do not fire when if you are in a cell.
Skip Columns When Tabbing
You can solve this problem by handling the DataGrid's CurrentCellChanged event, and simulate a Tab keypress when the user selects an "inaccessible" column.
Enable the column-skipping mechanism for the input grid and for the columns with the specified index
save the array of column indexes in the grid's Tag property
attach the grid's CurrentCellChanged event to the GenDataGrid_CurrentCellChanged event handler
Public Sub EnableDataGridColumnSkip(ByVal grid As DataGrid, _
ByVal ParamArray columnsToSkip() As Integer)
grid.Tag = columnsToSkip
AddHandler grid.CurrentCellChanged, AddressOf GenDataGrid_CurrentCellChanged
End Sub
Disable the column-skipping mechanism for the input grid
' detach the grid's CurrentCellChanged event from the GenDataGrid_CurrentCellChanged event handler
Public Sub DisableDataGridColumnSkip(ByVal grid As DataGrid)
RemoveHandler grid.CurrentCellChanged, AddressOf GenDataGrid_CurrentCellChanged
End Sub
Handle the grid's CurrentCellChanged, to skip the columns whose index is found in the array stored in grid's Tag property
cast the generic sender Object to a DataGrid
cast the grid's Tag to an array of Integers, that contains the indexes of the columns to skip
get the index of the current column
if the current column's index is found in the columnsToSkip array, simulate a TAB key press,
Public Sub GenDataGrid_CurrentCellChanged(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim grid As DataGrid = DirectCast(sender, DataGrid)
Dim columnsToSkip() As Integer = DirectCast(grid.Tag, Integer())
Dim currColIndex As Integer = grid.CurrentCell.ColumnNumber
' so that the focus is moved to the next column in the same row, or to the next row if this is the last column
If Array.IndexOf(columnsToSkip, currColIndex) > -1 Then
SendKeys.Send("{TAB}")
End If
End Sub
© 2024 Better Solutions Limited. All Rights Reserved. © 2024 Better Solutions Limited TopPrevNext