{ Copyright (C) 2026 - Uladzimir Karpenka, EW8BAK This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. } unit FlatListBox; { Custom list box with canvas rendering. Use instead of TListBox where native widgetset styling is undesirable. } {$mode objfpc}{$H+} interface uses Classes, SysUtils, Controls, Graphics, Forms, LCLType, LMessages, Types, Math, AppTheme, DpiUtils; type TFlatListBox = class(TCustomControl) private FItems: TStringList; FItemIndex: Integer; FHotIndex: Integer; FTopIndex: Integer; FMouseDownIndex: Integer; FClrOuterBG: TColor; FClrBG: TColor; FClrBGHot: TColor; FClrBorder: TColor; FClrText: TColor; FClrTextDim: TColor; FClrSelBG: TColor; FClrSelText: TColor; FClrScroll: TColor; function ItemHeight: Integer; function VisibleCount: Integer; function ItemAt(Y: Integer): Integer; procedure ItemsChanged(Sender: TObject); procedure SetItemIndex(V: Integer); procedure SetTopIndex(V: Integer); procedure EnsureItemVisible(Idx: Integer); procedure CMTextChanged(var Msg: TLMessage); message CM_TEXTCHANGED; protected procedure Paint; override; procedure MouseEnter; override; procedure MouseLeave; override; 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 KeyDown(var Key: Word; Shift: TShiftState); override; function DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean; override; function DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure SetAppTheme(const T: TAppTheme); property Items: TStringList read FItems; property ItemIndex: Integer read FItemIndex write SetItemIndex; property TopIndex: Integer read FTopIndex write SetTopIndex; property Align; property Anchors; property Enabled; property Font; property ParentShowHint; property PopupMenu; property ShowHint; property TabOrder; property TabStop; property Tag; property Visible; property OnClick; property OnDblClick; // Клавиатура: собственный KeyDown обрабатывает стрелки/PgUp/Home, а // необработанные клавиши (Enter и прочее) достаются владельцу через // inherited — поэтому событие имеет смысл публиковать. property OnKeyDown; end; implementation const BASE_ITEM_H = 22; BASE_PAD_X = 8; BASE_SCROLL_W = 3; constructor TFlatListBox.Create(AOwner: TComponent); begin inherited Create(AOwner); ControlStyle := ControlStyle + [csOpaque, csCaptureMouse, csClickEvents, csDoubleClicks]; FItems := TStringList.Create; FItems.OnChange := @ItemsChanged; FItemIndex := -1; FHotIndex := -1; FTopIndex := 0; FMouseDownIndex := -1; Width := 180; Height := 140; TabStop := True; Cursor := crDefault; Font.Size := 8; FClrOuterBG := TColor($00181818); FClrBG := TColor($00101010); FClrBGHot := TColor($000C3010); FClrBorder := TColor($00303030); FClrText := TColor($00CCCCCC); FClrTextDim := TColor($00666666); FClrSelBG := TColor($00183618); FClrSelText := TColor($0000FF88); FClrScroll := TColor($00387838); end; destructor TFlatListBox.Destroy; begin FItems.Free; inherited Destroy; end; function TFlatListBox.ItemHeight: Integer; begin Canvas.Font.Assign(Font); Result := DpiScale(BASE_ITEM_H); if Result < Canvas.TextHeight('Ag') + DpiScale(7) then Result := Canvas.TextHeight('Ag') + DpiScale(7); end; function TFlatListBox.VisibleCount: Integer; begin Result := Max(1, (Height - DpiScale(2)) div ItemHeight); end; function TFlatListBox.ItemAt(Y: Integer): Integer; begin Result := FTopIndex + (Y - DpiScale(1)) div ItemHeight; if (Result < 0) or (Result >= FItems.Count) then Result := -1; end; procedure TFlatListBox.SetAppTheme(const T: TAppTheme); begin FClrOuterBG := T.Panel; FClrBG := T.BG; FClrBGHot := T.BtnActive; FClrBorder := T.Border; FClrText := T.Text; FClrTextDim := T.TextDim; FClrSelBG := T.SliderTrackFill; FClrSelText := T.BtnTextActive; FClrScroll := T.BtnBorderActive; Font.Color := T.Text; Invalidate; end; procedure TFlatListBox.ItemsChanged(Sender: TObject); begin if FItems.Count = 0 then FItemIndex := -1 else if FItemIndex >= FItems.Count then FItemIndex := FItems.Count - 1; if FHotIndex >= FItems.Count then FHotIndex := -1; SetTopIndex(FTopIndex); Invalidate; end; procedure TFlatListBox.SetItemIndex(V: Integer); begin V := EnsureRange(V, -1, FItems.Count - 1); if FItemIndex = V then begin EnsureItemVisible(FItemIndex); Exit; end; FItemIndex := V; EnsureItemVisible(FItemIndex); Invalidate; end; procedure TFlatListBox.SetTopIndex(V: Integer); begin V := EnsureRange(V, 0, Max(0, FItems.Count - VisibleCount)); if FTopIndex = V then Exit; FTopIndex := V; Invalidate; end; procedure TFlatListBox.EnsureItemVisible(Idx: Integer); var VC: Integer; begin if Idx < 0 then Exit; VC := VisibleCount; if Idx < FTopIndex then SetTopIndex(Idx) else if Idx >= FTopIndex + VC then SetTopIndex(Idx - VC + 1); end; procedure TFlatListBox.CMTextChanged(var Msg: TLMessage); begin Invalidate; end; procedure TFlatListBox.Paint; var i, ItemIdx, IH, Y, TextY, Pad, ScrollW, ThumbTop, ThumbH: Integer; R, TextR: TRect; begin Canvas.Brush.Color := FClrOuterBG; Canvas.Brush.Style := bsSolid; Canvas.Pen.Style := psClear; Canvas.FillRect(ClientRect); Canvas.Brush.Color := FClrBG; Canvas.Brush.Style := bsSolid; Canvas.Pen.Color := FClrBorder; Canvas.Pen.Style := psSolid; Canvas.Pen.Width := 1; Canvas.Rectangle(0, 0, Width, Height); Canvas.Font.Assign(Font); IH := ItemHeight; Pad := DpiScale(BASE_PAD_X); ScrollW := DpiScale(BASE_SCROLL_W); for i := 0 to VisibleCount - 1 do begin ItemIdx := FTopIndex + i; if ItemIdx >= FItems.Count then Break; Y := DpiScale(1) + i * IH; R := Rect(DpiScale(1), Y, Width - DpiScale(1), Y + IH); if ItemIdx = FItemIndex then begin Canvas.Brush.Color := FClrSelBG; Canvas.Font.Color := FClrSelText; end else if (ItemIdx = FHotIndex) and Enabled then begin Canvas.Brush.Color := FClrBGHot; Canvas.Font.Color := FClrText; end else begin Canvas.Brush.Color := FClrBG; if Enabled then Canvas.Font.Color := FClrText else Canvas.Font.Color := FClrTextDim; end; Canvas.Pen.Style := psClear; Canvas.Brush.Style := bsSolid; Canvas.FillRect(R); Canvas.Brush.Style := bsClear; TextY := Y + (IH - Canvas.TextHeight('Ag')) div 2; TextR := Rect(Pad, Y, Width - Pad - ScrollW, Y + IH); Canvas.TextRect(TextR, TextR.Left, TextY, FItems[ItemIdx]); end; if FItems.Count > VisibleCount then begin ThumbH := Max(DpiScale(16), MulDiv(Height - DpiScale(4), VisibleCount, FItems.Count)); ThumbTop := DpiScale(2) + MulDiv(Height - DpiScale(4) - ThumbH, FTopIndex, Max(1, FItems.Count - VisibleCount)); Canvas.Brush.Color := FClrScroll; Canvas.Brush.Style := bsSolid; Canvas.Pen.Style := psClear; Canvas.FillRect(Rect(Width - DpiScale(5), ThumbTop, Width - DpiScale(2), ThumbTop + ThumbH)); end; end; procedure TFlatListBox.MouseEnter; begin inherited; Invalidate; end; procedure TFlatListBox.MouseLeave; begin inherited; FHotIndex := -1; Invalidate; end; procedure TFlatListBox.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited; if (Button <> mbLeft) or not Enabled then Exit; SetFocus; FMouseDownIndex := ItemAt(Y); if FMouseDownIndex >= 0 then SetItemIndex(FMouseDownIndex); end; procedure TFlatListBox.MouseMove(Shift: TShiftState; X, Y: Integer); var Idx: Integer; begin inherited; if PtInRect(ClientRect, Point(X, Y)) then Idx := ItemAt(Y) else Idx := -1; if FHotIndex = Idx then Exit; FHotIndex := Idx; Invalidate; end; procedure TFlatListBox.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin inherited; if Button <> mbLeft then Exit; FMouseDownIndex := -1; end; procedure TFlatListBox.KeyDown(var Key: Word; Shift: TShiftState); var OldIndex: Integer; begin inherited; if not Enabled then Exit; OldIndex := FItemIndex; case Key of VK_UP: begin if FItems.Count > 0 then SetItemIndex(EnsureRange(FItemIndex - 1, 0, FItems.Count - 1)); Key := 0; end; VK_DOWN: begin if FItems.Count > 0 then SetItemIndex(EnsureRange(FItemIndex + 1, 0, FItems.Count - 1)); Key := 0; end; VK_PRIOR: begin if FItems.Count > 0 then SetItemIndex(EnsureRange(FItemIndex - VisibleCount, 0, FItems.Count - 1)); Key := 0; end; VK_NEXT: begin if FItems.Count > 0 then SetItemIndex(EnsureRange(FItemIndex + VisibleCount, 0, FItems.Count - 1)); Key := 0; end; VK_HOME: begin if FItems.Count > 0 then SetItemIndex(0); Key := 0; end; VK_END: begin if FItems.Count > 0 then SetItemIndex(FItems.Count - 1); Key := 0; end; end; if (Key = 0) and (FItemIndex <> OldIndex) then Click; end; function TFlatListBox.DoMouseWheelUp(Shift: TShiftState; MousePos: TPoint): Boolean; begin SetTopIndex(FTopIndex - 3); Result := True; end; function TFlatListBox.DoMouseWheelDown(Shift: TShiftState; MousePos: TPoint): Boolean; begin SetTopIndex(FTopIndex + 3); Result := True; end; end.