Customizing The Windows Forms DataGrid
Customizing The Windows Forms DataGrid
Synopsis
The following discussion steps you through customizing the look and functionality of a Windows Forms DataGrid. A DataGridBoolColumn is derived that exposes a BoolValChanged event that is raised anytime the checkbox value changes. Also derived is a DataGridTextBoxColumn that exposes a CellFormatting event that allows control of the cell's Font, ForeColor and BackColor on a cellby-cell basis. The DataGrid's CurrentCellChanged event is used to force the focus to go to a particular column, no matter which column was initially clicked in a row. Also shown is how to dynamically change the appearance of cells in a DataGrid, depending upon the value of key cells. Here, a key cell is a cell whose value determines the appearance of other cells. Further demonstrated is how to make a check box column react to the initial click in the cell to change its checked state and how to control which columns appear in the DataGrid, as well as their order, independent of how the columns occur in the underlying DataSource. Finally, it is demonstrated how to use the Windows Forms ToolTip control to display tips that vary row-by-row in the DataGrid. In short, a DataGrid should look as the one in Figure 1:
Introduction
The following contains a goal that is two-fold. The first goal is to show how easy it is to use the Windows Forms DataGrid for basic display of tabular data on a form. The second goal is to demonstrate how to customize the DataGrid's default behavior, allowing more flexibility in how to use the DataGrid. In doing so, it is demonstrated how to create classes that can be used in projects to enable similar functionality in applications. If using the Designer to display a DataGrid using a DataTable is a familiar practice, skip ahead to the section titled Customizing the Grid. The following examples use VB .NET to show code snippets. The code samples include both VB .NET and C# sample projects.
The Basics
Start out by creating a Windows Forms Application in Visual Studio .NET, dropping a DataGrid onto the Form, and adding a DataSource. Do all of this from the Designer. To bring up and display a DataGrid bound to an ADO.NET data source, exactly one line of code is written. To allow users to edit the displayed values and save them back to the data source, a second line of code is written. This discussion assumes that the DataBase Samples that ship with the .NET Framework are installed. If not, you can do so from this link (change the hard disk drive location, depending on your Visual Studio .NET install location): C:\Program Files\Microsoft Visual Studio.NET\FrameworkSDK\Samples\StartSamples.htm The following steps create an application showing the Northwind Products table in a DataGrid.
3
1. 2. On the File menu, select New, and then select Projects to create a new Windows Application project. Name the project CustomDataGrid. From the Toolbox, drag a DataGrid onto the displayed Form. Size it to generally fill the Form, and set its Anchor property to all four sides. The anchored Form in Visual Studio should look similar to Figure 2.
Figure 2 The Anchored Form in Visual Studio 3. Next, from the View menu, select the Server Explorer Window. Under Data Connections, open the Northwind node from the Tables list. Drag the Products table onto your Form on the Design Surface. After doing so, two components should display in the Components tray at the bottom of the Design Surface, a SqlConnection1 and a SqlDataAdapter1, as shown in Figure 3.
Figure 3 Form with SqlConnection and SqlDataAdapter Components 4. Right-click the SqlDataAdapter1 component and select Generate DataSet. The Generate DataSet dialog box appears, as shown in Figure 4. Press ENTER to accept the default action, which is to create a typed dataset, and place an instance of this dataset into your Component tray.
Figure 4 Generating the DataSet 5. 6. On the Design Surface, click the DataGrid, and then on its property grid, set the DataSource property to DataSet11Products. Add a Form Load event handler by double-clicking an empty spot on the Form. In this event handler, type the following single line of code:
Copy Me.SqlDataAdapter1.Fill(Me.DataSet11)
7. Finally, compile and run your project. The grid should appear as illustrated in Figure 5.
6
Figure 5 The DataGrid Produced with the Designer and One Line of Code
Copy PrivateSubForm1_Load(ByValsenderAsSystem.Object,_
7
ByValeAsSystem.EventArgs)HandlesMyBase.Load Me.SqlDataAdapter1.Fill(Me.DataSet11) 'Step1:CreateaDataGridTableStyle& 'setmappingnametotable. DimtableStyleAsNewDataGridTableStyle() tableStyle.MappingName="Products" 'Step2:CreateDataGridColumnStyleforeachcol 'wewanttoseeinthegridandinthe 'orderthatwewanttoseethem. 'Discontinued. DimdiscontinuedColAsNewDataGridBoolColumn() discontinuedCol.MappingName="Discontinued" discontinuedCol.HeaderText="" discontinuedCol.Width=30 'turnofftristate discontinuedCol.AllowNull=False tableStyle.GridColumnStyles.Add(discontinuedCol) 'Step2:ProductID DimcolumnAsNewDataGridTextBoxColumn() column.MappingName="ProductID" column.HeaderText="ID" column.Width=30 tableStyle.GridColumnStyles.Add(column) 'Step2:ProductName column=NewDataGridTextBoxColumn() column.MappingName="ProductName" column.HeaderText="Name" column.Width=140 tableStyle.GridColumnStyles.Add(column) 'Step2:QuantityPerUnit column=NewDataGridTextBoxColumn() column.MappingName="QuantityPerUnit" column.HeaderText="QuantityPerUnit" tableStyle.GridColumnStyles.Add(column) 'Step2:UnitPrice column=NewDataGridTextBoxColumn() column.MappingName="UnitPrice" column.HeaderText="UnitPrice" tableStyle.GridColumnStyles.Add(column) 'Step2:UnitsInStock column=NewDataGridTextBoxColumn() column.MappingName="UnitsInStock" column.HeaderText="UnitsInStock" tableStyle.GridColumnStyles.Add(column) 'Step2:UnitsOnOrder column=NewDataGridTextBoxColumn() column.MappingName="UnitsOnOrder" column.HeaderText="UnitsOnOrder" tableStyle.GridColumnStyles.Add(column)
8
'Step2:ReorderLevel column=NewDataGridTextBoxColumn() column.MappingName="ReorderLevel" column.HeaderText="ReorderLevel" tableStyle.GridColumnStyles.Add(column) 'Step3:Addthetablestyletothedatagrid Me.DataGrid1.TableStyles.Add(tableStyle) EndSub
Figure 6 is the DataGrid after adding the previous code. The columns should be exactly the columns previously specified as needed, and the Discontinued column should appear first.
9
DimdiscontinuedColumnAsInteger=0 DimvalAsObject=Me.DataGrid1(_ Me.DataGrid1.CurrentRowIndex,_ discontinuedColumn) DimproductDiscontinuedAsBoolean=CBool(val) IfproductDiscontinuedThen Me.DataGrid1.CurrentCell=_ NewDataGridCell(_ Me.DataGrid1.CurrentRowIndex,_ discontinuedColumn) EndIf EndSub
Copy PrivateafterCurrentCellChangedAsBoolean=False PrivateSubdataGrid1_Click(ByValsenderAsSystem.Object,_ ByValeAsSystem.EventArgs)HandlesDataGrid1.Click DimdiscontinuedColumnAsInteger=0 DimptAsPoint=Me.dataGrid1.PointToClient(_ Control.MousePosition) DimhtiAsDataGrid.HitTestInfo=_ Me.dataGrid1.HitTest(pt) DimbmbAsBindingManagerBase=_ Me.BindingContext(Me.dataGrid1.DataSource,_ Me.dataGrid1.DataMember) IfafterCurrentCellChanged_ AndAlsohti.Row<bmb.Count_ AndAlsohti.Type=DataGrid.HitTestType.Cell_ AndAlsohti.Column=discontinuedColumnThen Me.DataGrid1(hti.Row,discontinuedColumn)=_ NotCBool(Me.DataGrid1(hti.Row,_ discontinuedColumn)) EndIf afterCurrentCellChanged=False EndSub'dataGrid1_Click 'addalinetothisexistinghandler
10
PrivateSubdataGrid1_CurrentCellChanged(_ ByValsenderAsSystem.Object,_ ByValeAsSystem.EventArgs)_ HandlesDataGrid1.CurrentCellChanged 'ifclickonadiscontinuedrow,thenset 'currentcelltocheckbox DimdiscontinuedColumnAsInteger=0 DimvalAsObject=Me.DataGrid1(_ Me.DataGrid1.CurrentRowIndex,_ discontinuedColumn) DimproductDiscontinuedAsBoolean=CBool(val) IfproductDiscontinuedThen Me.DataGrid1.CurrentCell=_ NewDataGridCell(Me.DataGrid1.CurrentRowIndex,_ discontinuedColumn) EndIf 'addthisline afterCurrentCellChanged=True EndSub'dataGrid1_CurrentCellChanged
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.
11
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.
Copy PublicDelegateSubFormatCellEventHandler(_ ByValsenderAsObject,_ ByValeAsDataGridFormatCellEventArgs) PublicClassDataGridFormatCellEventArgs InheritsEventArgs PublicSubNew(ByValrowAsInteger,_ ByValcolAsInteger,_ ByValcellValueAsObject) EndSub'New EndClass
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.
12
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.
Copy DimeAsDataGridFormatCellEventArgs=Nothing 'Getthecolumnnumber DimcolAsInteger=_ Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me) 'Createtheeventargsobject e=NewDataGridFormatCellEventArgs(_ rowNum,col,_ Me.GetColumnValueAtRow([source],rowNum)) 'firetheformattingevent RaiseEventSetCellFormat(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:
Copy 'assumewewillcallthebaseclass DimcallBaseClassAsBoolean=True 'checkthebrushesreturnedfromtheevent IfNot(e.BackBrushIsNothing)Then backBrush=e.BackBrush EndIf IfNot(e.ForeBrushIsNothing)Then foreBrush=e.ForeBrush EndIf 'checktheUseBaseClassDrawingproperty IfNote.UseBaseClassDrawingThen callBaseClass=False EndIf IfcallBaseClassThen MyBase.Paint(g,bounds,_ [source],rowNum,backBrush,_ foreBrush,alignToRight) EndIf
13
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.
Copy 'ifTextFontset,thenmustcalldrawstring IfNot(e.TextFontIsNothing)Then Try DimcharWidthAsInteger=_ Fix(Math.Ceiling(g.MeasureString("c",_ e.TextFont,20,_ StringFormat.GenericTypographic).Width)) DimsAsString=_ Me.GetColumnValueAtRow([source],_ rowNum).ToString() DimmaxCharsAsInteger=_ 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) CatchexAsException Console.WriteLine(ex.Message.ToString()) EndTry Catch'emptycatch EndTry callBaseClass=False EndIf
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 rigorouslyrepeatedly calling MeasureString to get the exact number of characters that would fitbut 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.
14
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:
Copy 'formvariablestocacheGDI+objects PrivatedisabledBackBrushAsBrush PrivatedisabledTextBrushAsBrush PrivatecurrentRowFontAsFont PrivatecurrentRowBackBrushAsBrush 'InForm1_LoadcreateandcachesomeGDI+objects. Me.disabledBackBrush=_ NewSolidBrush(SystemColors.InactiveCaptionText) Me.disabledTextBrush=_ NewSolidBrush(SystemColors.GrayText) Me.currentRowFont=_ NewFont(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
15
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:
Copy PrivateSubFormatGridRow(ByValsenderAsObject,_ ByValeAsDataGridFormatCellEventArgs) DimdiscontinuedColumnAsInteger=0 'Conditionallysetpropertiesinedependingupone.Rowande.Col. DimdiscontinuedAsBoolean=CBool(_ IIf(e.Column<>discontinuedColumn,_ Me.DataGrid1(e.Row,discontinuedColumn),_ e.CurrentCellValue)) 'Checkifdiscontinued? Ife.Column>discontinuedColumnAndAlso_ CBool(Me.DataGrid1(e.Row,discontinuedColumn))Then e.BackBrush=Me.disabledBackBrush e.ForeBrush=Me.disabledTextBrush 'currentrow? ElseIfe.Column>discontinuedColumnAndAlso_ e.Row=Me.DataGrid1.CurrentRowIndexThen e.BackBrush=Me.currentRowBackBrush e.TextFont=Me.currentRowFont EndIf EndSub
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. Take a Look 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.
16
Figure 7 The DataGrid with Colored Rows 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.
17
... EndSub'New PublicPropertyColumn()AsInteger ... EndProperty PublicPropertyRow()AsInteger ... EndProperty PublicReadOnlyPropertyBoolValue()AsBoolean ... EndProperty EndClass'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:
Copy PublicClassClickableBooleanColumn InheritsDataGridBoolColumn 'changedevent PublicEventBoolValueChanged_ AsBoolValueChangedEventHandler 'setvariablestostarttrackingboolchanges ProtectedOverloadsOverridesSubEdit(...) Me.lockValue=True Me.beingEdited=True Me.saveRow=rowNum Me.saveValue=CBool(_ MyBase.GetColumnValueAtRow(_ [source],rowNum))
18
MyBase.Edit(...) EndSub'Edit 'overriddentohandleBoolChangeevent ProtectedOverloadsOverridesSubPaint(...) DimcolNumAsInteger=_ Me.DataGridTableStyle.GridColumnStyles.IndexOf(Me) 'usedtohandletheboolchanging ManageBoolValueChanging(rowNum,colNum) MyBase.Paint(...) EndSub'Paint 'turnofftrackingboolchanges ProtectedOverridesFunctionCommit(...)AsBoolean Me.lockValue=True Me.beingEdited=False ReturnMyBase.Commit(dataSource,rowNum) EndFunction'Commit 'fieldstotrackboolvalue PrivatesaveValueAsBoolean=False PrivatesaveRowAsInteger=1 PrivatelockValueAsBoolean=False PrivatebeingEditedAsBoolean=False PublicConstVK_SPACEAsInteger=32 'neededtogetthespacebarchangingoftheboolvalue <System.Runtime.InteropServices.DllImport("user32.dll")>_ SharedFunctionGetKeyState(_ ByValnVirtKeyAsInteger)AsShort EndFunction 'firetheboolchangeeventifthevaluechanges PrivateSubManageBoolValueChanging(_ ByValrowNumAsInteger,_ ByValcolNumAsInteger) DimmousePos_ AsPoint=Me.DataGridTableStyle.DataGrid.PointToClient(_ Control.MousePosition) DimdgAsDataGrid=Me.DataGridTableStyle.DataGrid DimisClickInCellAsBoolean=_ Control.MouseButtons=MouseButtons.LeftAndAlso_ dg.GetCellBounds(dg.CurrentCell).Contains(mousePos) DimchangingAsBoolean=_ dg.FocusedAndAlsoisClickInCell_ OrElseGetKeyState(VK_SPACE)<0 'orspacebar IfNotlockValueAndAlso_ beingEditedAndAlso_ changingAndAlso_ saveRow=rowNumThen saveValue=NotsaveValue lockValue=False 'firetheevent
19
DimeAsNewBoolValueChangedEventArgs(_ rowNum,colNum,saveValue) RaiseEventBoolValueChanged(Me,e) EndIf IfsaveRow=rowNumThen lockValue=False EndIf EndSub'ManageBoolValueChanging EndClass
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.
Copy 'Discontinued. DimdiscontinuedColAsNewClickableBooleanColumn() discontinuedCol.MappingName="Discontinued" discontinuedCol.HeaderText="" discontinuedCol.Width=30 'turnofftristate discontinuedCol.AllowNull=False AddHandlerdiscontinuedCol.BoolValueChanged,_ AddressOfBoolCellClicked 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.
Copy PrivateSubBoolCellClicked(ByValsenderAsObject,_ ByValeAsBoolValueChangedEventArgs) DimgAsDataGrid=Me.DataGrid1 DimdiscontinuedColAsNew_ ClickableBooleanColumn=_ g.TableStyles(0).GridColumnStyles("Discontinued") g.EndEdit(discontinuedCol,e.Row,False) RefreshRow(e.Row) g.BeginEdit(discontinuedCol,e.Row) EndSub 'Forcesarepaintofgivenrow. PrivateSubRefreshRow(ByValrowAsInteger) DimrAsRectangle=_ Me.dataGrid1.GetCellBounds(row,0) r=NewRectangle(r.Right,r.Top,_ Me.dataGrid1.Width,r.Height) Me.dataGrid1.Invalidate(r)
20
EndSub'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.
Copy PrivatehitRowAsInteger PrivatetoolTip1AsSystem.Windows.Forms.ToolTip 'InForm1_Load Me.hitRow=1 Me.toolTip1=NewToolTip() Me.toolTip1.InitialDelay=300 AddHandlerMe.DataGrid1.MouseMove,_ AddressOfdataGrid1_MouseMove PrivateSubdataGrid1_MouseMove(_ ByValsenderAsObject,_ ByValeAsMouseEventArgs) DimgAsDataGrid=Me.dataGrid1 DimhtiAsDataGrid.HitTestInfo=_ g.HitTest(NewPoint(e.X,e.Y)) DimbmbAsBindingManagerBase=_
21
Me.BindingContext(g.DataSource,g.DataMember) Ifhti.Row<bmb.CountAndAlso_ hti.Type=DataGrid.HitTestType.CellAndAlso_ hti.Row<>hitRowThen IfNot(Me.toolTip1IsNothing)AndAlso_ Me.toolTip1.ActiveThen Me.toolTip1.Active=False'turnitoff EndIf DimtipTextAsString="" IfCBool(Me.DataGrid1(hti.Row,0))Then tipText=Me.DataGrid1(hti.Row,2).ToString()&_ "discontinued" IftipText<>""Then 'newhitrow hitRow=hti.Row Me.toolTip1.SetToolTip(Me.DataGrid1,tipText) 'makeitactivesoitcanshow Me.toolTip1.Active=True Else hitRow=1 EndIf Else hitRow=1 EndIf EndIf EndSub'dataGrid1_MouseMove
Conclusion
22
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.