Contributor: GARRET WILSON

{*******************************************************************************
  TRichButton
  Copyright  1997 Mentor Computer Solutions
  Version 1.0 revised February 2, 1997

  Author:       Garret Wilson
                Garret@MentorComputer.com

  Company:      Mentor Computer Solutions
                RR 2 Box 246
                Chelsea, OK 74016 USA
                (918) 789-2734
                http://www.MentorComputer.com

  Status:       Freeware. Source may be redistributed in whole, providing that
                the copyright information is also included.

  Description:  TRichButton provides a button that can include rich text,
                including bold, italics, different fonts, etc. To use
                TRichButton, access the Lines, Font, Color, DefAttributes,
                SelAttributes, and Paragraph properties, which function
                identically to those that come with the standard TRichEdit
                control.

  Acknowledgements: TRichButton was developed in part by referring to the
                    Borland source code for TCustomPanel and TRichEdit. Some
                    features of TRichButton originated from ideas implemented in
                    TTransBitmap, which is Copyright  1996 Alan GARNY,
                    gry@physiol.ox.ac.uk, http://www.physiol.ox.ac.uk/~gry
                    and these instances are indicated.
*******************************************************************************}

unit RichButton;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, ComCtrls, ExtCtrls;

type

  TRichButtonState = (stUp, stDown, stDisabled);

  TRichButton = class(TCustomControl)
  private
    FAlignment: TAlignment;     {storage for properties}
    FAllowDown: Boolean;
    FBevelInner: TPanelBevel;
    FBevelOuter: TPanelBevel;
    FBevelWidth: TBevelWidth;
    FBorderWidth: TBorderWidth;
    FBorderStyle: TBorderStyle;
    FFocus: Boolean;
    FFocusColor: TColor;
    FFocusWidth: TWidth;
    FFullRepaint: Boolean;
    FLocked: Boolean;
    FState: TRichButtonState;
    FOnResize: TNotifyEvent;
    FSelAttributes: TTextAttributes;
    FDefAttributes: TTextAttributes;
    FParagraph: TParaAttributes;
    HasFocus:Boolean;     {variables used internally}
    MouseCaught:Boolean;
    OrigState:TRichButtonState;
    RichEdit:TRichEdit;
    procedure CMColorChanged(var Message: TMessage); message CM_COLORCHANGED;
    procedure CMFontChanged(var Message: TMessage); message CM_FONTCHANGED;
    procedure CMTextChanged(var Message: TMessage); message CM_TEXTCHANGED;
    procedure CMCtl3DChanged(var Message: TMessage); message CM_CTL3DCHANGED;
    procedure CMIsToolControl(var Message: TMessage); message CM_ISTOOLCONTROL;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
    procedure SetAlignment(Value: TAlignment);
    procedure SetAllowDown(Value:Boolean);  {modified from TTransBitmap}
    procedure SetBevelInner(Value: TPanelBevel);
    procedure SetBevelOuter(Value: TPanelBevel);
    procedure SetBevelWidth(Value: TBevelWidth);
    procedure SetBorderWidth(Value: TBorderWidth);
    procedure SetBorderStyle(Value: TBorderStyle);
    procedure SetFocus(Value:Boolean);  {modified from TTransBitmap}
    procedure SetFocusColor(Value:TColor);  {modified from TTransBitmap}
    procedure SetFocusWidth(Value:TWidth);  {modified from TTransBitmap}
    function GetLines:TStrings;
    procedure SetLines(Value:TStrings);
    procedure SetState(Value:TRichButtonState); {modified from TTransBitmap}
    procedure ReadData(Reader: TReader);
        {internal routines}
    function GetWorkRect:TRect;  {modified from TTransBitmap}
  protected
    procedure CreateParams(var Params: TCreateParams); override;
    procedure CreateWnd; override;
    procedure AlignControls(AControl: TControl; var Rect: TRect); override;
    procedure Paint; override;
    procedure Resize; dynamic;
    property FullRepaint: Boolean read FFullRepaint write FFullRepaint default True;
  public
    constructor Create(AOwner: TComponent); override;
    property DefAttributes:TTextAttributes read FDefAttributes write FDefAttributes; {properties stored in TRichEdit}
    property SelAttributes:TTextAttributes read FSelAttributes write FSelAttributes;
    property Paragraph:TParaAttributes read FParagraph;
  published
    property Align;
    property Alignment:TAlignment read FAlignment write SetAlignment default taCenter;
    property AllowDown:Boolean read FAllowDown write SetAllowDown default False;
    property BevelInner:TPanelBevel read FBevelInner write SetBevelInner default bvNone;
    property BevelOuter:TPanelBevel read FBevelOuter write SetBevelOuter default bvRaised;
    property BevelWidth:TBevelWidth read FBevelWidth write SetBevelWidth default 2;
    property BorderWidth:TBorderWidth read FBorderWidth write SetBorderWidth default 0;
    property BorderStyle:TBorderStyle read FBorderStyle write SetBorderStyle default bsNone;
    property DragCursor;
    property DragMode;
    property Enabled;
    property Color default clBtnFace;
    property Ctl3D;
    property Focus:Boolean read FFocus write SetFocus default False;
    property FocusColor:TColor read FFocusColor write SetFocusColor default clHighlight;
    property FocusWidth:TWidth read FFocusWidth write SetFocusWidth default 2;
    property Font;
    property Height default 25;
    property Locked:Boolean read FLocked write FLocked default False;
    property ParentColor default False;
    property ParentCtl3D;
    property ParentFont;
    property ParentShowHint;
    property PopupMenu;
    property ShowHint;
    property State:TRichButtonState read FState Write SetState default stUp;
    property TabOrder;
    property TabStop;
    property Visible;
    property Width default 75;
    property OnClick;
    property OnDblClick;
    property OnDragDrop;
    property OnDragOver;
    property OnEndDrag;
    property OnEnter;
    property OnExit;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnResize: TNotifyEvent read FOnResize write FOnResize;
    property OnStartDrag;
    property Lines:TStrings read GetLines write SetLines; {properties stored in TRichEdit}
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Additional', [TRichButton]);
end;

constructor TRichButton.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := [csAcceptsControls, csCaptureMouse, csOpaque, csDoubleClicks, csReplicatable];
  RichEdit:=TRichEdit.Create(Self); {create the RTF control}
  RichEdit.Parent:=Self;  {set the TRichButton as the parent}
  FDefAttributes:=RichEdit.DefAttributes;  {use the DefAttributes of the RichEdit}
  FSelAttributes:=RichEdit.SelAttributes;  {use the SelAttributes of the RichEdit}
  FParagraph:=RichEdit.Paragraph;  {use the Paragraph of the RichEdit}
  Width:=75;
  Height:=25;
  FAlignment := taCenter;
  FAllowDown:=False;
  BevelOuter := bvRaised;
  BevelWidth:=2;
  FBorderStyle := bsNone;
  Color:=clBtnFace;
  FFocus:=False;
  FFocusColor:=clHighlight;
  FFocusWidth:=2;
  FFullRepaint := True;
  ParentColor:=False;
  FState:=stUp;
  MouseCaught:=False;
  HasFocus:=False;
end;

procedure TRichButton.CreateParams(var Params: TCreateParams);
const
  BorderStyles: array[TBorderStyle] of Longint = (0, WS_BORDER);
begin
  inherited CreateParams(Params);
  with Params do
  begin
    Style := Style or BorderStyles[FBorderStyle];
    if NewStyleControls and Ctl3D and (FBorderStyle = bsSingle) then
    begin
      Style := Style and not WS_BORDER;
      ExStyle := ExStyle or WS_EX_CLIENTEDGE;
    end;
  end;
end;

procedure TRichButton.CreateWnd;
begin
  inherited CreateWnd;  {call the inherited CreatWnd() procedure}
  RichEdit.BorderStyle:=bsNone; {don't show a border on the RTF control}
  RichEdit.Enabled:=False;  {disable the RTF control altogether, to get rid of the cursor}
  RichEdit.ReadOnly:=True;  {don't allow the rich text to be changed}
  RichEdit.TabStop:=False;  {don't allow the rich text to tabbed to}
  RichEdit.ParentColor:=False;  {don't use the parent color}
  RichEdit.ParentCtl3D:=False;  {don't use the parent's Ctl3D style}
  RichEdit.ParentFont:=False;  {don't use the parent font}
  RichEdit.Font:=Font;  {set the RichEdit to the same font as the button}
  RichEdit.Color:=Color;  {set the RichEdit to the same color as the button}
  if csDesigning in ComponentState then {if we are designing the component}
  begin
    RichEdit.Paragraph.Alignment:=taCenter; {center the text}
    RichEdit.Lines.Add(Name); {show the name of the control}
  end;
end;

procedure TRichButton.CMTextChanged(var Message: TMessage);
begin
  Invalidate;
end;

procedure TRichButton.CMColorChanged(var Message: TMessage);
begin
  inherited;
  if Parent<>Nil then {if we have a parent (for some reason, we must have this or an error will occur upon creation)}
    RichEdit.Color:=Color;  {set the RichEdit to the same color}
end;

procedure TRichButton.CMFontChanged(var Message: TMessage);
begin
  inherited;
  RichEdit.Font:=Font;  {set the RichEdit to the same font}
end;

procedure TRichButton.CMCtl3DChanged(var Message: TMessage);
begin
  if NewStyleControls and (FBorderStyle = bsSingle) then RecreateWnd;
  inherited;
end;

procedure TRichButton.CMIsToolControl(var Message: TMessage);
begin
  if not FLocked then Message.Result := 1;
end;

procedure TRichButton.Resize;
begin
  RichEdit.BoundsRect:=GetWorkRect;  {change the size of the RTF control}
  if FullRepaint then Invalidate;
  if Assigned(FOnResize) then FOnResize(Self);
end;

procedure TRichButton.WMSize(var Message: TWMSize);
begin
  inherited;
  if not (csLoading in ComponentState) then Resize;
end;

procedure TRichButton.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); {modified from TTransBitmap}
var
  rect:TRect;
  overControl:Boolean;
begin
  inherited MouseDown(Button, Shift, X, Y); {call the inherited MouseDown() procedure}
  rect:=GetWorkRect;  {find the working area of the button}
  overControl:=(X>=rect.Left) and (x<=rect.Right) and (y>=rect.Top) and (y<=rect.Bottom); {see if the mouse is inside the pressable part of the button}
  if (overControl) and (Button=mbLeft) and (FState<>stDisabled) then {if this was the left mouse button, and the button isn't disabled}
  begin
    MouseCaught:=True;  {show that the left mouse button has been pressed down on the button}
    OrigState:=FState;  {keep track of the original state of the button, in case we allow it to stay down}
    if FState<>stDown then  {if the button isn't down already, put it down}
    begin
      FState:=stDown; {put the button down}
      Realign;  {realign the controls in the button}
      Invalidate;  {invalidate the button for repainting}
    end;
  end;
end;

procedure TRichButton.MouseMove(Shift: TShiftState; X, Y: Integer); {modified from TTransBitmap}
var
  newState:TRichButtonState;
  needRepaint:Boolean;
  newHasFocus:Boolean;
  rect:TRect;
begin
  inherited MouseMove(Shift, X, Y); {call the inherited MouseMove() procedure}
  needRepaint:=False; {assume we don't need to repaint the button}
  rect:=GetWorkRect;  {find the working area of the button}
  newHasFocus:=(X>=rect.Left) and (x<=rect.Right) and (y>=rect.Top) and (y<=rect.Bottom); {see if the mouse is still inside the button}
  if HasFocus<>newHasFocus then {if we have went to a different focus state by the mouse movement}
  begin
    HasFocus:=newHasFocus;  {show our new focus state}
    needRepaint:=FFocus;  {if should accept show focus, we should repaint}
  end;
  if MouseCaught then {if the mouse was originally clicked on the button}
  begin
    if not FAllowDown or (OrigState<>stDown) then {if we don't allow the button to be down (or it isn't down, anyway)}
    begin
      if HasFocus then  {update the state of the button, based on whether the mouse is inside the button or not}
        newState:=stDown  {if the mouse is inside, put the button down}
      else                  {if the mouse is outside}
        newState:=stUp;       {bring the button up}
      if newState<>FState then  {if the state has changed}
      begin
        FState:=newState; {change the state permanently}
        needRepaint:=True;  {show that we should repaint the button}
      end;
    end;
  end
  else  {if the mouse is just moving over the control, and wasn't originally click in the control}
    MouseCapture:=FFocus and HasFocus; {if we should show focus, and we have the focus, send messages to the control so we'll know when we lose focus}
  if needRepaint then {if we need to repaint}
  begin
    Realign;  {realign the controls in the button}
    Invalidate;  {invalidate the button for repainting}
  end;
end;

procedure TRichButton.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); {modified from TTransBitmap}
var
  overControl:Boolean;
  rect:TRect;
begin
  inherited MouseUp(Button, Shift, X, Y); {call the inherited MouseUp() procedure}
  if MouseCaught then {if the mouse was originally clicked over the control}
  begin
    MouseCaught:=False; {show that the mouse is no longer caught}
    rect:=GetWorkRect;  {find the working area of the button}
    overControl:=(X>=rect.Left) and (x<=rect.Right) and (y>=rect.Top) and (y<=rect.Bottom); {see if the mouse is still inside the button}
    if FAllowDown and overControl then  {if we should allow the button to stay down, and the mouse button was released over the control}
    begin
      if OrigState=stDown then FState:=stUp else FState:=stDown;  {set the new state to the opposite of what it was originally}
    end
    else  {if this is a typical "non-stay-down" button}
      FState:=stUp; {the button goes up after the mouse is released}
    HasFocus:=False;  {show that we no longer have the focus}
    Realign;  {realign the controls in the button}
    Invalidate;  {invalidate the button for repainting}
    if overControl then Click;  {if they released the mouse button over the control, call the OnClick() event}
  end;
end;

function TRichButton.GetWorkRect:TRect;  {modified from TTransBitmap}
var
  delta:Integer;  {number of units to remove from left, right, top, and bottom to get the work rectangle}
begin
  delta:=FBorderWidth;  {always start with the border width}
  if FFocus then    {if we show the focus when the mouse moves over the button}
    Inc(delta, FFocusWidth);    {allow for the focus rectangle}
  if FBevelOuter<>bvNone then {if we have an outer bevel}
    Inc(delta, FBevelWidth);  {take the outer bevel away from our work rectangle}
  if FBevelInner<>bvNone then {if we have an inner bevel}
    Inc(delta, FBevelWidth);  {take the inner bevel away from our work rectangle}
  Result:=GetClientRect;  {get the coordinates of the control}
  InflateRect(Result, -delta, -delta);  {remove the non-work areas from our work rectangle}
end;

procedure TRichButton.AlignControls(AControl: TControl; var Rect: TRect);
var
  BevelSize: Integer;
begin
  BevelSize := BorderWidth;
  if BevelOuter <> bvNone then Inc(BevelSize, BevelWidth);
  if BevelInner <> bvNone then Inc(BevelSize, BevelWidth);
  InflateRect(Rect, -BevelSize, -BevelSize);
  inherited AlignControls(AControl, Rect);
end;

procedure TRichButton.ReadData(Reader: TReader);
begin
  ShowHint := Reader.ReadBoolean;
end;

procedure TRichButton.Paint;
var
  Rect, WorkRect:TRect;
  TopColor, BottomColor: TColor;
  FontHeight: Integer;
  procedure AdjustColors(Bevel: TPanelBevel); {routine supplied by Borland, optimized in TTransBitmap, further optimized in TRichButton}
  begin
    if (Bevel=bvLowered) or (FState=stDown) then {if the bevel is lowered and the button is up}
    begin
      TopColor:=clBtnShadow;          {show the top and bottom colors normally}
      BottomColor:=clBtnHighlight;
    end
    else      {if the bevel is not lowered}
    begin
      TopColor:=clBtnHighlight;  {switch the top and bottom colors}
      BottomColor:=clBtnShadow;
    end;
  end;
begin
  Rect:=GetClientRect;  {get the rectangle that outlines the control}
  WorkRect:=GetWorkRect;  {get the working area}
  if FState=stDown then {if the button is down}
  begin
    OffsetRect(WorkRect, 2, 1);     //move the text down and to the right to similate a click
    InflateRect(WorkRect, -2, -1);  //
  end;
  RichEdit.BoundsRect:=WorkRect;  {make sure the RTF control is positioned correctly}
  RichEdit.Refresh; {make sure that the RTF control is updated (we only need this if the button has been hidden; there should be a way to make this more efficient)}
  RichEdit.Invalidate; {make sure that the RTF control is updated (we only need this if the button has been hidden; there should be a way to make this more efficient)}
  RichEdit.Update; {we need to call both Invalidate and Update; Refresh apparently does *not* do this inside the Paint procedure}
  if FFocus then  {if we should show the focus when the mouse is over the control}
  begin
    if HasFocus then  {if we do actually have the focus}
      Frame3D(Canvas, Rect, FFocusColor, FFocusColor, FFocusWidth)  {show the focus}
    else  {if the mouse isn't over the button}
      Frame3D(Canvas, Rect, clBtnFace, clBtnFace, FFocusWidth); {show the focus outline normally}
  end;
  if BevelOuter<>bvNone then  {if we have an outer bevel}
  begin
    AdjustColors(BevelOuter); {determine the colors to use for the outer bevel}
    Frame3D(Canvas, Rect, TopColor, BottomColor, BevelWidth); {draw the outer bevel}
  end;
  Frame3D(Canvas, Rect, Color, Color, BorderWidth); {draw the border}
  if BevelInner<>bvNone then  {if we have an inner bevel}
  begin
    AdjustColors(BevelInner); {determine the colors to use for the outer bevel}
    Frame3D(Canvas, Rect, TopColor, BottomColor, BevelWidth); {draw the inner bevel}
  end;
end;

procedure TRichButton.SetAlignment(Value: TAlignment);
begin
  FAlignment := Value;
  Invalidate;
end;

procedure TRichButton.SetAllowDown(Value:Boolean);  {modified from TTransBitmap}
begin
  if FallowDown<>Value then {if the value is really being changed}
  begin
    FAllowDown:=Value;  {update the variable}
    if (not FAllowDown) and (FState=stDown) then  {if we shouldn't allow the button to be down, but it is}
    begin
      FState:=stUp; {bring the button up}
      Realign;  {realign the controls in the button}
      Invalidate;  {invalidate the button for repainting}
    end;
  end;
end;

procedure TRichButton.SetBevelInner(Value: TPanelBevel);
begin
  FBevelInner := Value;
  Realign;
  Invalidate;
end;

procedure TRichButton.SetBevelOuter(Value: TPanelBevel);
begin
  FBevelOuter := Value;
  Realign;
  Invalidate;
end;

procedure TRichButton.SetBevelWidth(Value: TBevelWidth);
begin
  FBevelWidth := Value;
  Realign;
  Invalidate;
end;

procedure TRichButton.SetBorderWidth(Value: TBorderWidth);
begin
  FBorderWidth := Value;
  Realign;
  Invalidate;
end;

procedure TRichButton.SetBorderStyle(Value: TBorderStyle);
begin
  if FBorderStyle <> Value then
  begin
    FBorderStyle := Value;
    RecreateWnd;
  end;
end;

procedure TRichButton.SetFocus(Value:Boolean);  {modified from TTransBitmap}
begin
  if FFocus<>Value then {if the value is really being changed}
  begin
    FFocus:=Value;  {set the new value}
    Realign;  {realign the controls in the button}
    Invalidate;  {invalidate the button for repainting}
  end;
end;

procedure TRichButton.SetFocusColor(Value:TColor);  {modified from TTransBitmap}
begin
  if FFocusColor<>Value then {if the value is really being changed}
  begin
    FFocusColor:=Value; {change the value}
    Invalidate;  {invalidate the button for repainting}
  end;
end;

procedure TRichButton.SetFocusWidth(Value:TWidth);  {modified from TTransBitmap}
begin
  if FFocusWidth<>Value then {if the value is really being changed}
  begin
    FFocusWidth:=Value; {change the value}
    Realign;  {realign the controls in the button}
    Invalidate;  {invalidate the button for repainting}
  end;
end;

function TRichButton.GetLines:TStrings;
begin
  Result:=RichEdit.Lines;  {get the richedit's lines}
end;

procedure TRichButton.SetLines(Value:TStrings);
begin
  RichEdit.Lines:=Value;  {set the richedit's lines}
end;

procedure TRichButton.SetState(Value:TRichButtonState); {modified from TTransBitmap}
begin
  if FState<>Value then {if the value is really being changed}
  begin
    if (Value=stDown) and (not FAllowDown) then {if they want the button down, but we don't allow it}
    begin
      if FState=stUp then {if the button is up, disable it, otherwise, bring it up}
        FState:=stDisabled
      else
        FState:=stUp;
    end
    else  {if they want to bring the button up, or they want to put it down and we allow it (or they want to disable it)}
      FState:=Value;  {actually change the state of the button}
    Realign;  {realign the controls in the button}
    Invalidate;  {invalidate the button for repainting}
  end;
end;

end.