Scrolling Panel (C#) - Overview

In this article I go into the how to design and implement a Custom UI control which will allow users to scroll up and down, minimising the amount of on screen menus and other graphical user interaction devices. We look at the design, and how to not just implement it, but create it in such a way that it is developer friendly as well, allowing it to be used over many different projects in many different ways.

The project follows these steps: 

-> Create the underlying Custom UI and the elements which will be used within it.

-> Handleing the events of the mouse on the main panel.

-> Handleing the events of the mouse on our own custom scroll bar.

-> The calculations needed for a smooth scoll

-> Saftey net implementation to stop the control from being broken by the user

-> Adding the desinger friendly part to the Custom UI.

-> Making some small minor, cosmetic, touches to finish off.

This tutorial was orginaly submitted to CodeProjects and the orginal can be found here. .

Scrolling Panel (C#) - Tutorial

Paint_on_a_panel.jpg

Introduction 

In this article we 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 click 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# looking to learn about Custom UI, some knowledge is expected such as setting up a projects 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 programme the whole back end system over again.

Setup

The way I've designed the UI to look it to have a main panel on the left hand side, which we will what we 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 in 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.

Variable Type Purpose
_IsMouseDown Bool If the mouse is pressed down for use with the main panel
_LastMouseMove Point The position of the last mouse position recorded
_IsMouseVDown Bool Same functionality as _IsMouseDown, but for the scrolling bar
        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

So 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 move 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 record the position of the mouse with the _LastMouseMove variable:

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

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

The main chunk will be in the 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 has, what the change is and if it would move the scrolling panel higher or lower than it is supposed to go. Lastly it saves the current mouse position:

            //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 Panel control. At this point, running the UI should result in the scrolling panelling 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.

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

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

And the MouseMove Event checks 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.

            //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 the 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 ScrollAt Panel based off where the scrollpanel is currently at, and CalculateScrollPanel, to calculate the position of ScrollPanel Panel based off the position of the ScrollAt control.

Calculating the scroll bars position

        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 panels position

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

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

              ...
                        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; with to be re-used is pretty useless. Now we'll turn our attention to making it more accessible to developers so it can edited and re-used within their projects.

Safety Net

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, where 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 to resize and re-position the ScrollAt panel and ScrollContainer panel.

            //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 checks 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:

                   //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 the 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 specify 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.

    //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 Studios Designer interface.

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

           // 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 cant. Adding a simple function for both the ScrollPanel panel's MouseEnter Event and MouseLeave Event can add this functionality.

MouseEnter Event:

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

MouseLeave Event:

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

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

Scrolling Panel (C#) - Supporting Files

The supporting files can be found at CodeProject here, and include all the source code as well as a final compiled exe application.