In-place editing of a subitem in a TListView
Asked Answered
H

4

9

I have a ListView with 3 columns and would like to edit the third column, aka Subitem[1]. If I set ListView.ReadOnly to False, it allows me to edit the caption of the selected item. Is there an easy way to do the same thing for the subitem? I would like to stay away from adding a borderless control on top that does the editing.

Hygrometric answered 31/5, 2012 at 14:12 Comment(0)
I
18

You can Edit a subitem of the listview (in report mode) using a TEdit, a custom message and handling the OnClick event of the ListView.

Try this sample

Const
  USER_EDITLISTVIEW = WM_USER + 666;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    procedure FormCreate(Sender: TObject);
    procedure ListView1Click(Sender: TObject);
  private
    ListViewEditor: TEdit;
    LItem: TListitem;
    procedure UserEditListView( Var Message: TMessage ); message USER_EDITLISTVIEW;
    procedure ListViewEditorExit(Sender: TObject);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

uses
  CommCtrl;
const
  EDIT_COLUMN = 2; //Index of the column to Edit

procedure TForm1.FormCreate(Sender: TObject);
Var
  I : Integer;
  Item : TListItem;
begin
  for I := 0 to 9 do
  begin
   Item:=ListView1.Items.Add;
   Item.Caption:=Format('%d.%d',[i,1]);
   Item.SubItems.Add(Format('%d.%d',[i,2]));
   Item.SubItems.Add(Format('%d.%d',[i,3]));
  end;

  //create the TEdit and assign the OnExit event
  ListViewEditor:=TEdit.Create(Self);
  ListViewEditor.Parent:=ListView1;
  ListViewEditor.OnExit:=ListViewEditorExit;
  ListViewEditor.Visible:=False;
end;

procedure TForm1.ListView1Click(Sender: TObject);
var
  LPoint: TPoint;
  LVHitTestInfo: TLVHitTestInfo;
begin
  LPoint:= listview1.ScreenToClient(Mouse.CursorPos);
  ZeroMemory( @LVHitTestInfo, SizeOf(LVHitTestInfo));
  LVHitTestInfo.pt := LPoint;
  //Check if the click was made in the column to edit
  If (ListView1.perform( LVM_SUBITEMHITTEST, 0, LPARAM(@LVHitTestInfo))<>-1) and ( LVHitTestInfo.iSubItem = EDIT_COLUMN ) Then
    PostMessage( self.Handle, USER_EDITLISTVIEW, LVHitTestInfo.iItem, 0 )
  else
    ListViewEditor.Visible:=False; //hide the TEdit 
end;

procedure TForm1.ListViewEditorExit(Sender: TObject);
begin
  If Assigned(LItem) Then
  Begin
    //assign the vslue of the TEdit to the Subitem
    LItem.SubItems[ EDIT_COLUMN-1 ] := ListViewEditor.Text;
    LItem := nil;
  End;
end;

procedure TForm1.UserEditListView(var Message: TMessage);
var
  LRect: TRect;
begin
  LRect.Top := EDIT_COLUMN;
  LRect.Left:= LVIR_BOUNDS;
  listview1.Perform( LVM_GETSUBITEMRECT, Message.wparam,  LPARAM(@LRect) );
  MapWindowPoints( listview1.Handle, ListViewEditor.Parent.Handle, LRect, 2 );
  //get the current Item to edit
  LItem := listview1.Items[ Message.wparam ];
  //set the text of the Edit 
  ListViewEditor.Text := LItem.Subitems[ EDIT_COLUMN-1];
  //set the bounds of the TEdit
  ListViewEditor.BoundsRect := LRect; 
  //Show the TEdit
  ListViewEditor.Visible:=True;
end;
Ivoryivorywhite answered 31/5, 2012 at 14:58 Comment(8)
This does most of the trick, but it does have a suborn bug that takes data to a new cell when changing focus.Icing
Is there an easy way to do this? I want to just edit in place so 1. I add a text box to List and on exit Assign TEdit. ListViewEditor:=TEdit.Create(Self); ListViewEditor.Parent:=ListView1; ListViewEditor.OnExit:=ListViewEditorExit; ListViewEditor.Visible:=False; Thsi doesnt seem to workMarkus
@siddharthtaunk The code in the answer works as intended and is AFAICS the minimum. I don't understand what you try to achieve. There are no shortcuts, you can not leave out code and hope it will work.Ogee
@siddharthtaunk Also, it may be that "vehystrix" answer suits you betterOgee
The Above worked with some minor modifications. Thanks Tom & RRUZMarkus
I want to add a TCombox box in the next coloum, What i did was changeed LVHitTestInfo.iSubItem = EDIT_COLUMN to (LVHitTestInfo.iSubItem = EDIT_COLUMN) or (LVHitTestInfo.iSubItem = EDIT_NEXT_COLUMN) But how DO i add it in UserEditListView. . I mean I how do I add a TComboBox to COloum 4 hereMarkus
LItem := listview1.Items[ Message.wparam ]; The LItems is show only 2 although I have added another column. what do I have to change in the handler? Although or the calling function?Markus
How to disable everything I mean text box in this place for only selected rows of the list. I want to make then non modifiable. I have not assigned any event handlers for this in the form create. And not creating the edits at all. I get exception . Please do let me know if the question is vague. This is based on a conditionMarkus
A
11

I wrote sample code on CodeCentral that shows how to do this.

How to use the Build-in Editor of TListView to Edit SubItems

Update:

Here is an updated version that should compile now:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, ComCtrls;

type
  TForm1 = class(TForm)
    ListView1: TListView;
    procedure ListView1Editing(Sender: TObject; Item: TListItem; var AllowEdit: Boolean);
    procedure ListView1Edited(Sender: TObject; Item: TListItem; var S: string);
    procedure ListView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure ListView1DrawItem(Sender: TCustomListView; Item: TListItem; Rect: TRect; State: TOwnerDrawState);
  private
    { Private declarations }
    ColumnToEdit: Integer;
    OldListViewEditProc: Pointer;
    hListViewEditWnd: HWND;
    ListViewEditWndProcPtr: Pointer;
    procedure ListViewEditWndProc(var Message: TMessage);
  public
    { Public declarations }
    constructor Create(Owner: TComponent); override;
    destructor Destroy; override;
  end;

var
  Form1: TForm1;

implementation

uses
  Commctrl;

{$R *.dfm}

type
  TListViewCoord = record
    Item: Integer;
    Column: Integer;
  end;

  TLVGetColumnAt = function(Item: TListItem; const Pt: TPoint): Integer;
  TLVGetColumnRect = function(Item: TListItem; ColumnIndex: Integer; var Rect: TRect): Boolean;
  TLVGetIndexesAt = function(ListView: TCustomListView; const Pt: TPoint; var Coord: TListViewCoord): Boolean;

  // TCustomListViewAccess provides access to the protected members of TCustomListView
  TCustomListViewAccess = class(TCustomListView);

var
  // these will be assigned according to the version of COMCTL32.DLL being used
  GetColumnAt: TLVGetColumnAt = nil;
  GetColumnRect: TLVGetColumnRect = nil;
  GetIndexesAt: TLVGetIndexesAt = nil;

//---------------------------------------------------------------------------
//  GetComCtl32Version
//
//  Purpose: Helper function to determine the version of CommCtrl32.dll that is loaded.
//---------------------------------------------------------------------------

var
  ComCtl32Version: DWORD = 0;

function GetComCtl32Version: DWORD;
type
  DLLVERSIONINFO = packed record
    cbSize: DWORD;
    dwMajorVersion: DWORD;
    dwMinorVersion: DWORD;
    dwBuildNumber: DWORD;
    dwPlatformID: DWORD;
  end;
  DLLGETVERSIONPROC = function(var dvi: DLLVERSIONINFO): Integer; stdcall;
var
  hComCtrl32: HMODULE;
  lpDllGetVersion: DLLGETVERSIONPROC;
  dvi: DLLVERSIONINFO;
  FileName: array[0..MAX_PATH] of Char;
  dwHandle: DWORD;
  dwSize: DWORD;
  pData: Pointer;
  pVersion: Pointer;
  uiLen: UINT;
begin
  if ComCtl32Version = 0 then
  begin
    hComCtrl32 := GetModuleHandle('comctl32.dll');
    if hComCtrl32 <> 0 then
    begin
      @lpDllGetVersion := GetProcAddress(hComCtrl32, 'DllGetVersion');
      if @lpDllGetVersion <> nil then
      begin
        ZeroMemory(@dvi, SizeOf(dvi));
        dvi.cbSize := SizeOf(dvi);
        if lpDllGetVersion(dvi) >= 0 then
          ComCtl32Version := MAKELONG(Word(dvi.dwMinorVersion), Word(dvi.dwMajorVersion));
      end;
      if ComCtl32Version = 0 then
      begin
        ZeroMemory(@FileName[0], SizeOf(FileName));
        if GetModuleFileName(hComCtrl32, FileName, MAX_PATH) <> 0 then
        begin
          dwHandle := 0;
          dwSize := GetFileVersionInfoSize(FileName, dwHandle);
          if dwSize <> 0 then
          begin
            GetMem(pData, dwSize);
            try
              if GetFileVersionInfo(FileName, dwHandle, dwSize, pData) then
              begin
                pVersion := nil;
                uiLen := 0;
                if VerQueryValue(pData, '\', pVersion, uiLen) then
                begin
                  with PVSFixedFileInfo(pVersion)^ do
                    ComCtl32Version := MAKELONG(LOWORD(dwFileVersionMS), HIWORD(dwFileVersionMS));
                end;
              end;
            finally
              FreeMem(pData);
            end;
          end;
        end;
      end;
    end;
  end;
  Result := ComCtl32Version;
end;

//---------------------------------------------------------------------------
//  Manual_GetColumnAt
//
//  Purpose: Returns the column index at the specified coordinates,
//    relative to the specified item
//---------------------------------------------------------------------------

function Manual_GetColumnAt(Item: TListItem; const Pt: TPoint): Integer;
var
  LV: TCustomListViewAccess;
  R: TRect;
  I: Integer;
begin
  LV := TCustomListViewAccess(Item.ListView);

  // determine the dimensions of the current column value, and
  // see if the coordinates are inside of the column value

  // get the dimensions of the entire item
  R := Item.DisplayRect(drBounds);

  // loop through all of the columns looking for the value that was clicked on
  for I := 0 to LV.Columns.Count-1 do
  begin
    R.Right := (R.Left + LV.Column[I].Width);
    if PtInRect(R, Pt) then
    begin
      Result := I;
      Exit;
    end;
    R.Left := R.Right;
  end;

  Result := -1;
end;

//---------------------------------------------------------------------------
//  Manual_GetColumnRect
//
//  Purpose: Calculate the dimensions of the specified column,
//    relative to the specified item
//---------------------------------------------------------------------------

function Manual_GetColumnRect(Item: TListItem; ColumnIndex: Integer; var Rect: TRect): Boolean;
var
  LV: TCustomListViewAccess;
  I: Integer;
begin
  Result := False;

  LV := TCustomListViewAccess(Item.ListView);

  // make sure the index is in the valid range
  if (ColumnIndex >= 0) and (ColumnIndex < LV.Columns.Count) then
  begin
    // get the dimensions of the entire item
    Rect := Item.DisplayRect(drBounds);

    // loop through the columns calculating the desired offsets
    for I := 0 to ColumnIndex-1 do
      Rect.Left := (Rect.Left + LV.Column[i].Width);
    Rect.Right := (Rect.Left + LV.Column[ColumnIndex].Width);

    Result := True;
  end;
end;

//---------------------------------------------------------------------------
//  Manual_GetIndexesAt
//
//  Purpose: Returns the Item and Column indexes at the specified coordinates
//---------------------------------------------------------------------------

function Manual_GetIndexesAt(ListView: TCustomListView; const Pt: TPoint; var Coord: TListViewCoord): Boolean;
var
  Item: TListItem;
begin
  Result := False;

  Item := ListView.GetItemAt(Pt.x, Pt.y);
  if Item <> nil then
  begin
    Coord.Item := Item.Index;
    Coord.Column := Manual_GetColumnAt(Item, Pt);
    Result := True;
  end;
end;

//---------------------------------------------------------------------------
//  ComCtl_GetColumnAt
//
//  Purpose: Returns the column index at the specified coordinates, relative to the specified item
//---------------------------------------------------------------------------

function ComCtl_GetColumnAt(Item: TListItem; const Pt: TPoint): Integer;
var
  HitTest: LV_HITTESTINFO;
begin
  Result := -1;

  ZeroMemory(@HitTest, SizeOf(HitTest));
  HitTest.pt := Pt;

  if ListView_SubItemHitTest(Item.ListView.Handle, @HitTest) > -1 then
  begin
    if HitTest.iItem = Item.Index then
      Result := HitTest.iSubItem;
  end;
end;

//---------------------------------------------------------------------------
//  ComCtl_GetColumnRect
//
//  Purpose: Calculate the dimensions of the specified column, relative to the specified item
//---------------------------------------------------------------------------

function ComCtl_GetColumnRect(Item: TListItem; ColumnIndex: Integer; var Rect: TRect): Boolean;
begin
  Result := ListView_GetSubItemRect(Item.ListView.Handle, Item.Index, ColumnIndex, LVIR_BOUNDS, @Rect);
end;

//---------------------------------------------------------------------------
//  ComCtl_GetIndexesAt
//
//  Purpose: Returns the Item and Column indexes at the specified coordinates
//---------------------------------------------------------------------------

function ComCtl_GetIndexesAt(ListView: TCustomListView; const Pt: TPoint; var Coord: TListViewCoord): Boolean;
var
  HitTest: LV_HITTESTINFO;
begin
  Result := False;

  ZeroMemory(@HitTest, SizeOf(HitTest));
  HitTest.pt := Pt;

  if ListView_SubItemHitTest(ListView.Handle, @HitTest) > -1 then
  begin
    Coord.Item := HitTest.iItem;
    Coord.Column := HitTest.iSubItem;
    Result := True;
  end;
end;

//---------------------------------------------------------------------------
//  TForm1    Constructor
//
//  Purpose:  Form constructor
//---------------------------------------------------------------------------

constructor TForm1.Create(Owner: TComponent);
begin
  inherited Create(Owner);

  // no editing yet
  ColumnToEdit := -1;
  OldListViewEditProc := nil;
  hListViewEditWnd := 0;

  ListViewEditWndProcPtr := MakeObjectInstance(ListViewEditWndProc);
  if ListViewEditWndProcPtr = nil then
    raise Exception.Create('Could not allocate memory for ListViewEditWndProc proxy');

  if GetComCtl32Version >= DWORD(MAKELONG(70, 4)) then
  begin
    @GetColumnAt := @ComCtl_GetColumnAt;
    @GetColumnRect := @ComCtl_GetColumnRect;
    @GetIndexesAt := @ComCtl_GetIndexesAt;
  end else
  begin
    @GetColumnAt := @Manual_GetColumnAt;
    @GetColumnRect := @Manual_GetColumnRect;
    @GetIndexesAt := @Manual_GetIndexesAt;
  end;
end;

//---------------------------------------------------------------------------
//  TForm1    Destructor
//
//  Purpose:  Form destructor
//---------------------------------------------------------------------------

destructor TForm1.Destroy;
begin
  if ListViewEditWndProcPtr <> nil then
    FreeObjectInstance(ListViewEditWndProcPtr);
  inherited Destroy;
end;

//---------------------------------------------------------------------------
//  ListViewEditWndProc
//
//  Purpose:  Custom Window Procedure for TListView's editor window
//---------------------------------------------------------------------------

procedure TForm1.ListViewEditWndProc(var Message: TMessage);
begin
  if Message.Msg = WM_WINDOWPOSCHANGING then
  begin
    // this inline editor has a bad habit of re-positioning itself
    // back on top of the Caption after every key typed in,
    // so let's stop it from moving
    with TWMWindowPosMsg(Message).WindowPos^ do flags := flags or SWP_NOMOVE;
    Message.Result := 0;
  end else
  begin
    // everything else
    Message.Result := CallWindowProc(OldListViewEditProc, hListViewEditWnd,
      Message.Msg, Message.WParam, Message.LParam);
  end;
end;

//---------------------------------------------------------------------------
//  ListView1DrawItem
//
//  Purpose:  Handler for the TListView::OnDrawItem event
//---------------------------------------------------------------------------

procedure TForm1.ListView1DrawItem(Sender: TCustomListView; Item: TListItem; Rect: TRect; State: TOwnerDrawState);
var
  LV: TCustomListViewAccess;
  R: TRect;
  P: TPoint;
  I: Integer;
  S: String;
begin
  LV := TCustomListViewAccess(Sender);

  // erase the entire item to start fresh
  R := Item.DisplayRect(drBounds);
  LV.Canvas.Brush.Color := LV.Color;
  LV.Canvas.FillRect(R);

  // see if the mouse is currently held down, and if so update the marker as needed
  if (GetKeyState(VK_LBUTTON) and $8000) <> 0 then
  begin
    // find the mouse cursor onscreen, convert the coordinates to client
    // coordinates on the list view
    GetCursorPos(P);
    ColumnToEdit := GetColumnAt(Item, LV.ScreenToClient(P));
  end;

  // loop through all of the columns drawing each column
  for I := 0 to LV.Columns.Count-1 do
  begin
    // determine the dimensions of the current column value
    if not GetColumnRect(Item, I, R) then
      Continue;

    // mimic the default behavior by only drawing a value as highlighted if
    // the entire item is selected, the particular column matches the marker,
    // and the ListView is not already editing
    if Item.Selected and (I = ColumnToEdit) and (not LV.IsEditing) then
    begin
      LV.Canvas.Brush.Color := clHighlight;
      LV.Canvas.Font.Color := clHighlightText;
    end else
    begin
      LV.Canvas.Brush.Color := LV.Color;
      LV.Canvas.Font.Color := LV.Font.Color;
    end;

    LV.Canvas.FillRect(R);

    // draw the column's text
    if I = 0 then
      S := Item.Caption
    else
      S := Item.SubItems[I-1];

    LV.Canvas.TextRect(R, R.Left + 2, R.Top, S);
  end;
end;

//---------------------------------------------------------------------------
//  ListView1Edited
//
//  Purpose:  Handler for the TListView::OnEdited event
//---------------------------------------------------------------------------

procedure TForm1.ListView1Edited(Sender: TObject; Item: TListItem; var S: string);
begin
  // ignore the Caption, let it do its default handling
  if ColumnToEdit <= 0 then Exit;

  // restore the previous window procedure for the inline editor
  if hListViewEditWnd <> 0 then
  begin
    SetWindowLongPtr(hListViewEditWnd, GWL_WNDPROC, LONG_PTR(OldListViewEditProc));
    hListViewEditWnd := 0;
  end;

  // assign the new text to the subitem being edited
  Item.SubItems[ColumnToEdit-1] := S;

  // prevent the default behavior from updating the Caption as well
  S := Item.Caption;
end;

//---------------------------------------------------------------------------
//  ListView1Editing
//
//  Purpose:  Handler for the TListView::OnEditing event
//---------------------------------------------------------------------------

procedure TForm1.ListView1Editing(Sender: TObject; Item: TListItem; var AllowEdit: Boolean);
var
  Wnd: HWND;
  R: TRect;
begin
  // ignore the Caption, let it do its default handling
  if ColumnToEdit <= 0 then Exit;

  // get the inline editor's handle
  Wnd := ListView_GetEditControl(ListView1.Handle);
  if Wnd = 0 then Exit;

  // determine the dimensions of the subitem being edited
  if not GetColumnRect(Item, ColumnToEdit, R) then Exit;

  // move the inline editor over the subitem
  MoveWindow(Wnd, R.Left, R.Top - 2, R.Right-R.Left, (R.Bottom-R.Top) + 4, TRUE);

  // update the inline editor's text with the subitem's text rather than the Caption
  SetWindowText(Wnd, PChar(Item.SubItems[ColumnToEdit-1]));

  // subclass the inline editor so we can catch its movements
  hListViewEditWnd := Wnd;
  OldListViewEditProc := Pointer(GetWindowLongPtr(Wnd, GWL_WNDPROC));
  SetWindowLongPtr(Wnd, GWL_WNDPROC, LONG_PTR(ListViewEditWndProcPtr));
end;

//---------------------------------------------------------------------------
//  ListView1MouseDown
//
//  Purpose:  Handler for the TListView::OnMouseDown event
//---------------------------------------------------------------------------

procedure TForm1.ListView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  Coord: TListViewCoord;
begin
  if GetIndexesAt(ListView1, Point(X, Y), Coord) then
  begin
    if Coord.Column <> ColumnToEdit then
    begin
      // update the marker
      ColumnToEdit := Coord.Column;

      // cancel the editing so that the listview won't go into
      // its edit mode immediately upon clicking the new item
      ListView1.Items[Coord.Item].CancelEdit;

      // update the display with a new highlight selection
      ListView1.Invalidate;
    end;
  end else
    ColumnToEdit := -1;
end;

end.
Accusal answered 31/5, 2012 at 14:45 Comment(8)
Nice code but it is a bit outdated. Will not compile in Delphi 2010.Loft
Well, I did originally post it, and last updated it, in 2005 afterall :) It is still relevant, just needs some minor tweaking.Accusal
I tried your code but I find that Item in Manual_GetIndexesAt is always nil regardless of where the mouse is clicked. Does your version work as expected?Loft
Under D2010, the Manual_...() functions should not be getting used at all, it should be using the ComCtl_...() functions instead. Is there a problem with GetComCtl32Version() returning the correct version number for comctl32.dll? Regardless, in order for TListView.GetItemAt() to support hit tests on sub-columns, you have to set the TListView.RowSelect property to true.Accusal
Thanks. The problem was I had TListView.RowSelect property set as False.Loft
What should GetComCtl32Version return with Windows 7? The ComCtl_...() functions are not being used unless I remark if GetComCtl32Version >= DWORD(MAKELONG(4, 70)) then?Loft
On my Win7 laptop, ComCtl32 is v6.16 when Runtime Themes are enabled and v5.82 when disabled. DllGetVersion() reports the right values, but the GetComCtl32Version() comparison evaluates to True only when Runtime Themes are disabled, because MAKELONG(4, 70) is 4587524 and MAKELONG(5, 82) is 5373957, but MAKELONG(6, 16) is 1048582! Swap the parameters when calling MAKELONG() (and fix a minor bug in GetComCtl32Version() when DllGetVersion() fails) and everything will work correctly. I have updated my answer.Accusal
@Remy - +1 for an updated example. How do you exit edit mode on a cell when the ListView is no longer in focus? For example: You enter edit mode on a cell and then change your mind about editing. The cell is now in edit mode. If you start mousing over the main menu to go somewhere else I'd like to take the ListView out of edit mode. How do you do that?Tulle
I
5

I took RRUZ's code and decided to make a self-contained unit of it, with a derived TListView object that supports multiple editable columns. It also allows you to move between editable items using the arrows, enter and tab.

unit EditableListView;

interface

uses
  Messages,
  Classes, StdCtrls, ComCtrls, System.Types,
  Generics.Collections;

Const
  ELV_EDIT = WM_USER + 16;

type
  TEditableListView = class(TListView)
  private
    FEditable: TList<integer>;

    FEditor: TEdit;
    FItem: TListItem;
    FEditColumn: integer;

    procedure EditListView(var AMessage: TMessage); message ELV_EDIT;

    procedure EditExit(Sender: TObject);
    procedure EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);

    procedure DoEdit;

    procedure CleanupEditable;
    function GetEditable(const I: integer): boolean;
    procedure SetEditable(const I: integer; const Value: boolean);
  protected
    procedure Click; override;
    function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    property Editable[const I: integer]: boolean read GetEditable write SetEditable;
  end;

implementation

uses
  Windows, SysUtils, CommCtrl, Controls;

{ TEditableListView }

constructor TEditableListView.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  FEditable := TList<integer>.Create;

  FEditor := TEdit.Create(self);
  FEditor.Parent := self;
  FEditor.OnExit := EditExit;
  FEditor.OnKeyDown := EditKeyDown;
  FEditor.Visible := false;

  ViewStyle := vsReport;   // Default to vsReport instead of vsIcon
end;

destructor TEditableListView.Destroy;
begin
  FEditable.Free;

  inherited Destroy;
end;

procedure TEditableListView.DoEdit;
begin
  if Assigned(FItem) Then
  begin
    // assign the value of the TEdit to the Subitem
    if FEditColumn = 0 then
      FItem.Caption := FEditor.Text
    else if FEditColumn > 0 then
      FItem.SubItems[FEditColumn - 1] := FEditor.Text;
  end;
end;

function TEditableListView.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean;
begin
  DoEdit;
  FEditor.Visible := false;
  SetFocus;

  Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
end;

procedure TEditableListView.CleanupEditable;
var
  I: integer;
begin
  for I := FEditable.Count - 1 downto 0 do
  begin
    if not Assigned(Columns.FindItemID(FEditable[I])) then
      FEditable.Delete(I);
  end;
end;

procedure TEditableListView.Click;
var
  LPoint: TPoint;
  LVHitTestInfo: TLVHitTestInfo;
begin
  LPoint := ScreenToClient(Mouse.CursorPos);
  FillChar(LVHitTestInfo, SizeOf(LVHitTestInfo), 0);
  LVHitTestInfo.pt := LPoint;
  // Check if the click was made in the column to edit
  if (perform(LVM_SUBITEMHITTEST, 0, LPARAM(@LVHitTestInfo)) <> -1) Then
    PostMessage(self.Handle, ELV_EDIT, LVHitTestInfo.iItem, LVHitTestInfo.iSubItem)
  else
    FEditor.Visible := false; //hide the TEdit

  inherited Click;
end;

procedure TEditableListView.EditExit(Sender: TObject);
begin
  DoEdit;
end;

procedure TEditableListView.EditKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  lNextRow, lNextCol: integer;
begin
  if Key in [VK_RETURN, VK_TAB, VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN] then
  begin
    DoEdit;

    lNextRow := FItem.Index;
    lNextCol := FEditColumn;
    case Key of
      VK_RETURN,
      VK_DOWN:
        lNextRow := lNextRow + 1;
      VK_UP:
        lNextRow := lNextRow - 1;
      VK_TAB,
      VK_RIGHT:
        lNextCol := lNextCol + 1;
      VK_LEFT:
        lNextCol := lNextCol - 1;
    end;

    if not ( (Key = VK_RIGHT) and (FEditor.SelStart+FEditor.SelLength < Length(FEditor.Text)) )
   and not ( (Key = VK_LEFT) and (FEditor.SelStart+FEditor.SelLength > 0) ) then
    begin
      Key := 0;

      if (lNextRow >= 0) and (lNextRow < Items.Count)
     and (lNextCol >= 0) and (lNextCol < Columns.Count) then
        PostMessage(self.Handle, ELV_EDIT, lNextRow, lNextCol);
    end;
  end;
end;

procedure TEditableListView.EditListView(var AMessage: TMessage);
var
  LRect: TRect;
begin
  if Editable[AMessage.LParam] then
  begin
    LRect.Top := AMessage.LParam;
    LRect.Left:= LVIR_BOUNDS;
    Perform(LVM_GETSUBITEMRECT, AMessage.wparam, LPARAM(@LRect));
    //get the current Item to edit
    FItem := Items[AMessage.wparam];
    FEditColumn := AMessage.LParam;
    //set the text of the Edit
    if FEditColumn = 0 then
      FEditor.Text := FItem.Caption
    else if FEditColumn > 0 then
      FEditor.Text := FItem.Subitems[FEditColumn-1]
    else
      FEditor.Text := '';
    //set the bounds of the TEdit
    FEditor.BoundsRect := LRect;
    //Show the TEdit
    FEditor.Visible := true;
    FEditor.SetFocus;
    FEditor.SelectAll;
  end
  else
    FEditor.Visible := false;
end;

function TEditableListView.GetEditable(const I: integer): boolean;
begin
  if (I > 0) and (I < Columns.Count) then
    Result := FEditable.IndexOf(Columns[I].ID) >= 0
  else
    Result := false;
  CleanupEditable;
end;

procedure TEditableListView.SetEditable(const I: integer; const Value: boolean);
var
  Lix: integer;
begin
  if (I > 0) and (I < Columns.Count) then
  begin
    Lix := FEditable.IndexOf(Columns[I].ID);
    if Value and (Lix < 0)then
      FEditable.Add(Columns[I].ID)
    else if not Value and (Lix >= 0) then
      FEditable.Delete(Lix);
  end;
  CleanupEditable;
end;

end.

EDIT1: Added detection for mousewheel scroll to exit editing.
EDIT2: Allow for moving the cursor within the edit box with the arrow keys

Ileana answered 27/8, 2015 at 13:45 Comment(0)
A
1

From the review queue:

For those interested, I've created a TListView extension based in RRUZ's answer

https://github.com/BakasuraRCE/TEditableListView

The code is as follows:

unit UnitEditableListView;

interface

uses
  Winapi.Windows,
  Winapi.Messages,
  Winapi.CommCtrl,
  System.Classes,
  Vcl.ComCtrls,
  Vcl.StdCtrls;

type
  ///
  /// Based on: https://stackoverflow.com/a/10836109
  ///
  TListView = class(Vcl.ComCtrls.TListView)
  strict private
    FListViewEditor: TEdit;
    FEditorItemIndex, FEditorSubItemIndex: Integer;
    FCursorPos: TPoint;

    // Create native item
    function CreateItem(Index: Integer; ListItem: TListItem): TLVItem;
    // Free TEdit
    procedure FreeEditorItemInstance;
    // Invalidate cursor position
    procedure ResetCursorPos;

    {
      TEdit Events
    }
    procedure ListViewEditorExit(Sender: TObject);
    procedure ListViewEditorKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure ListViewEditorKeyPress(Sender: TObject; var Key: Char);
    {
      Override Events
    }
    procedure Click; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;

    {
      Windows Events
    }
    { TODO -cenhancement : Scroll edit control with listview }
    procedure WMMouseWheel(var Message: TWMMouseWheel); message WM_MOUSEWHEEL;
    procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL;
    procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    ///
    /// Start edition on local position
    ///
    procedure EditCaptionAt(Point: TPoint);
  end;

implementation

uses
  Vcl.Controls;

{ TListView }

procedure TListView.Click;
begin
  inherited;
  // Get current point
  FCursorPos := ScreenToClient(Mouse.CursorPos);
  FreeEditorItemInstance;
end;

constructor TListView.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  // Create the TEdit and assign the OnExit event
  FListViewEditor := TEdit.Create(AOwner);
  with FListViewEditor do
  begin
    Parent := Self;
    OnKeyDown := ListViewEditorKeyDown;
    OnKeyPress := ListViewEditorKeyPress;
    OnExit := ListViewEditorExit;
    Visible := False;
  end;

end;

destructor TListView.Destroy;
begin
  // Free TEdit
  FListViewEditor.Free;
  inherited;
end;

procedure TListView.EditCaptionAt(Point: TPoint);
var
  Rect: TRect;
  CursorPos: TPoint;
  HitTestInfo: TLVHitTestInfo;
  CurrentItem: TListItem;
begin
  // Set position to handle
  HitTestInfo.pt := Point;

  // Get item select
  if ListView_SubItemHitTest(Handle, @HitTestInfo) = -1 then
    Exit;

  with HitTestInfo do
  begin
    FEditorItemIndex := iItem;
    FEditorSubItemIndex := iSubItem;
  end;

  // Nothing?
  if (FEditorItemIndex < 0) or (FEditorItemIndex >= Items.Count) then
    Exit;

  if FEditorSubItemIndex < 0 then
    Exit;

  CurrentItem := Items[ItemIndex];

  if not CanEdit(CurrentItem) then
    Exit;

  // Get bounds
  ListView_GetSubItemRect(Handle, FEditorItemIndex, FEditorSubItemIndex, LVIR_LABEL, @Rect);

  // set the text of the Edit
  if FEditorSubItemIndex = 0 then
    FListViewEditor.Text := CurrentItem.Caption
  else
  begin
    FListViewEditor.Text := CurrentItem.SubItems[FEditorSubItemIndex - 1];
  end;
  // Set the bounds of the TEdit
  FListViewEditor.BoundsRect := Rect;
  // Show the TEdit
  FListViewEditor.Visible := True;
  // Set focus
  FListViewEditor.SetFocus;
end;

procedure TListView.ResetCursorPos;
begin
  // Free cursos pos
  FCursorPos := Point(-1, -1);
end;

procedure TListView.FreeEditorItemInstance;
begin
  FEditorItemIndex := -1;
  FEditorSubItemIndex := -1;
  FListViewEditor.Visible := False; // Hide the TEdit
end;

procedure TListView.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);

  // F2 key start edit
  if (Key = VK_F2) then
    EditCaptionAt(FCursorPos);
end;

///
/// Create a LVItem
///
function TListView.CreateItem(Index: Integer; ListItem: TListItem): TLVItem;
begin
  with Result do
  begin
    mask := LVIF_PARAM or LVIF_IMAGE or LVIF_GROUPID;
    iItem := index;
    iSubItem := 0;
    iImage := I_IMAGECALLBACK;
    iGroupId := -1;
    pszText := PChar(ListItem.Caption);
{$IFDEF CLR}
    lParam := ListItem.GetHashCode;
{$ELSE}
    lParam := Winapi.Windows.lParam(ListItem);
{$ENDIF}
  end;
end;

procedure TListView.ListViewEditorExit(Sender: TObject);
begin
  // I have an instance?
  if FEditorItemIndex = -1 then
    Exit;

  // Assign the value of the TEdit to the Subitem
  if FEditorSubItemIndex = 0 then
    Items[FEditorItemIndex].Caption := FListViewEditor.Text
  else
    Items[FEditorItemIndex].SubItems[FEditorSubItemIndex - 1] := FListViewEditor.Text;

  // Raise OnEdited event
  Edit(CreateItem(FEditorItemIndex, Items[FEditorItemIndex]));

  // Free instanse
  FreeEditorItemInstance;
end;

procedure TListView.ListViewEditorKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  // ESCAPE key exit of editor
  if Key = VK_ESCAPE then
    FreeEditorItemInstance;
end;

procedure TListView.ListViewEditorKeyPress(Sender: TObject; var Key: Char);
begin
  // Update item on press ENTER
  if (Key = #$0A) or (Key = #$0D) then
    FListViewEditor.OnExit(Sender);
end;

procedure TListView.WMHScroll(var Message: TWMHScroll);
begin
  inherited;
  // Reset cursos pos
  ResetCursorPos;
  // Free instanse
  FreeEditorItemInstance;
end;

procedure TListView.WMMouseWheel(var Message: TWMMouseWheel);
begin
  inherited;
  // Reset cursos pos
  ResetCursorPos;
  // Free instanse
  FreeEditorItemInstance;
end;

procedure TListView.WMVScroll(var Message: TWMVScroll);
begin
  inherited;
  // Reset cursos pos
  ResetCursorPos;
  // Free instanse
  FreeEditorItemInstance;
end;

end.

The original poster's, Bakasura, answer had been deleted:

Screenshot of original answer

Actable answered 30/6, 2017 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.