So, what is a scrollbar? In short it consists of 4 parts: an up arrow, a region to move a handle, the handle, and a down arrow. The hard part is not to draw or setup the actual control, but to translate between screen coordinates and the values the scrollbar represents. The scrollbar might represent 100000 items of “something”, but since you have (for instance) only 400 pixels to represent that sum – you have to translate between the visual and the abstract.
The math…
function TW3CustomScrollBar.calcSizeOfHandle:Integer; var mTemp: Integer; Begin mTemp:=TInteger.PercentOfValue(PageSize,Total); result:=round( mTemp * getArea / 100); end; function TW3CustomScrollBar.PositionToPixelOffset(aPosition:Integer):Integer; var mTemp: Integer; Begin mTemp:=TInteger.PercentOfValue(aPosition,Total); result:=round( mTemp * getArea / 100); end; function TW3CustomScrollBar.PixelOffsetToPosition(aPxPos:Integer):Integer; var mTemp: Integer; Begin mTemp:=TInteger.PercentOfValue(aPxPos,getarea); result:=trunc( (mTemp * Total) / 100 ); end;
The baseclass…
type TW3ScrollbarLowerBtn = Class(TW3CustomControl); TW3ScrollbarHigherBtn = class(TW3CustomControl); TW3ScrollbarHandle = class(TW3CustomControl); TW3CustomScrollBar = Class(TW3CustomControl) private FUpBtn: TW3ScrollbarLowerBtn; FDownBtn: TW3ScrollbarHigherBtn; FHandle: TW3ScrollbarHandle; FTotal: Integer; FPageSize: Integer; FPosition: Integer; protected Procedure setTotal(aValue:Integer);virtual; procedure setPageSize(aValue:Integer);virtual; Procedure setPosition(aValue:Integer);virtual; Procedure setPositionNoCalc(aValue:Integer);virtual; procedure InitializeObject; override; procedure FinalizeObject; override; function calcSizeOfHandle:Integer; function PositionToPixelOffset(aPosition:Integer):Integer; function PixelOffsetToPosition(aPxPos:Integer):Integer; Procedure Recalculate;virtual;abstract; function getArea:Integer;virtual;abstract; public class function supportAdjustment:Boolean;override; Property MinButton:TW3ScrollbarLowerBtn read FUpBtn; property MaxButton:TW3ScrollbarHigherBtn read FDownBtn; Property DragHandle:TW3ScrollbarHandle read FHandle; published Property Total:Integer read FTotal write setTotal; Property PageSize:Integer read FPageSize write setPageSize; Property Position:Integer read FPosition write setPosition; End;
The advantage of having a custom control like that is that you can style them. Smart automatically maps the classnames to the styles in the style sheet. So if you define a CSS class called TW3ScrollbarLowerBtn, it will be automatically applied to that control. This means you can alter the scrollbars with your own global theme if you have some css skills.
The implementation of the baseclass…
//########################################################################### // TW3CustomScrollBar //########################################################################### procedure TW3CustomScrollBar.InitializeObject; Begin inherited; FUpBtn:=TW3ScrollbarLowerBtn.Create(self); FDownBtn:=TW3ScrollbarHigherBtn.Create(self); FHandle:=TW3ScrollbarHandle.Create(self); end; procedure TW3CustomScrollBar.FinalizeObject; Begin FUpBtn.free; FDownBtn.free; FHandle.free; inherited; end; class function TW3CustomScrollBar.supportAdjustment:Boolean; Begin result:=true; end; Procedure TW3CustomScrollBar.setTotal(aValue:Integer); Begin aValue:=TInteger.EnsureRange(aValue,0,MAX_INT); if aValue<>FTotal then Begin Ftotal:=aValue; if FPageSize>FTotal then FPageSize:=FTotal; if FPosition>FTotal-FPageSize then Begin if (FTotal-FPageSize) FPosition:=0 else FPosition:=FTotal-FPageSize; end; ReCalculate; LayoutChildren; end; end; procedure TW3CustomScrollBar.setPageSize(aValue:Integer); Begin aValue:=TInteger.EnsureRange(aValue,0,FTotal); if aValue<>FPageSize then Begin FPageSize:=aValue; if FTotal>0 then Begin ReCalculate; LayoutChildren; end; end; end; Procedure TW3CustomScrollBar.setPosition(aValue:Integer); Begin aValue:=TInteger.EnsureRange(aValue,0,FTotal-FPageSize); if aValue<>FPosition then Begin FPosition:=aValue; if FTotal>0 then Begin ReCalculate; LayoutChildren; end; end; end; Procedure TW3CustomScrollBar.setPositionNoCalc(aValue:Integer); Begin aValue:=TInteger.EnsureRange(aValue,0,FTotal-FPageSize); if aValue<>FPosition then FPosition:=aValue; end; function TW3CustomScrollBar.calcSizeOfHandle:Integer; var mTemp: Integer; Begin mTemp:=TInteger.PercentOfValue(PageSize,Total); result:=round( mTemp * getArea / 100); end; function TW3CustomScrollBar.PositionToPixelOffset(aPosition:Integer):Integer; var mTemp: Integer; Begin mTemp:=TInteger.PercentOfValue(aPosition,Total); result:=round( mTemp * getArea / 100); end; function TW3CustomScrollBar.PixelOffsetToPosition(aPxPos:Integer):Integer; var mTemp: Integer; Begin mTemp:=TInteger.PercentOfValue(aPxPos,getarea); result:=trunc( (mTemp * Total) / 100 ); end;
The vertical bar
Deriving from this skeleton class, we can now easily implement both horizontal and vertical scrollbars. Let’s start with the vertical. This one is a bit more complex since we need to dig into JavaScript events directly. I did this not to hijack the published events (like mousedown etc) making sure these can still be used.
Interface:
TW3VerticalScrollbar = Class(TW3CustomScrollBar) private FDragSize: Integer; FDragPos: Integer; FMoving: Boolean; FEntry: Integer; procedure jsmousedown(eventObj:JMouseEvent); procedure jsmousemove(eventObj:JMouseEvent); procedure jsmouseup(eventObj:JMouseEvent); Procedure doMouseDown(button:TMouseButton; shiftState:TShiftState;x,y:Integer); procedure doMouseUp(button:TMouseButton; shiftState:TShiftState;x,y:Integer); procedure doMouseMove(shiftState:TShiftState;x,y:Integer); protected procedure InitializeObject; override; procedure Resize;Override; function getArea:Integer;override; Procedure Recalculate;override; End;
Implementation:
procedure TW3VerticalScrollbar.InitializeObject; Begin inherited; (* minbutton.color:=clGreen; maxButton.color:=clCyan; draghandle.Color:=clRed; *) (* Avoid using up the exposed event handlers *) handle.addEventListener('mousedown',@jsmousedown,false); handle.addEventListener('mousemove',@jsmousemove,false); handle.addEventListener('mouseup',@jsmouseup,false); end; procedure TW3VerticalScrollbar.jsmousedown(eventObj:JMouseEvent); Begin eventObj.preventDefault(); var sr := ScreenRect; var shiftState := TShiftState.Current; shiftState.MouseButtons := shiftState.MouseButtons or (1 shl eventObj.button); shiftState.MouseEvent := eventObj; doMouseDown(eventObj.button, shiftState, eventObj.clientX-sr.Left, eventObj.clientY-sr.Top); end; procedure TW3VerticalScrollbar.jsmousemove(eventObj:JMouseEvent); Begin eventObj.preventDefault(); var sr := ScreenRect; var shiftState := TShiftState.Current; shiftState.MouseEvent := eventObj; doMouseMove(shiftState, eventObj.clientX-sr.Left, eventObj.clientY-sr.Top); end; procedure TW3VerticalScrollbar.jsmouseup(eventObj:JMouseEvent); Begin eventObj.preventDefault(); var sr := ScreenRect; var shiftState := TShiftState.Current; shiftState.MouseButtons := shiftState.MouseButtons and not (1 shl eventObj.button); shiftState.MouseEvent := eventObj; doMouseUp(eventObj.button, shiftState, eventObj.clientX-sr.Left, eventObj.clientY-sr.Top); end; function TW3VerticalScrollbar.getArea:Integer; Begin result:=Height; result:=MaxButton.top - minbutton.boundsRect.bottom; exit; if minButton.Visible then dec(result,MinButton.Height); if maxButton.Visible then dec(result,MaxButton.height); end; Procedure TW3VerticalScrollbar.doMouseDown(button:TMouseButton; shiftState:TShiftState;x,y:Integer); Begin if button=mbLeft then begin if dragHandle.BoundsRect.ContainsPos(x,y) then begin FEntry:=y - dragHandle.top; FMoving:=True; end; end; end; procedure TW3VerticalScrollbar.doMouseMove(shiftState:TShiftState; x,y:Integer); var mNewPos: Integer; dy: Integer; Begin if FMoving then Begin (* take offset on draghandle into account *) dy:=y - FEntry; (* position draghandle *) draghandle.top:=TInteger.EnsureRange(dy, minButton.top + minButton.height, maxButton.top - FDragSize); (* Update position based on draghandle *) mNewPos:=PixelOffsetToPosition(draghandle.top-MinButton.BoundsRect.Bottom); setpositionNoCalc(mNewPos); end; end; procedure TW3VerticalScrollbar.doMouseUp(button:TMouseButton; shiftState:TShiftState;x,y:Integer); Begin if FMoving then Begin FMoving:=False; setPosition(PixelOffsetToPosition (draghandle.top-MinButton.BoundsRect.Bottom)); end; end; procedure TW3VerticalScrollbar.Resize; var mTop: Integer; Begin inherited; MinButton.SetBounds(0,0,width,width); MaxButton.setBounds(0,(height-width),width,width); Recalculate; mTop:=MinButton.top + minButton.Height + FDragPos; DragHandle.SetBounds(2,mTop,width-4,FDragSize + Border.getVSpace); end; Procedure TW3VerticalScrollbar.Recalculate; Begin FDragSize:=calcSizeOfHandle; FDragPos:=PositionToPixelOffset(Position); end;
The style:
.TW3ScrollbarLowerBtn, .TW3ScrollbarHigherBtn { border: 2px ridge rgba(0,0,0,0.3); background-color: #FFFFFF; overflow: hidden; border-radius: 25px; -webkit-border-radius: 25px; -moz-border-radius: 25px; -ms-border-radius: 25px; -o-border-radius: 25px; } .TW3ScrollbarHandle { border: 2px ridge rgba(0,0,0,0.3); background-color: #FFFFFF; overflow: hidden; border-radius: 25px; -webkit-border-radius: 25px; -moz-border-radius: 25px; -ms-border-radius: 25px; -o-border-radius: 25px; } .TW3VerticalScrollbar { border: 2px ridge rgba(0,0,0,0.3); background-color: #FFFFFF; overflow: hidden; -webkit-touch-callout: none; -webkit-user-select: none; border-radius: 25px; -webkit-border-radius: 25px; -moz-border-radius: 25px; -ms-border-radius: 25px; -o-border-radius: 25px; }