Click here to Skip to main content
Click here to Skip to main content

A variation on the default TrackBar

By , 17 Aug 2004
 

Sample Image - ColorTrackBar.jpg

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

  1. Download source code and unzip it.
  2. Open VS.NET's Tool menu and select "Add/Remove ToolBox Items".
  3. Click "Browse", and navigate to the ColorTrackBar.dll in the bin\Release directory of the source code.
  4. Click Open and then OK, the ColorTrackBar control will now be in your ToolBox, just drag the control onto your form.
  5. 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);
        //build shape
        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);
        //build shape
        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);
  //
  // Fill background
  //
  SolidBrush bgBrush = new SolidBrush(ControlPaint.Dark(PathColor));
  g.FillRegion(bgBrush, new Region(PaintPath));
  bgBrush.Dispose();
  //
  // The gradient brush
  //
  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);
      //only get the bar region
      FirstRegion.Intersect(FirstRect);
      SecondRegion.Intersect(SecondRect);
      // Paint upper half
      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();
      // Paint lower half
      // (SecondRect.Y - 1 because there would be
      // a dark line in the middle of the bar)
      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);
      //only get the bar region
      FirstRegion.Intersect(FirstRect);
      SecondRegion.Intersect(SecondRect);
      // Paint left half
      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();
      // Paint right half
      // (SecondRect.X - 1 because there would be a dark line
      // in the middle of the bar)
      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:

  1. WM_LBUTTONDOWN (0x0201)
  2. WM_LBUTTONUP (0x0202)
  3. 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();
}
//WM_MOUSEMOVE
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
    {
      //force redraw
      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.

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

About the Author

Guinness4Strength
Software Developer (Senior)
United States United States
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: DrawBackGround Errormembercpramhofer7 Feb '06 - 10:49 
i even get this exception!
 
seems like an memory error!
GeneralRe: DrawBackGround ErrormembertheYaz12 Apr '06 - 11:11 
Has anyone figured out a solution to this error??
GeneralRe: DrawBackGround ErrormemberSalte221 Aug '06 - 21:55 
This problem occured for me too. The reason for the error (in my case) was in the "PaintRectangle" method (called in the "DrawControl" method).
 
The line that caused the exception was this one:
 
brush = new LinearGradientBrush(new Point(SecondRect.Width / 2, SecondRect.Top - 1),
new Point(SecondRect.Width / 2, SecondRect.Bottom, RectColor,
ControlPaint.Dark(RectColor));
 
And the exception was thrown when the points created in the method had negative values (-1, -2) iirc. I'm not sure what caused the negative values yet, but i think it has something to do with a repaint being ordered before the control has finished painting it self.
 
When I caught this exception (instead of throwing new exceptions) the application could continue to run, but the tracker would dissapear (not be drawn) when the error occured.
GeneralKeeping Cursor on tracksussneauva26 Sep '05 - 10:17 
Big Grin | :-D Nice job. Thank you for saving me the work!
One suggestion - I notice that if your mouse strays outside the bar while dragging the tracker, it stops responding to mouse movement until it returns. You can correct
this by changing the following line (around line 450 in the .cs file) in the WM_MOUSEMOVE section
 
from
if(leftbuttonDown && ClientRectangle.Contains(CurPoint))
 
to
if(leftbuttonDown)
 

 
Cheers!
GeneralRe: Keeping Cursor on trackmemberGuinness4Strength26 Sep '05 - 16:45 
Thanks...
Stoppping the motion of the track bar when the mouse left the border was obviously part of my design, but it may not be desirable by all. Hope it works well for you....
GeneralRe: Keeping Cursor on trackmemberboomboomwa9 Mar '06 - 12:09 
also, if you do that, change:
 
public static ushort LowWord(uint value)
{
return (ushort)(value & 0xFFFF);
}
public static ushort HighWord(uint value)
{
return (ushort)(value >> 16);
}
to:
 
public static short LowWord(uint value)
{
return (short)(value & 0xFFFF);
}
public static short HighWord(uint value)
{
return (short)(value >> 16);
}
 
so that CurPoint will return negative values.
GeneralRemove a call to OnScrollmemberpearljam14521 Dec '04 - 22:55 
I think you need to remove one of the calls to OnScroll(). This will be in the function DrawControl(). I say this because if this call exists, then OnScroll keeps getting called even when the control is getting repainted. Whereas it should get called only when the user clicks in the control
GeneralRe: Remove a call to OnScrollmemberaaronn14 Jun '05 - 11:27 
I have to agree with the pearljam.. I was getting some unexpected behaviour until I commented out the OnScroll in the DrawControl. Seems to handle scrolling and setting trackerValue no problem now.
 
Nice example of a Custom Control by the way..

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 18 Aug 2004
Article Copyright 2004 by Guinness4Strength
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid