Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

ScrollSelector

4.10/5 (8 votes)
30 Aug 2009CPOL5 min read 21.9K   295  
An animated, scrollable TabControl with some design-time functionality.

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:

Suggestion 1 - using group boxes

or a tab control like this:

Suggestion 2 - using 'normal' TabControl

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):

The ScrollSelector 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 TabPages 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:

VB
<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:

VB
#Region "Imports"
 mports System.ComponentModel
Imports System.ComponentModel.Design
Imports System.Windows.Forms
Imports System.Windows.Forms.Design
'Imports System.Runtime.InteropServices
#End Region

Public Class ScrollDesigner
    Inherits ParentControlDesigner

#Region "Private Constant Declarations"
    'All constants and calls through API can be found in a program
    'called API-Viewer. I strongly suggest the use of the program
    'when programming on the Windows Platform.
    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
                    'Ignore
            End Select
        End If

        MyBase.WndProc(m)
    End Sub

End Class

And then, I attached it to the control like before:

VB
<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:

The control is painted wrong

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)