I need a component for entering ranges. I was thinking along the lines of a trackbar with two markers. Are there "native Delphi" components that are meant for this purpose or that can simulate it easily?
-
Note: there is a bug in Themes: if you use themes/skins, the SelStart/SelEnd markers will not appear. – Gabriel Oct 02 '20 at 12:34
5 Answers
I got a few minutes over and wrote this:
unit RangeSelector;
interface
uses
SysUtils, Windows, Messages, Graphics, Classes, Controls, UxTheme, Dialogs;
type
TRangeSelectorState = (rssNormal, rssDisabled, rssThumb1Hover, rssThumb1Down, rssThumb2Hover, rssThumb2Down, rssBlockHover, rssBlockDown);
TRangeSelector = class(TCustomControl)
private
{ Private declarations }
FBuffer: TBitmap;
FMin,
FMax,
FSelStart,
FSelEnd: real;
FTrackPos,
FSelPos,
FThumbPos1,
FThumbPos2: TRect;
FState: TRangeSelectorState;
FDown: boolean;
FPrevX,
FPrevY: integer;
FOnChange: TNotifyEvent;
FDblClicked: Boolean;
FThumbSize: TSize;
procedure SwapBuffers;
procedure SetMin(Min: real);
procedure SetMax(Max: real);
procedure SetSelStart(SelStart: real);
procedure SetSelEnd(SelEnd: real);
function GetSelLength: real;
procedure UpdateMetrics;
procedure SetState(State: TRangeSelectorState);
function DeduceState(const X, Y: integer; const Down: boolean): TRangeSelectorState;
function BarWidth: integer; inline;
function LogicalToScreen(const LogicalPos: real): real;
procedure UpdateThumbMetrics;
protected
{ Protected declarations }
procedure Paint; override;
procedure WndProc(var Message: TMessage); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer; Y: Integer); override;
procedure MouseLeave(Sender: TObject);
procedure DblClick; override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
{ Published declarations }
property Anchors;
property Min: real read FMin write SetMin;
property Max: real read FMax write SetMax;
property SelStart: real read FSelStart write SetSelStart;
property SelEnd: real read FSelEnd write SetSelEnd;
property SelLength: real read GetSelLength;
property Enabled;
property Visible;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
end;
procedure Register;
implementation
uses Math;
procedure Register;
begin
RegisterComponents('Rejbrand 2009', [TRangeSelector]);
end;
function IsIntInInterval(x, xmin, xmax: integer): boolean; inline;
begin
IsIntInInterval := (xmin <= x) and (x <= xmax);
end;
function PointInRect(const X, Y: integer; const Rect: TRect): boolean; inline;
begin
PointInRect := IsIntInInterval(X, Rect.Left, Rect.Right) and
IsIntInInterval(Y, Rect.Top, Rect.Bottom);
end;
function IsRealInInterval(x, xmin, xmax: extended): boolean; inline;
begin
IsRealInInterval := (xmin <= x) and (x <= xmax);
end;
{ TRangeSelector }
function TRangeSelector.BarWidth: integer;
begin
result := Width - 2*FThumbSize.cx;
end;
constructor TRangeSelector.Create(AOwner: TComponent);
begin
inherited;
FBuffer := TBitmap.Create;
FMin := 0;
FMax := 100;
FSelStart := 20;
FSelEnd := 80;
FDown := false;
FPrevX := -1;
FPrevY := -1;
FDblClicked := false;
end;
procedure TRangeSelector.UpdateThumbMetrics;
var
theme: HTHEME;
const
DEFAULT_THUMB_SIZE: TSize = (cx: 12; cy: 20);
begin
FThumbSize := DEFAULT_THUMB_SIZE;
if UxTheme.UseThemes then
begin
theme := OpenThemeData(Handle, 'TRACKBAR');
if theme <> 0 then
try
GetThemePartSize(theme, FBuffer.Handle, TKP_THUMBTOP, TUTS_NORMAL, nil, TS_DRAW, FThumbSize);
finally
CloseThemeData(theme);
end;
end;
end;
destructor TRangeSelector.Destroy;
begin
FBuffer.Free;
inherited;
end;
function TRangeSelector.GetSelLength: real;
begin
result := FSelEnd - FSelStart;
end;
function TRangeSelector.LogicalToScreen(const LogicalPos: real): real;
begin
result := FThumbSize.cx + BarWidth * (LogicalPos - FMin) / (FMax - FMin)
end;
procedure TRangeSelector.DblClick;
var
str: string;
begin
FDblClicked := true;
case FState of
rssThumb1Hover, rssThumb1Down:
begin
str := FloatToStr(FSelStart);
if InputQuery('Initial value', 'Enter new initial value:', str) then
SetSelStart(StrToFloat(str));
end;
rssThumb2Hover, rssThumb2Down:
begin
str := FloatToStr(FSelEnd);
if InputQuery('Final value', 'Enter new final value:', str) then
SetSelEnd(StrToFloat(str));
end;
end;
end;
function TRangeSelector.DeduceState(const X, Y: integer; const Down: boolean): TRangeSelectorState;
begin
result := rssNormal;
if not Enabled then
Exit(rssDisabled);
if PointInRect(X, Y, FThumbPos1) then
if Down then
result := rssThumb1Down
else
result := rssThumb1Hover
else if PointInRect(X, Y, FThumbPos2) then
if Down then
result := rssThumb2Down
else
result := rssThumb2Hover
else if PointInRect(X, Y, FSelPos) then
if Down then
result := rssBlockDown
else
result := rssBlockHover;
end;
procedure TRangeSelector.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
if FDblClicked then
begin
FDblClicked := false;
Exit;
end;
FDown := Button = mbLeft;
SetState(DeduceState(X, Y, FDown));
end;
procedure TRangeSelector.MouseLeave(Sender: TObject);
begin
if Enabled then
SetState(rssNormal)
else
SetState(rssDisabled);
end;
procedure TRangeSelector.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
inherited;
if FState = rssThumb1Down then
SetSelStart(FSelStart + (X - FPrevX) * (FMax - FMin) / BarWidth)
else if FState = rssThumb2Down then
SetSelEnd(FSelEnd + (X - FPrevX) * (FMax - FMin) / BarWidth)
else if FState = rssBlockDown then
begin
if IsRealInInterval(FSelStart + (X - FPrevX) * (FMax - FMin) / BarWidth, FMin, FMax) and
IsRealInInterval(FSelEnd + (X - FPrevX) * (FMax - FMin) / BarWidth, FMin, FMax) then
begin
SetSelStart(FSelStart + (X - FPrevX) * (FMax - FMin) / BarWidth);
SetSelEnd(FSelEnd + (X - FPrevX) * (FMax - FMin) / BarWidth);
end;
end
else
SetState(DeduceState(X, Y, FDown));
FPrevX := X;
FPrevY := Y;
end;
procedure TRangeSelector.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
inherited;
FDown := false;
SetState(DeduceState(X, Y, FDown));
end;
procedure TRangeSelector.Paint;
var
theme: HTHEME;
begin
inherited;
FBuffer.Canvas.Brush.Color := Color;
FBuffer.Canvas.FillRect(ClientRect);
if UxTheme.UseThemes then
begin
theme := OpenThemeData(Handle, 'TRACKBAR');
if theme <> 0 then
try
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_TRACK, TRS_NORMAL, FTrackPos, nil);
case FState of
rssDisabled:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMB, TUS_DISABLED, FSelPos, nil);
rssBlockHover:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMB, TUS_HOT, FSelPos, nil);
rssBlockDown:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMB, TUS_PRESSED, FSelPos, nil);
else
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMB, TUS_NORMAL, FSelPos, nil);
end;
case FState of
rssDisabled:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBBOTTOM, TUBS_DISABLED, FThumbPos1, nil);
rssThumb1Hover:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBBOTTOM, TUBS_HOT, FThumbPos1, nil);
rssThumb1Down:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBBOTTOM, TUBS_PRESSED, FThumbPos1, nil);
else
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBBOTTOM, TUBS_NORMAL, FThumbPos1, nil);
end;
case FState of
rssDisabled:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBTOP, TUTS_DISABLED, FThumbPos2, nil);
rssThumb2Hover:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBTOP, TUTS_HOT, FThumbPos2, nil);
rssThumb2Down:
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBTOP, TUTS_PRESSED, FThumbPos2, nil);
else
DrawThemeBackground(theme, FBuffer.Canvas.Handle, TKP_THUMBTOP, TUTS_NORMAL, FThumbPos2, nil);
end;
finally
CloseThemeData(theme);
end;
end
else
begin
DrawEdge(FBuffer.Canvas.Handle, FTrackPos, EDGE_SUNKEN, BF_RECT);
FBuffer.Canvas.Brush.Color := clHighlight;
FBuffer.Canvas.FillRect(FSelPos);
case FState of
rssDisabled:
DrawEdge(FBuffer.Canvas.Handle, FSelPos, EDGE_BUMP, BF_RECT or BF_MONO);
rssBlockHover:
DrawEdge(FBuffer.Canvas.Handle, FSelPos, EDGE_RAISED, BF_RECT);
rssBlockDown:
DrawEdge(FBuffer.Canvas.Handle, FSelPos, EDGE_SUNKEN, BF_RECT);
else
DrawEdge(FBuffer.Canvas.Handle, FSelPos, EDGE_ETCHED, BF_RECT);
end;
case FState of
rssDisabled:
DrawEdge(FBuffer.Canvas.Handle, FThumbPos1, EDGE_BUMP, BF_RECT or BF_MONO);
rssThumb1Hover:
DrawEdge(FBuffer.Canvas.Handle, FThumbPos1, EDGE_RAISED, BF_RECT);
rssThumb1Down:
DrawEdge(FBuffer.Canvas.Handle, FThumbPos1, EDGE_SUNKEN, BF_RECT);
else
DrawEdge(FBuffer.Canvas.Handle, FThumbPos1, EDGE_ETCHED, BF_RECT);
end;
case FState of
rssDisabled:
DrawEdge(FBuffer.Canvas.Handle, FThumbPos2, EDGE_BUMP, BF_RECT or BF_MONO);
rssThumb2Hover:
DrawEdge(FBuffer.Canvas.Handle, FThumbPos2, EDGE_RAISED, BF_RECT);
rssThumb2Down:
DrawEdge(FBuffer.Canvas.Handle, FThumbPos2, EDGE_SUNKEN, BF_RECT);
else
DrawEdge(FBuffer.Canvas.Handle, FThumbPos2, EDGE_ETCHED, BF_RECT);
end;
end;
SwapBuffers;
end;
procedure TRangeSelector.UpdateMetrics;
begin
UpdateThumbMetrics;
FBuffer.SetSize(Width, Height);
FTrackPos := Rect(FThumbSize.cx, FThumbSize.cy + 2, Width - FThumbSize.cx, Height - FThumbSize.cy - 2);
FSelPos := Rect(round(LogicalToScreen(FSelStart)),
FTrackPos.Top,
round(LogicalToScreen(FSelEnd)),
FTrackPos.Bottom);
with FThumbPos1 do
begin
Top := 0;
Left := round(LogicalToScreen(FSelStart) - FThumbSize.cx / 2);
Right := Left + FThumbSize.cx;
Bottom := Top + FThumbSize.cy;
end;
with FThumbPos2 do
begin
Top := Self.Height - FThumbSize.cy;
Left := round(LogicalToScreen(FSelEnd) - FThumbSize.cx / 2);
Right := Left + FThumbSize.cx;
Bottom := Top + FThumbSize.cy;
end;
end;
procedure TRangeSelector.WndProc(var Message: TMessage);
begin
inherited;
case Message.Msg of
WM_SIZE:
UpdateMetrics;
end;
end;
procedure TRangeSelector.SetMax(Max: real);
begin
if FMax <> Max then
begin
FMax := Max;
UpdateMetrics;
Paint;
end;
end;
procedure TRangeSelector.SetMin(Min: real);
begin
if FMin <> Min then
begin
FMin := Min;
UpdateMetrics;
Paint;
end;
end;
procedure TRangeSelector.SetSelEnd(SelEnd: real);
begin
if (FSelEnd <> SelEnd) and IsRealInInterval(SelEnd, FMin, FMax) then
begin
FSelEnd := SelEnd;
if FSelStart > FSelEnd then
FSelStart := FSelEnd;
UpdateMetrics;
Paint;
if Assigned(FOnChange) then
FOnChange(Self);
end;
end;
procedure TRangeSelector.SetSelStart(SelStart: real);
begin
if (FSelStart <> SelStart) and IsRealInInterval(SelStart, FMin, FMax) then
begin
FSelStart := SelStart;
if FSelStart > FSelEnd then
FSelEnd := FSelStart;
UpdateMetrics;
Paint;
if Assigned(FOnChange) then
FOnChange(Self);
end;
end;
procedure TRangeSelector.SetState(State: TRangeSelectorState);
begin
if State <> FState then
begin
FState := State;
Paint;
end;
end;
procedure TRangeSelector.SwapBuffers;
begin
BitBlt(Canvas.Handle,
0,
0,
Width,
Height,
FBuffer.Canvas.Handle,
0,
0,
SRCCOPY);
end;
end.
There are still a few things to improve, such as 1) add keyboard interface, 2) make the display of the markers optional and add more appearance settings, 4) snap to integer grid, and 3) add the ability to enter a value by numbers Try double-clicking a thumb!.
The control works both with and without visual themes enabled and is completely double-buffered.

- 105,602
- 8
- 282
- 384
-
6
-
-
@SveinBringsli - Agree. I always said that in my opinion, Andreas R is the best Delphi SO member! Thanks Andreas! – Gabriel Oct 02 '20 at 12:37
-
Hi. I tried the component and it works REALLY nice & polished. However, small warning for those that use skins in their app: it will not apply skin's color. For example, on Auric skin (which is a dark skin), the component is still white. – Gabriel Oct 05 '20 at 11:44
-
@InTheNameOfScience: Indeed. When I wrote this answer, I don't even think VCL styles were invented! In addition, personally I dislike VCL styles, so you will probably never see any control created by me with explicit support for VCL styles. – Andreas Rejbrand Oct 05 '20 at 12:19
-
You don't like them how they look or how they are implemented? I think both of them (look and implementation) have their own problems, but you can still get some decent looking apps out of them. – Gabriel Oct 05 '20 at 12:22
-
There are many bugs in the implementation of VCL styles. In general, every time you try to alter the standard Win32 appearance, you risk introducing bugs and non-standard behaviour that is very apparent to those who have lived in front of a Win32 desktop for the last 25 years! Just by looking at the Qs at SO, you find examples. If you really want a high-quality Win32 GUI (which behaves as expected and works with ATs, works for keyboard users etc.), you should use the OS's default controls without modifications. ... – Andreas Rejbrand Oct 05 '20 at 12:26
-
(cont.) It is much better with a 100% bug-free Win 95-like GUI than a buggy, flickering, mouse-only themed one. (For the same reasons, I don't touch FMX. I the first comment to [this answer](https://stackoverflow.com/a/63229707/282848), I give my impression of playing around for five minutes with FMX.) – Andreas Rejbrand Oct 05 '20 at 12:27
In addition to Andreas' nice answer and component, hereby another slider component that is capable of:
- displaying a range,
- displaying a filtered range within that range,
- dragging the grips and the green bar,
- double clicking a grip for keyboard entry,
- tabbing through the grips for keyboard entry,
- displaying different data types,
- restricting values to a step size.
(Source: NLDelphi.com)

- 43,011
- 8
- 105
- 200
I don't know of anything like this, although there probably is such a thing. I'd be concerned about the usability issues of moving one of the markers on top of the other. When I ask for ranges in my app I just ask the user to type the numbers in.

- 601,492
- 42
- 1,072
- 1,490
TTrackBar has SelStart, SelEnd and ShowSelRange. However they don't seem to have much use - they are nearly invisible if themed and AFAICT the user can't move the Sel* markers.

- 13,748
- 1
- 45
- 83
I suggest a pair of spin edits. The user can click up/down if they want to but most people will just want to enter their values:

- 65,725
- 40
- 181
- 316