ivobauer
asked on
How to Remember/Restore TListView column settings?
Dear Experts!
I have TListView control with its ViewStyle property set to vsReport. FullDrag property is set to True to allow changing column order at run-time by dragging the mouse. The task is to remember every listview column's properties as well as column order before the form is destroyed and to restore those after the form is created again.
I wrote these routines for saving/loading the (TListView.Columns) collection to/from stream:
procedure SaveCollectionToStream(Str eam: TStream; Collection: TCollection);
begin
with TWriter.Create(Stream, 1024) do
try
WriteCollection(Collection );
finally
Free;
end;
end;
procedure LoadCollectionFromStream(S tream: TStream; Collection: TCollection);
begin
with TReader.Create(Stream, 1024) do
try
CheckValue(vaCollection);
ReadCollection(Collection) ;
finally
Free;
end;
end;
Unfortunately I discovered that with this technique only column "layout" information was saved/restored together with column's header order. Relationship between a specific column and displayed text of a subitem (or item caption) was lost. I will demonstrate this behaviour in the following steps (assume listview with 5 columns):
1) Original relationship between columns and the subitems they display (after form creation):
Columns[0] -> Item.Caption
Columns[1] -> Item.SubItems[0]
Columns[2] -> Item.SubItems[1]
Columns[3] -> Item.SubItems[2]
Columns[4] -> Item.SubItems[3]
2) The same relationship after some of the columns were dragged by the user:
Columns[1] -> Item.SubItems[0]
Columns[0] -> Item.Caption
Columns[3] -> Item.SubItems[2]
Columns[4] -> Item.SubItems[3]
Columns[2] -> Item.SubItems[1]
3) The same relationship after the columns collection was saved, form destroyed, again created and columns loaded:
Columns[1] -> Item.Caption
Columns[0] -> Item.SubItems[0]
Columns[3] -> Item.SubItems[1]
Columns[4] -> Item.SubItems[2]
Columns[2] -> Item.SubItems[3]
I looked into the VCL source and found that TListColumn object has private field FOrderTag, whose purpose is, imo, to maintain a link to a subitem for text display (0-caption, 1-SubItems[0], 2-SubItems[1], and so on...). Further, TListColumn object also makes use of ListView_SetColumnOrderArr ay procedure to do this job (see overridden TListColumn.SetIndex method). I tried to mimics this logic but after a call to ListView_SetColumnOrderArr ay the column headers were somehow exchanged... Please help.
Thanks in advance, Ivo.
I have TListView control with its ViewStyle property set to vsReport. FullDrag property is set to True to allow changing column order at run-time by dragging the mouse. The task is to remember every listview column's properties as well as column order before the form is destroyed and to restore those after the form is created again.
I wrote these routines for saving/loading the (TListView.Columns) collection to/from stream:
procedure SaveCollectionToStream(Str
begin
with TWriter.Create(Stream, 1024) do
try
WriteCollection(Collection
finally
Free;
end;
end;
procedure LoadCollectionFromStream(S
begin
with TReader.Create(Stream, 1024) do
try
CheckValue(vaCollection);
ReadCollection(Collection)
finally
Free;
end;
end;
Unfortunately I discovered that with this technique only column "layout" information was saved/restored together with column's header order. Relationship between a specific column and displayed text of a subitem (or item caption) was lost. I will demonstrate this behaviour in the following steps (assume listview with 5 columns):
1) Original relationship between columns and the subitems they display (after form creation):
Columns[0] -> Item.Caption
Columns[1] -> Item.SubItems[0]
Columns[2] -> Item.SubItems[1]
Columns[3] -> Item.SubItems[2]
Columns[4] -> Item.SubItems[3]
2) The same relationship after some of the columns were dragged by the user:
Columns[1] -> Item.SubItems[0]
Columns[0] -> Item.Caption
Columns[3] -> Item.SubItems[2]
Columns[4] -> Item.SubItems[3]
Columns[2] -> Item.SubItems[1]
3) The same relationship after the columns collection was saved, form destroyed, again created and columns loaded:
Columns[1] -> Item.Caption
Columns[0] -> Item.SubItems[0]
Columns[3] -> Item.SubItems[1]
Columns[4] -> Item.SubItems[2]
Columns[2] -> Item.SubItems[3]
I looked into the VCL source and found that TListColumn object has private field FOrderTag, whose purpose is, imo, to maintain a link to a subitem for text display (0-caption, 1-SubItems[0], 2-SubItems[1], and so on...). Further, TListColumn object also makes use of ListView_SetColumnOrderArr
Thanks in advance, Ivo.
Better store columns positions in the registry. Something like this:
procedure SaveListColumnsSequence(Co lumns: TListColumns);
var
Reg: TRegistry;
I: Integer;
begin
Reg := TRegistry.Create;
try
Reg.OpenKey(ListViewKey, True);
for I := 0 to Columns.Count - 1 do
Reg.WriteInteger(IntToStr( Columns[I] .ID), Columns[I].Index);
finally
Reg.Free;
end;
end;
procedure LoadListColumnsSequence(Co lumns: TListColumns);
var
Reg: TRegistry;
I, J: Integer;
begin
Reg := TRegistry.Create;
try
if not Reg.KeyExists(ListViewKey) then
Exit;
Reg.OpenKey(ListViewKey, False);
for I := 0 to Columns.Count - 1 do
Columns[I].Index := Reg.ReadInteger(IntToStr(C olumns[I]. ID));
finally
Reg.Free;
end;
end;
I didn't test this code... But I think it will work...
Best regards,
Alexey Zverev.
procedure SaveListColumnsSequence(Co
var
Reg: TRegistry;
I: Integer;
begin
Reg := TRegistry.Create;
try
Reg.OpenKey(ListViewKey, True);
for I := 0 to Columns.Count - 1 do
Reg.WriteInteger(IntToStr(
finally
Reg.Free;
end;
end;
procedure LoadListColumnsSequence(Co
var
Reg: TRegistry;
I, J: Integer;
begin
Reg := TRegistry.Create;
try
if not Reg.KeyExists(ListViewKey)
Exit;
Reg.OpenKey(ListViewKey, False);
for I := 0 to Columns.Count - 1 do
Columns[I].Index := Reg.ReadInteger(IntToStr(C
finally
Reg.Free;
end;
end;
I didn't test this code... But I think it will work...
Best regards,
Alexey Zverev.
ASKER
First, thanks to both of you for the proposed solutions. But neither Epsylon's one nor alzv's one works correctly. Your solutions are basically the same - an idea is to remember the order of each column either by using column's tag property like Epsylon did or using column's ID like alzv did.
But if you try to run your solutions (changing the column index), you will see that only order of column HEADERS was restored and the order of columns for each listitem was not changed (i.e. Caption|SubiItems[0]|SubiI tems[1]|.. .)!
Best regards, Ivo.
But if you try to run your solutions (changing the column index), you will see that only order of column HEADERS was restored and the order of columns for each listitem was not changed (i.e. Caption|SubiItems[0]|SubiI
Best regards, Ivo.
Hi Ivo,
I may sound stupid with this attempt, but I haven't used ListView a lot my self. I better understood it when I displayed the Listview as a "ViewStyle = vsReport". Where it appeared that all the columns are indicating the levels of index of the items. For example, although you shift arount the columns item.caption is still the root level of your list, and item.subitem[0], is also the next level down and so on. And the next time you create the form, the only thing you realy changed was the captions of the columns that represent each level of your item list. It seams that for presentation purposes, on runtime when you shift the columns, they carry their items with them, but when you recreate the form the columns carry their new value of index level and therefore the items will stay in the original order, unless you recreate your items just before you destroy the form in the first instance. i.e.
Columns[1] -> Item.SubItems[0]
Columns[0] -> Item.Caption
Columns[3] -> Item.SubItems[2]
Columns[4] -> Item.SubItems[3]
Columns[2] -> Item.SubItems[1]
you create a delete the item.caption and you create a new item.subitem[0], and then add as its subitems the
item.caption and the rest in the order that you want and then save them.
i hope I didn't confuse things.
good luck
ysd
I may sound stupid with this attempt, but I haven't used ListView a lot my self. I better understood it when I displayed the Listview as a "ViewStyle = vsReport". Where it appeared that all the columns are indicating the levels of index of the items. For example, although you shift arount the columns item.caption is still the root level of your list, and item.subitem[0], is also the next level down and so on. And the next time you create the form, the only thing you realy changed was the captions of the columns that represent each level of your item list. It seams that for presentation purposes, on runtime when you shift the columns, they carry their items with them, but when you recreate the form the columns carry their new value of index level and therefore the items will stay in the original order, unless you recreate your items just before you destroy the form in the first instance. i.e.
Columns[1] -> Item.SubItems[0]
Columns[0] -> Item.Caption
Columns[3] -> Item.SubItems[2]
Columns[4] -> Item.SubItems[3]
Columns[2] -> Item.SubItems[1]
you create a delete the item.caption and you create a new item.subitem[0], and then add as its subitems the
item.caption and the rest in the order that you want and then save them.
i hope I didn't confuse things.
good luck
ysd
ivo,
The code provided by alzv and Epsylon is correct (I tested both). The catch is, if your list view is already loaded then only the columns "appear" to change. Wrap the column changes with
ListView.Items.BeginUpdate
// ... Column changes ...
lvChild.Items.EndUpdate;
You should then see the list items in the correct positions.
Regards,
Russell
Hello.
I tested my code also. It works fine.
Here is full testing source:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, ExtDlgs, ComCtrls, ShellAPI, ShlObj,
Buttons;
type
TForm1 = class(TForm)
ListView1: TListView;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
Registry;
procedure SaveListColumnsSequence(Co lumns: TListColumns);
var
Reg: TRegistry;
I: Integer;
begin
Reg := TRegistry.Create;
try
Reg.OpenKey('\Alexey Zverev\Test\ListView1\', True);
for I := 0 to Columns.Count - 1 do
Reg.WriteInteger(IntToStr( Columns[I] .ID), Columns[I].Index);
finally
Reg.Free;
end;
end;
procedure LoadListColumnsSequence(Co lumns: TListColumns);
var
Reg: TRegistry;
I, J: Integer;
begin
Reg := TRegistry.Create;
try
if not Reg.KeyExists('\Alexey Zverev\Test\ListView1\') then
Exit;
Reg.OpenKey('\Alexey Zverev\Test\ListView1\', False);
for I := 0 to Columns.Count - 1 do
Columns[I].Index := Reg.ReadInteger(IntToStr(C olumns[I]. ID));
finally
Reg.Free;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
LoadListColumnsSequence(Li stView1.Co lumns);
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
SaveListColumnsSequence(Li stView1.Co lumns);
end;
end.
I tested my code also. It works fine.
Here is full testing source:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ExtCtrls, ExtDlgs, ComCtrls, ShellAPI, ShlObj,
Buttons;
type
TForm1 = class(TForm)
ListView1: TListView;
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
Registry;
procedure SaveListColumnsSequence(Co
var
Reg: TRegistry;
I: Integer;
begin
Reg := TRegistry.Create;
try
Reg.OpenKey('\Alexey Zverev\Test\ListView1\', True);
for I := 0 to Columns.Count - 1 do
Reg.WriteInteger(IntToStr(
finally
Reg.Free;
end;
end;
procedure LoadListColumnsSequence(Co
var
Reg: TRegistry;
I, J: Integer;
begin
Reg := TRegistry.Create;
try
if not Reg.KeyExists('\Alexey Zverev\Test\ListView1\') then
Exit;
Reg.OpenKey('\Alexey Zverev\Test\ListView1\', False);
for I := 0 to Columns.Count - 1 do
Columns[I].Index := Reg.ReadInteger(IntToStr(C
finally
Reg.Free;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
LoadListColumnsSequence(Li
end;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
SaveListColumnsSequence(Li
end;
end.
ASKER
Hi all again!
Sorry for the delay but I've been temporarily out of the city...
To ysd: I read your proposed solution for the several times but I'm somehow unable to extract the idea. Sorry.
To rllibby: Your idea -> wrapping the column changes into BeginUpdate..EndUpdate block helped me A LOT to build *fully working* code. However, alzv's code is not 100% correct, see below.
To alzv: Your code sometimes works and sometimes not. Try to run it again and follow these steps to see the failure:
1) At designtime, add 5 columns into listview. Assign them names, for example 'first', 'second', and so on. You could add also some items if you want.
2) Now run the project -> columns now appear in ascending order, that's ok. Drag them with mouse to be in descending order, i.e. 'Fifth' -> 'Fourth' -> ... -> 'First'. Close the form.
3) Open the form again. You should see the columns in descending order but some of them are incorrectly mixed.
Neverhtless, I have written *working* solution by myself. It uses a slightly different idea based on rllibby's and alzv's solutions. I am posting the full unit code so you can test it and get me know what do you think about it.
Best regards, Ivo.
Sorry for the delay but I've been temporarily out of the city...
To ysd: I read your proposed solution for the several times but I'm somehow unable to extract the idea. Sorry.
To rllibby: Your idea -> wrapping the column changes into BeginUpdate..EndUpdate block helped me A LOT to build *fully working* code. However, alzv's code is not 100% correct, see below.
To alzv: Your code sometimes works and sometimes not. Try to run it again and follow these steps to see the failure:
1) At designtime, add 5 columns into listview. Assign them names, for example 'first', 'second', and so on. You could add also some items if you want.
2) Now run the project -> columns now appear in ascending order, that's ok. Drag them with mouse to be in descending order, i.e. 'Fifth' -> 'Fourth' -> ... -> 'First'. Close the form.
3) Open the form again. You should see the columns in descending order but some of them are incorrectly mixed.
Neverhtless, I have written *working* solution by myself. It uses a slightly different idea based on rllibby's and alzv's solutions. I am posting the full unit code so you can test it and get me know what do you think about it.
Best regards, Ivo.
ASKER
Here is the code:
==========
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ComCtrls;
type
TForm1 = class(TForm)
ListView1: TListView;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses IniFiles;
{$R *.DFM}
const
SColumnSectionFmt = 'ListView.Column.Index=%d' ;
SColumnId = 'ID';
SColumnWidth = 'Width';
procedure SaveColumnsOrder(LV: TListView; const FileName: string);
var
Ini: TIniFile;
I: Integer;
Section: string;
begin
Ini := TIniFile.Create(FileName);
try
for I := 0 to LV.Columns.Count - 1 do
with Ini, LV.Columns[I] do
begin
Section := Format(SColumnSectionFmt, [I]);
WriteInteger(Section, SColumnId, ID);
WriteInteger(Section, SColumnWidth, Width);
end;
finally
Ini.Free;
end;
end;
procedure LoadColumnsOrder(LV: TListView; const FileName: string);
var
Ini: TIniFile;
I: Integer;
Section: string;
Column: TListColumn;
begin
Ini := TIniFile.Create(FileName);
try
LV.Items.BeginUpdate;
try
for I := 0 to LV.Columns.Count - 1 do
begin
Section := Format(SColumnSectionFmt, [I]);
Column := TListColumn(LV.Columns.Fin dItemID(
Ini.ReadInteger(Section, SColumnId, I)));
if Assigned(Column) then with Column do
begin
Index := I;
Width := Ini.ReadInteger(Section, SColumnWidth, Width);
end;
end;
finally
LV.Items.EndUpdate;
end;
finally
Ini.Free;
end;
end;
const
SIniFileName = '\LVColumnOrder.cfg';
procedure TForm1.FormCreate(Sender: TObject);
begin
LoadColumnsOrder(ListView1 , ExtractFilePath(Applicatio n.ExeName) + SIniFileName);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
SaveColumnsOrder(ListView1 , ExtractFilePath(Applicatio n.ExeName) + SIniFileName);
end;
end.
==========
==========
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ComCtrls;
type
TForm1 = class(TForm)
ListView1: TListView;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses IniFiles;
{$R *.DFM}
const
SColumnSectionFmt = 'ListView.Column.Index=%d'
SColumnId = 'ID';
SColumnWidth = 'Width';
procedure SaveColumnsOrder(LV: TListView; const FileName: string);
var
Ini: TIniFile;
I: Integer;
Section: string;
begin
Ini := TIniFile.Create(FileName);
try
for I := 0 to LV.Columns.Count - 1 do
with Ini, LV.Columns[I] do
begin
Section := Format(SColumnSectionFmt, [I]);
WriteInteger(Section, SColumnId, ID);
WriteInteger(Section, SColumnWidth, Width);
end;
finally
Ini.Free;
end;
end;
procedure LoadColumnsOrder(LV: TListView; const FileName: string);
var
Ini: TIniFile;
I: Integer;
Section: string;
Column: TListColumn;
begin
Ini := TIniFile.Create(FileName);
try
LV.Items.BeginUpdate;
try
for I := 0 to LV.Columns.Count - 1 do
begin
Section := Format(SColumnSectionFmt, [I]);
Column := TListColumn(LV.Columns.Fin
Ini.ReadInteger(Section, SColumnId, I)));
if Assigned(Column) then with Column do
begin
Index := I;
Width := Ini.ReadInteger(Section, SColumnWidth, Width);
end;
end;
finally
LV.Items.EndUpdate;
end;
finally
Ini.Free;
end;
end;
const
SIniFileName = '\LVColumnOrder.cfg';
procedure TForm1.FormCreate(Sender: TObject);
begin
LoadColumnsOrder(ListView1
end;
procedure TForm1.FormDestroy(Sender:
begin
SaveColumnsOrder(ListView1
end;
end.
==========
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
procedure TForm1.FormCreate(Sender: TObject);
var lc: TListColumn;
begin
lc := ListView1.Columns.Add;
lc.Caption := 'A';
lc.Tag := 1;
lc := ListView1.Columns.Add;
lc.Caption := 'B';
lc.Tag := 2;
lc := ListView1.Columns.Add;
lc.Caption := 'C';
lc.Tag := 3;
end;
No you can identify each column's original index because the tags will be written to the stream along with the column names.