Solved

How to Remember/Restore TListView column settings?

Posted on 2001-06-14
9
1,072 Views
Last Modified: 2008-02-26
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(Stream: TStream; Collection: TCollection);
begin
  with TWriter.Create(Stream, 1024) do
  try
    WriteCollection(Collection);
  finally
    Free;
  end;
end;

procedure LoadCollectionFromStream(Stream: 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_SetColumnOrderArray procedure to do this job (see overridden TListColumn.SetIndex method). I tried to mimics this logic but after a call to ListView_SetColumnOrderArray the column headers were somehow exchanged... Please help.


Thanks in advance, Ivo.
0
Comment
Question by:ivobauer
9 Comments
 
LVL 13

Expert Comment

by:Epsylon
ID: 6190528
You can use the Tag property of each column:

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.
0
 
LVL 3

Expert Comment

by:alzv
ID: 6190545
Better store columns positions in the registry. Something like this:
procedure SaveListColumnsSequence(Columns: 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(Columns: 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(Columns[I].ID));
  finally
    Reg.Free;
  end;
end;

I didn't test this code... But I think it will work...

Best regards,
Alexey Zverev.
0
 
LVL 2

Author Comment

by:ivobauer
ID: 6190697
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]|SubiItems[1]|...)!

Best regards, Ivo.
0
 
LVL 2

Expert Comment

by:ysd
ID: 6191076
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

0
How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

 
LVL 26

Expert Comment

by:Russell Libby
ID: 6191167

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


0
 
LVL 3

Expert Comment

by:alzv
ID: 6194557
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(Columns: 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(Columns: 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(Columns[I].ID));
  finally
    Reg.Free;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  LoadListColumnsSequence(ListView1.Columns);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  SaveListColumnsSequence(ListView1.Columns);
end;

end.
0
 
LVL 2

Author Comment

by:ivobauer
ID: 6226534
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.
0
 
LVL 2

Author Comment

by:ivobauer
ID: 6226535
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.FindItemID(
          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(Application.ExeName) + SIniFileName);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  SaveColumnsOrder(ListView1, ExtractFilePath(Application.ExeName) + SIniFileName);
end;

end.

==========
0
 
LVL 3

Accepted Solution

by:
modder earned 0 total points
ID: 6326366
I will refund your points and make this a PAQ

modder
Community Support
0

Featured Post

Enabling OSINT in Activity Based Intelligence

Activity based intelligence (ABI) requires access to all available sources of data. Recorded Future allows analysts to observe structured data on the open, deep, and dark web.

Join & Write a Comment

Suggested Solutions

Have you ever had your Delphi form/application just hanging while waiting for data to load? This is the article to read if you want to learn some things about adding threads for data loading in the background. First, I'll setup a general applica…
Hello everybody This Article will show you how to validate number with TEdit control, What's the TEdit control? TEdit is a standard Windows edit control on a form, it allows to user to write, read and copy/paste single line of text. Usua…
Get a first impression of how PRTG looks and learn how it works.   This video is a short introduction to PRTG, as an initial overview or as a quick start for new PRTG users.
You have products, that come in variants and want to set different prices for them? Watch this micro tutorial that describes how to configure prices for Magento super attributes. Assigning simple products to configurable: We assigned simple products…

757 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

21 Experts available now in Live!

Get 1:1 Help Now