Introduction
For a project I am working on, I needed to supply the user with some advanced search options. The options were grouped and only one group should be used at a time. For this article, I have simplified it and made it a simple matter of choosing between three different popular search engines. My first thoughts on how to present this was either to use group boxes like this:
or a tab control like this:
The tab control was the most interesting solution in my opinion, because it would show less controls at a time and thus present a more "clean" look. However, I wanted a more sophisticated presentation than that.
So, I came up with the concept of the ScrollSelector
, which is basically a TabControl
with different panels (scroll panels - not tab pages). You scroll between the panels by clicking up/down buttons. To top it off, I added a header to the control as well. The final control looked like this (top part, the rest is a normal WebBrowser
control):
Background
I have implemented a lot of controls before, and initially thought to myself: "I can whip together this control in a couple of minutes" - Wrong! I had missed one very important thing: as opposed to other "normal" controls, you should be able to switch between the panels at design-time as well - so that you can place the controls you want inside the different panels.
Just look at the TabControl
: When you place it on a form, you can interact with it in the designer and switch between the TabPage
s by clicking the tabs. When you create a compound control (usually a UserControl), and you place it on the form, it only shows a static image of the User Control. Sure, you can change the look by changing properties in the property grid, but if you have a button in the User Control, you cannot react to clicks on the button in the designer.
Implementing design-time interaction
The first thing I had to do was to change the designer to make the control act as a container for other controls. Like this:
<Designer(GetType(ParentControlDesigner))> _
Public Class ScrollSelector
...
End Class
I'm not 100% sure, but I think this used to be much easier before .NET... As far as I can recall (that's a long time ago), you only had to set a property before, and presto! - the control was a container control...
I had provided a SelectedIndex
property, and with that, you can change the selected panel in the property grid at design time. That worked, but I was not satisfied with that. I wanted to be able to click the up/down buttons in the designer and have the panels change - like with the TabControl
.
So, I looked around for info about how to interact with the control at design-time, and after a lot of searching, I found out that you have to supply a special designer. So I had to do a Designer
class (ScrollDesigner
) that I inherited from ParentControlDesigner
:
#Region "Imports"
mports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Windows.Forms
Imports System.Windows.Forms.Design
#End Region
Public Class ScrollDesigner
Inherits ParentControlDesigner
#Region "Private Constant Declarations"
Private Const WM_LBUTTONUP As Integer = &H202
Private Const WM_PAINT As Integer = &HF
Private Const WM_LBUTTONDOWN As Integer = &H201
Private Const WM_MOUSEMOVE As Integer = &H200
Private Const WM_MOVING As Integer = &H216
#End Region
Private m_MouseOver As Boolean = False
Protected Overrides Sub OnMouseEnter()
m_MouseOver = True
MyBase.OnMouseEnter()
End Sub
Protected Overrides Sub OnMouseLeave()
m_MouseOver = False
MyBase.OnMouseLeave()
End Sub
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
If m_MouseOver Then
Select Case m.Msg
Case WM_LBUTTONDOWN
Dim cur As Point = Cursor.Position
Dim hitPoint As Point
Dim meAsControl As ScrollSelector = CType(Me.Control, ScrollSelector)
Dim bHitUp As Boolean = False
Dim bHitDown As Boolean = False
hitPoint = Me.Control.PointToClient(New Point(cur.X, cur.Y))
If hitPoint.X > (meAsControl.Width - meAsControl.pnlButtons.Width) _
And hitPoint.X < meAsControl.Width Then
If meAsControl.HeaderAlignment = ScrollHeaderAlignment.Bottom Or _
meAsControl.lblHeader.Visible = False Then
bHitUp = (hitPoint.Y < meAsControl.pnlButtons2.Height \ 2)
bHitDown = _
((hitPoint.Y < meAsControl.pnlButtons2.Height) And Not bHitUp)
Else
If hitPoint.Y > meAsControl.lblHeader.Height Then
bHitUp = (hitPoint.Y < ((meAsControl.pnlButtons2.Height \ 2) + _
meAsControl.lblHeader.Height))
bHitDown = ((hitPoint.Y < (meAsControl.pnlButtons2.Height + _
meAsControl.lblHeader.Height)) And Not bHitUp)
End If
End If
End If
If bHitUp Then meAsControl.ScrollUp()
If bHitDown Then meAsControl.ScrollDown()
Case Else
End Select
End If
MyBase.WndProc(m)
End Sub
End Class
And then, I attached it to the control like before:
<Designer(GetType(ScrollDesigner))> _
Public Class ScrollSelector
...
End Class
That worked. Basically, I override the WndProc
method to intercept mouse-clicks on the control. Then, I determine if the mouse click is on one of the buttons, and if it is, I call the appropriate method in the control.
Control quirks
There are a couple of things with the control - as it is now - that could be better:
Problem 1: Sometimes the designer paints the control wrong. The header label is docked to the top of the control and should therefore always fill out the entire length of the control. The panels containing the buttons are docked to the right side, and should therefore always be at the right side.
But in my demo app, I dock the control to the top of a form. Most of the time it looks OK, but sometimes I see this:
Because the control is docked to the top of the form and the header label to the top of the control, the label should have the same width as the control itself. You can see that the control has the correct width (or the panels have the correct width at least), but the header label has the default size - not the size as the resized control should have forced it to have.
And, the buttons are in the default position compared to the control's left side - not docked to the right side.
I don't understand this - It must be a bug in .NET's handling of the Dock
property.
Problem 2: When the panels are changed using the up/down buttons in the designer, the corresponding SelectedIndex
property in the property grid is not changed - even though the variable containing the index is updated by the method called by the designer.
However, I consider this a minor problem and will leave it as is for now...
Improvement suggestion
I wanted to make the header properties a single property with appropriate subproperties (like the Font
property is), and I made a Header
class to hold the properties. But, no matter what I did, I got serialization problems with the new Header
property, so in the end, I gave up and made the subproperties separate properties in the control. That works, but it is not as nice a solution as the one I had wanted. I'll look into it again later when I have more time...
History
- Version 1.0 (31 August 2009) - Initial release.