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.
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