unit Examples.WinForms.Knobs;

interface

uses
  System.ComponentModel,
  System.Collections,
  System.Drawing,
  System.Drawing.Drawing2D,
  System.Windows.Forms,
  System.Timers;

const
  MinKnobRadius = 15;
  MinKnobEdge   = 0;
  MaxKnobEdge   = 100;

  dAngleToRadian = Math.PI / 1800;
  dRadianToAngle = 1800 / Math.PI;

  // Property descriptions
  SAngle         = 'The current angle of the pointer. The angle is in ' +
                   'tenths of a degree clockwise, and 0 is at the bottom ' +
                   'of the dial.';
  SBorderStyle   = 'Indicates whether or not the radio dial should have a ' +
                   'border.';
  SButtonEdge    = 'The "sloping" edge of the button, as percentage of the ' +
                   'radius.';
  SDefaultValue  = 'The default value of the dial.';
  SDrawPointer   = 'Occurs when the user should draw the pointer.';
  SLargeChange   = 'The number of positions the pointer moves in ' +
                   'response to PGUP, PGDN or Shift + (arrow key).';
  SMaximum       = 'The maximum of the value range.';
  SMaximumAngle  = 'The maximum of the angle range.';
  SMinimum       = 'The minimum of the value range.';
  SMinimumAngle  = 'The minimum of the angle range.';
  SPointerColor  = 'The color of the position pointer on the dial.';
  SPointerSize   = 'The size of the position pointer, as percentage of the ' +
                   'knob face.';
  SPointerShape  = 'The shape of the pointer, or OwnerDraw.';
  SRadius        = 'The radius of the dial.';
  SRepeatDelay   = 'Delay, in milliseconds, before a mouse click is repeated';
  SRepeatRate    = 'Delay, in milliseconds, between repeats of a mouse click';
  SSmallChange   = 'The number of positions the pointer moves in response ' +
                   'to keyboard input (arrow keys)';
  STickFrequency = 'The number of positions between tick marks.';
  STickStyle     = 'Indicates how ticks appear on the dial.';
  SValue         = 'The value of the dial.';
  SValueChanged  = 'Occurs when the value of the dial changes.';


type
  KnobTickStyle = (None, Auto, Manual);
  KnobPointerShape = (Line, Triangle, Dot, OwnerDraw);
  KnobBorderStyle = System.Windows.Forms.BorderStyle;

  KnobDrawEventArgs = class
  private
    mRect: Rectangle;
    mGraphics: Graphics;
  public
    constructor Create(ARect: Rectangle; AGraphics: Graphics);
    property DrawRect: Rectangle read mRect;
    property Graphics: Graphics read mGraphics;
  end;

  KnobTickLength = (Long = 10, Middle = 6, Short = 4);

  KnobDrawEventHandler = procedure(sender: System.Object; e: KnobDrawEventArgs) of object;

  [DefaultEvent('ValueChanged')]
  Knob = class(System.Windows.Forms.Control)
  public
    // NEW nested type
    type
      Tick = record
        Value: Integer;
        Length: KnobTickLength;
        Changed: Boolean;
        Color: System.Drawing.Color;
        constructor Create(AValue: Integer; ALength: KnobTickLength;
          AColor: Color; AChanged: Boolean);
      end;
   private
  // NEW code folding regions
  {$REGION 'private members'}
    mValueChanged: EventHandler;
    mDrawPointer: KnobDrawEventHandler;
    mBitmap: Bitmap;
    mBitmapInvalid: Boolean;
    mBorderStyle: KnobBorderStyle;
    mButtonEdge: Integer;
    mDefaultValue: Integer;
    mTickFrequency: Integer;
    mLargeChange: Integer;
    mMaximum: Integer;
    mMaximumAngle: Integer;
    mMinimum: Integer;
    mMinimumAngle: Integer;
    mPointerRect: Rectangle;
    mPointerColor: Color;
    mPointerSize: Integer;
    mPointerShape: KnobPointerShape;
    mValue: Integer;
    mRadius: Integer;
    mSize: Integer;
    mSmallChange: Integer;
    mTicks: ArrayList;
    mTickStyle: KnobTickStyle;
    mIncrementing: Boolean;
    mRepeatTimer: System.Timers.Timer;
    mRepeatRate: Integer;
    mRepeatDelay: Integer;
  {$ENDREGION}
  {$REGION 'private methods'}
  // NEW strict private
    strict private
    function CalcBounds(var Width, Height: Integer): Boolean;
    function PointToRad(APoint, ACenter: System.Drawing.Point): Double;
    procedure UpdateSize;
    procedure TimerExpired(Source: TObject; e: System.Timers.ElapsedEventArgs);
    function GetLightColor: Color;
    function GetFaceColor: Color;
    function GetDarkColor: Color;
    function GetCenterX: Integer;
    function GetCenterY: Integer;
    function GetCenter: Point;
    function GetAngle: Integer;
    procedure SetAngle(Value: Integer);
    procedure SetValue(Value: Integer);
    procedure SetRadius(Value: Integer);
    procedure SetBorderStyle(Value: BorderStyle);
    procedure SetButtonEdge(Value: Integer);
    procedure SetTickFrequency(Value: Integer);
    procedure SetPointerColor(Value: Color);
    procedure SetPointerSize(Value: Integer);
    procedure SetPointerShape(Value: KnobPointerShape);
    procedure SetDefaultValue(Value: Integer);
    procedure SetLargeChange(Value: Integer);
    procedure SetMaximum(Value: Integer);
    procedure SetMinimum(Value: Integer);
    procedure SetMaximumAngle(Value: Integer);
    procedure SetMinimumAngle(Value: Integer);
    procedure SetSmallChange(value: Integer);
    procedure SetTickStyle(Value: KnobTickStyle);
  // NEW strict protected
  strict protected
    function get_CreateParams: CreateParams; override;
    function IsInputKey(KeyData: Keys): Boolean; override;
    function AngleToValue(Angle: Integer): Integer;
    function ValueToAngle(Pos: Integer): Integer;
    procedure BitmapNeeded; virtual;
    procedure SetTicks(Value: KnobTickStyle);
    procedure DrawTick(Canvas: Graphics; var T: Knob.Tick); virtual;
    procedure DrawFocusRectangle;
    procedure OnBackColorChanged(e: EventArgs); override;
    procedure OnDrawPointer; virtual;
    procedure OnValueChanged; virtual;
    procedure OnKeyDown(E: KeyEventArgs); override;
    procedure OnMouseDown(E: MouseEventArgs); override;
    procedure OnMouseMove(E: MouseEventArgs); override;
    procedure OnMouseUp(E: MouseEventArgs); override;
    procedure OnPaint(E: PaintEventArgs); override;
    procedure OnForeColorChanged(E: EventArgs); override;
    procedure OnParentBackColorChanged(E: EventArgs); override;
    procedure OnGotFocus(E: EventArgs); override;
    procedure OnLostFocus(E: EventArgs); override;
    procedure OnSystemColorsChanged(E: EventArgs); override;
    procedure SetBoundsCore(X, Y, Width, Height: Integer; Specified: BoundsSpecified); override;
    procedure DrawButton; virtual;
    procedure DrawTicks; virtual;
    procedure IncValue(Shift: Keys); virtual;
    procedure DecValue(Shift: Keys); virtual;
  {$ENDREGION}
  public
    constructor Create;
    // NEW static functions
    class function SlantedGradient(Rect: Rectangle; Angle: Single;
      Light, Center, Dark: Color): Brush; static;
    class function AngleToPoint(Angle: Integer; Center: Point;
      Radius: Integer): Point; static;
    class function AngleToRad(Angle: Integer): Double; static;
    class function RadToAngle(Rad: Double): Integer; static;
    procedure SetAngleParams(Angle, Min, Max: Integer); virtual;
    procedure SetParams(Pos, Min, Max: Integer); virtual;
    procedure SetTick(AValue: Integer; ALength: KnobTickLength;
      AColor: Color); virtual;
    procedure ClearTicks; virtual;
    procedure ClearTick(AValue: Integer); virtual;

    property LightColor: Color read GetLightColor;
    property FaceColor: Color read GetFaceColor;
    property DarkColor: Color read GetDarkColor;
    property Bitmap: Bitmap read mBitmap;
    property CenterX: Integer read GetCenterX;
    property CenterY: Integer read GetCenterY;
    property Center: Point read GetCenter;
  published
    // properties
    // NEW attributes
    [Category('Behavior'), Description(SAngle)]
    property Angle: Integer read GetAngle write SetAngle;

    [Category('Appearance'), Description(SBorderStyle)]
    property BorderStyle: KnobBorderStyle read mBorderStyle write SetBorderStyle;

    [Category('Appearance'), Description(SButtonEdge)]
    property ButtonEdge: Integer read mButtonEdge write SetButtonEdge;

    [Category('Behavior'), Description(SDefaultValue)]
    property DefaultValue: Integer read mDefaultValue write SetDefaultValue;

    [Category('Behavior'), Description(SLargeChange)]
    property LargeChange: Integer read mLargeChange write SetLargeChange;

    [Category('Behavior'), Description(SMaximum)]
    property Maximum: Integer read mMaximum write SetMaximum;

    [Category('Behavior'), Description(SMaximumAngle)]
    property MaximumAngle: Integer read mMaximumAngle write SetMaximumAngle;

    [Category('Behavior'), Description(SMinimum)]
    property Minimum: Integer read mMinimum write SetMinimum;

    [Category('Behavior'), Description(SMinimumAngle)]
    property MinimumAngle: Integer read mMinimumAngle write SetMinimumAngle;

    [Category('Appearance'), Description(SPointerColor)]
    property PointerColor: Color read mPointerColor write SetPointerColor;

    [Category('Appearance'), Description(SPointerSize)]
    property PointerSize: Integer read mPointerSize write SetPointerSize;

    [Category('Appearance'), Description(SPointerShape)]
    property PointerShape: KnobPointerShape read mPointerShape write SetPointerShape;

    [Category('Appearance'), Description(SRadius)]
    property Radius: Integer read mRadius write SetRadius;

    [Category('Behavior'), Description(SRepeatDelay)]
    property RepeatDelay: Integer read mRepeatDelay write mRepeatDelay;

    [Category('Behavior'), Description(SRepeatRate)]
    property RepeatRate: Integer read mRepeatRate write mRepeatRate;

    [Category('Behavior'), Description(SSmallChange)]
    property SmallChange: Integer read mSmallChange write SetSmallChange;

    [Category('Appearance'), Description(STickFrequency)]
    property TickFrequency: Integer read mTickFrequency write SetTickFrequency;

    [Category('Appearance'), Description(STickStyle)]
    property TickStyle: KnobTickStyle read mTickStyle write SetTickStyle;

    [Category('Behavior'), Description(SValue)]
    property Value: Integer read mValue write SetValue;

    // events

    // NEW multitarget events (add / remove)
    [Category('Behavior'), Description(SValueChanged)]
    property ValueChanged: EventHandler add mValueChanged remove mValueChanged;

    [Category('Appearance'), Description(SDrawPointer)]
    property DrawPointer: KnobDrawEventHandler add mDrawPointer remove mDrawPointer;
  end;

implementation

const
  WS_BORDER        = $800000;
  WS_EX_STATICEDGE = $020000;
  SM_CXBORDER      = 5;

const
  // Minimum border for the BorderStyle border.
  cMinBorder = 1;
  // Mimimum border to make room for the ticks.
  // NEW enum syntax
  cTickBorder = Integer(KnobTickLength.Long);

constructor KnobDrawEventArgs.Create(ARect: Rectangle;
  AGraphics: Graphics);
begin
  inherited Create;
  mRect := ARect;
  mGraphics := AGraphics;
end;

constructor Knob.Tick.Create(AValue: Integer; ALength: KnobTickLength; AColor: Color; AChanged: Boolean);
begin
  Value := AValue;
  Length := ALength;
  Color := AColor;
  Changed := AChanged;
end;

// Calculate the minimum size of the control and adjust
// width and height accordingly.
function Knob.CalcBounds(var Width, Height: Integer): Boolean;
var
  Size: Integer;
begin
  Result := False;
  Size := MinKnobRadius + cMinBorder + Integer(KnobTickLength.Long);
  if mBorderStyle = KnobBorderStyle.FixedSingle then
    Inc(Size, SystemInformation.BorderSize.Width);
  Size := 2 * Size + 1;
  if Width < Size then
  begin
    Width := Size;
    Result := True;
  end;
  if Height < Size then
  begin
    Height := Size;
    Result := True;
  end;
end;

// Convert APoint to an angle (relative to ACenter) in radians,
// where bottom is 0, left is Pi/2, top is Pi and so on.
function Knob.PointToRad(APoint, ACenter: System.Drawing.Point): Double;
var
  N: Integer;
begin
  N := APoint.X - ACenter.X;
  if N = 0 then
    Result := 0.5 * Pi
  else
    Result := ArcTan((ACenter.Y - APoint.Y) / N);
  if N < 0 then
    Result := Result + Pi;
  // Ensure bottom is 0, etc.
  Result := 1.5 * Pi - Result;
end;

// Makes sure the internal size is up to date.
procedure Knob.UpdateSize;
begin
  mSize := 2 * (cMinBorder + mRadius + Integer(KnobTickLength.Long)) + 1;
end;

// Handle timer elapsed events for mouse repeats.
procedure Knob.TimerExpired(Source: TObject; e: System.Timers.ElapsedEventArgs);
begin
  mRepeatTimer.Enabled := False;
  mRepeatTimer.Interval := mRepeatRate;
  if mIncrementing then
    IncValue(Control.ModifierKeys)
  else
    DecValue(Control.ModifierKeys);
  mRepeatTimer.Enabled := True;
end;

function Knob.GetLightColor: Color;
begin
  Result := Color.White;
end;

function Knob.GetFaceColor: Color;
begin
  Result := ForeColor;
end;

function Knob.GetDarkColor: Color;
begin
  Result := Color.FromArgb(ForeColor.R div 3,
                           ForeColor.G div 3,
                           ForeColor.B div 3);
end;

// Make sure that arrow keys (with or without Shift) are used for
// input, and not for form navigation.
function Knob.IsInputKey(KeyData: Keys): Boolean;
begin
  if ((KeyData and Keys.Modifiers) or Keys.Shift = Keys.Shift) and
     ((KeyData and Keys.KeyCode) in [Keys.Up, Keys.Down, Keys.Left, Keys.Right]) then
    Result := True
  else
    Result := inherited IsInputKey(KeyData)
end;

// React on changes of the back color
procedure Knob.OnBackColorChanged(e: EventArgs);
begin
  inherited;
  mBitmapInvalid := True;
  Invalidate;
end;

// Converts an Angle value to a Value value.
function Knob.AngleToValue(Angle: Integer): Integer;
begin
  Result := mMinimum + (mMaximum - mMinimum) *
                       (Angle - mMinimumAngle) div
                       (mMaximumAngle - mMinimumAngle);
end;

// Converts a Value value to an Angle value.
function Knob.ValueToAngle(Pos: Integer): Integer;
begin
  Result := mMinimumAngle + (mMaximumAngle - mMinimumAngle) *
                            (Pos - mMinimum) div (mMaximum - mMinimum);
end;

// Ensures a bitmap is there when we need it.
// The bitmap is used for the internal buffering of the dial.
procedure Knob.BitmapNeeded;
begin
  if not Assigned(mBitmap) then
  begin
    mBitmap := System.Drawing.Bitmap.Create(mSize + 1, mSize + 1);
    mBitmapInvalid := True;
  end;
  if mBitmapInvalid then
  begin
    if mBitmap.Width <> mSize + 1 then
      mBitmap := System.Drawing.Bitmap.Create(mSize + 1, mSize + 1);
    DrawButton;
    DrawTicks;
  end;
end;

// Handles changes of Value by calling ValueChanged
procedure Knob.OnValueChanged;
begin
  if Assigned(mValueChanged) then
    mValueChanged(Self, EventArgs.Empty);
end;

// Clears all ticks.
procedure Knob.ClearTicks;
begin
  mTicks.Clear;
end;

// Set border styles according to BorderStyle property
function Knob.get_CreateParams: CreateParams;
begin
  Result := inherited get_CreateParams;
  case mBorderStyle of
    KnobBorderStyle.FixedSingle: Result.Style := Result.Style or WS_BORDER;
    KnobBorderStyle.Fixed3D: Result.ExStyle := Result.ExStyle or WS_EX_STATICEDGE;
  end;
end;

// Draw border, mainly to display the focus rectangle.
// Couldn't get PaintControl.DrawFocusRectangle to work as I
// wanted, so I had to do this.
procedure Knob.DrawFocusRectangle;
var
  Rect: Rectangle;
  Canvas: Graphics;
  DrawPen: Pen;
begin
  Rect := ClientRectangle;
  Rect.Width := Rect.Width - 1;
  Rect.Height := Rect.Height - 1;
  Rect.Inflate(-1, -1);

  Canvas := CreateGraphics;
  if Focused then
  begin
    // Can't modify SystemPens.ControlText
    DrawPen := Pen.Create(SystemColors.ControlText);
    DrawPen.DashStyle := DashStyle.Dot;
  end
  else
  begin
    DrawPen := Pen.Create(BackColor);
    DrawPen.DashStyle := DashStyle.Solid;
  end;
  Canvas.DrawRectangle(DrawPen, Rect);
  DrawPen.Free;
  Canvas.Free;
end;

// Creates a square gradient brush, with top in "Light", center in
// "Center" and bottom in "Dark" colors, which is then rotated Angle
// degrees around its center point.
class function Knob.SlantedGradient(Rect: Rectangle; Angle: Single;
  Light, Center, Dark: Color): Brush;
type
  PointFArray = array of PointF;
  ColorArray = array of Color;
var
  HSize: Single;
  Gradient: PathGradientBrush;
begin
  HSize := 0.5 * Rect.Height;

  Gradient := PathGradientBrush.Create(
    // NEW way of creating dynamic arrays
    PointFArray.Create(
      PointF.Create(-HSize, -HSize),
      PointF.Create(HSize, -HSize),
      PointF.Create(HSize, 0.0),
      PointF.Create(HSize, HSize),
      PointF.Create(-HSize, HSize),
      PointF.Create(-HSize, 0.0)
  ));

  // Use MatrixOrder.Append to get the "usual" order. I have no idea why .NET
  // uses MatrixOrder.Prepend by default.
  Gradient.RotateTransform(-Angle, MatrixOrder.Append);
  Gradient.TranslateTransform(Rect.X + HSize, Rect.Y + HSize, MatrixOrder.Append);
  Gradient.CenterColor := Center;
  Gradient.SurroundColors :=
    ColorArray.Create(Light, Light, Center, Dark, Dark, Center);
  Result := Gradient;
end;

// Draw button on bitmap.
procedure Knob.DrawButton;
var
  Size: Integer;
  ButtonRect, BitmapRect: Rectangle;
  Canvas: Graphics;
  Solid: SolidBrush;
  Gradient: Brush;
  Liner: Pen;
  Edge: Integer;
begin
  Size := 2 * mRadius + 1;
  ButtonRect := Rectangle.Create(0, 0, Size, Size);
  Canvas := Graphics.FromImage(mBitmap);

  // Clear background.
  Solid := SolidBrush.Create(BackColor);
  BitmapRect := Rectangle.Create(0, 0, mSize, mSize);
  Canvas.FillRectangle(Solid, BitmapRect);
  Solid.Free;

  // Set drawing origin to top left of circle
  Canvas.TranslateTransform(0.5 * mSize - mRadius, 0.5 * mSize - mRadius);

  // Create a gradient boxing the dial, with top highlight, middle
  // face and bottom shadow, then rotate it -45 degrees around
  // the center of the dial.
  Gradient := SlantedGradient(Rectangle.Create(0, 0, Size, Size), 45.0, LightColor, FaceColor, DarkColor);
  Canvas.FillEllipse(Gradient, 0, 0, Size, Size);
  Gradient.Free;

  // Draw top
  Edge := mButtonEdge * mRadius div 100 + 1;
  Canvas.SmoothingMode := SmoothingMode.AntiAlias;

  Solid := SolidBrush.Create(FaceColor);
  Canvas.FillEllipse(Solid, Edge, Edge, Size - 2 * Edge, Size - 2 * Edge);
  Solid.Free;

  Liner := Pen.Create(FaceColor);
  Canvas.DrawEllipse(Liner, Edge, Edge, Size - 2 * Edge, Size - 2 * Edge);
  Liner.Free;

  // Draw outline.
  Liner := SystemPens.ControlText;
  Canvas.DrawEllipse(Liner, 0, 0, Size, Size);

  Canvas.Free;
end;

function Lowest(A, B, C: Integer): Integer;
begin
  Result := Math.Min(A, Math.Min(B, C));
end;

function Highest(A, B, C: Integer): Integer;
begin
  Result := Math.Max(A, Math.Max(B, C));
end;

// Draws the different kinds of pointer.
// For OwnerDraw, calls DrawPointer event, if that is assigned.

procedure Knob.OnDrawPointer;
var
  SmallRadius, InnerRadius: Integer;
  Canvas: Graphics;
  APen, Liner: Pen;
  ABrush: Brush;
  Inner, Outer: Point;
  Points: array[0..2] of Point;
  EllipsePath: GraphicsPath;
begin
  if not IsHandleCreated then
    Exit;
  InnerRadius := (100 - mButtonEdge) * mRadius div 100 - 1;
  if mPointerRect.Left < 0 then
    mPointerRect := Rectangle.Create(
       CenterX - InnerRadius,
       CenterY - InnerRadius,
       2 * InnerRadius + 1,  // width!
       2 * InnerRadius + 1); // height!

  // Be sure to Free this one!
  Canvas := CreateGraphics;
  Canvas.DrawImage(mBitmap, mPointerRect, mPointerRect, GraphicsUnit.Pixel);

  APen := Pen.Create(mPointerColor);
  ABrush := SolidBrush.Create(mPointerColor);

  try
    case mPointerShape of
      KnobPointerShape.Line:
        begin
          Outer := AngleToPoint(Angle, Center, InnerRadius);
          Inner := AngleToPoint(Angle, Center, (101 - mPointerSize) * InnerRadius div 100);
          Canvas.DrawLine(APen, Outer, Inner);

          mPointerRect := Rectangle.Create(
            Math.Min(Inner.X, Outer.X),
            Math.Min(Inner.Y, Outer.Y),
            Abs(Inner.X - Outer.X),
            Abs(Inner.Y - Outer.Y));
        end;

      KnobPointerShape.Triangle:
        begin
          SmallRadius := mPointerSize * InnerRadius div 100;
          Points[0] := AngleToPoint(Angle, Center, InnerRadius);
          Points[1] := AngleToPoint(Angle - 1500, Points[0], SmallRadius);
          Points[2] := AngleToPoint(Angle + 1500, Points[0], SmallRadius);

          Canvas.FillPolygon(ABrush, Points);

          mPointerRect := Rectangle.FromLTRB(
            Lowest(Points[0].X, Points[1].X, Points[2].X),
            Lowest(Points[0].Y, Points[1].Y, Points[2].Y),
            Highest(Points[0].X, Points[1].X, Points[2].X),
            Highest(Points[0].Y, Points[1].Y, Points[2].Y));
        end;

      KnobPointerShape.Dot:
        begin
          SmallRadius := mPointerSize * InnerRadius div 200;
          Inner := AngleToPoint(Angle, Center, InnerRadius - SmallRadius);
          if Inner.X > Center.X then Inner.X := Inner.X + 1;
          if Inner.Y > Center.Y then Inner.Y := Inner.Y + 1;
          mPointerRect := Rectangle.Create(
            Inner.X - SmallRadius,
            Inner.Y - SmallRadius,
            2 * SmallRadius + 1,
            2 * SmallRadius + 1);
          Canvas.FillEllipse(ABrush, mPointerRect);
          Canvas.SmoothingMode := SmoothingMode.AntiAlias;
          Liner := Pen.Create(FaceColor);
          Canvas.DrawEllipse(Liner, mPointerRect);
          Liner.Free;
        end;

      // Prepare data for user (RadioDrawEventArgs) and adjust
      // pointer rect. Call event handler protected by a
      // try-finally construct.
      KnobPointerShape.OwnerDraw:
        begin
          if Assigned(mDrawPointer) then
          begin
            SmallRadius := mPointerSize * InnerRadius div 200;
            Outer := AngleToPoint(Angle, Center, InnerRadius - SmallRadius);
            if Outer.X > CenterX then Outer.X := Outer.X + 1;
            if Outer.Y > CenterY then Outer.Y := Outer.Y + 1;
            mPointerRect := Rectangle.Create(
              Outer.X - SmallRadius,
              Outer.Y - SmallRadius,
              2 * SmallRadius,
              2 * SmallRadius);

            // Create a clipping region to protect the area
            // outside the button face.
            EllipsePath := GraphicsPath.Create;
            mPointerrect.Inflate(1, 1);
            EllipsePath.AddEllipse(mPointerRect);
            mPointerrect.Inflate(-1, -1);
            Canvas.Clip := System.Drawing.Region.Create(EllipsePath);

            try
              mDrawPointer(Self, KnobDrawEventArgs.Create(mPointerRect, Canvas));
            finally
              Canvas.Clip.Free;
              EllipsePath.Free;
            end;
          end;
        end;
    end;
  finally
    mPointerRect.Inflate(1, 1);
    APen.Free;
    ABrush.Free;
    Canvas.Free;
  end;
end;

// Draws one tick, using the data in the Tick struct.
procedure Knob.DrawTick(Canvas: Graphics; var T: Knob.Tick);
var
  ValueAngle: Integer;
  APen: Pen;
  P1, P2: Point;
begin
  ValueAngle := ValueToAngle(T.Value);
  APen := Pen.Create(T.Color);
  P1 := AngleToPoint(ValueAngle, Center, mRadius);
  P2 := AngleToPoint(ValueAngle, Center, mRadius + Integer(T.Length));
  Canvas.DrawLine(APen, P1, P2);
  APen.Free;
  T.Changed := False;
end;

// Draws all ticks in the mTicks ArrayList.
procedure Knob.DrawTicks;
var
  T: Knob.Tick;
  I: Integer;
  Canvas: Graphics;
begin
  if Assigned(mBitmap) then
  begin
    Canvas := Graphics.FromImage(mBitmap);
    if (mTickStyle <> KnobTickStyle.None) and
       Assigned(mTicks) then
    for I := 0 to mTicks.Count - 1 do
    begin
      T := mTicks[I] as Knob.Tick;
      DrawTick(Canvas, T);
      mTicks[I] := T;
    end;
    Canvas.Free;
  end;
end;

// Handles keyboard input.
procedure Knob.OnKeyDown(E: KeyEventArgs);
begin
  case E.KeyCode of
    Keys.Up, Keys.Right:
      IncValue(E.Modifiers);
    Keys.Down, Keys.Left:
      DecValue(E.Modifiers);
    Keys.PageUp:
      IncValue(E.Modifiers or Keys.Shift);
    Keys.PageDown:
      DecValue(E.Modifiers or Keys.Shift);
    Keys.Home:
      Value := mMinimum;
    Keys.End:
      Value := mMaximum;
    else
      begin
        inherited OnKeyDown(E);
        Exit;
      end;
  end;
  E.Handled := True;
  inherited OnKeyDown(E);
end;

// Handles mouse clicks anywhere in the control.
procedure Knob.OnMouseDown(E: MouseEventArgs);
var
  A: Integer;
begin
  inherited OnMouseDown(E);
  if not Focused then
  begin
    Focus;
    Invalidate;
  end;
  if mPointerRect.Contains(E.X, E.Y) then
    // capture mouse if clicked in the pointer rectangle.
    Capture := True
  else
  begin
    // otherwise use the position to increment or decrement
    // Value.
    A := RadToAngle(PointToRad(Point.Create(E.X, E.Y), Center));
    if A < Angle then
    begin
      DecValue(Control.ModifierKeys);
      mIncrementing := False;
    end
    else
    begin
      IncValue(Control.ModifierKeys);
      mIncrementing := True;
    end;
    if not Assigned(mRepeatTimer) then
      mRepeatTimer := System.Timers.Timer.Create;
    mRepeatTimer.Interval := mRepeatDelay;
    Include(mRepeatTimer.Elapsed, TimerExpired);
    mRepeatTimer.Enabled := True;
  end;
end;

// If we are capturing, the Angle follows the mouse.
procedure Knob.OnMouseMove(E: MouseEventArgs);
begin
  inherited;
  if Capture then
    Angle := RadToAngle(PointToRad(Point.Create(E.X, E.Y), Center));
end;

// Release any capture, and stop the repeat timer.
procedure Knob.OnMouseUp(E: MouseEventArgs);
begin
  inherited;
  if Assigned(mRepeatTimer) then
    mRepeatTimer.Enabled := False;
  Capture := False;
end;

procedure Knob.OnPaint(E: PaintEventArgs);
var
  ABrush: Brush;
begin
  ABrush := SolidBrush.Create(Parent.BackColor);
  E.Graphics.FillRectangle(ABrush, DisplayRectangle);
  ABrush.Free;
  UpdateSize;
  BitmapNeeded;
  E.Graphics.DrawImage(mBitmap, 0, 0);
  DrawFocusRectangle;
  OnDrawPointer;
end;

// Creates Tick objects for each necessary tick, depending on
// TickStyle.
procedure Knob.SetTicks(Value: KnobTickStyle);
var
  TextColor: Color;
  Len: KnobTickLength;
  I: Integer;
begin
  TextColor := SystemColors.ControlText;
  mTicks.Clear;
  if Value <> KnobTickStyle.None then
  begin
    SetTick(mMinimum, KnobTickLength.Long, TextColor);
    SetTick(mMaximum, KnobTickLength.Long, TextColor);
  end;
  if Value = KnobTickStyle.Auto then
  begin
    Len := KnobTickLength.Middle;
    I := mMinimum + mTickFrequency;
    while I < mMaximum do
    begin
      SetTick(I, Len, TextColor);
      if Len = KnobTickLength.Middle then
        Len := KnobTickLength.Long
      else
        Len := KnobTickLength.Middle;
      Inc(I, mTickFrequency);
    end;
  end;
end;

// Increments Value, depending on the status of Shift and Ctrl keys.
procedure Knob.IncValue(Shift: Keys);
begin
  if Shift and Keys.Shift = Keys.Shift then
    Value := Value + mLargeChange
  else if Shift and Keys.Control = Keys.Control then
    Value := mMaximum
  else
    Value := Value + mSmallChange;
end;

// Decrements Value, depending on the status of Shift and Ctrl keys.
procedure Knob.DecValue(Shift: Keys);
begin
  if Shift and Keys.Shift = Keys.Shift then
    Value := Value - mLargeChange
  else if Shift and Keys.Control = Keys.Control then
    Value := mMinimum
  else
    Value := Value - mSmallChange;
end;

// Redraw if foreground color changed.
procedure Knob.OnForeColorChanged(E: EventArgs);
begin
  mBitmapInvalid := True;
  inherited;
end;

// Redraw if parent background color changed.
procedure Knob.OnParentBackColorChanged(E: EventArgs);
begin
  mBitmapInvalid := True;
  inherited;
end;

// Draw a focus rectangle if control received focus
procedure Knob.OnGotFocus(E: EventArgs);
begin
  inherited;
  if IsHandleCreated then
    DrawFocusRectangle;
end;

// Remove the focus rectangle if control lost focus.
procedure Knob.OnLostFocus(E: EventArgs);
begin
  inherited;
  if IsHandleCreated then
    DrawFocusRectangle;
end;

// Redraw if one of the system colors changed.
procedure Knob.OnSystemColorsChanged(E: EventArgs);
begin
  mBitmapInvalid := True;
  inherited;
end;

// Updates all layout parameters at once. Used to make sure
// the control doesn't get too small, and perhaps for AutoInc.
procedure Knob.SetBoundsCore(X, Y, Width, Height: Integer; Specified: BoundsSpecified);
begin
  mBitmapInvalid := CalcBounds(Width, Height);
  inherited;

  // Make Radius ridiculously high, to ensure redraw.
  Radius := Width + Height;
end;

constructor Knob.Create;
begin
  inherited Create;
  SetStyle(ControlStyles.StandardClick or ControlStyles.UserPaint or
    ControlStyles.Selectable, True);
  UpdateStyles;
  mTicks := ArrayList.Create;
  mBorderStyle := KnobBorderStyle.None;
  mButtonEdge := 5;
  mDefaultValue := 0;
  mTickFrequency := 1;
  mLargeChange := 2;
  mMaximum := 10;
  mMaximumAngle := 3300;
  mMinimum := 0;
  mMinimumAngle := 300;
  mPointerColor := SystemColors.ControlText;
  mPointerSize := 33;
  mRadius := MinKnobRadius;
  mSmallChange := 1;
  TabStop := True;
  mTickStyle := KnobTickStyle.Auto;
  mBitmapInvalid := True;
  mPointerRect := Rectangle.Create(-1, 0, 0, 0);
  Width := 51;
  Height := 51;
  mRepeatDelay := 400;
  mRepeatRate := 100;
  Value := 0;
  ForeColor := SystemColors.Control;
  SetTicks(mTickStyle);
end;

function Knob.GetCenterX: Integer;
begin
  Result := mSize div 2;
end;

function Knob.GetCenterY: Integer;
begin
  Result := mSize div 2;
end;

function Knob.GetCenter: Point;
begin
  Result := Point.Create(mSize div 2, mSize div 2);
end;

class function Knob.AngleToPoint(Angle: Integer; Center: Point;
  Radius: Integer): Point;
var
  RadAngle: Double;
begin
  RadAngle := AngleToRad(Angle);
  Result := Point.Create(
    Center.X - Round(Radius * Sin(RadAngle)),
    Center.Y + Round(Radius * Cos(RadAngle)));
end;

function Knob.GetAngle: Integer;
begin
  Result := ValueToAngle(mValue);
end;

procedure Knob.SetAngle(Value: Integer);
begin
  SetAngleParams(Value, mMinimumAngle, mMaximumAngle);
end;

// Ensures that the angle values Angle, MinimumAngle and
// MaximumAngle do not conflict.
procedure Knob.SetAngleParams(Angle, Min, Max: Integer);
var
  Invalid: Boolean;
  Pos: Integer;
begin
  if Max < Min then
    raise Exception.Create('Property out of range');

  if Angle < Min then
    Angle := Min
  else if Angle > Max then
    Angle := Max;

  Invalid := False;

  if mMinimumAngle <> Min then
  begin
    mMinimumAngle := Min;
    Invalid := True;
  end;

  if mMaximumAngle <> Max then
  begin
    mMaximumAngle := Max;
    Invalid := True;
  end;

  if Invalid then
  begin
    mBitmapInvalid := True;
    Invalidate;
  end;

  Pos := AngleToValue(Angle);
  if Pos <> mValue then
    SetParams(Pos, mMinimum, mMaximum);
end;


// Ensures that the positional values Value, Minimum and
// Maximum do not conflict, and that the necessary redraws
// are performed.
procedure Knob.SetParams(Pos, Min, Max: Integer);
var
  Changed, Invalid, MustDrawTicks: Boolean;
begin
  if Max < Min then
    raise Exception.Create('Property out of range');

  if Pos < Min then
    Pos := Min
  else if Pos > Max then
    Pos := Max;

  Changed := False;
  Invalid := False;
  MustDrawTicks := False;

  if mMinimum <> Min then
  begin
    mMinimum := Min;
    Invalid := True;
    MustDrawTicks := True;
  end;

  if mMaximum <> Max then
  begin
    mMaximum := Max;
    Invalid := True;
    MustDrawTicks := True;
  end;

  if mValue <> Pos then
  begin
    mValue := Pos;
    OnDrawPointer;
    Changed := True;
  end;

  if MustDrawTicks then
    SetTicks(mTickStyle);

  if Invalid then
  begin
    mBitmapInvalid := True;
    Invalidate;
  end;

  if Changed then
    OnValueChanged;
end;

class function Knob.AngleToRad(Angle: Integer): Double;
begin
  Result := dAngleToRadian * Angle;
end;

class function Knob.RadToAngle(Rad: Double): Integer;
begin
  Result := Round(dRadianToAngle * Rad);
end;

procedure Knob.SetValue(Value: Integer);
begin
  SetParams(Value, mMinimum, mMaximum);
end;

// Creates one tick value with given value, length and color.
procedure Knob.SetTick(AValue: Integer; ALength: KnobTickLength; AColor: Color);
var
  I: Integer;
  T: Knob.Tick;
  Canvas: Graphics;
begin
  if (AValue < mMinimum) or (AValue > mMaximum) then
    raise Exception.Create('Tick value out of range');
  for I := 0 to mTicks.Count - 1 do
  begin
    T := Knob.Tick(mTicks[I]);
    if T.Value = AValue then
    begin
      if (T.Length <> ALength) or (T.Color <> AColor) then
      begin
        T.Length := ALength;
        T.Color := AColor;
        T.Changed := True;
        Invalidate;
        mTicks[I] := T;
      end;
      Exit;
    end;
  end;
  T := Knob.Tick.Create(AValue, ALength, AColor, True);
  mTicks.Add(T);
  if IsHandleCreated then
  begin
    Canvas := Graphics.FromImage(mBitmap);
    DrawTick(Canvas, T);
    Canvas.Free;
    Canvas := CreateGraphics;
    DrawTick(Canvas, T);
    Canvas.Free;
  end;
end;

procedure Knob.SetRadius(Value: Integer);
var
  MaxRadius: Integer;
begin
  if Width <= Height then
    MaxRadius := (Width - 1) div 2 - cMinBorder - cTickBorder
  else
    MaxRadius := (Height - 1) div 2 - cMinBorder - cTickBorder;
  if mBorderStyle = KnobBorderStyle.FixedSingle then
    Dec(MaxRadius, SystemInformation.BorderSize.Width);
  if Value > MaxRadius then
    Value := MaxRadius;
  if Value < MinKnobRadius then
    Value := MinKnobRadius;
  if Value <> mRadius then
  begin
    mRadius := Value;
    mBitmapInvalid := True;
    Invalidate;
  end;
  UpdateSize;
end;

procedure Knob.SetBorderStyle(Value: KnobBorderStyle);
begin
  if Value <> mBorderStyle then
  begin
    mBorderStyle := Value;
    if IsHandleCreated then
    begin
      RecreateHandle;
      DrawFocusRectangle;
    end;
  end;
end;

procedure Knob.SetButtonEdge(Value: Integer);
begin
  if Value < MinKnobEdge then
    Value := MinKnobEdge
  else if Value > MaxKnobEdge then
    Value := MaxKnobEdge;
  if Value <> mButtonEdge then
  begin
    mButtonEdge := Value;
    if not mBitmapInvalid then
    begin
      mBitmapInvalid := True;
      Invalidate;
    end;
  end;
end;

procedure Knob.SetTickFrequency(Value: Integer);
begin
  if Value <> mTickFrequency then
  begin
    mTickFrequency := Value;
    if mTickStyle = KnobTickStyle.Auto then
    begin
      ClearTicks;
      SetTicks(mTickStyle);
    end;
    mBitmapInvalid := True;
    Invalidate;
  end;
end;

procedure Knob.SetPointerColor(Value: Color);
begin
  if mPointerColor <> Value then
  begin
    mPointerColor := Value;
    OnDrawPointer;
  end;
end;

procedure Knob.SetPointerSize(Value: Integer);
begin
  if Value > 100 then
    Value := 100
  else if Value < 1 then
    Value := 1;
  if Value <> mPointerSize then
  begin
    mPointerSize := Value;
    OnDrawPointer;
  end;
end;

procedure Knob.SetPointerShape(Value: KnobPointerShape);
begin
  if Value <> mPointerShape then
  begin
    mPointerShape := Value;
    Invalidate;
  end;
end;

procedure Knob.SetDefaultValue(Value: Integer);
begin
  if Value <> mDefaultValue then
  begin

    // Room for future extensions,
    // like showing an extra tick style.

    mDefaultValue := Value;
  end;
end;

procedure Knob.SetLargeChange(Value: Integer);
begin
  if Value < mSmallChange + 1 then
    Value := mSmallChange + 1;
  mLargeChange := Value;
end;

procedure Knob.SetMaximum(Value: Integer);
begin
  SetParams(mValue, mMinimum, Value);
end;

procedure Knob.SetMinimum(Value: Integer);
begin
  SetParams(mValue, Value, mMaximum);
end;

procedure Knob.SetMaximumAngle(Value: Integer);
begin
  SetAngleParams(ValueToAngle(mValue), mMinimumAngle, Value);
end;

procedure Knob.SetMinimumAngle(Value: Integer);
begin
  SetAngleParams(ValueToAngle(mValue), Value, mMaximumAngle);
end;

procedure Knob.SetSmallChange(value: Integer);
begin
  if Value > mLargeChange then
    Value := mLargeChange div 2;
  if Value < 1 then
    Value := 1;
  mSmallChange := Value;
end;

procedure Knob.SetTickStyle(Value: KnobTickStyle);
begin
  if Value <> mTickStyle then
  begin
    mTickStyle := Value;
    ClearTicks;
    SetTicks(Value);
    mBitmapInvalid := True;
    Invalidate;
  end;
end;

procedure Knob.ClearTick(AValue: Integer);
var
  I: Integer;
begin
  for I := 0 to mTicks.Count - 1 do
    if Knob.Tick(mTicks[I]).Value = AValue then
    begin
      mTicks.RemoveAt(I);
      Break;
    end;
  Invalidate;
end;

end.
