In-place editing of a subitem in a TListView - delphi

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.

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;

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.

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

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:

Related

How to create popup menu with scroll bar that also supports sub-menus

I want to add scroll bars (and/or scroll wheel support) to my existing Delphi application's popup menus, because they are often higher than the screen, and the built in scrolling is not good enough. How to make a popup menu with scrollbar? would be a great solution for me, except that it doesn't support sub-menus, which I absolutely require. The author of that solution hasn't been on StackOverflow since last July, so I don't think he'll reply to my comment. Can anyone see how to modify that code to add support for sub-menus? In case it matters, I need it to work with Delphi 2007.
I share #KenWhite's reservations about how users might receive a huge menu. So apologies to him and readers whose sensibilities the following might offend ;=)
Anyway, I hope the code below shows that in principle, it is straightforward
to create a TreeView based on a TPopUpMenu (see the routine PopUpMenuToTree) which reflects the structure of the PopUpMenu, including sub-items,
and make use of the TreeView's automatic vertical scroll bar. In the code, the
PopUpMenu happens to be on the same form as the TreeView, but that's only for
compactness, of course - the PopUpMenu could be on anothe form entirely.
As mentioned in a comment, personally I would base something like this on a
TVirtualTreeView (http://www.soft-gems.net/index.php/controls/virtual-treeview)
because it is far more customisable than a standard TTreeView.
type
TForm1 = class(TForm)
PopupMenu1: TPopupMenu;
TreeView1: TTreeView; // alClient-aligned
Start1: TMenuItem;
procedure FormCreate(Sender: TObject);
procedure TreeView1Click(Sender: TObject);
private
protected
procedure MenuItemClick(Sender : TObject);
procedure PopUpMenuToTree(PopUpMenu : TPopUpMenu; TreeView : TTreeView);
public
end;
var
Form1: TForm1;
[...]
procedure TForm1.FormCreate(Sender: TObject);
var
Item,
SubItem : TMenuItem;
i,
j : Integer;
begin
// (Over)populate a PopUpMenu
for i := 1 to 50 do begin
Item := TMenuItem.Create(PopUpMenu1);
Item.Caption := 'Item ' + IntToStr(i);
Item.OnClick := MenuItemClick;
PopUpMenu1.Items.Add(Item);
for j := 1 to 5 do begin
SubItem := TMenuItem.Create(PopUpMenu1);
SubItem.Caption := Format('Item %d Subitem %d ', [i, j]);
SubItem.OnClick := MenuItemClick;
Item.Add(SubItem);
end;
end;
// Populate a TreeView from the PopUpMenu
PopUpMenuToTree(PopUpMenu1, TreeView1);
end;
procedure TForm1.MenuItemClick(Sender: TObject);
var
Item : TMenuItem;
begin
if Sender is TMenuItem then
Caption := TMenuItem(Sender).Caption + ' clicked';
end;
procedure TForm1.PopUpMenuToTree(PopUpMenu: TPopUpMenu;
TreeView: TTreeView);
// Populates the TreeView with the Items in the PopUpMenu
var
i : Integer;
Item : TMenuItem;
RootNode : TTreeNode;
procedure AddItem(Item : TMenuItem; ParentNode : TTreeNode);
var
Node : TTreeNode;
j : Integer;
begin
Node := TreeView.Items.AddChildObject(ParentNode, Item.Caption, Item);
for j := 0 to Item.Count - 1 do begin
AddItem(Item.Items[j], Node);
end;
end;
begin
TreeView.Items.BeginUpdate;
TreeView.Items.Clear;
try
for i := 0 to PopUpMenu.Items.Count - 1 do begin
AddItem(PopUpMenu.Items[i], Nil);
end;
finally
TreeView.Items.EndUpdate;
end;
end;
procedure TForm1.TreeView1Click(Sender: TObject);
var
Node : TTreeNode;
Item : TMenuItem;
begin
if Sender is TTreeView then begin
Node := TTreeView(Sender).Selected;
Item := TMenuItem(Node.Data);
Item.Click;
end;
end;
Here's what I have done, by merging How to make a popup menu with scrollbar?, MartynA's code, and some of my own:
unit PopupUnit;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
StdCtrls, Menus, ComCtrls;
type
TPopupMode = (pmStandard, pmCustom);
TPopupMenu = class(Menus.TPopupMenu)
private
FPopupForm: TForm;
FPopupMode: TPopupMode;
public
constructor Create(AOwner: TComponent); override;
procedure Popup(X, Y: Integer); override;
property PopupForm: TForm read FPopupForm write FPopupForm;
property PopupMode: TPopupMode read FPopupMode write FPopupMode;
end;
type
TPopupForm = class(TForm)
private
FPopupForm: TForm;
FPopupMenu: TPopupMenu;
FTreeView: TTreeView;
procedure DoResize;
procedure TreeViewClick(Sender: TObject);
procedure TreeViewCollapsedOrExpanded(Sender: TObject; Node: TTreeNode);
procedure TreeViewKeyPress(Sender: TObject; var Key: Char);
procedure WMActivate(var AMessage: TWMActivate); message WM_ACTIVATE;
protected
procedure CreateParams(var Params: TCreateParams); override;
public
constructor Create(AOwner: TComponent; APopupForm: TForm;
APopupMenu: TPopupMenu); reintroduce;
end;
var
PopupForm: TPopupForm;
implementation
{$R *.dfm}
{ TPopupForm }
constructor TPopupForm.Create(AOwner: TComponent; APopupForm: TForm;
APopupMenu: TPopupMenu);
procedure AddItem(Item : TMenuItem; ParentNode : TTreeNode);
var
I : Integer;
Node : TTreeNode;
begin
if Item.Caption <> '-' then begin
Node := FTreeView.Items.AddChildObject(ParentNode, Item.Caption, Item);
Node.ImageIndex := Item.ImageIndex;
for I := 0 to Item.Count - 1 do begin
AddItem(Item.Items[I], Node);
end;
end;
end;
var
I: Integer;
begin
inherited Create(AOwner);
BorderStyle := bsNone;
FPopupForm := APopupForm;
FPopupMenu := APopupMenu;
FTreeView := TTreeView.Create(Self);
FTreeView.Parent := Self;
FTreeView.Align := alClient;
FTreeView.BorderStyle := bsSingle;
FTreeView.Color := clMenu;
FTreeView.Images := FPopupMenu.Images;
FTreeView.ReadOnly := TRUE;
FTreeView.ShowHint := FALSE;
FTreeView.ToolTips := FALSE;
FTreeView.OnClick := TreeViewClick;
FTreeView.OnCollapsed := TreeViewCollapsedOrExpanded;
FTreeView.OnExpanded := TreeViewCollapsedOrExpanded;
FTreeView.OnKeyPress := TreeViewKeyPress;
FTreeView.Items.BeginUpdate;
try
FTreeView.Items.Clear;
for I := 0 to FPopupMenu.Items.Count - 1 do
begin
AddItem(FPopupMenu.Items[I], NIL);
end;
finally
FTreeView.Items.EndUpdate;
end;
DoResize;
end;
procedure TPopupForm.CreateParams(var Params: TCreateParams);
begin
inherited;
Params.WindowClass.Style := Params.WindowClass.Style or CS_DROPSHADOW;
end;
procedure TPopupForm.DoResize;
const
BORDER = 2;
var
ItemRect, TVRect : TRect;
MF : TForm;
Node : TTreeNode;
begin
TVRect := Rect(0, 0, 0, 0);
Node := FTreeView.Items[0];
while Node <> NIL do begin
ItemRect := Node.DisplayRect(TRUE);
ItemRect.Right := ItemRect.Right + FTreeView.Images.Width + 1;
if ItemRect.Left < TVRect.Left then
TVRect.Left := ItemRect.Left;
if ItemRect.Right > TVRect.Right then
TVRect.Right := ItemRect.Right;
if ItemRect.Top < TVRect.Top then
TVRect.Top := ItemRect.Top;
if ItemRect.Bottom > TVRect.Bottom then
TVRect.Bottom := ItemRect.Bottom;
Node := Node.GetNextVisible;
end;
MF := Application.MainForm;
if Top + TVRect.Bottom - TVRect.Top > MF.Top + MF.ClientHeight then begin
TVRect.Bottom := TVRect.Bottom -
(Top + TVRect.Bottom - TVRect.Top - (MF.Top + MF.ClientHeight));
end;
if Left + TVRect.Right - TVRect.Left > MF.Left + MF.ClientWidth then begin
TVRect.Right := TVRect.Right -
(Left + TVRect.Right - TVRect.Left - (MF.Left + MF.ClientWidth));
end;
ClientHeight := TVRect.Bottom - TVRect.Top + BORDER * 2;
ClientWidth := TVRect.Right - TVRect.Left + BORDER * 2;
end;
procedure TPopupForm.TreeViewClick(Sender: TObject);
var
Node : TTreeNode;
Item : TMenuItem;
begin
if Sender is TTreeView then begin
Node := TTreeView(Sender).Selected;
if assigned(Node) then begin
Item := TMenuItem(Node.Data);
if assigned(Item.OnClick) then begin
Item.Click;
Close;
end;
end;
end;
end;
procedure TPopupForm.TreeViewCollapsedOrExpanded(Sender: TObject;
Node: TTreeNode);
begin
DoResize;
end;
procedure TPopupForm.TreeViewKeyPress(Sender: TObject; var Key: Char);
begin
if Ord(Key) = VK_RETURN then begin
TreeViewClick(Sender);
end
else if Ord(Key) = VK_ESCAPE then begin
Close;
end;
end;
procedure TPopupForm.WMActivate(var AMessage: TWMActivate);
begin
SendMessage(FPopupForm.Handle, WM_NCACTIVATE, 1, 0);
inherited;
if AMessage.Active = WA_INACTIVE then
Release;
FTreeView.Select(NIL, []);
end;
{ TPopupMenu }
constructor TPopupMenu.Create(AOwner: TComponent);
begin
inherited;
FPopupMode := pmStandard;
end;
procedure TPopupMenu.Popup(X, Y: Integer);
begin
case FPopupMode of
pmCustom:
with TPopupForm.Create(nil, FPopupForm, Self) do
begin
Top := Y;
Left := X;
Show;
end;
pmStandard: inherited;
end;
end;
end.

how do i correctly draw gif image from resource inside listview?

i have listview item that i try to add image to its subitem as status-image i already can set image from image list but i want to get ride of image list and use images from resource i already created resource file and try to add Tgifimage to item draw but now image image not drawing
here is my code
procedure TForm1.Add_Item(strCaption: String; ListView: TListView;
strFile: String; boolBlink: Boolean; strUniqueID: String;
CurrentStatus: string);
var
Item: TListItem;
begin
Item := ListView1.Items.Add;
Item.Caption := '';
Item.SubItems.Add(strCaption);// subitem 0
Item.SubItems.AddObject( '0', nil ); // subitem 1
Item.SubItems.Add( strUniqueID ); // subitem 2 // UniqueID
Item.SubItems.Add('0'); // subitem 3 // Next User Idx (beside)
Item.SubItems.Add(Currentstatus); // subitem 4 // StateIdx
Item.Data := nil;
SetItemStatusGif(Item, Currentstatus); // here start to set status
end;
// here setitemStatusGif procedure
procedure TForm1.SetItemStatusGif(Item: TListItem; State: String);
var
ResStream: TResourceStream;
aGif: TGifImage;
strStateImg: String;
ImgIdx: Integer;
begin
strStateImg := 'State_' + State;
ImgIdx := StatusGifs.IndexOf(strStateImg);
if ImgIdx <> -1 then
aGif := TGifImage(StatusGifs.Objects[ImgIdx])
else
begin
try
ResStream := TResourceStream.Create(HInstance, strStateImg, RT_RCDATA);
try
aGif := TGifImage.Create;
try
aGif.LoadFromStream(ResStream);
aGif.Transparent := True;
StatusGifs.AddObject(strStateImg, aGif);
except
aGif.Free;
raise;
end;
finally
ResStream.Free;
end;
except
aGif := nil;
end;
end;
Item.SubItems.Objects[1] := aGif;
ListView1.UpdateItems(Item.Index, Item.Index);
end;
// here listview draw event code
procedure TForm1.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
Rect: TRect; State: TOwnerDrawState);
Var
xOff, yOff: Integer;
R: TRect;
i: Integer;
NewRect: TRect;
begin
With TListView(Sender).Canvas do
begin // User State Image
if (StrToint(Item.SubItems[1]) <> 0) And (Item.SubItems[1] <> '') then
begin
NewRect := Rect;
NewRect.Left := NewRect.Left + 2;
NewRect.Width := 24;
Newrect.Height := 23;
NewRect.Top := NewRect.Top;
NewRect.Bottom := NewRect.Bottom;
if Panel2.Visible AND (Item.Index = 0) then
//do nothing
else
Sender.Canvas.StretchDraw( NewRect, TGIFImage( Item.SubItems.Objects[1]) );
end;
end;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
ListView1.Invalidate; // This is for animation over ListView Canvas
end;
We covered this a month ago in your other question:
how do i update listview item index inside thread
In that question, you were downloading images from online, where the download thread creates the TGifImage object and assigns it to a TListItem for drawing. Now, you want to add resource images. You still have to create a TGifImage object for them, and assign that to your TListItem object so you can draw it. You just don't need to use a thread to handle that. When you add a new item to the list, you can create the TGifImage object immediately and fill it from the resource, eg:
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ListView1Deletion(Sender: TObject; Item: TListItem);
...
private
StatusGifs: TStringList;
procedure Add_Item(strCaption: String; ListView: TListView; strFile: String; boolBlink: Boolean; strUniqueID: String; CurrentStatus: string);
procedure StatuseHandle;
procedure SetItemStatusGif(Item: TListItem; State: String);
...
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
StatusGifs := TStringList.Create(True);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
StatusGifs.Free;
end;
procedure TForm1.ListView1Deletion(Sender: TObject; Item: TListItem);
begin
TGifImage(Item.SubItems.Objects[1]).Free;
TGifImage(Item.Data).Free;
end;
procedure TForm1.Add_Item(strCaption: String; ListView: TListView; strFile: String; boolBlink: Boolean; strUniqueID: String; CurrentStatus: string);
var
Item: TListItem;
begin
Item := ListView1.Items.Add;
Item.Caption := '';
Item.SubItems.Add( strCaption ); // subitem 0
Item.SubItems.AddObject( 'IMA', TGifImage.Create ); // subitem 1
Item.SubItems.Add( strUniqueID ); // subitem 2 // UniqueID
Item.SubItems.Add('0'); // subitem 3 // Next User Idx (beside)
Item.SubItems.Add(Currentstatus); // subitem 4 // StateIdx
Item.Data := nil; // populated by TURLDownload
SetItemStatusGif(Item, Currentstatus);
TURLDownload.Create(strFile, UpdateVisual, Item);
end;
procedure TForm1.StatuseHandle;
var
i : integer;
Item : TListItem;
begin
try
for i := 0 to ListView1.Items.Count-1 do
begin
Item := ListView1.Items[i];
if Item.SubItems[2] = Trim(LineToid) then
begin
Item.SubItems[4] := LineTostatus;
SetItemStatusGif(Item, LineTostatus);
end;
end;
except
end;
end;
procedure TForm1.SetItemStatusGif(Item: TListItem; State: String);
var
ResStream : TResourceStream;
aGif : TGifImage;
strStateImg : String;
ImgIdx: Integer;
begin
strStateImg := 'State_' + State;
ImgIdx := StatusGifs.IndexOf(strStateImg);
if ImgIdx <> -1 then
aGif := TGifImage(StatusGifs.Objects[ImgIdx])
else
begin
try
ResStream := TResourceStream.Create(HInstance, strStateImg, RT_RCDATA);
try
aGif := TGifImage.Create;
try
aGif.LoadFromStream(ResStream);
aGif.Transparent := True;
StatusGifs.AddObject(strStateImg, aGif);
except
aGif.Free;
raise;
end;
finally
ResStream.Free;
end;
except
aGif := nil;
end;
end;
TGifImage(Item.SubItems.Objects[1]).Assign(aGif);
ListView1.UpdateItems(Item.Index, Item.Index);
end;

Ownerdraw TListBox child controls are not moved by scrolling

procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
begin
inherited;
TListBox(Control).Canvas.FillRect(Rect);
TListBox(Control).Canvas.TextOut(Rect.Left+5, Rect.Top+8, TListBox(Control).Items[Index]);
if odSelected in State then
begin
Button.Left:=Rect.Right-80;
Button.Top:=Rect.Top+4;
Button.Visible:=true;
Button.Invalidate;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ListBox1.DoubleBuffered:=true;
ListBox1.ItemHeight:=30;
ListBox1.Style:=lbOwnerDrawFixed;
Button:=TButton.Create(ListBox1);
Button.Parent:=ListBox1;
Button.DoubleBuffered:=true;
Button.Visible:=false;
Button.Width:=50;
Button.Height:=20;
Button.Caption:='BTN';
end;
The repaint problem only exists when using ScrollBar or sending WM_VSCROLL message to my ListBox. All normally drawn when I change selection by using keyboard arrows or mouse clicks. Problem also not exists when selected item are visible by scrolling and not leave visible area.
I think that Button.Top property still have an old value before DrawItem calls, and change (to -30px for example) later.
The problem is that you are using the OnDrawItem event to make changes to the UI (in this case, positioning the button). Do not do that, the event is for DRAWING ONLY.
I would suggest that you either:
subclass the ListBox to handle the WM_VSCROLL message and have your message handler reposition the button as needed.
var
PrevListBoxWndProc: TWndMethod;
procedure TForm1.FormCreate(Sender: TObject);
begin
PrevListBoxWndProc := ListBox1.WindowProc;
ListBox1.WindowProc := ListBoxWndProc;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
ListBox1.WindowProc := PrevListBoxWndProc;
end;
procedure TForm1.PositionButton(Index: Integer);
var
R: TRect;
begin
if Index <= -1 then
Button.Visible := False
else
begin
R := ListBox1.ItemRect(Index);
Button.Left := R.Right - 80;
Button.Top := R.Top + 4;
Button.Visible := True;
end;
end;
var
LastIndex: Integer = -1;
procedure TForm1.ListBox1Click(Sender: TObject);
var
Index: Integer;
begin
Index := ListBox1.ItemIndex;
if Index <> LastIndex then
begin
LastIndex := Index;
PositionButton(Index);
end;
end;
procedure TForm1.ListBoxWndProc(var Message: TMessage);
begin
PrevListBoxWndProc(Message);
if Message.Msg = WM_VSCROLL then
PositionButton(ListBox1.ItemIndex);
end;
get rid of the TButton altogether. Use OnDrawItem to draw an image of a button (you can use DrawFrameControl() or DrawThemeBackground() for that) directly onto the ListBox, and then use the OnMouseDown/Up or OnClick event to check if the mouse is over the "button" and if so act accordingly as needed.
var
MouseX: Integer = -1;
MouseY: Integer = -1;
procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
Rect: TRect; State: TOwnerDrawState);
var
R: TRect;
P: TPoint;
BtnState: UINT;
begin
TListBox(Control).Canvas.FillRect(Rect);
TListBox(Control).Canvas.TextOut(Rect.Left+5, Rect.Top+8, TListBox(Control).Items[Index]);
if not (odSelected in State) then Exit;
R := Rect(Rect.Right-80, Rect.Top+4, Rect.Right-30, Rect.Top+24);
P := Point(MouseX, MouseY);
BtnState := DFCS_BUTTONPUSH;
if PtInRect(R, P) then BtnState := BtnState or DFCS_PUSHED;
DrawFrameControl(TListBox(Control).Canvas.Handle, R, DFC_BUTTON, BtnState);
InflateRect(R, -4, -4);
DrawText(TListBox(Control).Canvas.Handle, 'BTN', 3, R, DT_CENTER or DT_VCENTER or DT_SINGLELINE);
end;
procedure TForm1.ListBox1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button <> mbLeft then Exit;
MouseX := X;
MouseY := Y;
ListBox1.Invalidate;
end;
procedure TForm1.ListBox1MouseUp(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button <> mbLeft then Exit;
MouseX := -1;
MouseY := -1;
ListBox1.Invalidate;
end;
procedure TForm1.ListBox1Click(Sender: TObject);
var
P: TPoint;
R: TRect;
Index: Integer;
begin
P := Point(MouseX, MouseY);
Index := ListBox1.ItemAtPos(P, True);
if (Index = -1) or (Index <> ListBox1.ItemIndex) then Exit;
R := ListBox1.ItemRect(Index);
R := Rect(R.Right-80, R.Top+4, R.Right-30, R.Top+24);
if not PtInRect(R, P) then Exit;
// click is on selected item's "button", do something...
end;

OnStartDrag not being called on control (DragMode = dmManual)

Using: Delphi XE2 Update 4.1, 32-bit VCL application, Windows 8
If DragMode is set to dmAutomatic the the OnStartDrag event is called; however if the DragMode is set to dmManual, the OnStartDrag event is bypassed.
Is this by design? How to ensure that OnStartDrag event is called?
EDIT: Code posted on request. The event in question is TTableDesigner.LblStartDrag which is not being executed after a call to BeginDrag (in TTableDesigner.LblOnMouseDown) .
unit uTableDesigner;
interface
uses
System.SysUtils, System.Classes, Vcl.Controls, Graphics, JvCaptionPanel,
StdCtrls, ExtCtrls;
type
TMyTable = record
TableName: String;
TableFields: TStrings;
TableObject: Pointer;
end;
PMyTable = ^TMyTable;
TTableDesigner = class(TCustomControl)
procedure CreateWnd; override;
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure LblOnMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
procedure LblDragDrop(Sender, Source: TObject; X, Y: Integer);
procedure LblDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
procedure LblEndDrag(Sender, Target: TObject; X, Y: Integer);
procedure LblStartDrag(Sender: TObject; var DragObject: TDragObject);
// procedure Paint; override;
private
{ Private declarations }
FTableList: TList;
FCaptionPanelList: TList;
FPanelSlot_Left: Integer;
FPanelSlot_Top: Integer;
FStartDragPnl: TJvCaptionPanel;
FDragHoverPnl: TJvCaptionPanel;
FEndDragPnl: TJvCaptionPanel;
procedure HighlightPanelLabel(ALabel: TLabel);
protected
{ Protected declarations }
public
{ Public declarations }
procedure AddTable(const ATableName: String; const AFields: TStrings);
procedure DeleteTable(const ATableName: String);
procedure DeleteAllTables;
published
{ Published declarations }
property Align;
property Visible;
property Color;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TTableDesigner]);
end;
constructor TTableDesigner.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FTableList := TList.Create;
FCaptionPanelList := TList.Create;
FPanelSlot_Left := 40;
FPanelSlot_Top := 40;
end;
destructor TTableDesigner.Destroy;
begin
DeleteAllTables;
FTableList.Free;
FCaptionPanelList.Free;
inherited;
end;
procedure TTableDesigner.CreateWnd;
begin
inherited;
end;
procedure TTableDesigner.AddTable(const ATableName: String; const AFields: TStrings);
var
pnl: TJvCaptionPanel;
c, h, j: Integer;
lbl: TLabel;
MyTable: PMyTable;
begin
pnl := TJvCaptionPanel.Create(Self);
pnl.Parent := Self;
pnl.Color := clWhite;
pnl.Caption := ATableName;
pnl.CaptionPosition := dpTop;
pnl.Left := FPanelSlot_Left;
pnl.Top := FPanelSlot_Top;
// FPanelSlot_Left := FPanelSlot_Left + pnl.Width + 40;
// if FPanelSlot_Left > ClientWidth - 100 then
// begin
// FPanelSlot_Left := 40;
//
// j := 0;
// for c := 0 to FTableList.Count - 1 do
// if j < TJvCaptionPanel(TMyTable(FTableList.Items[c]^).TableObject).Height then
// j := TJvCaptionPanel(TMyTable(FTableList.Items[c]^).TableObject).Height;
//
// FPanelSlot_Top := FPanelSlot_Top + j + 40;
// end;
h := 0;
for c := 0 to AFields.Count - 1 do
begin
lbl := TLabel.Create(pnl);
lbl.Parent := pnl;
lbl.Align := alTop;
lbl.Caption := AFields[c];
lbl.Transparent := False;
lbl.ParentColor := False;
lbl.DragKind := dkDrag;
lbl.OnMouseDown := LblOnMouseDown;
lbl.OnDragDrop := LblDragDrop;
lbl.OnDragOver := LblDragOver;
lbl.OnEndDrag := LblEndDrag;
lbl.OnStartDrag := LblStartDrag;
// lbl.DragMode := dmAutomatic;
h := h + lbl.Height + 4;
end;
pnl.ClientHeight := pnl.CaptionHeight + h;
MyTable := AllocMem(SizeOf(TMyTable));
Initialize(MyTable^);
MyTable.TableName := ATableName;
MyTable.TableFields := TStringList.Create;
MyTable.TableFields.Assign(AFields);
MyTable.TableObject := pnl;
FTableList.Add(MyTable);
end;
procedure TTableDesigner.DeleteTable(const ATableName: String);
var
c: Integer;
begin
for c := 0 to FTableList.Count - 1 do
if TMyTable(FTableList.Items[c]^).TableName = ATableName then
begin
TJvCaptionPanel(TMyTable(FTableList.Items[c]^).TableObject).Free;
TMyTable(FTableList.Items[c]^).TableFields.Free;
Finalize(TMyTable(FTableList.Items[c]^));
FreeMem(FTableList.Items[c]);
FTableList.Delete(c);
Break;
end;
end;
procedure TTableDesigner.DeleteAllTables;
var
c: Integer;
begin
for c := FTableList.Count - 1 downto 0 do
begin
TJvCaptionPanel(TMyTable(FTableList.Items[c]^).TableObject).Free;
TMyTable(FTableList.Items[c]^).TableFields.Free;
Finalize(TMyTable(FTableList.Items[c]^));
FreeMem(FTableList.Items[c]);
FTableList.Delete(c);
end;
end;
procedure TTableDesigner.HighlightPanelLabel(ALabel: TLabel);
var
pnl: TJvCaptionPanel;
c: Integer;
begin
pnl := TJvCaptionPanel(ALabel.Parent);
for c := 0 to pnl.ControlCount - 1 do
if pnl.Controls[c] = ALabel then
TLabel(pnl.Controls[c]).Color := clHighlight
else
TLabel(pnl.Controls[c]).Color := pnl.Color;
end;
procedure TTableDesigner.LblOnMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
HighlightPanelLabel(TLabel(Sender));
BeginDrag(False, 4);
end;
procedure TTableDesigner.LblDragDrop(Sender, Source: TObject; X, Y: Integer);
begin
FEndDragPnl := TJvCaptionPanel(TLabel(Sender).Parent);
FEndDragPnl.Color := clWhite;
end;
procedure TTableDesigner.LblDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
begin
FDragHoverPnl := TJvCaptionPanel(TLabel(Sender).Parent);
FDragHoverPnl.Color := clGreen;
Accept := True;
end;
procedure TTableDesigner.LblEndDrag(Sender, Target: TObject; X, Y: Integer);
begin
TJvCaptionPanel(TLabel(Sender).Parent).Color := clPurple;
end;
procedure TTableDesigner.LblStartDrag(Sender: TObject; var DragObject: TDragObject);
begin
FStartDragPnl := TJvCaptionPanel(TLabel(Sender).Parent);
FStartDragPnl.Color := clRed;
end;
// procedure TTableDesigner.Paint;
// var
// c: Integer;
// begin
// inherited;
//
// // Canvas.Pen.Mode := pmBlack;
// // Canvas.Pen.Color := clBlack;
// // Canvas.Pen.Style := psSolid;
// // Canvas.Pen.Width := 1;
// // Canvas.MoveTo(50, 50);
// // Canvas.LineTo(500, 500);
//
// end;
end.
You're in a method of 'TTableDesigner', if you do not qualify a method 'Self' is implied. So the 'BeginDrag' call applies to the TableDesigner object.
You'd rather call 'TLabel(Sender).BeginDrag(..'.

Component to display log info in Delphi

I have a number of complex processing tasks that will produce messages, warnings, and fatal errors. I want to be able to display these messages in a task-independent component. My requirements are:
Different kinds of messages are displayed in different font and/or background colors.
The display can be filtered to include or exclude each kind of message.
The display will properly handle long messages by wrapping them and displaying the entire message.
Each message can have a data reference of some kind attached, and the message can be selected as an entity (eg, writing into an RTF memo won't work).
In essence, I'm looking for some kind of listbox like component that supports colors, filtering, and line wrapping. Can anyone suggest such a component (or another one) to use as the basis for my log display?
Failing that, I'll write my own. My initial thought is that I should base the component on a TDBGrid with a built-in TClientDataset. I would add messages to the client dataset (with a column for message type) and handle filtering through data set methods and coloring through the grid's draw methods.
Your thoughts on this design are welcome.
[Note: At this time I'm not particularly interested in writing the log to a file or integrating with Windows logging (unless doing so solves my display problem)]
I've written a log component that does most of what you need and it is based on VitrualTreeView. I've had to alter the code a bit to remove some dependencies, but it compiles fine (although it hasn't been tested after the alterations). Even if it's not exactly what you need, it might give you a good base to get started.
Here's the code
unit UserInterface.VirtualTrees.LogTree;
// Copyright (c) Paul Thornton
interface
uses
Classes, SysUtils, Graphics, Types, Windows, ImgList,
Menus,
VirtualTrees;
type
TLogLevel = (llNone,llError,llInfo,llWarning,llDebug);
TLogLevels = set of TLogLevel;
TLogNodeData = record
LogLevel: TLogLevel;
Timestamp: TDateTime;
LogText: String;
end;
PLogNodeData = ^TLogNodeData;
TOnLog = procedure(Sender: TObject; var LogText: String; var
CancelEntry: Boolean; LogLevel: TLogLevel) of object;
TOnPopupMenuItemClick = procedure(Sender: TObject; MenuItem:
TMenuItem) of object;
TVirtualLogPopupmenu = class(TPopupMenu)
private
FOwner: TComponent;
FOnPopupMenuItemClick: TOnPopupMenuItemClick;
procedure OnMenuItemClick(Sender: TObject);
public
constructor Create(AOwner: TComponent); override;
property OnPopupMenuItemClick: TOnPopupMenuItemClick read
FOnPopupMenuItemClick write FOnPopupMenuItemClick;
end;
TVirtualLogTree = class(TVirtualStringTree)
private
FOnLog: TOnLog;
FOnAfterLog: TNotifyEvent;
FHTMLSupport: Boolean;
FAutoScroll: Boolean;
FRemoveControlCharacters: Boolean;
FLogLevels: TLogLevels;
FAutoLogLevelColours: Boolean;
FShowDateColumn: Boolean;
FShowImages: Boolean;
FMaximumLines: Integer;
function DrawHTML(const ARect: TRect; const ACanvas: TCanvas;
const Text: String; Selected: Boolean): Integer;
function GetCellText(const Node: PVirtualNode; const Column:
TColumnIndex): String;
procedure SetLogLevels(const Value: TLogLevels);
procedure UpdateVisibleItems;
procedure OnPopupMenuItemClick(Sender: TObject; MenuItem: TMenuItem);
procedure SetShowDateColumn(const Value: Boolean);
procedure SetShowImages(const Value: Boolean);
procedure AddDefaultColumns(const ColumnNames: array of String;
const ColumnWidths: array of Integer);
function IfThen(Condition: Boolean; TrueResult,
FalseResult: Variant): Variant;
function StripHTMLTags(const Value: string): string;
function RemoveCtrlChars(const Value: String): String;
protected
procedure DoOnLog(var LogText: String; var CancelEntry: Boolean;
LogLevel: TLogLevel); virtual;
procedure DoOnAfterLog; virtual;
procedure DoAfterCellPaint(Canvas: TCanvas; Node: PVirtualNode;
Column: TColumnIndex; CellRect: TRect); override;
procedure DoGetText(Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var Text: String); override;
procedure DoFreeNode(Node: PVirtualNode); override;
function DoGetImageIndex(Node: PVirtualNode; Kind: TVTImageKind;
Column: TColumnIndex; var Ghosted: Boolean; var Index: Integer):
TCustomImageList; override;
procedure DoPaintText(Node: PVirtualNode; const Canvas: TCanvas;
Column: TColumnIndex; TextType: TVSTTextType); override;
procedure Loaded; override;
public
constructor Create(AOwner: TComponent); override;
procedure Log(Value: String; LogLevel: TLogLevel = llInfo;
TimeStamp: TDateTime = 0);
procedure LogFmt(Value: String; const Args: array of Const;
LogLevel: TLogLevel = llInfo; TimeStamp: TDateTime = 0);
procedure SaveToFileWithDialog;
procedure SaveToFile(const Filename: String);
procedure SaveToStrings(const Strings: TStrings);
procedure CopyToClipboard; reintroduce;
published
property OnLog: TOnLog read FOnLog write FOnLog;
property OnAfterLog: TNotifyEvent read FOnAfterLog write FOnAfterLog;
property HTMLSupport: Boolean read FHTMLSupport write FHTMLSupport;
property AutoScroll: Boolean read FAutoScroll write FAutoScroll;
property RemoveControlCharacters: Boolean read
FRemoveControlCharacters write FRemoveControlCharacters;
property LogLevels: TLogLevels read FLogLevels write SetLogLevels;
property AutoLogLevelColours: Boolean read FAutoLogLevelColours
write FAutoLogLevelColours;
property ShowDateColumn: Boolean read FShowDateColumn write
SetShowDateColumn;
property ShowImages: Boolean read FShowImages write SetShowImages;
property MaximumLines: Integer read FMaximumLines write FMaximumLines;
end;
implementation
uses
Dialogs,
Clipbrd;
resourcestring
StrSaveLog = '&Save';
StrCopyToClipboard = '&Copy';
StrTextFilesTxt = 'Text files (*.txt)|*.txt|All files (*.*)|*.*';
StrSave = 'Save';
StrDate = 'Date';
StrLog = 'Log';
constructor TVirtualLogTree.Create(AOwner: TComponent);
begin
inherited;
FAutoScroll := TRUE;
FHTMLSupport := TRUE;
FRemoveControlCharacters := TRUE;
FShowDateColumn := TRUE;
FShowImages := TRUE;
FLogLevels := [llError, llInfo, llWarning, llDebug];
NodeDataSize := SizeOf(TLogNodeData);
end;
procedure TVirtualLogTree.DoAfterCellPaint(Canvas: TCanvas; Node: PVirtualNode;
Column: TColumnIndex; CellRect: TRect);
var
ColWidth: Integer;
begin
inherited;
if Column = 1 then
begin
if FHTMLSupport then
ColWidth := DrawHTML(CellRect, Canvas, GetCellText(Node,
Column), Selected[Node])
else
ColWidth := Canvas.TextWidth(GetCellText(Node, Column));
if not FShowDateColumn then
ColWidth := ColWidth + 32; // Width of image
if ColWidth > Header.Columns[1].MinWidth then
Header.Columns[1].MinWidth := ColWidth;
end;
end;
procedure TVirtualLogTree.DoFreeNode(Node: PVirtualNode);
var
NodeData: PLogNodeData;
begin
inherited;
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
NodeData.LogText := '';
end;
function TVirtualLogTree.DoGetImageIndex(Node: PVirtualNode; Kind: TVTImageKind;
Column: TColumnIndex; var Ghosted: Boolean;
var Index: Integer): TCustomImageList;
var
NodeData: PLogNodeData;
begin
Images.Count;
if ((FShowImages) and (Kind in [ikNormal, ikSelected])) and
(((FShowDateColumn) and (Column <= 0)) or
((not FShowDateColumn) and (Column = 1))) then
begin
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
case NodeData.LogLevel of
llError: Index := 3;
llInfo: Index := 2;
llWarning: Index := 1;
llDebug: Index := 0;
else
Index := 4;
end;
end;
Result := inherited DoGetImageIndex(Node, Kind, Column, Ghosted, Index);
end;
procedure TVirtualLogTree.DoGetText(Node: PVirtualNode; Column: TColumnIndex;
TextType: TVSTTextType; var Text: String);
begin
inherited;
if (TextType = ttNormal) and ((Column <= 0) or (not FHTMLSupport)) then
Text := GetCellText(Node, Column)
else
Text := '';
end;
procedure TVirtualLogTree.DoOnAfterLog;
begin
if Assigned(FOnAfterLog) then
FOnAfterLog(Self);
end;
procedure TVirtualLogTree.DoOnLog(var LogText: String; var
CancelEntry: Boolean; LogLevel: TLogLevel);
begin
if Assigned(FOnLog) then
FOnLog(Self, LogText, CancelEntry, LogLevel);
end;
procedure TVirtualLogTree.DoPaintText(Node: PVirtualNode; const Canvas: TCanvas;
Column: TColumnIndex; TextType: TVSTTextType);
begin
inherited;
Canvas.Font.Color := clBlack;
end;
function TVirtualLogTree.GetCellText(const Node: PVirtualNode; const
Column: TColumnIndex): String;
var
NodeData: PLogNodeData;
begin
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
case Column of
-1, 0: Result := concat(DateTimeToStr(NodeData.Timestamp), '.',
FormatDateTime('zzz', NodeData.Timestamp));
1: Result := NodeData.LogText;
end;
end;
procedure TVirtualLogTree.AddDefaultColumns(
const ColumnNames: array of String; const ColumnWidths: array of Integer);
var
i: Integer;
Column: TVirtualTreeColumn;
begin
Header.Columns.Clear;
if High(ColumnNames) <> high(ColumnWidths) then
raise Exception.Create('Number of column names must match the
number of column widths.') // Do not localise
else
begin
for i := low(ColumnNames) to high(ColumnNames) do
begin
Column := Header.Columns.Add;
Column.Text := ColumnNames[i];
if ColumnWidths[i] > 0 then
Column.Width := ColumnWidths[i]
else
begin
Header.AutoSizeIndex := Column.Index;
Header.Options := Header.Options + [hoAutoResize];
end;
end;
end;
end;
procedure TVirtualLogTree.Loaded;
begin
inherited;
TreeOptions.PaintOptions := TreeOptions.PaintOptions - [toShowRoot,
toShowTreeLines, toShowButtons] + [toUseBlendedSelection,
toShowHorzGridLines, toHideFocusRect];
TreeOptions.SelectionOptions := TreeOptions.SelectionOptions +
[toFullRowSelect, toRightClickSelect];
AddDefaultColumns([StrDate,
StrLog],
[170,
120]);
Header.AutoSizeIndex := 1;
Header.Columns[1].MinWidth := 300;
Header.Options := Header.Options + [hoAutoResize];
if (PopupMenu = nil) and (not (csDesigning in ComponentState)) then
begin
PopupMenu := TVirtualLogPopupmenu.Create(Self);
TVirtualLogPopupmenu(PopupMenu).OnPopupMenuItemClick :=
OnPopupMenuItemClick;
end;
SetShowDateColumn(FShowDateColumn);
end;
procedure TVirtualLogTree.OnPopupMenuItemClick(Sender: TObject;
MenuItem: TMenuItem);
begin
if MenuItem.Tag = 1 then
SaveToFileWithDialog
else
if MenuItem.Tag = 2 then
CopyToClipboard;
end;
procedure TVirtualLogTree.SaveToFileWithDialog;
var
SaveDialog: TSaveDialog;
begin
SaveDialog := TSaveDialog.Create(Self);
try
SaveDialog.DefaultExt := '.txt';
SaveDialog.Title := StrSave;
SaveDialog.Options := SaveDialog.Options + [ofOverwritePrompt];
SaveDialog.Filter := StrTextFilesTxt;
if SaveDialog.Execute then
SaveToFile(SaveDialog.Filename);
finally
FreeAndNil(SaveDialog);
end;
end;
procedure TVirtualLogTree.SaveToFile(const Filename: String);
var
SaveStrings: TStringList;
begin
SaveStrings := TStringList.Create;
try
SaveToStrings(SaveStrings);
SaveStrings.SaveToFile(Filename);
finally
FreeAndNil(SaveStrings);
end;
end;
procedure TVirtualLogTree.CopyToClipboard;
var
CopyStrings: TStringList;
begin
CopyStrings := TStringList.Create;
try
SaveToStrings(CopyStrings);
Clipboard.AsText := CopyStrings.Text;
finally
FreeAndNil(CopyStrings);
end;
end;
function TVirtualLogTree.IfThen(Condition: Boolean; TrueResult,
FalseResult: Variant): Variant;
begin
if Condition then
Result := TrueResult
else
Result := FalseResult;
end;
function TVirtualLogTree.StripHTMLTags(const Value: string): string;
var
TagBegin, TagEnd, TagLength: integer;
begin
Result := Value;
TagBegin := Pos( '<', Result); // search position of first <
while (TagBegin > 0) do
begin
TagEnd := Pos('>', Result);
TagLength := TagEnd - TagBegin + 1;
Delete(Result, TagBegin, TagLength);
TagBegin:= Pos( '<', Result);
end;
end;
procedure TVirtualLogTree.SaveToStrings(const Strings: TStrings);
var
Node: PVirtualNode;
begin
Node := GetFirst;
while Assigned(Node) do
begin
Strings.Add(concat(IfThen(FShowDateColumn,
concat(GetCellText(Node, 0), #09), ''), IfThen(FHTMLSupport,
StripHTMLTags(GetCellText(Node, 1)), GetCellText(Node, 1))));
Node := Node.NextSibling;
end;
end;
function TVirtualLogTree.RemoveCtrlChars(const Value: String): String;
var
i: Integer;
begin
// Replace CTRL characters with <whitespace>
Result := '';
for i := 1 to length(Value) do
if (AnsiChar(Value[i]) in [#0..#31, #127]) then
Result := Result + ' '
else
Result := Result + Value[i];
end;
procedure TVirtualLogTree.Log(Value: String; LogLevel: TLogLevel;
TimeStamp: TDateTime);
var
CancelEntry: Boolean;
Node: PVirtualNode;
NodeData: PLogNodeData;
DoScroll: Boolean;
begin
CancelEntry := FALSE;
DoOnLog(Value, CancelEntry, LogLevel);
if not CancelEntry then
begin
DoScroll := ((not Focused) or (GetLast = FocusedNode)) and (FAutoScroll);
Node := AddChild(nil);
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
begin
NodeData.LogLevel := LogLevel;
if TimeStamp = 0 then
NodeData.Timestamp := now
else
NodeData.Timestamp := TimeStamp;
if FRemoveControlCharacters then
Value := RemoveCtrlChars(Value);
if FAutoLogLevelColours then
case LogLevel of
llError: Value := concat('<font-color=clRed>', Value,
'</font-color>');
llInfo: Value := concat('<font-color=clBlack>', Value,
'</font-color>');
llWarning: Value := concat('<font-color=clBlue>', Value,
'</font-color>');
llDebug: Value := concat('<font-color=clGreen>', Value,
'</font-color>')
end;
NodeData.LogText := Value;
IsVisible[Node] := NodeData.LogLevel in FLogLevels;
DoOnAfterLog;
end;
if FMaximumLines <> 0 then
while RootNodeCount > FMaximumLines do
DeleteNode(GetFirst);
if DoScroll then
begin
//SelectNodeEx(GetLast);
ScrollIntoView(GetLast, FALSE);
end;
end;
end;
procedure TVirtualLogTree.LogFmt(Value: String; const Args: Array of
Const; LogLevel: TLogLevel; TimeStamp: TDateTime);
begin
Log(format(Value, Args), LogLevel, TimeStamp);
end;
procedure TVirtualLogTree.SetLogLevels(const Value: TLogLevels);
begin
FLogLevels := Value;
UpdateVisibleItems;
end;
procedure TVirtualLogTree.SetShowDateColumn(const Value: Boolean);
begin
FShowDateColumn := Value;
if Header.Columns.Count > 0 then
begin
if FShowDateColumn then
Header.Columns[0].Options := Header.Columns[0].Options + [coVisible]
else
Header.Columns[0].Options := Header.Columns[0].Options - [coVisible]
end;
end;
procedure TVirtualLogTree.SetShowImages(const Value: Boolean);
begin
FShowImages := Value;
Invalidate;
end;
procedure TVirtualLogTree.UpdateVisibleItems;
var
Node: PVirtualNode;
NodeData: PLogNodeData;
begin
BeginUpdate;
try
Node := GetFirst;
while Assigned(Node) do
begin
NodeData := GetNodeData(Node);
if Assigned(NodeData) then
IsVisible[Node] := NodeData.LogLevel in FLogLevels;
Node := Node.NextSibling;
end;
Invalidate;
finally
EndUpdate;
end;
end;
function TVirtualLogTree.DrawHTML(const ARect: TRect; const ACanvas:
TCanvas; const Text: String; Selected: Boolean): Integer;
(*DrawHTML - Draws text on a canvas using tags based on a simple
subset of HTML/CSS
<B> - Bold e.g. <B>This is bold</B>
<I> - Italic e.g. <I>This is italic</I>
<U> - Underline e.g. <U>This is underlined</U>
<font-color=x> Font colour e.g.
<font-color=clRed>Delphi red</font-color>
<font-color=#FFFFFF>Web white</font-color>
<font-color=$000000>Hex black</font-color>
<font-size=x> Font size e.g. <font-size=30>This is some big text</font-size>
<font-family> Font family e.g. <font-family=Arial>This is
arial</font-family>*)
function CloseTag(const ATag: String): String;
begin
Result := concat('/', ATag);
end;
function GetTagValue(const ATag: String): String;
var
p: Integer;
begin
p := pos('=', ATag);
if p = 0 then
Result := ''
else
Result := copy(ATag, p + 1, MaxInt);
end;
function ColorCodeToColor(const Value: String): TColor;
var
HexValue: String;
begin
Result := 0;
if Value <> '' then
begin
if (length(Value) >= 2) and (copy(Uppercase(Value), 1, 2) = 'CL') then
begin
// Delphi colour
Result := StringToColor(Value);
end else
if Value[1] = '#' then
begin
// Web colour
HexValue := copy(Value, 2, 6);
Result := RGB(StrToInt('$'+Copy(HexValue, 1, 2)),
StrToInt('$'+Copy(HexValue, 3, 2)),
StrToInt('$'+Copy(HexValue, 5, 2)));
end
else
// Hex or decimal colour
Result := StrToIntDef(Value, 0);
end;
end;
const
TagBold = 'B';
TagItalic = 'I';
TagUnderline = 'U';
TagBreak = 'BR';
TagFontSize = 'FONT-SIZE';
TagFontFamily = 'FONT-FAMILY';
TagFontColour = 'FONT-COLOR';
TagColour = 'COLOUR';
var
x, y, idx, CharWidth, MaxCharHeight: Integer;
CurrChar: Char;
Tag, TagValue: String;
PreviousFontColour: TColor;
PreviousFontFamily: String;
PreviousFontSize: Integer;
PreviousColour: TColor;
begin
ACanvas.Font.Size := Canvas.Font.Size;
ACanvas.Font.Name := Canvas.Font.Name;
//if Selected and Focused then
// ACanvas.Font.Color := clWhite
//else
ACanvas.Font.Color := Canvas.Font.Color;
ACanvas.Font.Style := Canvas.Font.Style;
PreviousFontColour := ACanvas.Font.Color;
PreviousFontFamily := ACanvas.Font.Name;
PreviousFontSize := ACanvas.Font.Size;
PreviousColour := ACanvas.Brush.Color;
x := ARect.Left;
y := ARect.Top + 1;
idx := 1;
MaxCharHeight := ACanvas.TextHeight('Ag');
While idx <= length(Text) do
begin
CurrChar := Text[idx];
// Is this a tag?
if CurrChar = '<' then
begin
Tag := '';
inc(idx);
// Find the end of then tag
while (Text[idx] <> '>') and (idx <= length(Text)) do
begin
Tag := concat(Tag, UpperCase(Text[idx]));
inc(idx);
end;
///////////////////////////////////////////////////
// Simple tags
///////////////////////////////////////////////////
if Tag = TagBold then
ACanvas.Font.Style := ACanvas.Font.Style + [fsBold] else
if Tag = TagItalic then
ACanvas.Font.Style := ACanvas.Font.Style + [fsItalic] else
if Tag = TagUnderline then
ACanvas.Font.Style := ACanvas.Font.Style + [fsUnderline] else
if Tag = TagBreak then
begin
x := ARect.Left;
inc(y, MaxCharHeight);
end else
///////////////////////////////////////////////////
// Closing tags
///////////////////////////////////////////////////
if Tag = CloseTag(TagBold) then
ACanvas.Font.Style := ACanvas.Font.Style - [fsBold] else
if Tag = CloseTag(TagItalic) then
ACanvas.Font.Style := ACanvas.Font.Style - [fsItalic] else
if Tag = CloseTag(TagUnderline) then
ACanvas.Font.Style := ACanvas.Font.Style - [fsUnderline] else
if Tag = CloseTag(TagFontSize) then
ACanvas.Font.Size := PreviousFontSize else
if Tag = CloseTag(TagFontFamily) then
ACanvas.Font.Name := PreviousFontFamily else
if Tag = CloseTag(TagFontColour) then
ACanvas.Font.Color := PreviousFontColour else
if Tag = CloseTag(TagColour) then
ACanvas.Brush.Color := PreviousColour else
///////////////////////////////////////////////////
// Tags with values
///////////////////////////////////////////////////
begin
// Get the tag value (everything after '=')
TagValue := GetTagValue(Tag);
if TagValue <> '' then
begin
// Remove the value from the tag
Tag := copy(Tag, 1, pos('=', Tag) - 1);
if Tag = TagFontSize then
begin
PreviousFontSize := ACanvas.Font.Size;
ACanvas.Font.Size := StrToIntDef(TagValue, ACanvas.Font.Size);
end else
if Tag = TagFontFamily then
begin
PreviousFontFamily := ACanvas.Font.Name;
ACanvas.Font.Name := TagValue;
end;
if Tag = TagFontColour then
begin
PreviousFontColour := ACanvas.Font.Color;
try
ACanvas.Font.Color := ColorCodeToColor(TagValue);
except
//Just in case the canvas colour is invalid
end;
end else
if Tag = TagColour then
begin
PreviousColour := ACanvas.Brush.Color;
try
ACanvas.Brush.Color := ColorCodeToColor(TagValue);
except
//Just in case the canvas colour is invalid
end;
end;
end;
end;
end
else
// Draw the character if it's not a ctrl char
if CurrChar >= #32 then
begin
CharWidth := ACanvas.TextWidth(CurrChar);
if y + MaxCharHeight < ARect.Bottom then
begin
ACanvas.Brush.Style := bsClear;
ACanvas.TextOut(x, y, CurrChar);
end;
x := x + CharWidth;
end;
inc(idx);
end;
Result := x - ARect.Left;
end;
{ TVirtualLogPopupmenu }
constructor TVirtualLogPopupmenu.Create(AOwner: TComponent);
function AddMenuItem(const ACaption: String; ATag: Integer): TMenuItem;
begin
Result := TMenuItem.Create(Self);
Result.Caption := ACaption;
Result.Tag := ATag;
Result.OnClick := OnMenuItemClick;
Items.Add(Result);
end;
begin
inherited Create(AOwner);
FOwner := AOwner;
AddMenuItem(StrSaveLog, 1);
AddMenuItem('-', -1);
AddMenuItem(StrCopyToClipboard, 2);
end;
procedure TVirtualLogPopupmenu.OnMenuItemClick(Sender: TObject);
begin
if Assigned(FOnPopupMenuItemClick) then
FOnPopupMenuItemClick(Self, TMenuItem(Sender));
end;
end.
If you add any additional features, maybe you could post them here.
I always like to use the VirtualTreeView by Mike Lischke for such a task. Its highly flexible and quite complex, but when you have understood how it works you can nearly acomplish any list or tree visualisation task with it.
I already did something similar with it, but did not encapsulate it in a component at that time.

Resources