Scrolling Panel






4.85/5 (20 votes)
Creating a custom UI panel for scrolling through a panel, using drag and scroll or the scroll bars.
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.
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 Type | Name | Functionality |
Panel |
ScrollPanel |
The scrolling panel. |
Panel |
ScrollAt |
The position of the scroll bar. |
Panel |
ScrollContainter |
What the scroll bar moves within. |
To make this all work, we need a few variables within our custom UI, all of which are private:
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. |
A function that we will need later on is a simple get-the-mouse-position function, which should look like this:
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:
//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 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:
//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.
//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 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.
//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:
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:
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:
...
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; 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.
//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:
//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:
//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.
//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.
[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 can't. 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 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.