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()
{
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 (!_IsMouseDown)
{
_IsMouseDown = true;
_LastMouseMove = GetMousePosition();
}
And when the mouse is up, MouseUp
event, we set the variable _IsMouseDown
to false
:
if (_IsMouseDown)
{
_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 (_IsMouseDown)
{
Point currentlMouse = GetMousePosition();
if (_LastMouseMove != currentlMouse)
{
if (ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) > 0)
{
ScrollPanel.Top = 0;
}
else
{
if (ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) <
(ScrollPanel.Height - this.Height)* -1)
{
ScrollPanel.Location = new Point(ScrollPanel.Location.X,
(ScrollPanel.Height - this.Height) * -1);
}
else
{
ScrollPanel.Location = new Point(ScrollPanel.Location.X,
ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
}
}
}
_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 (!_IsMouseVDown)
{
_IsMouseVDown = true;
_LastMouseMove = GetMousePosition();
}
The MouseUp
event sets _IsMouseVDown
to false, telling the MouseMove
event that the button is not down.
if (_IsMouseVDown)
{
_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 (_IsMouseVDown)
{
Point currentlMouse = GetMousePosition();
if (_LastMouseMove != currentlMouse)
{
if (ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) <
ScrollContainter.Location.Y)
{
ScrollAt.Location = new Point(ScrollAt.Location.X,
ScrollContainter.Location.Y);
}
else
{
if (ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y) >
ScrollContainter.Height +
ScrollContainter.Location.Y - ScrollAt.Height)
{
ScrollAt.Location = new Point(ScrollAt.Location.X,
ScrollContainter.Height +
ScrollContainter.Location.Y - ScrollAt.Height);
}
else
{
ScrollAt.Location = new Point(ScrollAt.Location.X,
ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
}
}
}
_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()
{
float CurrentlyLookingAt = Math.Abs(ScrollPanel.Location.Y);
float Percent = (CurrentlyLookingAt / (ScrollPanel.Height - this.Height)) * 100;
float ScrollMovementArea = ScrollContainter.Height - ScrollAt.Height;
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()
{
float ScrollMovementArea = ScrollContainter.Height - ScrollAt.Height;
float Percent = ((ScrollAt.Location.Y -
ScrollContainter.Location.Y) / ScrollMovementArea) * 100;
float ScrollArea = (ScrollPanel.Height - this.Height);
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
{
ScrollPanel.Location = new Point(ScrollPanel.Location.X,
ScrollPanel.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
}
}
CalculateScrollBar();
}
_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
{
ScrollAt.Location = new Point(ScrollAt.Location.X,
ScrollAt.Location.Y + (currentlMouse.Y - _LastMouseMove.Y));
}
}
CalculateScrollPanel();
}
_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.
ScrollContainter.Left = this.Width - ScrollContainter.Width - 4;
ScrollAt.Left = this.Width - ScrollContainter.Width - 3;
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 (ScrollPanel.Height < this.Height)
{
_DisableScrolling = true;
ScrollAt.Enabled = false;
ScrollContainter.Enabled = false;
}
else
{
_DisableScrolling = false;
ScrollAt.Enabled = true;
ScrollContainter.Enabled = true;
}
Both of the MouseDown
events should start off like this:
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.
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((
(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))]
[Docking(DockingBehavior.Ask)]
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
.
[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:
Cursor = System.Windows.Forms.Cursors.Hand;
MouseLeave
event:
Cursor = System.Windows.Forms.Cursors.Default;
Adding a simple attribute to the class will let developers adjust the height of the ScrollAt
panel.
[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.