unit Shape;
{*******************************************************************************
  ShapesDemo
  Written by David Clegg, davidclegg@optusnet.com.au.

  This unit contains the base Shape class used to render onto a GDI+ drawing
  surface.
*******************************************************************************}

interface

uses
  System.Drawing, System.Drawing.Drawing2D, System.Collections;

type

  //Forward declaration
  TShape = class;

  /// <summary>
  /// Strongly typed collection of TShape objects.
  /// </summary>
  TShapes = class(CollectionBase)
  private
    function GetShape(Index: integer): TShape;
    procedure SetShape(Index: integer; Value: TShape);
  public
    property Shape[Index: integer]: TShape read GetShape write SetShape; default;
    function Add(pShape: TShape): integer;
    function IndexOf(pShape: TShape): integer;
    procedure Remove(pShape: TShape);
    function Contains(pShape: TShape): boolean;
    procedure Insert(pIndex: integer; pShape: TShape);
  end;

	/// <summary>
	/// Base class for all shapes to descend from. It implements the logic for
	/// drawing an outline of the shape, and for filling the shape using the
	/// specified brush. It implements the IDisposable interface as the canvas
	/// reference needs to be disposed
	/// Concrete classes need only implement the GetShape method which should
	/// return a GraphicsPath instance representing the bounds of the shape.
	/// As a contrast, some concrete classes may also implement alternative
	/// methods for drawing the outline and filling the shape.
	/// </summary>
  TShape = class
  private
    FOldStartPoint: Point;
    FOldEndPoint: Point;
    FCanvas: Graphics;
    FDisposed: boolean;
    FStartPoint: Point;
    FEndPoint: Point;
    FDrawBrush: Brush;
    procedure DoOutline(pPen: Pen; pStartPoint, pEndPoint: Point);
    function GetRectangle: Rectangle;
  protected
    property Canvas: Graphics read FCanvas;
		function GetShape(pStartPoint, pEndPoint: Point): GraphicsPath; virtual; abstract;
    function DoGetRectangle(pStartPoint, pEndPoint: Point): Rectangle;
  public
    StartPoint: Point;
    EndPoint: Point;
    procedure Dispose;
    procedure Draw(pStartPoint, pEndPoint: Point; pBrush: Brush); overload;
    procedure Draw; overload;
    procedure DrawOutline(pStartPoint, pEndPoint: Point);
    property DrawBrush: Brush read FDrawBrush write FDrawBrush;
    property Rectangle: System.Drawing.Rectangle read GetRectangle;
    constructor Create(pCanvas: Graphics);
    destructor Destroy; override;
  end;

implementation

{ TShapes }
function TShapes.Add(pShape: TShape): integer;
begin
  Result := List.Add(pShape);
end;

procedure TShapes.Remove(pShape: TShape);
begin
  List.Remove(pShape);
end;

procedure TShapes.Insert(pIndex: integer; pShape: TShape);
begin
  List.Insert(pIndex, pShape);
end;

function TShapes.IndexOf(pShape: TShape): integer;
begin
  Result := List.IndexOf(pShape);
end;

function TShapes.Contains(pShape: TShape): boolean;
begin
  Result := List.Contains(pShape);
end;

function TShapes.GetShape(Index: integer): TShape;
begin
  Result := List[Index] as TShape;
end;

procedure TShapes.SetShape(Index: integer; Value: TShape);
begin
  List[Index] := Value;
end;

{ TShape }
constructor TShape.Create(pCanvas: Graphics);
begin
  inherited Create;
  FCanvas := pCanvas;
  FDisposed := False;
  FStartPoint := Point.Create(0, 0);
  FEndPoint := Point.Create(0, 0);
end;

/// <summary>
/// Destructor is used to call .Dispose to ensure that fCanvas.Dispose
/// is called if it hasn't already been done.
/// </summary>
destructor TShape.Destroy;
begin
  Dispose;
  inherited;
end;

/// <summary>
/// Implement IDisposable.Dispose to ensure the fCanvas Graphics reference
/// is explicitly disposed.
/// </summary>
procedure TShape.Dispose;
begin
  if not FDisposed then
    FCanvas.Dispose;
  FDisposed := True;
end;

/// <summary>
/// Draws a solid shape based on the startPoint and endPoint
/// co-ordinates. Concrete classes must implement the GetShape method
/// to determine the bounds for the shape.
/// </summary>
procedure TShape.Draw(pStartPoint, pEndPoint: Point; pBrush: Brush);
begin
  //Draw the shape
  FCanvas.FillRegion(pBrush, Region.Create(GetShape(pStartPoint, pEndPoint)));

  //As the outline has a size of 1, it will still be visible along
  //some points of the shape. Draw another outline which uses a pen
  //the same color as the fill if it uses a SolidBrush, or one that
  //uses the same color as the original outline for other brush types.
  if pBrush is SolidBrush then
    DoOutline(Pen.Create((SolidBrush(pBrush)).Color, 1), pStartPoint, pEndPoint)
  else
    DoOutline(Pen.Create(Color.Black, 1), pStartPoint, pEndPoint);
end;

/// <summary>
/// Draws a solid shape based on the StartPoint, EndPoint and Brush properties
/// </summary>
procedure TShape.Draw;
begin
  Draw(StartPoint, EndPoint, FDrawBrush);
end;

/// <summary>
/// Draws an outline of a shape based in the startPoint and endPoint
/// co-ordinates. Concrete classes must implement the GetShape method
/// to determine the bounds for the shape.
/// </summary>
procedure TShape.DrawOutline(pStartPoint, pEndPoint: Point);
var
  lPen: Pen;
begin
  //First erase the last outline
  lPen := Pen.Create(Color.White, 1);
  DoOutline(lPen, FOldStartPoint, FOldEndPoint);

  //Draw the new outline
  lPen := Pen.Create(Color.Black, 1);
  DoOutline(lPen, pStartPoint, pEndPoint);

  //Save the start and end points
  FOldStartPoint := pStartPoint;
  FOldEndPoint := pEndPoint;
end;

/// <summary>
/// Return a Rectangle structure to represent the bounds of the shape.
/// </summary>
function TShape.DoGetRectangle(pStartPoint, pEndPoint: Point): Rectangle;
begin
  Result := System.Drawing.Rectangle.FromLTRB(pStartPoint.X, pStartPoint.Y,
    pEndPoint.X, pEndPoint.Y);
end;

/// <summary>
/// Return a Rectangle structure, based on FStartPoint and FEndPoint
/// </summary>
function TShape.GetRectangle: Rectangle;
begin
  Result := DoGetRectangle(FStartPoint, FEndPoint);
end;

/// <summary>
/// Draws the outline for the shape.
/// </summary>
procedure TShape.DoOutline(pPen: Pen; pStartPoint, pEndPoint: Point);
var
  lGraphicsPath: GraphicsPath;
begin
  lGraphicsPath := GetShape(pStartPoint, pEndPoint);
  FCanvas.DrawPath(pPen, lGraphicsPath);
end;

end.

