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

Scrolling Panel

4.85/5 (20 votes)
16 Jul 2011CPOL8 min read 135K   15.6K  
Creating a custom UI panel for scrolling through a panel, using drag and scroll or the scroll bars.

ScrollAblePanel.jpg

Introduction

In this article, we will look at how to design and implement a custom UI control allowing us to fill a panel with controls, and then reduce the screen space used by allowing the user to scroll through the controls either by clicking and dragging the panel or through the scroll bar.

This type of control is similar to the type of controls seen in many 3D and 2D art applications such as Autodesk's 3Ds Max and Autodesk's Maya, and is a great way to trim down a large GUI with user friendly and intuitive controls.

Background

This article is aimed at C# users looking to learn about custom UIs; some knowledge is expected such as setting up a project and creating an interface using the form designer.

Using the code

The reason I've opted to make this a custom UI control is that it can then be used in other projects and re-used within the same form without having to program the whole backend system over again.

Setup

The way I've designed the UI to look is to have a main panel on the left hand side, which is what we will scroll, and our own custom scroll bar on the right hand side. Although we could use Microsoft's built-in scroll bar (VScrollBar) or create our own as a separate UI element, I'm choosing to build it all into the same component as it gives us a greater degree of freedom with regards to how it looks and how it interacts with the scroll bar.

Layout.jpg

The picture above outlines how I laid my design out. The yellow area is the ScrollPanel, the blue area is the ScrollContainter, and the green area is the ScrollAt.

UI Element TypeNameFunctionality
PanelScrollPanelThe scrolling panel.
PanelScrollAtThe position of the scroll bar.
PanelScrollContainterWhat the scroll bar moves within.

To make this all work, we need a few variables within our custom UI, all of which are private:

VariableTypePurpose
_IsMouseDownBoolIf the mouse is pressed down for use with the main panel.
_LastMouseMovePointThe position of the last mouse position recorded.
_IsMouseVDownBoolSame functionality as _IsMouseDown, but for the scrolling bar.

A function that we will need later on is a simple get-the-mouse-position function, which should look like this:

C#
private Point GetMousePosition()
{
    //Returns the positon of the mouse within the screen
    return (this.PointToScreen(System.Windows.Forms.Control.MousePosition));
}

This will allow us to get the screen position of the mouse when we need it, without a lot of repeated code.

Interactive elements

Drag scrolling

The first step in creating the scrolling panel should be identifying how we want it to work. In this case, we want the user to click on the panel, move the mouse, and the panel moves with the mouse.

In order to do this, we need to know when the mouse has been pressed down whilst on the panel, MouseDown event. We do this by setting the _IsMouseDown value to true and recording the position of the mouse with the _LastMouseMove variable:

C#
//if the mouse is not already pressed 
if (!_IsMouseDown)
{
    //set the mouse to down (Main panel)
    _IsMouseDown = true;
    //Record the position of the mouse down
    _LastMouseMove = GetMousePosition();
}

And when the mouse is up, MouseUp event, we set the variable _IsMouseDown to false:

C#
if (_IsMouseDown)
{
    //finished scrolling
    _IsMouseDown = false;
}

The main chunk will be in when the mouse is moving, MouseMove event. The event works by checking _IsMouseDown to see if the mouse has been pressed down, checking if there has been any movement since the last check, and if there was, what the change is and if it would move the scrolling panel higher or lower than it is supposed to. Lastly, it saves the current mouse position:

C#
//if the mouse is down, aka we're scrolling
if (_IsMouseDown)
{
    //grab the current mouse position and see if it has moved
    Point currentlMouse = GetMousePosition();
    if (_LastMouseMove != currentlMouse)
    {
        //check if it would be going over the top of the panel
        if (ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) > 0)
        {
            //if it is, set it to the top
            ScrollPanel.Top = 0;
        }
        else
        {
            //check if it would be going past the bottom of the panel
            if (ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) < 
               (ScrollPanel.Height - this.Height)* -1)
            {
                //if it is, set it to the bottom
                ScrollPanel.Location = new Point(ScrollPanel.Location.X, 
                            (ScrollPanel.Height - this.Height) * -1);
            }
            else
            {
                //other wise move it based off the change in mouse positon
                ScrollPanel.Location = new Point(ScrollPanel.Location.X, 
                  ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
            }
        }
    }
    //record the current mouse as the last mouse
    _LastMouseMove = GetMousePosition();
}

All of these event handlers should be set up for use with the ScrollPanel control. At this point, running the UI should result in the scrolling panel being scrollable with the mouse.

Bar scrolling

The scroll bar works in a very similar fashion to the scrolling panel, the MouseDown event sets _IsMouseVDown to true and records the position of the mouse.

C#
//if the mouse is not already pressed
if (!_IsMouseVDown)
{
    //set the mouse to down (Scroll Bars)
    _IsMouseVDown = true;
    //Record the position of the mouse down
    _LastMouseMove = GetMousePosition();
}

The MouseUp event sets _IsMouseVDown to false, telling the MouseMove event that the button is not down.

C#
if (_IsMouseVDown)
{
    //finished scrolling
    _IsMouseVDown = false;
}

And the MouseMove event checks if the mouse is down (_IsMouseVDown) and calculates its position based off the movement of the mouse, whilst being constrained by the panel behind it, ScrollContainter in terms of height.

C#
//if the mouse is down, aka we're scrolling with the bar
if (_IsMouseVDown)
{
    //grab the current mouse position and see if it has moved
    Point currentlMouse = GetMousePosition();
    if (_LastMouseMove != currentlMouse)
    {
        //check if it would be going over the top of the scroll bar
        if (ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) < 
                                   ScrollContainter.Location.Y)
        {
            //if it is, set it to the top
            ScrollAt.Location = new Point(ScrollAt.Location.X, 
                                    ScrollContainter.Location.Y);
        }
        else
        {
            //check if it would be going past the bottom of the scroll bar
            if (ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) > 
                ScrollContainter.Height + 
                ScrollContainter.Location.Y - ScrollAt.Height)
            {
                //if it is, set it to the bottom
                ScrollAt.Location = new Point(ScrollAt.Location.X, 
                  ScrollContainter.Height + 
                  ScrollContainter.Location.Y - ScrollAt.Height);
            }
            else
            {
                //other wise move it based off the change in mouse positon
                ScrollAt.Location = new Point(ScrollAt.Location.X, 
                  ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
            }
        }
    }
    //record the current mouse as the last mouse
    _LastMouseMove = GetMousePosition();
}

All of these event handlers should be set up for use with the ScrollAt panel control. At this point, running the UI should result in the scroll bar being changeable.

Calculations

With both the panel and the scrollbar being able to change, we need to tie them in, so when the user is using one method, the other is updated and they are both in sync.

To do this, we will need two functions: CalculateScrollBar, to calculate the position of the ScrollAt panel based off where the scroll panel is currently at, and CalculateScrollPanel, to calculate the position of the ScrollPanel based off the position of the ScrollAt control.

Calculating the scroll bars position is done like this:

C#
private void CalculateScrollBar()
{
    //Get the Y position currently at the top of the panel, being looked at
    float CurrentlyLookingAt = Math.Abs(ScrollPanel.Location.Y);
    //Find out what percent it is through the document, getting rid
    //of the height of the panel so we go from 0-100
    float Percent = (CurrentlyLookingAt / (ScrollPanel.Height - this.Height)) * 100;
    //get the maximum movement area up and down for the ScrollAt panel
    float ScrollMovementArea = ScrollContainter.Height - ScrollAt.Height;
    //Translate the percentage looked at to the percentage along the scroll bar
    ScrollAt.Location = new Point(ScrollAt.Location.X, Convert.ToInt32(
      (ScrollMovementArea/100) * Percent) + ScrollContainter.Location.Y);
}

Calculating the scroll panel's position is done as:

C#
private void CalculateScrollPanel()
{
    //get the maximum movement area up and down for the ScrollAt panel
    float ScrollMovementArea = ScrollContainter.Height - ScrollAt.Height;
    //Find out how along the scroll bar we currently are
    float Percent = ((ScrollAt.Location.Y - 
          ScrollContainter.Location.Y) / ScrollMovementArea) * 100;
    //Get the maximum movement area for the scroll panel
    float ScrollArea = (ScrollPanel.Height - this.Height);
    //Translate the percentage along the scroll bar to the percentage along the scroll panel
    ScrollPanel.Location = new Point(ScrollPanel.Location.X, 
                Convert.ToInt32((ScrollArea / 100) * Percent) * -1);
}

To tie these in with the event handlers, the MouseMove event on both the ScrollAt panel and ScrollPanel panel need to be slightly adjusted.

ScrollPanel should be amended to include the function call to CalculateScrollBar() once the panel has been moved. It should now look like this:

C#
...
            else
            {
                //other wise move it based off the change in mouse positon
                ScrollPanel.Location = new Point(ScrollPanel.Location.X, 
                   ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
            }
        }
        //re-calculate the scroll bar based off our new main position
        CalculateScrollBar();
    }
    //record the current mouse as the last mouse
    _LastMouseMove = GetMousePosition();
}

ScrollAt should be amended to include the function call to CalculateScrollPanel() once the scroll bar has been moved. It should now look like this:

C#
...
            else
            {
                //other wise move it based off the change in mouse positon
                ScrollAt.Location = new Point(ScrollAt.Location.X, 
                  ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
            }
        }
        //other wise move it based off the change in mouse positon
        CalculateScrollPanel();
    }
    //record the current mouse as the last mouse
    _LastMouseMove = GetMousePosition();
}

Running the custom UI now will result in a fairly functional UI control, with working scroll bars and a scrolling panel. But right now, it can only be edited within the custom UI project; to be re-used, it is pretty useless. Now we'll turn our attention to making it more accessible to developers so it can be edited and re-used within their projects.

Safety net

When exposing elements and variables within a custom UI, the first thing I do is to think how it could be broken, and then build in simple hard coded fail safes to keep the UI from working incorrectly.

The first "safety net" is to correct a problem which can arise when the scroll bar is not resized if the UI component is resized. To fix this, a simple event handler is added to the UI control SizeChanged event. All the code does is resize and re-position the ScrollAt panel and ScrollContainer panel.

C#
//Set the X Position of the Scroll Bar
ScrollContainter.Left = this.Width - ScrollContainter.Width - 4;
ScrollAt.Left = this.Width - ScrollContainter.Width - 3;
//Set the Height of the scroll bar
ScrollContainter.Height = this.Height - (ScrollContainter.Location.Y * 2);

The second "safety net" follows on, and is if the scrolling panel (ScrollPanel) is smaller than the custom UI window. If this is run, then ScrollPanel will pop from the top of the screen to the bottom. To correct this, we just create a new private bool variable called _DisableScrolling, perform a simple check when the UI control is re-sized, and check the two heights, and if needed, makes _DisableScrolling true. This is in turn used in the two MouseDown events and just disables the scrolling.

This should be added to the end of the SizeChanged event:

C#
//If the panel created to scroll is too small,
//turn off scrolling and disable the scroll bars.
if (ScrollPanel.Height < this.Height)
{
    _DisableScrolling = true;
    ScrollAt.Enabled = false;
    ScrollContainter.Enabled = false;
}
else
{
    //otherwise allow scrolling
    _DisableScrolling = false;
    ScrollAt.Enabled = true;
    ScrollContainter.Enabled = true;
}

Both of the MouseDown events should start off like this:

C#
//if the mouse is not already pressed and scrolling isnt not disabled
if ((!_IsMouseDown) && (!_DisableScrolling))
{

Designer friendly

Making the component developer friendly is a major point of this custom UI, and the user/developer should have the ability to edit the ScrollPanel panel enough to add more controls, change the size, and change things like the colour.

To allow this in .NET, we need to create our own internal custom Control Designer class specific for this class. The class will return to the designer the panel ScrollPanel through an attribute called EditablePanel, all of which we will set up now.

C#
//The Desinger Class
internal class ScrollAblePanelDesigner : 
         System.Windows.Forms.Design.ParentControlDesigner
{
    public override void Initialize(
           System.ComponentModel.IComponent component)
    {
        base.Initialize(component);

        if (this.Control is ScrollAblePanelControl)
        {
            this.EnableDesignMode((
                //get the EditablePanel attritubute
                //from the class ScrollAblePanelControl
               (ScrollAblePanelControl)this.Control).EditablePanel, "EditablePanel");
        }
    }
}

To assign properties to a class, they need to be defined before the class. Adding these lines before the custom UI class is declared will tie the custom UI element with the ScrollAblePanelDesigner class used by Visual Studio's Designer interface.

C#
[Designer(typeof(ScrollAblePanelDesigner))]
//Set the desiner to the custom ScrollAblePanel designer

[Docking(DockingBehavior.Ask)]
//propts the user to dock the control

The ScrollAblePanelDesigner class uses an attribute called EditablePanel; to create this within the custom UI class, we need to add the following lines which will get and return the ScrollPanel.

C#
// Defines the property EditablePanel, where the scroll content can be edited
[Category("Appearance")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Panel EditablePanel
{
    get { return ScrollPanel; }
}

Final touches

To finish off the article, there are a few cosmetic touch up's I made to my version of this custom UI to increase its usefulness from a developer perspective.

By using a different mouse cursor when the mouse is over the panel can help users tell when they can scroll and when they can't. Adding a simple function for both the ScrollPanel panel's MouseEnter event and MouseLeave event can add this functionality.

MouseEnter event:

C#
//If the mouse enters the panel, change the cursor so the user knows they can scroll
Cursor = System.Windows.Forms.Cursors.Hand;

MouseLeave event:

C#
//restore the cursor to defualt when out of the panel
Cursor = System.Windows.Forms.Cursors.Default;

Adding a simple attribute to the class will let developers adjust the height of the ScrollAt panel.

C#
// ScrollAt Bar Size Control
[Category("Appearance")]
[Description("Gets or sets the size the scroll bar widget")]
public int ScrollBarSize
{
    get { return ScrollAt.Height; }
    set { 
        ScrollAt.Height = value;
        CalculateScrollBar();
    }
}

My last touch on this project is to tie in the SizeChange event within the ScrollPanel panel with the SizeChange event for the custom UI that we already have. This will allow users and developers to change the height of the ScrollPanel at run-time and the system will adapt and activate/de-active the scroll bars as needed.

Points of interest

For me, this project seemed simple enough in concept, but tweaking the formula to make it feel and interact right was harder than expected. I personally learnt a lot about integrating developer side options for custom UI controls. I can see a few more updates coming as this gets into production with my projects and its use grows.

History

  • 16 July 2011 - Created and submitted.

License

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