Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

How to skin scrollbars for Panels, in C#

0.00/5 (No votes)
13 Jul 2006 2  
This article shows you how to create a skinnable scrollbar as a user control, and use it in a Panel to replace the ugly Windows scrollbar.

Sample Image - customscrollbar1.gif

  Sample Image - customscrollbar2.gif

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:

Sample Image - customscrollbar3.gif

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.

  1. Up arrow image
  2. Down arrow image
  3. Thumb top image
  4. Thumb top span image
  5. Thumb middle image
  6. Thumb bottom span image
  7. 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:

  • Scroll
  • ValueChanged

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.

//Our channel color that we will expose later

protected Color moChannelColor = Color.Empty;

//Our images for the scrollbar that we will expose later 

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;

//Our properties that we will expose later 


protected int moLargeChange = 10;
protected int moSmallChange = 1;
protected int moMinimum = 0;
protected int moMaximum = 100;
protected int moValue = 0;

//Our private variables for internal use 

private int nClickPoint;
private int moThumbTop = 0; 
private bool moThumbDown = false;
private bool moThumbDragging = false;
//Our public events that we are exposing


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) {
    //set the mode to nearest neighbour so when

    //we span our thumb graphics it doesn't try 

    //to blur or antialias anything

    e.Graphics.InterpolationMode = 
      System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor; 
    //draw up arrow

    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));

    //draw channel left and right border colors

    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)));

    //draw channel

    e.Graphics.FillRectangle(oChannelBrush, new Rectangle(1, 
                             UpArrowImage.Height, this.Width - 2, 
                             (this.Height - DownArrowImage.Height)));
    //draw thumb contrl

    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;
    //draw top part of thumb now

    e.Graphics.DrawImage(ThumbTopImage, new Rectangle(1, nTop, 
                       this.Width - 2, ThumbTopImage.Height));
    nTop += ThumbTopImage.Height;

    //draw top span thumb

    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; 
    //draw middle part of thumb

    e.Graphics.DrawImage(ThumbMiddleImage, new Rectangle(1, nTop, 
                       this.Width - 2, ThumbMiddleImage.Height));

    nTop += ThumbMiddleImage.Height;
    //draw botom span thumb

    rect = new Rectangle(1, nTop, this.Width - 2, nSpanHeight*2);
    e.Graphics.DrawImage(ThumbBottomSpanImage, rect);
    nTop += nSpanHeight;
    //draw bottom part of thumb

    e.Graphics.DrawImage(ThumbBottomImage, 
               new Rectangle(1, nTop, this.Width - 2, nSpanHeight));

    //draw down arrow

    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))
    {
    //hit the thumb

    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;
                //figure out value

                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;
                //figure out value

                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);
            }

            //figure out value

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here