Introduction
I was looking for a better looking TrackBar control, when I stumbled upon Alan Zhao's ColorProgressBar code and saw how simple it would be to create my own TrackBar control. So, I came up with the ColorTrackBar
. First, let's get some basic syntax down for naming the parts or areas of this control.
- Bar - This is the background area of the control.
- Tracker - This is the user control portion of the
ColorTrackBar
which will move to set the value.
Control Properties
BarBorderColor
- Sets/Gets the color to be used when drawing the bar's border.
BarColor
- Sets/Gets the color of the control's Bar.
TrackerBorderColor
- Sets/Gets the Tracker's border color.
TrackerColor
- Sets/Gets the Tracker's color.
Minimum
- Sets/Gets the lowest possible number that the ColorTrackBar
control can return.
Maximum
- Sets/Gets the highest value the control can return.
Value
- Sets/Gets the current position of the Tracker relative to the maximum and minimum values.
MaximumValueSide
- This property allows the user to decide which direction on the control will increase the Value
property, the opposite will obviously decrease Value
. Possible values are Left
, Right
, Top
, and Bottom
.
BarOrientation
- Select either Horizontal
or Vertical
.
ControlCornerStyle
- Select either Square
or Rounded
corners.
TrackerSize
- Specify the width or height of the tracker depending on the BarOrientation
selection. Note: if you have selected Rounded
corners, you cannot set the Tracker size.
Control Events
Scroll
- This event is fired when the position of the Tracker is changed.
ValueChanged
- This event fires when the numeric value of the Value
property is changed.
Using The Control
- Download source code and unzip it.
- Open VS.NET's Tool menu and select "Add/Remove ToolBox Items".
- Click "Browse", and navigate to the ColorTrackBar.dll in the bin\Release directory of the source code.
- Click Open and then OK, the
ColorTrackBar
control will now be in your ToolBox, just drag the control onto your form.
- Set the control's properties, and wire-up an event handle, and you're ready to go.
Inside the Code
There are probably three areas of interest within the source code for this control, the first being drawing the rounded corners:
protected GraphicsPath DrawRoundedCorners(Rectangle Rect,
Color BorderColor,Graphics g)
{
GraphicsPath gPath = new GraphicsPath();
try
{
Pen LinePen = new Pen(BorderColor,borderWidth+1);
switch(barOrientation)
{
case Orientations.Horizontal:
Rectangle LeftRect,RightRect;
LeftRect=new Rectangle(Rect.X,Rect.Y+1,
Rect.Height-1,Rect.Height-2);
RightRect = new Rectangle(Rect.X+
(Rect.Width-Rect.Height),Rect.Y+1,
Rect.Height-1,Rect.Height-2);
gPath.AddArc(LeftRect,90,180);
gPath.AddLine(LeftRect.X+LeftRect.Width/2+2,
LeftRect.Top+1,RightRect.X+(RightRect.Width/2)-1,
RightRect.Top+1);
gPath.AddArc(RightRect,270,180);
gPath.AddLine(RightRect.X+(RightRect.Width/2),
RightRect.Bottom, LeftRect.X+(LeftRect.Width/2),
LeftRect.Bottom);
gPath.CloseFigure();
g.DrawPath(LinePen,gPath);
break;
case Orientations.Vertical:
Rectangle TopRect,BotRect;
TopRect=new Rectangle(Rect.X+1,Rect.Y,
Rect.Width-2,Rect.Width-1);
BotRect = new Rectangle(Rect.X+1,Rect.Y+
(Rect.Height-Rect.Width),
Rect.Width-2,Rect.Width-1);
gPath.AddArc(TopRect,180,180);
gPath.AddLine(TopRect.Right,
TopRect.Y+TopRect.Height/2,
BotRect.Right,
BotRect.Y+BotRect.Height/2+1);
gPath.AddArc(BotRect,0,180);
gPath.AddLine(BotRect.Left+1,
BotRect.Y+BotRect.Height/2-1,
TopRect.Left+1,TopRect.Y+TopRect.Height/2+2);
gPath.CloseFigure();
g.DrawPath(LinePen,gPath);
break;
default:
break;
}
}
catch(Exception Err)
{
throw new Exception("DrawRoundedCornersException: "
+Err.Message);
}
return gPath;
}
The rounded corners are created by drawing two half-circles based on the control's height. First, I calculated two sub-rectangles from the ClientRectangle
in which to draw the circles, then just connected the open ends of the half-circles for the rounded control.
The second area of interest would be the painting of the rounded control, this is where I followed Alan Zhao's lead, using the GradientBrush
to give it a 3-D appearance.
protected void PaintPath(GraphicsPath PaintPath,
Color PathColor,Graphics g)
{
Region FirstRegion,SecondRegion;
FirstRegion = new Region(PaintPath);
SecondRegion= new Region(PaintPath);
SolidBrush bgBrush = new SolidBrush(ControlPaint.Dark(PathColor));
g.FillRegion(bgBrush, new Region(PaintPath));
bgBrush.Dispose();
LinearGradientBrush brush;
Rectangle FirstRect,SecondRect;
Rectangle RegionRect = Rectangle.Truncate(PaintPath.GetBounds());
switch(barOrientation)
{
case Orientations.Horizontal:
FirstRect= new Rectangle(RegionRect.X,RegionRect.Y,
RegionRect.Width, RegionRect.Height / 2);
SecondRect=new Rectangle(RegionRect.X,
RegionRect.Height / 2, RegionRect.Width,
RegionRect.Height / 2);
FirstRegion.Intersect(FirstRect);
SecondRegion.Intersect(SecondRect);
brush = new LinearGradientBrush(
new Point(FirstRect.Width/2,FirstRect.Top),
new Point(FirstRect.Width/2,FirstRect.Bottom),
ControlPaint.Dark(PathColor),
PathColor);
g.FillRegion(brush, FirstRegion);
brush.Dispose();
brush = new LinearGradientBrush(
new Point(SecondRect.Width/2,
SecondRect.Top-1),
new Point(SecondRect.Width/2,
SecondRect.Bottom),
PathColor,
ControlPaint.Dark(PathColor));
g.FillRegion(brush, SecondRegion);
brush.Dispose();
break;
case Orientations.Vertical:
FirstRect= new Rectangle(RegionRect.X,RegionRect.Y,
RegionRect.Width/2, RegionRect.Height);
SecondRect=new Rectangle(RegionRect.Width / 2,
RegionRect.Y, RegionRect.Width/2,
RegionRect.Height);
FirstRegion.Intersect(FirstRect);
SecondRegion.Intersect(SecondRect);
brush = new LinearGradientBrush(
new Point(FirstRect.Left, FirstRect.Height/2),
new Point(FirstRect.Right,FirstRect.Height/2),
ControlPaint.Dark(PathColor),
PathColor);
g.FillRegion(brush, FirstRegion);
brush.Dispose();
brush = new LinearGradientBrush(
new Point(SecondRect.Left - 1,SecondRect.Height/2),
new Point(SecondRect.Right,SecondRect.Height/2),
PathColor,
ControlPaint.Dark(PathColor));
g.FillRegion(brush, SecondRegion);
brush.Dispose();
break;
default:
break;
}
}
First, I used GraphicsPath.GetBounds
to get a RectangleF
object which I then truncate into a regular Rectangle
object. From there, I calculate the upper/lower or left/right halves of the control. In order to "filter" out the corners of the rectangle, I then get the Region
that intersects both the GraphicPath
's rectangle and the GraphicsPath
shape (the rounded corners). I then just fill the two halves with background color.
Finally, I think the movement of the Tracker needs some explaining. I have never created any control that moves like this one, so forgive my kludgey solution. All the important code for the movement is in the WndProc()
method.
I handle three messages:
WM_LBUTTONDOWN
(0x0201
)
WM_LBUTTONUP
(0x0202
)
WM_MOUSEMOVE
(0x0200
)
if(m.Msg==0x0201)
{
Point CurPoint=new Point(LowWord((uint)m.LParam),
HighWord((uint)m.LParam));
if(trackRect.Contains(CurPoint))
{
if(!leftbuttonDown)
{
leftbuttonDown=true;
switch(this.barOrientation)
{
case Orientations.Horizontal:
mousestartPos= CurPoint.X-trackRect.X;
break;
case Orientations.Vertical:
mousestartPos= CurPoint.Y-trackRect.Y;
break;
}
}
}
else
{
int OffSet=0;
switch(this.barOrientation)
{
case Orientations.Horizontal:
if(trackRect.Right+(CurPoint.X-trackRect.X
-(trackRect.Width/2))>=this.Width)
OffSet=this.Width-trackRect.Right-1;
else if(trackRect.Left+(CurPoint.X-
trackRect.X-(trackRect.Width/2))<=0)
OffSet=(trackRect.Left-1)*-1;
else
OffSet=CurPoint.X-trackRect.X-(trackRect.Width/2);
trackRect.Offset(OffSet,0);
trackerValue=(int)( ((trackRect.X-1) *
(barMaximum-barMinimum))/(this.Width-trackSize-2));
if(maxSide==Poles.Left)
trackerValue=(trackerValue-(barMaximum-barMinimum))*-1;
break;
case Orientations.Vertical:
if(trackRect.Bottom+(CurPoint.Y-trackRect.Y-
(trackRect.Height/2))>=this.Height)
OffSet=this.Height-trackRect.Bottom-1;
else if(trackRect.Top+(CurPoint.Y-
trackRect.Y-(trackRect.Height/2))<=0)
OffSet=(trackRect.Top-1)*-1;
else
OffSet=CurPoint.Y-trackRect.Y-(trackRect.Height/2);
trackRect.Offset(0,OffSet);
trackerValue=(int)( ((trackRect.Y-1) *
(barMaximum-barMinimum))/(this.Height-trackSize-2));
if(maxSide==Poles.Top)
trackerValue=(trackerValue-(barMaximum-barMinimum))*-1;
break;
default:
break;
}
trackerValue+=barMinimum;
this.Invalidate();
if(OffSet!=0)
{
OnScroll();
OnValueChanged();
}
}
this.Focus();
}
if(m.Msg==0x0200)
{
int OldValue=trackerValue;
Point CurPoint=new Point(LowWord((uint)m.LParam),
HighWord((uint)m.LParam));
if(leftbuttonDown && ClientRectangle.Contains(CurPoint))
{
int OffSet=0;
try
{
switch(this.barOrientation)
{
case Orientations.Horizontal:
if(trackRect.Right+(CurPoint.X-trackRect.X
-mousestartPos)>=this.Width)
OffSet=this.Width-trackRect.Right-1;
else if(trackRect.Left+(CurPoint.X-
trackRect.X-mousestartPos)<=0)
OffSet=(trackRect.Left-1)*-1;
else
OffSet=CurPoint.X-trackRect.X-mousestartPos;
trackRect.Offset(OffSet,0);
trackerValue=(int)( ((trackRect.X-1) *
(barMaximum-barMinimum))/(this.Width-trackSize-2));
if(maxSide==Poles.Left)
trackerValue=(trackerValue-(barMaximum-barMinimum))*-1;
break;
case Orientations.Vertical:
if(trackRect.Bottom+(CurPoint.Y-
trackRect.Y-mousestartPos)>=this.Height)
OffSet=this.Height-trackRect.Bottom-1;
else if(trackRect.Top+(CurPoint.Y-
trackRect.Y-mousestartPos)<=0)
OffSet=(trackRect.Top-1)*-1;
else
OffSet=CurPoint.Y-trackRect.Y-mousestartPos;
trackRect.Offset(0,OffSet);
trackerValue=(int)( ((trackRect.Y-1) *
(barMaximum-barMinimum))/(this.Height-trackSize-2));
if(maxSide==Poles.Top)
trackerValue=(trackerValue-(barMaximum-barMinimum))*-1;
break;
}
}
catch(Exception){}
finally
{
trackerValue+=barMinimum;
this.Invalidate();
if(OffSet!=0)
{
OnScroll();
OnValueChanged();
}
}
}
}
WM_LEFTBUTTONDOWN
Basically, if the user clicks inside the Tracker's region, I save that initial point in mousestartPos
, which I then use to calculate the offset of the Tracker's rectangle or region when the user drags the Tracker. If the initial point is not in the Tracker's region, but still within the Bar's region, I jump the Tracker's center to the left click position.
WM_MOUSEMOVE
When I receive the MOVE message, I check to see that the left button is down with my leftbuttonDown
state variable, and offset the Tracker rectangle based on the initial start position, the current mouse position, and the current Tracker rectangle. Then, I calculate the Value
(trackerValue
) based on the new Tracker rectangle.
WM_LBUTTONUP
if(m.Msg==0x0202)
{
leftbuttonDown=false;
}
When the left button up message is received, I simply set my state variable back to false
to stop any movement, and reset
allows the mousestartPos
to be reset next time the user clicks on the control.
Conclusion
I removed many of the standard Control events from the designer using the ControlDesigner
class. These can be restored very easily by editing the ColorTrackBar
source code.
I hope others find this control useful, please send me any suggestions or comments.