HomeDigital EditionSearch Dotnet Cd
ASP.NET C# Certification Exams The CLI Data Access Editorials Extending .NET Fundamentals Interoperability Interviews Migrate Mobile .NET Mono .NET Interface Object-Oriented Programming Open Source Optimization Product/Book Reviews Security Source Code UML Visual Studio .NET

Tips & Tricks for Using Windows Form Controls:
Protected members offer solutions to seemingly impossible problems

A speaking engagement at Tech-Ed 2003 in India prompted me to put my thoughts on paper for this article illustrating some common tips and tricks to accomplish often-requested Windows Forms controls feature requests. These tips and tricks are based on discussions in various newsgroups online as well as Microsoft presentations.

Displaying Controls Your Way
List boxes/combo boxes and menus are handled by controls that are not particularly exciting in terms of their display. Figure 1 shows a different kind of list box in which each item is displayed in the font that it names. This list box also features a "Variable Height" check box, which, if checked, causes the items to be drawn with a variable height. Download the source code for this article from below, run the solution, and choose "OwnerDraw" to run this sample. If the Variable Height box is checked, as you click each font you will see each item displayed in a different height.

Accomplishing this is an easy, three-step task involving setting the DrawMode property; handling two events, MeasureItem and DrawItem, to override the automatic drawing that Windows provides; and finally using GDI+ to draw the items.

Setting the DrawMode Property
The ListBox control has a DrawMode property that specifies how items are drawn. By default Windows handles the task of drawing the list box items. The DrawMode property is by default Normal. The DrawMode property supports three different values based on the DrawMode enumeration.

Table 1

In this example the DrawMode property has been set to OwnerDrawVariable. Note: A multicolumn list box cannot have a variable height.

Handling Events
You need to handle two events, MeasureItem and DrawItem. If you're just drawing items but not manipulating their height, you need not handle the MeasureItem event. Similarly, if you've set the DrawMode property to Owner- DrawFixed, you don't need to handle MeasureItem. Both of these events provide a DrawEventArgs parameter that provides a number of properties and methods, such as Bounds, Graphic Context, Index, and State, which indicate the state of the item. Figure 2 show a partial list of the parameters and methods exposed in the DrawItem event.

Drawing Items with GDI+
In the ListBox DrawItem event we will use GDI+ to draw each item to be displayed in the list box. But first an array of available fonts is created in the Sub FillFonts() method. This array is specified as the data source for the list box. The key code, however, is in the ListBox DrawItem event, which causes each font in the array to be drawn in its own font type.

Listing 1 shows the code for the ListBox DrawItem event. The DrawItemEventArgs.Graphics property gets the graphics surface on which to draw the item. Depending on the selection state of the item, i.e., whether it's selected or unselected, we draw the item using different brushes. We draw the actual text using the Graphics.DrawString method. We draw the font text in its own font within the bounds as illustrated in the code below.

e.Graphics.DrawString(ff.Name,
fnt, br, e.Bounds.X, e.Bounds.Y)

When working with graphics objects you need to make sure all graphics objects are disposed correctly. This is handled in the last section of code in Listing 1.

The ListBox MeasureItem event (see Listing 2) is called only if the Variable Height check box is checked. If it is, then the ListBox.DrawMode property is set to DrawMode.OwnerDraw Variable. In the ListBox.MeasureItem event the Graphics.MeasureString method is used to measure the specified string when drawn with the specified Font object.

Dim szf As SizeF = e.Graphics.MeasureString(fnt.Name, fnt)

The size returned is used to specify the height for the item. However, the space between items is usually not sufficient and the items may appear to be a bit too closely packed. In order to increase the spacing between objects, I've added an extra +4, as shown in the following line of code.

e.ItemHeight = CInt(szf.Height + 4)

That's about all that's needed to create a ListBox control that draws each item with a different height and in a different font. The example shows some other functions, such as CalcFontStyle, which helps to determine if the specified font has support for Regular, Bold, and Italic styles. Check the source for more details.

The same concept can also be applied to menus, as shown in Figure 3. Although there are subtle differences, the basic steps remain the same. Menu items have an OwnerDraw property instead of the DrawMode property used for the ListBox/ComboBox controls. The OwnerDraw property can be set to True or False. As with the ListBox control, you will need to handle the DrawItem and MeasureItem events in your code. Finally, use GDI+ to draw each item. I'm not going to illustrate the code here in detail, but I encourage you to look through the source code provided. Comments at relevant parts of the code highlight the key points. In addition, if you view the Task List in VS.NET 2003, you will see that I have created tasks to help you view significant parts of the code. In Figure 4 the Fill Colors Menu task is highlighted. Doubleclicking each task will take you to the relevant parts of the code. The OwnerDraw form has the code for the ListBox, as well as the Menu.


Figure 3

Figure 4

Exposing Protected Members
Much of the functionality of Windows Forms controls is protected; it's only available in the control class and in classes that inherit from the control. But sometimes you will feel tied down by this and will want to use the protected functionality yourself. The way to do it is to inherit from the control and expose the functionality you require. You can create an actual custom control or simply create a class in your project. Experimenting with protected members is one of the most exciting and enriching experiences available to you as a .NET developer. I'm going to give you just a taste of the wealth of protected members out there.

Synchronized Scrolling DataGrids
Let's examine the following scenario in which a customer had two DataGrids on a form. The customer wanted the ability to scroll either DataGrid and have the other scroll automatically too. Both DataGrids were tied to the same data source (see Figure 5).

A weird request? Perhaps, but it was a challenge and had to be done. But how do you go about it? You need to access the topmost row being displayed, and then sync that with the other DataGrid. Searching in the help file on the DataGrid control revealed that there is no such current topmost row property to hook into. There is a CurrentRow- Index property, but frankly it was of no use. I looked into protected properties in the help file (there are huge numbers of protected properties) and aha! – there is a property called VertScrollBar that, according to the help file, gets the vertical scroll bar of the control. I thought I hit the jackpot. Let's see what happens when we try it out.

As mentioned earlier, I can create a class in my project and inherit from it. In the following code snippet I've added this code to the end of my form class definition.

Public Class MyDataGrid
Inherits DataGrid
Public Property CurrentRow()
As Integer
Get
Return
Me.VertScrollBar.Value
End Get
Set(ByVal Value As
Integer)
Me.VertScrollBar.Value = Value
End Set
End Property
End Class

Here I create the MyDataGrid class, which inherits from the DataGrid class. It defines a public property called CurrentRow() and returns the value of VertScrollBar.

Then I employ a quick-anddirty trick for using MyDataGrid in the form's code. In the "Windows Form Designer Generated code" section I do a find-and-replace to replace "System.Windows.Forms.Data Grid" with "MyDataGrid". Four instances are replaced. I need to rebuild the project so that the MyDataGrid control is compiled into the assembly and referenced correctly. Hence, when you go to the design view you will see the DataGrids, which otherwise you wouldn't.

Note: I have already done the necessary replacements in the source code. This sample assumes the presence of a SQL Server database on the local machine and the presence of the NorthWind sample database. You will need to change the SQLConnection object's ConnectionString to reflect your individual database source. Also Integrated Security is set to SSPI; you may need to change this for your individual environment.

The next thing that's needed is to respond to the scroll event. The following code handles that.

Private Sub DataGrid1_Scroll(ByVal
sender As Object, ByVal e As
System.EventArgs) Handles
DataGrid1.Scroll
DataGrid2.CurrentRow = DataGrid1.CurrentRow
End Sub

Hit F5 to run the project and try it out. Choose the SynchScrollDataGrid form and click the "Run" button. Pulling the scroll bar of the first DataGrid down causes the second DataGrid scroll bar to move simultaneously, but the data doesn't scroll.

The second DataGrid is still stuck on the first row although its scroll bar is moving. In Figure 6 the DataGrid on the left has been scrolled down – and the scroll bar of the second DataGrid has also moved correspondingly – but the second DataGrid still shows its first row.

There must be something else we can use to satisfy the customer's request. Going further into the documentation, I find another protected member, the GridVScrolled method, which listens for the vertical scroll bar's scroll event. This method takes a parameter, ScrollEventArgs, which contains the event data and provides data for the Scroll event.

So the way to accomplish our goal is to change the setting of the CurrentRow property by using the GridVScrolled method. Here's the CurrentRow property with the changed set statement highlighted:

Public Property CurrentRow() As
Integer
Get
Return
Me.VertScrollBar.Value
End Get
Set(ByVal Value As Integer)
Me.GridVScrolled(Me, NewScrollEventArgs(ScrollEventType
.LargeIncrement, Value))
End Set
End Property
End Class

You need to make this change in the code, replacing the line that used the VertScrollBar property. Make the change, hit F5 to run the application, and you're all set.

A note of caution: using protected members can be a dangerous thing. They have been marked as protected for a reason: their implementations could change, or they just might disappear down the line. Therefore, you should restrict your use of protected members to areas where you absolutely need it.

Conclusion
Each and every control available with Windows Forms holds a host of hidden gems in its protected members. Explore them when you're stuck with a control problem that seems impossible; most often you will find a solution out there.

Author Bio
Sanjay Shetty is CEO of Wireless Strategist & Consultants, which specializes in consulting with software companies on .NET, providing consultancy on .NET architecture, design, and mobility, along with migration to .NET. Sanjay is the Microsoft Regional Director for Mumbai, India, and an active speaker at various Microsoft conferences. sanjay@wirelessstrategist.com




Listing 1

Private Sub lstDemo_DrawItem(ByVal sender As
Object, ByVal e As
System.Windows.Forms.DrawItemEventArgs) Handles
lstDemo.DrawItem
Dim fnt As Font
Try
Dim ff As FontFamily = ffs(e.Index)
Dim fntStyle As FontStyle =
CalcFontStyle(ff)
Dim br As Brush
' Figure out what color to use, based
' on the selection state of the item.
If (e.State And
DrawItemState.Selected) = DrawItemState.Selected
Then
br = Brushes.White
Else
br = Brushes.Black
End If
fnt = New Font(ff,
lstDemo.Font.Size, fntStyle)
' Draw the text.
e.DrawBackground()
e.Graphics.DrawString(ff.Name, fnt,
_
br, e.Bounds.X, e.Bounds.Y)
e.DrawFocusRectangle()
Catch
' Disregard errors. Probably not a great
idea.
Finally
fnt.Dispose()
End Try
End Sub

Listing 2

Private Sub lstDemo_MeasureItem(ByVal sender As
Object, ByVal e As
System.Windows.Forms.MeasureItemEventArgs) Handles
lstDemo.MeasureItem
' This code isn't called unless the DrawMode
is set to
' OwnerDrawVariable.
'
Dim ff As FontFamily = ffs(e.Index)
Dim fntStyle As FontStyle = CalcFontStyle(ff)
Try
Dim fnt As New Font(ff, lstDemo.Font.Size,
fntStyle, GraphicsUnit.Pixel)
Dim szf As SizeF =
e.Graphics.MeasureString(fnt.Name, fnt)
' Add in some extra space hence the +4 to
leave room for margins.
e.ItemHeight = CInt(szf.Height + 4)
Catch exp As Exception
' This effectively "hides" the font.
e.ItemHeight = 0
End Try
End Sub

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.

  E-mail: info@sys-con.com