unit PCFormRoll;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  EError = class(Exception);

  TRollerMode = ( mdRollDown, mdRollUp );
  TRollerNotifyEvent = procedure ( Sender: TObject; mode: TRollerMode ) of object;

  TPCRoller = class(TComponent)
  private
    { Private declarations }
    DefWinProc: TFarProc;
    DefWinProcInstance: Pointer;
    FEnabled: Boolean;
    FOnRoller: TRollerNotifyEvent;
//    FOnFormDestroy: TNotifyEvent;
    FRoller: Boolean;
    FVisible: Boolean;
    FRemainHeight: Integer;

    ButtonRect: TRect;
    DrawPushed: Boolean;
    Pushed: Boolean;
    RestoreWndProc: Boolean;
    rgn: HRGN;
    OwnerHandle: THandle;

    procedure CalcArea;
    function  GetShortHeight: Integer;
    function  HookMainWndProc( var Msg: TMessage ): Boolean;
    procedure HookWndProc;
    procedure UnHookWndProc;
//    procedure OnRollerDestroy( Sender: TObject );
    procedure PaintRollerButton;
    procedure Rollup;
    procedure SetEnabled( Val: Boolean );
    procedure SetRoller( Val: Boolean );
    procedure SetVisible( Val: Boolean );
    procedure WinProc(var Msg: TMessage);
    procedure SetRemainHeight( Val: Integer );

  public
    { Public declarations }
    constructor Create(Owner: TComponent); override;
    destructor Destroy; override;
  published
    { Published declarations }
    property Enabled: Boolean read FEnabled write SetEnabled;
    property RemainHeight: Integer read FRemainHeight write SetRemainHeight;
    property Roller: Boolean read FRoller write SetRoller default False;
    property Visible: Boolean read FVisible write SetVisible default True;
    property OnRoller: TRollerNotifyEvent read FOnRoller write FOnRoller;
  end;

var
  si: UINT;

implementation

const
  WM_ROLLREDRAW         = WM_USER + 2000;
  WM_PAINTEDGE          = WM_USER + 2001;
  WM_RECREATE           = WM_USER + 2002;
  WM_ROLLER_BUTTON_DOWN = WM_USER + 2003;


constructor TPCRoller.Create(Owner: TComponent);
begin
  inherited;
  FRoller := False;
  FVisible := True;
  DrawPushed := False;
  Pushed := False;
  FRemainHeight := 0;
//  if not (csDesigning in ComponentState) then
//  begin
    Application.HookMainWindow( HookMainWndProc );
//    FOnFormDestroy := TForm(Owner).OnDestroy;
//    TForm(Owner).OnDestroy := OnRollerDestroy;
//  end;
  HookWndProc;
end;

destructor TPCRoller.Destroy;
//var
//  proc: TNotifyEvent;
begin
//  if not (csDesigning in ComponentState) then
    Application.UnhookMainWindow( HookMainWndProc );
//  proc := OnRollerDestroy;
//  if Assigned(Owner) and (@proc = @TForm(Owner).OnDestroy) then
//    TForm(Owner).OnDestroy := FOnFormDestroy;
  UnHookWndProc;
  inherited;
  if csDesigning in ComponentState then
    SetWindowPos( OwnerHandle, 0, 0, 0, 0, 0, SWP_FRAMECHANGED or SWP_DRAWFRAME or
                  SWP_NOZORDER or SWP_NOMOVE or SWP_NOSIZE or SWP_NOACTIVATE );
//    ReDrawWindow( OwnerHandle, nil, 0, RDW_FRAME or RDW_INVALIDATE or RDW_UPDATENOW );
end;

procedure TPCRoller.HookWndProc;
begin
  OwnerHandle := TForm(Owner).Handle;
  rgn := 0;
  if not (Owner is TForm) then
    raise EError.Create('Class of the owner is not a TForm!');
  with TForm(Owner) do
//  if not (csDesigning in ComponentState) then
  begin    
    DefWinProcInstance := MakeObjectInstance(WinProc);
    DefWinProc := Pointer(SetWindowLong(OwnerHandle, GWL_WNDPROC, Longint(DefWinProcInstance)));
    RestoreWndProc := False;
    CalcArea;
  end;
end;

procedure TPCRoller.UnHookWndProc;
begin
  if RestoreWndProc then Exit;
  RestoreWndProc := True;
//  if not (csDesigning in ComponentState) then
//  begin
    SetWindowLong( OwnerHandle, GWL_WNDPROC, Longint(DefWinProc) );
    FreeObjectInstance( DefWinProcInstance );
//  end;
  if rgn <> 0 then
    DeleteObject( rgn );
  rgn := 0;
end;

function TPCRoller.HookMainWndProc( var Msg: TMessage ): Boolean;
begin
  Result := False;
  if Msg.Msg = WM_RECREATE then
  begin
    if Msg.lParam <> longint(Self) then exit;
    HookWndProc;
//    UpdateCaption;
  end;
end;
(* If the TPCRoller is created dynamically, it's destructor don't *)
(* be called automatically when the owner form is free. So must be *)
(* triggered the UnHookWndProc by this event .                     *)
(*
procedure TPCRoller.OnRollerDestroy( Sender: TObject );
begin
  SetWindowLong(TForm(Owner).Handle, GWL_WNDPROC, Longint(DefWinProc));
  FreeObjectInstance(DefWinProcInstance);
  RestoreWndProc := True;
  if Assigned( FOnFormDestroy ) then
    FOnFormDestroy( Sender );
end;
  *)
procedure TPCRoller.CalcArea;
var
  x1, y1, x2, y2: Integer;
  Icons: TBorderIcons;
  Style: TFormBorderStyle;
  AWidth: Integer;
  wr: TRect;
//  Style, ExStyle: UINT;
begin
//  Style := GetWindowLong( TForm(Owner).Handle, GWL_STYLE );
//  ExStyle := GetWindowLong( TForm(Owner).Handle, GWL_EXSTYLE );
  with TForm(Owner) do
  begin
    GetWindowRect( OwnerHandle, wr );
    AWidth := wr.Right - wr.Left;
    if csDesigning in ComponentState then
    begin
      Icons := [biSystemMenu, biMinimize, biMaximize];
      Style := bsSizeable;
    end
    else
    begin
      Icons := BorderIcons;
      Style := BorderStyle;
    end;
  end;
  if Style in [bsSizeToolWin, bsToolWindow] then
  begin
    if Style = bsToolWindow then
      x2 := GetSystemMetrics(SM_CXFIXEDFRAME) + 2
    else
      x2 := GetSystemMetrics(SM_CXSIZEFRAME) + 2;
    if biSystemMenu in Icons then
      Inc(x2, GetSystemMetrics(SM_CXSMSIZE) + 2);
    if Style = bsToolWindow then
      y1 := GetSystemMetrics(SM_CYFIXEDFRAME) + 2
    else
      y1 := GetSystemMetrics(SM_CYSIZEFRAME) + 2;
    y2 := y1 + GetSystemMetrics(SM_CYSMSIZE) - 4;
    x2 := AWidth - x2;
    x1 := x2 - GetSystemMetrics(SM_CXSMSIZE) + 2;
  end
  else
  begin
    if Style in [bsSingle, bsSizeable, bsDialog] then
    begin
      if Style = bsSingle then
        x2 := GetSystemMetrics(SM_CYFIXEDFRAME) + 2
      else
        x2 := GetSystemMetrics(SM_CXSIZEFRAME) + 2;
      if biSystemMenu in Icons then
      begin
        Inc(x2, GetSystemMetrics(SM_CXSIZE) + 2);
        if (Style <> bsDialog) and (Icons * [biMinimize, biMaximize] <> []) then
          Inc(x2, GetSystemMetrics(SM_CXSIZE) * 2 - 6)
        else if biHelp in Icons then
          Inc(x2, GetSystemMetrics(SM_CXSIZE) - 4);
      end;
      if Style in [bsSingle, bsDialog] then
        y1 := GetSystemMetrics(SM_CYFIXEDFRAME) + 2
      else
        y1 := GetSystemMetrics(SM_CYSIZEFRAME) + 2;
      y2 := y1 + GetSystemMetrics(SM_CYSIZE) - 4;
      x2 := AWidth - x2;
      x1 := x2 - GetSystemMetrics(SM_CXSIZE) + 2;
    end;
  end;
  SetRect( ButtonRect, x1 - AWidth, y1, x2 - AWidth, y2 );
end;

procedure TPCRoller.PaintRollerButton;
var
  h, dc: HDC;
  st: UINT;
  p: array[0..2] of TPoint;
  b: HBRUSH;
  r: HRGN;
  rect, OldRect: TRect;
  w: Integer;
  OldBmp, Bmp: HBitmap;

  procedure DrawTriangle( Up: Boolean; dd: Integer; co: Integer );
  var
    pen: HPEN;
    d, x1, x2, y1, y2: Integer;
  begin
    if DrawPushed then
      d := 1 + dd
    else
      d := 0 + dd;
    with rect do
    begin
      x1 := Left + 1 + d;
      x2 := Right - 3 + d;
      y1 := Top + d;
      y2 := Bottom + d;
    end;
    p[0].x := x1 + 3;
    p[1].x := ((x1 + x2) shr 1);
    p[2].x := p[1].x + p[1].x - p[0].x;
//    p[2].x := x2 - 3;
    if Up then
    begin
      p[0].y := ((y1 + y2) shr 1) - 2;
      p[1].y := y1 + 2;
      p[2].y := p[0].y;
    end
    else
    begin
      p[0].y := ((y1 + y2) shr 1);
      p[1].y := p[0].y + p[1].x - p[0].x;
//      p[1].y := y2 - 4;
      p[2].y := p[0].y;
    end;
    case co of
      0:
      begin
        pen := SelectObject( dc, GetStockObject( BLACK_PEN ) );
        b := SelectObject( dc, GetStockObject( BLACK_BRUSH ) );
      end;
      1:
      begin
        pen := SelectObject( dc, GetStockObject( WHITE_PEN ) );
        b := SelectObject( dc, GetStockObject( WHITE_BRUSH ) );
      end;
      else
      begin
        pen := SelectObject( dc, CreatePen( PS_SOLID, 0, GetSysColor( COLOR_BTNSHADOW ) ) );
        b := SelectObject( dc, GetStockObject( DKGRAY_BRUSH ) );
      end;
    end;
    if (Up and not FRoller) or (not Up and FRoller) then
      PolyGon( dc, p, 3 )
    else
    begin
      Inc( p[2].x );
      if Up then
        Inc( p[2].y )
      else
        Dec( p[2].y );
      PolyLine( dc, p, 3 );
      Inc( p[0].x );
      Dec( p[2].x );
      if Up then
        Inc( p[1].y )
      else
        Dec( p[1].y );
      PolyLine( dc, p, 3 );
    end;
    SelectObject( dc, b );
    if co = 2 then
      DeleteObject( SelectObject( dc, pen ) )
    else
      SelectObject( dc, pen );
  end;

begin
//  if not (csDesigning in ComponentState) then
    with TForm(Owner) do
      if not (BorderStyle = bsNone) then
      begin
        GetWindowRect( OwnerHandle, rect );
        w := rect.Right - rect.Left;
        rect.Left := ButtonRect.Left + w;
        rect.Right := ButtonRect.Right + w;
        rect.Top := ButtonRect.Top;
        rect.Bottom := ButtonRect.Bottom;
        if FVisible then
        begin
          h := GetWindowDC( OwnerHandle );
          try
            if h = 0 then Exit;
            dc := CreateCompatibleDC( h );
            try
              if dc = 0 then
                Exit;
              Bmp := CreateCompatibleBitmap( h, rect.Right - rect.Left, rect.Bottom - rect.Top );
              If Bmp = 0 then
                Exit;
              OldRect := rect;
              OffsetRect( rect, -rect.Left, -rect.Top );
              OldBmp := SelectObject( dc, Bmp );
              try
                if DrawPushed then
                  st := DFCS_PUSHED
                else
                  st := 0;
                DrawFrameControl( dc, rect, DFC_BUTTON, DFCS_BUTTONPUSH or st );
                if FEnabled then
                begin
                  DrawTriangle( True, 0, 0 );
                  DrawTriangle( False, 0, 0 );
                end
                else
                begin
                  DrawTriangle( True, 1, 1 );
                  DrawTriangle( False, 1, 1 );
                  DrawTriangle( True, 0, 2 );
                  DrawTriangle( False, 0, 2 );
                end;
                BitBlt( h, OldRect.Left, OldRect.Top, OldRect.Right - OldRect.Left,
                        OldRect.Bottom - OldRect.Top, dc, 0, 0, SRCCOPY);
              finally
                SelectObject( dc, OldBmp );
              end;
            finally
              DeleteDC( dc );
            end;
          finally
            ReleaseDC( OwnerHandle, h );
          end;
        end
        else
        begin
          r := CreateRectRgn(
            (* Get the most width of the border *)
            rect.Left - GetSystemMetrics(SM_CXSIZEFRAME),
            0,
            rect.Right,
            rect.Bottom );
          SendMessage( OwnerHandle, WM_NCPAINT, r, 0 );
          DeleteObject( r );
        end;
      end;
end;

procedure TPCRoller.SetEnabled( Val: Boolean );
begin
  if Val <> FEnabled then
  begin
    FEnabled := Val;
    PaintRollerButton;
  end;
end;

procedure TPCRoller.SetRemainHeight( Val: Integer );
begin
  if FRemainHeight <> Val then
  begin
    FRemainHeight := Val;
    if FRoller then
      RollUp;
  end;
end;

function TPCRoller.GetShortHeight: Integer;
begin
  with TForm(Owner) do
  begin
    if BorderStyle in [bsSingle, bsDialog, bsToolWindow] then
      Result := GetSystemMetrics( SM_CYCAPTION ) +
        GetSystemMetrics(SM_CYFIXEDFRAME) + 3
    else
      Result := GetSystemMetrics( SM_CYCAPTION ) +
        GetSystemMetrics(SM_CYSIZEFRAME) + 3;
  end;
  Inc( Result, FRemainHeight );
end;


procedure TPCRoller.Rollup;
var
  r: TRect;
  g: HRGN;
begin
  begin
    g := rgn;
    GetWindowRect( OwnerHandle, r );
    rgn := CreateRectRgn( 0, 0, r.Right - r.Left, GetShortHeight );
    SetWindowRgn( OwnerHandle, rgn, True );
    if g <> 0 then
      DeleteObject( g );
  end;
end;

procedure TPCRoller.SetRoller( Val: Boolean );
var
  r: TRect;
  g: HRGN;
begin
  if FEnabled and (Val <> FRoller) then
  begin
    FRoller := Val;
    if Val then
    begin
      Rollup;
      if Assigned( FOnRoller ) then FOnRoller( Self, mdRollUp );
    end
    else
    begin
      GetWindowRect( OwnerHandle, r );
      g := CreateRectRgn( 0, 0, r.Right - r.Left, GetShortHeight - 3 );
      SetWindowRgn( OwnerHandle, g, True );

      SetWindowRgn( OwnerHandle, 0, True );
      DeleteObject( g );
      if rgn <> 0 then
      begin
        DeleteObject( rgn );
        rgn := 0;
      end;
      if Assigned( FOnRoller ) then FOnRoller( Self, mdRollDown );
    end;
    PaintRollerButton;
  end;
end;

procedure TPCRoller.SetVisible( Val: Boolean );
begin
  if Val <> FVisible then
  begin
    FVisible := Val;
    CalcArea;
    if Val then
      PaintRollerButton;
  end;
end;

procedure TPCRoller.WinProc(var Msg: TMessage);
var
  h: HRGN;
  br, wr: TRect;
  dc: HDC;
  pen: HPEN;
  w: Integer;

  procedure Default;
  begin
    with Msg do
      Result := CallWindowProc(DefWinProc, OwnerHandle, Msg, wParam, lParam);
  end;

  function InArea( wr: TRect; InClient: Boolean ): Boolean;
  var
    p, pp: TPoint;
  begin
    p.x := Msg.lParamLo;
    p.y := Smallint(Msg.lParamHi);
    with TForm(Owner) do
    begin
      if InClient then
        p := ClientToScreen( p );
      Dec( p.x, Left );
      Dec( p.y, Top );
      if FormStyle = fsMDIChild then
      begin
        pp.x := 0;
        pp.y := 0;
        Windows.ClientToScreen( Application.MainForm.ClientHandle, pp );
        Dec( p.x, pp.x );
        Dec( p.y, pp.y );
      end;
    end;
    w := wr.Right - wr.Left;
    SetRect( br, ButtonRect.Left + w, ButtonRect.Top, ButtonRect.Right + w, ButtonRect.Bottom );
    Result := PtInRect( br, p );
  end;

begin
  if not FVisible then
    Default
  else
    case Msg.Msg of
      WM_SIZE://, WM_WINDOWPOSCHANGED:
      begin
        Default;
        CalcArea;
        if FRoller then
          RollUp;
      end;
      WM_MOUSEMOVE:
      begin
        if Pushed then
        begin
          GetWindowRect( OwnerHandle, wr );
          if not InArea( wr, True ) then
          begin
            if DrawPushed then
            begin
              DrawPushed := False;
              if FEnabled then
                PaintRollerButton;
            end;
          end
          else
          begin
            if not DrawPushed then
            begin
              DrawPushed := True;
              if FEnabled then
                PaintRollerButton;
            end;
          end;
          Msg.Result := 1;
        end
        else
          Default;
      end;
      WM_LBUTTONUP, WM_LBUTTONDBLCLK:
      begin
        DrawPushed := False;
        if Pushed then
        begin
          GetWindowRect( OwnerHandle, wr );
          if InArea( wr, True ) then
            Roller := not FRoller
          else if FEnabled then
            PaintRollerButton;
          Msg.Result := 1;
        end
        else
          Default;
        Pushed := False;
        ReleaseCapture;
      end;
      WM_ROLLER_BUTTON_DOWN:
      begin
        SetCapture( OwnerHandle );
        DrawPushed := True;
        Pushed := True;
        if FEnabled then
          PaintRollerButton;
        Msg.Result := 1;
      end;
      WM_NCLBUTTONDOWN, WM_NCLBUTTONDBLCLK:
      begin
        GetWindowRect( OwnerHandle, wr );
        if InArea( wr, False ) then
        begin
          SendMessage( OwnerHandle, WM_ROLLER_BUTTON_DOWN, 0, 0 );
          if Msg.Msg <> WM_NCLBUTTONDBLCLK then
            Default
          else
            Msg.Result := 1;
        end
        else
        begin
          Default;
          PaintRollerButton;
        end;
      end;
      WM_SETTINGCHANGE, WM_DISPLAYCHANGE(*, WM_WININICHANGE*):
      begin
        Default;
        CalcArea;
        if FRoller then
          Rollup;
      end;
      WM_NCACTIVATE:
      begin
        Default;
        if not TWMNCActivate(Msg).Active then
          PaintRollerButton;
      end;
      WM_NCPAINT:
      begin
        PaintRollerButton;
        dc := GetWindowDC( OwnerHandle );
        (* Don't get the width of window from the TForm's    *)
        (* property 'Width', because it is not being updated *)
        GetWindowRect( OwnerHandle, wr );
        w := wr.Right - wr.Left;
        SetRect(
          br,
          w + ButtonRect.Left,
          ButtonRect.Top,
          w + ButtonRect.Right,
          ButtonRect.Bottom );
        GetWindowRect( OwnerHandle, wr );
        h := CreateRectRgnIndirect( wr );
        try
          if SelectClipRgn( dc, Msg.wParam ) = ERROR then
            SelectClipRgn( dc, h );
          OffsetClipRgn( dc, -wr.Left, -wr.Top );
          ExcludeClipRect( dc, br.Left, br.Top, br.Right, br.Bottom );
          OffsetClipRgn( dc, wr.Left, wr.Top );
          GetClipRgn( dc, h );
          Msg.Result := CallWindowProc( DefWinProc, OwnerHandle, Msg.Msg, h, Msg.lParam )
        finally
          DeleteObject( h );
          ReleaseDC( OwnerHandle, dc );
        end;
        if TForm(Owner).FormStyle = fsMDIChild then
          PostMessage( OwnerHandle, WM_ROLLREDRAW, 0, 0 );
        if FRoller then
          PostMessage( OwnerHandle, WM_PAINTEDGE, 0, 0 );
      end;
      WM_ROLLREDRAW:
      begin
        Msg.Result := 1;
        PaintRollerButton;
      end;
      WM_SETCURSOR:
      begin
        Msg.Result := 1;
        case TWMSetCursor(Msg).HitTest of
          HTTOP,
          HTBOTTOM:      SetCursor(LoadCursor(0, MakeIntResource(IDC_SIZENS)));
          HTLEFT,
          HTRIGHT:       SetCursor(LoadCursor(0, MakeIntResource(IDC_SIZEWE)));
          HTTOPRIGHT,
          HTBOTTOMLEFT:  SetCursor(LoadCursor(0, MakeIntResource(IDC_SIZENESW)));
          HTTOPLEFT,
          HTBOTTOMRIGHT: SetCursor(LoadCursor(0, MakeIntResource(IDC_SIZENWSE)));
        else
          Default;
        end;
      end;
      WM_PAINTEDGE: (* Suggestion by Trav *)
      begin
        dc := GetWindowDC( OwnerHandle );
        GetWindowRect( OwnerHandle, wr );
        wr.Right := wr.Right - wr.Left;
        wr.Left := 0;
        wr.Top  := 0;
        wr.Bottom := GetShortHeight;
        pen := SelectObject( dc, CreatePen( PS_SOLID, 0, GetSysColor( COLOR_BTNFACE ) ) );
        MoveToEx( dc, 1, wr.Bottom - 3, nil );
        LineTo( dc, wr.Right - 1, wr.Bottom - 3 );
        DeleteObject( SelectObject( dc, pen ) );
        DrawEdge( dc, wr, EDGE_RAISED, BF_LEFT or BF_BOTTOM or BF_RIGHT );
        ReleaseDC( OwnerHandle, dc );
      end;
      WM_DESTROY:
      begin
        if not (csDestroying in ComponentState) then
        begin
          UnHookWndProc;
          PostMessage( Application.Handle, WM_RECREATE, 0, Longint(Self) );
        end;
        Default;
      end;
      else
        Default;
    end;
end;

end.
