How to skin scrollbars for Panels, in C#
By Greg Ellis.
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.
| C# Windows (Win2K, WinXP), .NET (.NET 1.0, .NET 1.1, .NET 2.0) Win32, VS (VS.NET2002, VS.NET2003, VS2005), GDI+, WinForms Dev Posted: 13 Jul 2006 Views: 14,967 |
|
| |||||||||||||||||||||||||||
IntroductionHave 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 StartedFirst thing we will do is create a new C# Windows Application project called TestApp. Open Form1.cs in Design mode, and add a Next, we want to throw some kind of a control into the So, open up Form.cs in View Code mode, and just under the 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 Skinning the Scrollbar (If only it were that simple :p)Now, we have a 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 Now, we start creating graphics and writing code to make our Creating the Scrollbar GraphicsSo 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:
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 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.
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 controlNow, 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:
and the following events:
and the following custom properties to facilitate our ability to skin:
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 ControlNow, 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:
First, we will instantiate all of our variables. Some are
Collapse
//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.
Collapse
[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
Collapse
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.
Collapse
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 ScrollbarSo now, we have to add our custom scrollbar to our form, beside our Next, add an instance of our Add the code shown heren just below the code that we added earlier to create the button, below the 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 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 ConclusionSo now, our custom scrollbar will scroll our Some things to improve are as follows:
About Greg Ellis
|