Introduction
Have you ever wanted to get rid of that ugly Windows scrollbar? I know I have many many times. Until now, it has been an extremely tedious and difficult task. Back in the days before .NET, it was a huge undertaking to get rid of the default scrollbars for listviews, list control, and any other control that has the default Windows scrollbar. If you want to see just how difficult it is, check out my other article I wrote for Visual C++ 6.0 MFC, How to Skin CListCtrl including scrollbars and column headers.
Fortunately for us, we now have user controls and panels in the .NET framework. This simplifies things a lot, but it's still not easy or straightforward to customize the look and feel of Windows scrollbars and have them work properly.
Getting Started
First thing we will do is create a new C# Windows Application project called TestApp. Open Form1.cs in Design mode, and add a Panel
control that is 179 pixels in height, and name it outerPanel
. Then, create another Panel
, but this time create it inside the OuterPanel
, and name it InnerPanel
. Now, set the innerPanel
's AutoScroll
property to True
.
Next, we want to throw some kind of a control into the innerPanel
so that we will have the ability to scroll in order to test our custom scrollbar. For some reason, we can not use the design editor to add a control to the innerPanel
, because it messes up the DisplayRectangle
property and does not return the correct value. So what we have to do is add the control to the innerPanel
using code, and then everything will work fine.
So, open up Form.cs in View Code mode, and just under the InitializeCompontent();
call, insert the following code:
public Form1()
{
InitializeComponent();
Button b = new Button();
b.Location = new Point(0, 900);
b.Text = "test";
this.innerPanel.Controls.Add(b);
}
This will add a button 900 pixels down from the top of the innerPanel
, and we should have a scrollbar showing up when we run the application.
Skinning the Scrollbar (If only it were that simple :p)
Now, we have a Panel
that will scroll, but it's ugly. So, how do we skin it? Well, there is actually no way to skin a Windows scrollbar, as far as I know. So what we have to do is create our own scrollbar as a user control. Basically, we have to mimic exactly what the Windows VScroll control does, but add the ability to be able to use graphics for the arrows, channel, and thumb tracking controls. Then, later on, we will write some more code that will enable us to hide the panel scrollbar, as well as make our custom user control scrollbar, control the panel's scrolling.
We start by adding a new Control Library Project to our solution, and we will rename the User Control that it adds by default, to CustomScrollbar
.
Now, we start creating graphics and writing code to make our CustomScrollbar
look and function the way we would like it to.
Creating the Scrollbar Graphics
So first, we have to figure out how to create graphics in a fashion that lends itself to being reusable in most instances. I know that a scrollbar always has four common properties:
- Up arrow
- Down arrow
- Thumb control
- Channel
This is cool, so we just need the ability to be able to supply an up arrow graphic, down arrow graphic, thumb control graphic, and a channel graphic/color.
However, there is one more thing.... The thumb control for a standard Windows scrollbar always sizes appropriately depending on the Minimum
/Maximum
and LargeChange
properties. This means that we have to give our thumb control the ability to change its size dynamically. Yes, this is not fun, because in order to do this using graphics, we have to split up our thumb control graphic into five different graphics, two of which will need to be spanned accordingly to how big our thumb control is supposed to be. Here is how I cut up my graphics:
So as you can see, we will have the following graphics which we will allow to be customized through the Properties panel on the user control.
- Up arrow image
- Down arrow image
- Thumb top image
- Thumb top span image
- Thumb middle image
- Thumb bottom span image
- Thumb bottom image
For this implementation, I will implement the channel portion as a solid color rather than as a graphic, just for demonstration. It's easy enough to change it to use an image if you like. Incidentally, it is also very easy to add the ability to have mouseover images for the arrows and thumb controls, but I won't be covering that. I will leave that as an exercise for all you developers out there :)
Preparing the properties and events for the scrollbar control
Now, we have our graphics all figured out. We just have to determine how we want our scrollbar to function. I don't like re-inventing the wheel or changing the things that I am used to. So, I am going to make our custom scrollbar work exactly like the VScroll or Windows scrollbar control, so that if I want to use my custom scrollbar in place of an ugly Windows scrollbar, I can just simply swap them out and it will work.
Therefore, my control will have the following properties exposed:
Maximum
- int
Minimum
- int
Value
- int
LargeChange
- int
SmallChange
- int
and the following events:
and the following custom properties to facilitate our ability to skin:
ChannelColor
- Color
DownArrowImage
- Image
ThumbBottomImage
- Image
ThumbBottomSpanImage
- Image
ThumbMiddleImage
- Image
ThumbTopImage
- Image
ThumbTopSpanImage
- Image
UpArrowImage
- Image
With all of these properties implemented properly in a user control, we will have ourselves a completely customizable scrollbar that will function exactly as a regular Windows scrollbar, but it will also have the ability to look as cool or uncool as we would like it :)
Implementing the Scrollbar User Control
Now, here is the tough part. We have to program our very own scrollbar control. This is not necessarily an easy task to accomplish. However, once you are done making this work, you should never have to code another one ever again, and if you do, at least you will have a code base to start from with what we have developed here.
In order to make our custom scrollbar, we will need to override and/or respond to the following events:
OnPaint
- override
MouseUp
- handle
MouseDown
- handle
MouseMove
- handle
First, we will instantiate all of our variables. Some are protected
, and will later be exposed, and some are private
for internal use only.
protected Color moChannelColor = Color.Empty;
protected Image moUpArrowImage = null;
protected Image moDownArrowImage = null;
protected Image moThumbArrowImage = null;
protected Image moThumbTopImage = null;
protected Image moThumbTopSpanImage = null;
protected Image moThumbBottomImage = null;
protected Image moThumbBottomSpanImage = null;
protected Image moThumbMiddleImage = null;
protected int moLargeChange = 10;
protected int moSmallChange = 1;
protected int moMinimum = 0;
protected int moMaximum = 100;
protected int moValue = 0;
private int nClickPoint;
private int moThumbTop = 0;
private bool moThumbDown = false;
private bool moThumbDragging = false;
public new event EventHandler Scroll = null;
public event EventHandler ValueChanged = null;
Now that we have all our of variables setup, we need to expose a bunch of them as properties, so that developers that are using the scrollbar control can access and modify the properties at design time.
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("LargeChange")]
public int LargeChange {
get { return moLargeChange; }
set { moLargeChange = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("SmallChange")]
public int SmallChange {
get { return moSmallChange; }
set { moSmallChange = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("Minimum")]
public int Minimum {
get { return moMinimum; }
set { moMinimum = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("Maximum")]
public int Maximum {
get { return moMaximum; }
set { moMaximum = value;
Invalidate();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Behavior"),
Description("Value")]
public int Value {
get { return moValue; }
set { moValue = value;
SetThumbTop();
}
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Channel Color")]
public Color ChannelColor
{
get { return moChannelColor; }
set { moChannelColor = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Up Arrow Graphic")]
public Image UpArrowImage {
get { return moUpArrowImage; }
set { moUpArrowImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Down Arrow Graphic")]
public Image DownArrowImage {
get { return moDownArrowImage; }
set { moDownArrowImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Top Graphic")]
public Image ThumbTopImage {
get { return moThumbTopImage; }
set { moThumbTopImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Top Span Graphic")]
public Image ThumbTopSpanImage {
get { return moThumbTopSpanImage; }
set { moThumbTopSpanImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Bottom Graphic")]
public Image ThumbBottomImage {
get { return moThumbBottomImage; }
set { moThumbBottomImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Bottom Arrow Graphic")]
public Image ThumbBottomSpanImage {
get { return moThumbBottomSpanImage; }
set { moThumbBottomSpanImage = value; }
}
[EditorBrowsable(EditorBrowsableState.Always), Browsable(true),
DefaultValue(false), Category("Skin"),
Description("Thumb Arrow Graphic")]
public Image ThumbMiddleImage {
get { return moThumbMiddleImage; }
set { moThumbMiddleImage = value; }
}
Now that all of our properties are exposed, let's start writing code to display our scrollbar. We will override the OnPaint
of our user control to accomplish this task, as shown below:
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
if (UpArrowImage != null) {
e.Graphics.DrawImage(UpArrowImage, new Rectangle(new Point(0,0),
new Size(this.Width, UpArrowImage.Height)));
}
Brush oChannelBrush = new SolidBrush(moChannelColor);
Brush oWhiteBrush = new SolidBrush(Color.FromArgb(255,255,255));
e.Graphics.FillRectangle(oWhiteBrush, new Rectangle(0,
UpArrowImage.Height, 1,
(this.Height-DownArrowImage.Height)));
e.Graphics.FillRectangle(oWhiteBrush, new Rectangle(this.Width-1,
UpArrowImage.Height, 1,
(this.Height - DownArrowImage.Height)));
e.Graphics.FillRectangle(oChannelBrush, new Rectangle(1,
UpArrowImage.Height, this.Width - 2,
(this.Height - DownArrowImage.Height)));
int nTrackHeight = (this.Height -
(UpArrowImage.Height +
DownArrowImage.Height));
float fThumbHeight = ((float)LargeChange / (float)Maximum) * nTrackHeight;
int nThumbHeight = (int)fThumbHeight;
if (nThumbHeight > nTrackHeight) {
nThumbHeight = nTrackHeight;
fThumbHeight = nTrackHeight;
}
if (nThumbHeight < 56) {
nThumbHeight = 56;
fThumbHeight = 56;
}
float fSpanHeight = (fThumbHeight - (ThumbMiddleImage.Height +
ThumbTopImage.Height + ThumbBottomImage.Height)) / 2.0f;
int nSpanHeight = (int)fSpanHeight;
int nTop = moThumbTop;
nTop += UpArrowImage.Height;
e.Graphics.DrawImage(ThumbTopImage, new Rectangle(1, nTop,
this.Width - 2, ThumbTopImage.Height));
nTop += ThumbTopImage.Height;
Rectangle rect = new Rectangle(1, nTop,
this.Width - 2, nSpanHeight);
e.Graphics.DrawImage(ThumbTopSpanImage, 1.0f,(float)nTop,
(float)this.Width-2.0f,
(float) fSpanHeight*2); nTop += nSpanHeight;
e.Graphics.DrawImage(ThumbMiddleImage, new Rectangle(1, nTop,
this.Width - 2, ThumbMiddleImage.Height));
nTop += ThumbMiddleImage.Height;
rect = new Rectangle(1, nTop, this.Width - 2, nSpanHeight*2);
e.Graphics.DrawImage(ThumbBottomSpanImage, rect);
nTop += nSpanHeight;
e.Graphics.DrawImage(ThumbBottomImage,
new Rectangle(1, nTop, this.Width - 2, nSpanHeight));
if (DownArrowImage != null) {
e.Graphics.DrawImage(DownArrowImage, new Rectangle(new Point(0,
( this.Height-DownArrowImage.Height)),
new Size(this.Width, DownArrowImage.Height)));
}
}
Now we have to write all the code to handle when a user clicks the up or down arrows, and the most difficult part, write the code to move the thumb control when the user clicks and drags the thumb portion of the control.
private void CustomScrollbar_MouseDown(object sender, MouseEventArgs e) {
Point ptPoint = this.PointToClient(Cursor.Position);
int nTrackHeight = (this.Height - (UpArrowImage.Height +
DownArrowImage.Height));
int nThumbHeight = GetThumbHeight();
int nTop = moThumbTop;
nTop += UpArrowImage.Height;
Rectangle thumbrect = new Rectangle(new Point(1, nTop),
new Size(ThumbMiddleImage.Width, nThumbHeight));
if (thumbrect.Contains(ptPoint))
{
nClickPoint = (ptPoint.Y - nTop);
this.moThumbDown = true;
}
Rectangle uparrowrect = new Rectangle(new Point(1, 0),
new Size(UpArrowImage.Width, UpArrowImage.Height));
if (uparrowrect.Contains(ptPoint))
{
int nRealRange = (Maximum - Minimum)-LargeChange;
int nPixelRange = (nTrackHeight - nThumbHeight);
if (nRealRange > 0)
{
if (nPixelRange > 0)
{
if ((moThumbTop - SmallChange) < 0)
moThumbTop = 0;
else
moThumbTop -= SmallChange;
float fPerc = (float)moThumbTop / (float)nPixelRange;
float fValue = fPerc * (Maximum - LargeChange);
moValue = (int)fValue;
Debug.WriteLine(moValue.ToString());
if (ValueChanged != null)
ValueChanged(this, new EventArgs());
if (Scroll != null)
Scroll(this, new EventArgs());
Invalidate();
}
}
}
Rectangle downarrowrect = new Rectangle(new Point(1,
UpArrowImage.Height+nTrackHeight),
new Size(UpArrowImage.Width, UpArrowImage.Height));
if (downarrowrect.Contains(ptPoint))
{
int nRealRange = (Maximum - Minimum) - LargeChange;
int nPixelRange = (nTrackHeight - nThumbHeight);
if (nRealRange > 0)
{
if (nPixelRange > 0)
{
if ((moThumbTop + SmallChange) > nPixelRange)
moThumbTop = nPixelRange;
else
moThumbTop += SmallChange;
float fPerc = (float)moThumbTop / (float)nPixelRange;
float fValue = fPerc * (Maximum-LargeChange);
moValue = (int)fValue;
if (ValueChanged != null)
ValueChanged(this, new EventArgs());
if (Scroll != null)
Scroll(this, new EventArgs());
Invalidate();
}
}
}
}
private void CustomScrollbar_MouseUp(object sender, MouseEventArgs e) {
this.moThumbDown = false;
this.moThumbDragging = false;
}
private void MoveThumb(int y) {
int nRealRange = Maximum - Minimum;
int nTrackHeight = (this.Height - (UpArrowImage.Height +
DownArrowImage.Height));
int nThumbHeight = GetThumbHeight();
int nSpot = nClickPoint;
int nPixelRange = (nTrackHeight - nThumbHeight);
if (moThumbDown && nRealRange > 0) {
if (nPixelRange > 0) {
int nNewThumbTop = y - (UpArrowImage.Height+nSpot);
if(nNewThumbTop<0)
{
moThumbTop = nNewThumbTop = 0;
}
else if(nNewThumbTop > nPixelRange)
{
moThumbTop = nNewThumbTop = nPixelRange;
}
else {
moThumbTop = y - (UpArrowImage.Height + nSpot);
}
float fPerc = (float)moThumbTop / (float)nPixelRange;
float fValue = fPerc * (Maximum-LargeChange);
moValue = (int)fValue;
Debug.WriteLine(moValue.ToString());
Application.DoEvents();
Invalidate();
}
}
}
private void CustomScrollbar_MouseMove(object sender, MouseEventArgs e) {
if(moThumbDown == true)
{
this.moThumbDragging = true;
}
if (this.moThumbDragging) {
MoveThumb(e.Y);
}
if(ValueChanged != null)
ValueChanged(this, new EventArgs());
if(Scroll != null)
Scroll(this, new EventArgs());
}
}
Now we are done writing the scrollbar user control. Keep in mind, I have left certain things out that are not that important. So please see the source code accompanied with this article. Now, we just need to the hide the default scrollbar on the panel, and hook up our custom scrollbar.
Hide the Default Scrollbar and Hook Up our Custom Scrollbar
So now, we have to add our custom scrollbar to our form, beside our Panel
s. So first, build our solution. Now, let's right click on our toobox and click "Choose Items..." and browse to the CustomControls.dll and add our CustomScrollbar
to the toolbox.
Next, add an instance of our CustomScrollbar
to the form beside the Panel
. Now, we will add the code to hook up the CustomScrollbar
so that it will scroll our panel that we setup.
Add the code shown heren just below the code that we added earlier to create the button, below the InitializeCompontent()
function:
Point pt = new Point(this.innerPanel.AutoScrollPosition.X,
this.innerPanel.AutoScrollPosition.Y);
this.customScrollbar1.Minimum = 0;
this.customScrollbar1.Maximum = this.innerPanel.DisplayRectangle.Height;
this.customScrollbar1.LargeChange = customScrollbar1.Maximum /
customScrollbar1.Height + this.innerPanel.Height;
this.customScrollbar1.SmallChange = 15;
this.customScrollbar1.Value = Math.Abs(this.innerPanel.AutoScrollPosition.Y);
Next, we implement the Scroll
event from our CustomScrollbar
control, and add the following code:
private void customScrollbar1_Scroll(object sender, EventArgs e)
{
innerPanel.AutoScrollPosition = new Point(0,
customScrollbar1.Value);
customScrollbar1.Invalidate();
Application.DoEvents();
}
Now, the only thing left is to hide the default scroll bar that the Panel
brings up. Well, that's actually very easy now, thanks to Panel
s. Remember the outerPanel
that we created? Well, all we have to do is decrease the width of the outerPanel
so that it hides the innerPanel
scrollbar, and that's it.
Conclusion
So now, our custom scrollbar will scroll our Panel
just as the regular scrollbar does. Only now are we able to change the look and feel of our scrollbar whenever we want. I did skip over some small details in the creation of this control, but all the code is available in the source code package above.
Some things to improve are as follows:
- Add ability to support mouse rollover images for the up and down arrows and the thumb control.
- Add ability to hold the mouse button down on arrows and have the control continually scroll.
- Add ability to click the channel portion of the scrollbar and have it scroll the amount of the
LargeChange
property.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.