1

After setting ListView in virtual mode ListView1.Selected.Top always returns 0. I'm using that property on double click on list view to show edit box at that position.

How can I resolve this?

Example of .pas and .dfm files are here. I want to open edit box on position where it is double clicked.

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls,Generics.Collections,Generics.Defaults;

type
  TLVData = record
    Column0: string;
    Column1: string;
    Column2: string;
  end;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    Edit1: TEdit;
    procedure ListView1DblClick(Sender: TObject);
    procedure NewEntry(i: integer);
    procedure FormShow(Sender: TObject);
    procedure ListView1Data(Sender: TObject; Item: TListItem);
  private
    { Private declarations }
  public
    { Public declarations }
  end;


var
  Form1: TForm1;
  LVDataList : TList<TLVData>;

implementation

{$R *.dfm}

procedure TForm1.NewEntry(i: integer);
var
  LVData:TLVData;
begin
  if not Assigned(LVDataList) then LVDataList := TList<TLVData>.Create;
  LVData.Column0 := 'Column0:' + IntToStr(i);
  LVData.Column1 := 'Column1:' + IntToStr(i);
  LVData.Column2 := 'Column2:' + IntToStr(i);

  LVDataList.Add(LVData);
end;

procedure TForm1.FormShow(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to 9 do
    NewEntry(i);
  ListView1.Items.Count := LVDataList.Count;
end;

procedure TForm1.ListView1Data(Sender: TObject; Item: TListItem);
var i: integer;
begin
  if not Assigned(Item) then Exit;

  Item.Caption := LVDataList.Items[Item.Index].Column0;
  Item.SubItems.Add(LVDataList.Items[Item.Index].Column0);
  Item.SubItems.Add(LVDataList.Items[Item.Index].Column1);
  Item.SubItems.Add(LVDataList.Items[Item.Index].Column2);
end;

procedure TForm1.ListView1DblClick(Sender: TObject);
begin
  Edit1.Text:=ListView1.Selected.SubItems[0];
  Edit1.Top:=ListView1.Top+ListView1.Selected.Top-2;
  Edit1.Width:=100;
  Edit1.Show;
  Edit1.SetFocus;
end;

end.

And .dfm:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 412
  ClientWidth = 784
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnShow = FormShow
  PixelsPerInch = 96
  TextHeight = 13
  object ListView1: TListView
    Left = 0
    Top = 0
    Width = 784
    Height = 412
    Align = alClient
    Columns = <
      item
        AutoSize = True
        Caption = 'Column0'
        MinWidth = 100
      end
      item
        AutoSize = True
        Caption = 'Column1'
        MinWidth = 100
      end
      item
        AutoSize = True
        Caption = 'Column2'
        MinWidth = 100
      end>
    GridLines = True
    HideSelection = False
    MultiSelect = True
    OwnerData = True
    ReadOnly = True
    RowSelect = True
    TabOrder = 0
    ViewStyle = vsReport
    OnData = ListView1Data
    OnDblClick = ListView1DblClick
  end
  object Edit1: TEdit
    Left = 360
    Top = 168
    Width = 121
    Height = 21
    TabOrder = 1
    Text = 'Edit1'
    Visible = False
  end
end
Ivan
  • 85
  • 6
  • What are you doing with that information. Knowing that may help people come up with an alternative. – David Heffernan May 16 '21 at 07:35
  • 1
    I'm showing edit box at that position. – Ivan May 16 '21 at 07:48
  • Could you produce a minimal yet complete, reproducible example that we can play with? Edit your question to publish the .pas and .dfm files for that example. – fpiette May 16 '21 at 08:02
  • Added simple example. – Ivan May 16 '21 at 09:18
  • 2
    When using virtual mode, `TListView` has to use a tempory `TListItem` to satisfy properties like `Selected`, `Items[]`, etc. I find this tends to cause subtle/unwanted side effects, so I usually just ignore such properties in virtual mode and go straight to the Win32 API to get the information I need, like `ListView_GetNextItem(LVNI_SELECTED)` and `ListView_GetItemRect()`, etc – Remy Lebeau May 16 '21 at 16:33

1 Answers1

1

I can reproduce your problem. I found a workaround: use the display rectangle of the selected item:

procedure TForm1.ListView1DblClick(Sender: TObject);
var
  Rect : TRect;
begin
  Rect        := ListView1.Selected.DisplayRect(drBounds);
  Edit1.Text  := ListView1.Selected.SubItems[0];
  Edit1.Top   := ListView1.Top + Rect.Top - 1;
  Edit1.Width :=100;
  Edit1.Show;
  Edit1.SetFocus;
end;

If you want to get the subitem, then you have to iterate thru the columns to find on which one the user clicked. We need to mouse position so we install an OnMouseDown event handler to save the mouse coordinates and use that to find the column.

  private
    FMouseDown : TPoint;

procedure TForm1.ListView1MouseDown(Sender: TObject; Button: TMouseButton;
    Shift: TShiftState; X, Y: Integer);
begin
    FMouseDown.X := X;
    FMouseDown.Y := Y;
end;

procedure TForm1.ListView1DblClick(Sender: TObject);
var
  Rect   : TRect;
  I      : Integer;
  X1, X2 : Integer;
  Col    : Integer;
begin
  if not Assigned(ListView1.Selected) then
      Exit;
  Rect := ListView1.Selected.DisplayRect(drBounds);
  X1   := 0;
  X2   := 0;
  Col  := -1;
  for I := 0 to ListView1.Columns.Count - 1 do begin
      X2 := X2 + ListView1.Columns[0].Width;
      if (FMouseDown.X >= X1) and (FMouseDown.X < X2) then begin
          Col := I;
          break;
      end;
      X1 := X2;
  end;
  if Col < 0 then
      Exit;

  Edit1.Text  := ListView1.Selected.SubItems[0];
  Edit1.Top   := ListView1.Top + Rect.Top - 1;
  Edit1.Left  := X1;
  Edit1.Width := X2 - X1; // Same width as column
  Edit1.Show;
  Edit1.SetFocus;
end;
fpiette
  • 11,983
  • 1
  • 24
  • 46
  • Additional question: How to get left position of column? Grid is in row select mode. Any better way then calculate it from every column width? – Ivan May 19 '21 at 09:13
  • 1
    Edited my answer. By the way, the next time you ask, you should better formulate your question. – fpiette May 19 '21 at 09:37