Search code examples
delphifiremonkeydelphi-xe8tlistview

Add TSwitch to every TListView item


I haven't attempted this because I wouldn't know where to begin.

Is it possible to add a FMX TSwitch into a FMX TListViewitem?

Any help/suggestions would be much appreciated.

Thanks,


Solution

  • You first have to keep in mind the whole design of the TListView control. It's meant to be very lightweight for when it contains a large number of items. You may have a million items, you surely don't want a million switch controls instantiated. Therefore, it's not meant for you to embed controls in each item as a container, such as the TListBox allows.

    That being said, it's assumed that you perform minimal drawing on each individual list item to be consistent with the design of the TListView. This requires creating virtual objects inherited from TListItemObject to be associated with each item. These objects are what allow the existing built-in elements of any item, such as the accessory or bitmap.

    Here's a very rough demo I threw together to get you started, you'd need to change the drawing how you need it to look.

    Start a new FMX application, drop a TListView, and use this unit in place of your main form's unit:

    unit Unit1;
    
    interface
    
    uses
      System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
      FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
      FMX.ListView.Types, FMX.ListView.Appearances, FMX.ListView.Adapters.Base,
      FMX.ListView, FMX.Controls.Presentation, FMX.StdCtrls;
    
    type
    
      TListItemSwitch = class(TListItemSimpleControl)
      private
        FIsChecked: Boolean;
        FOnSwitch: TNotifyEvent;
        procedure SetIsChecked(const AValue: Boolean);
      protected
        function MouseDown(const Button: TMouseButton; const Shift: TShiftState; const MousePos: TPointF): Boolean;
          override;
        procedure DoSwitch; virtual;
      public
        constructor Create(const AOwner: TListItem); override;
        destructor Destroy; override;
    
        procedure Render(const Canvas: TCanvas; const DrawItemIndex: Integer; const DrawStates: TListItemDrawStates;
          const SubPassNo: Integer = 0); override;
    
      public
        property IsChecked: Boolean read FIsChecked write SetIsChecked;
        property OnSwitch: TNotifyEvent read FOnSwitch write FOnSwitch;
      end;
    
      TForm1 = class(TForm)
        ListView1: TListView;
        procedure ListView1UpdateObjects(const Sender: TObject;
          const AItem: TListViewItem);
        procedure FormCreate(Sender: TObject);
      private
        { Private declarations }
      public
        { Public declarations }
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.fmx}
    
    { TListItemSwitch }
    
    constructor TListItemSwitch.Create(const AOwner: TListItem);
    begin
      inherited;
    
    end;
    
    destructor TListItemSwitch.Destroy;
    begin
    
      inherited;
    end;
    
    procedure TListItemSwitch.DoSwitch;
    begin
      FIsChecked:= not FIsChecked;
      if Assigned(OnSwitch) then
        OnSwitch(Self);
    end;
    
    function TListItemSwitch.MouseDown(const Button: TMouseButton;
      const Shift: TShiftState; const MousePos: TPointF): Boolean;
    begin
      if (Button = TMouseButton.mbLeft) and Enabled then begin
        DoSwitch;
      end;
      inherited;
    end;
    
    procedure TListItemSwitch.Render(const Canvas: TCanvas;
      const DrawItemIndex: Integer; const DrawStates: TListItemDrawStates;
      const SubPassNo: Integer);
    var
      R, R2: TRectF;
    begin
      inherited;
    
      R:= Self.LocalRect;
      R2:= R;
    
      Canvas.BeginScene;
      try
    
        Canvas.Stroke.Kind:= TBrushKind.None;
        Canvas.Fill.Kind:= TBrushKind.Solid;
        Canvas.Fill.Color:= TAlphaColorRec.Skyblue;
        Canvas.FillRect(R, 8, 8,
          [TCorner.TopLeft, TCorner.TopRight, TCorner.BottomLeft, TCorner.BottomRight],
          1.0, TCornerType.Round);
    
        if IsChecked then begin
          R2.Left:= R.Right - 20;
          R2.Width:= 20;
        end else begin
          R2.Left:= R.Left;
          R2.Width:= 20;
        end;
    
        Canvas.Fill.Color:= TAlphaColorRec.Black;
        Canvas.FillRect(R2, 8, 8,
          [TCorner.TopLeft, TCorner.TopRight, TCorner.BottomLeft, TCorner.BottomRight],
          1.0, TCornerType.Round);
    
      finally
        Canvas.EndScene;
      end;
    
    end;
    
    procedure TListItemSwitch.SetIsChecked(const AValue: Boolean);
    begin
      FIsChecked:= AValue;
    end;
    
    { TForm1 }
    
    procedure TForm1.FormCreate(Sender: TObject);
    var
      I: TListViewItem;
    begin
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
      I:= ListView1.Items.Add;
    
    end;
    
    procedure TForm1.ListView1UpdateObjects(const Sender: TObject;
      const AItem: TListViewItem);
    var
      S: TListItemSwitch;
    begin
      S:= AItem.Objects.FindObject('Switch') as TListItemSwitch;
      if S = nil then begin
        S:= TListItemSwitch.Create(AItem);
        S.Name:= 'Switch';
        S.Align:= TListItemAlign.Trailing;
        S.VertAlign:= TListItemAlign.Center;
        S.Width:= 50;
        S.Height:= 20;
        S.IsChecked:= False;
      end;
    
    end;
    
    end.
    

    Screenshot

    NOTE: This was written in Delphi 10 Seattle.

    Your only other options I believe are to either:

    1. Instantiate a TSwitch for each item and render it using the same method as above (Very sloppy, I do not recommend)
    2. Figure out how to implement the drawing of the standard TSwitch using styles, again using the same method as above (which is probably the best option for performance and visual adaption)
    3. Resort to a TListBox instead, depending on how you intend to use the list (which would be very heavy on a large number of items)

    I went a little more in-depth about the differences between a TListView and a TListBox in Firemonkey in a separate question / answer.