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;
}
