In-place editing of a subitem in a TListView

一笑奈何 提交于 2019-12-03 07:46:07

问题


I have a ListView with 3 columns and would like to edit the third column, aka Subitem[1]. If I set ListView.ReadOnly to True, 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.


回答1:


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;



回答2:


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.



回答3:


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




回答4:


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:



来源:https://stackoverflow.com/questions/10835355/in-place-editing-of-a-subitem-in-a-tlistview

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!