Jump to content


Check out our Community Blogs

Register and join over 40,000 other developers!


Recent Status Updates

View All Updates

Photo
- - - - -

Basic Working with TListView

delphi tlistview report style

  • Please log in to reply
No replies to this topic

#1 Luthfi

Luthfi

    CC Leader

  • Expert Member
  • PipPipPipPipPipPipPip
  • 1320 posts
  • Programming Language:PHP, Delphi/Object Pascal, Pascal, Transact-SQL
  • Learning:C, Java, PHP

Posted 20 January 2013 - 09:06 AM

Overview

When you want to show information in table-like manner, you might want to consider using TListView in vsReport style. This windows control actually is quite popular when you don't want/need db-aware control and don't want to use third party libray. Another option would be to use TStringGrid. But in this article we will discuss the basics of working with TListView in vsReport style.

TListView documentation in Delphi (Embarcadero)
TListView documentation in Lazarus/FreePascal


Initial Setup

These are the basic setup to work with table-like TListView.

Set ViewStyle to vsReport

Of course the very first step is to change the TListView's ViewStyle property to vsReport, duh!


Defining Columns

After changing the style to vsReport we need to define columns of our "table". In design time we use Columns Editor like shown below.
DesignTime_01_ColumnsEditor.png

You can open Columns Editor for a TListView either by right click on the TListView and choose menu item Columns editor..., like shown below
DesignTime_03_OpenColumnsEditor_02.png

or you can click on ellipsis button next to property Columns in the Object Inspector, like shown below.

DesignTime_02_OpenColumnsEditor_01.png

In Columns Editor you can add, remove, and reorder items which define the TListView's columns.

In runtime, you can add column using code like below.

var
  vNewColumn: TListColumn;
  ..
begin
  ...
  vNewColumn := ListView1.Columns.Add;
  vNewColumn.Caption := 'Title';
  vNewColumn.Width := 100;
  ...
end;

To delete n-th column:

  ListView1.Columns.Delete(n);

Managing Rows, the "Easy" Way

In the "easy" way we use default behavior of TListView in showing the information. We only need to provide it with the strings to be displayed through its items (i.e. records or rows). Each item correspond with one row. The items are of TListItem class. In this case we are interested in its Caption and SubItems property.

TListItem's Caption will be displayed in the first column. Subsequent columns content came from strings contained in SubItems property. First string in SubItems will be shown in second column, second string shown in third column, and so on. You must be careful here. Suppose you don't want to show nothing in second column, you still need to store empty string in location 0 of SubItems. You can not simply skip it.

In design time, you can use Items Editor to add or remove "row" to your TListView.

DesignTime_05_ItemsEditor.png

Invoke Items Editor either by right click on the TListView and choose menu item Items Editor... or click ellipsis button next in Items property in Object Inspector.

While managing rows using Items Editor is very easy, but it's often that you need to alter the rows in runtime. Therefore you still need to know the "map" between the row cells and Caption and SubItems (in order to be able to alter the cell(s) content) and how to add and delete item/row.


Adding New Item

To add new item, you can ask Items property of the TListView to create new TListItem object for you by calling its Add method, then adjust the returned TListItem content. You can use code pattern like this.

var
  vNewItem: TListItem;
begin
  ...
  vNewItem := ListView1.Items.Add;
  vNewItem.Caption := 'First column';
  vNewItem.SubItems.Add('Second column');
  vNewItem.SubItems.Add('Third column');
  ...
end;

Delete Item

There are several ways to delete a row.

  • If you have the reference to TListItem instance(s) of the row(s) that you want to delete, you can simply free them. After they're destroyed, row(s) associated with them will no longer exist in the TListView.

    For example, TListView have ItemFocused property. It is a reference to TListItem instance that currently focused. It will be nil When there is no focused item. So if you want to delete item that is currently focused, you can simply use the following code.
      ListView1.ItemFocused.Free;
    
  • The code is safe, even when there is no focused item. Because calling Free on nil will not generate error.
  • If you want all currently selected rows to be deleted, just call the TListView's DeleteSelected method.
  • If you know the index(s) of the row(s) to be deleted in the Items list, you can call the Items' Delete method by passing the index(s). Note that when you want to delete several rows using this approach, you must be aware that each removal will make the other indexes to be invalid. You have to do adjustment after each removal.
    For example, if you want to delete items at indexes 1, 7, 9, 12, 20, you may want to use the following code.
    var
      i, j: Integer;
      vIndexes: array of integer;
    begin
      ...
      SetLength(vIndexes, 5);
      vIndexes[0] := 1;
      vIndexes[1] := 7;
      vIndexes[2] := 9;
      vIndexes[3] := 12;
      vIndexes[4] := 20;
    
      for i := 0 to Length(vIndexes)-1 do
      begin
        ListView1.Items.Delete(vIndexes[i]);
        // adjust the rest of the indexes
        for j := i+1 to Length(vIndexes)-1 do
          Dec(vIndexes[j]);
      end;
      ...
    end;
    
  • Modify Cell Content

    For this, we must respect the "map" of Caption and SubItems property of a TListItem with the columns. Remember that the first cell content always coming from the Caption and subsequent cell content came from SubItems which index one less from the corresponding column index.

    For example, you can use the following code to modify the content of a column cell, using the column index.[/i]
    procedure Change(AListView: TListView; const ARowIndex, AColIndex: Integer; const ANewContent: string);
    var
      i: Integer;
      vItem: TListItem;
    begin
      vItem := AListView.Items[ARowIndex];
      if AColIndex=0 then
        vItem.Caption := ANewContent
      else begin
        for i := vItem.SubItems.Count to AColIndex+1 do
        vItem.SubItems.Add('');
        vItem.SubItems[AColIndex] := ANewContent;
      end;
    end;
    
    Managing Rows, "Harder" Way

    Although I marked the following method of managing rows as harder, do not be afraid. Because the difficulty difference is not that big. You don't need to be an expert to employ this "harder" way. Actually many experts often use the easier way in many situation for that approach offers faster implementation.

    In this "harder" way, we do not maintain data in TListItem instances. We still use it as "proxy" to the TListView, but we store the actual data in other location. We also need to "draw" the data ourself, instead of letting TListView does everything the standard way. This approach offers the following benefits.
    • SSmaller memory usage. Because it's lesser possibility of data duplication.
    • Virtually unlimited possibility in presenting the data. We can draw the text in more interesting ways than offered in the standard way.
    • Lesser code needed to synchronize data changes.
    Data Property of TListItem

    Of course first thing to do is to associate our data container with corresponding TListItem instances. For this, we usually use Data property of TListItem. This property is of pointer type. Therefore you can store any reference to memory location. Including ordinary pointer, class, object, or anything you can cast as pointer (e.g. you can store integer by casting it first to pointer).

    For example, let's say that I have customers information stored in instances of TCustomer class. TCustomer class was declared like this.
    type
      TCustomer=class
      private
        FName: string;
        FStatus: TCustStatus;
        FID: Integer;
      public
        property ID: Integer read FID write FID;
        property Name: string read FName write FName;
        property Status: TCustStatus read FStatus write FStatus;
      end;
    
    And when populating ListView1, I use something like the following codes.
    var
      vItem: TListItem;
      vCust: TCustomer;
    begin
      ListView1.Clear;
    
      vCust := FAdvCustomers.Add;
      with vCust do
      begin
        ID := 1;
        Name := 'One';
        Status := csActive;
      end;
      vItem := ListView1.Items.Add;
      vItem.Data := vCust;
    
      vCust := FAdvCustomers.Add;
      with vCust do
      begin
        ID := 2;
        Name := 'Two';
        Status := csActive;
      end;
      vItem := ListView1.Items.Add;
      vItem.Data := vCust;
    
      vCust := FAdvCustomers.Add;
      with vCust do
      begin
        ID := 3;
        Name := 'Three';
        Status := csBanned;
      end;
      vItem := ListView1.Items.Add;
      vItem.Data := vCust;
    end;
    
    See that I did not adjust Caption or SubItems property of any created TListItem instances. All data goes only to TCustomer instances. Later when we need more information, such as when we need to draw the texts, we need to retrieve TCustomer instance for the TListItem's Data property. Like shown below when we need to do the drawing. The code is event handler for OnDrawItem event.
    procedure TForm1.ListView1DrawItem(Sender: TCustomListView;
    Item: TListItem; Rect: TRect; State: TOwnerDrawState);
    var
      i: Integer;
      x, y: Integer;
      vCust: TCustomer;
    begin
      if Item=nil then Exit;
      vCust := Item.Data;
      if vCust=nil then Exit;
    
      if vCust.ID=2 then
        x := 2;
    
      with ListView1.Canvas do
      begin
        if odSelected in State then
        begin
          Brush.Color := clNavy;
          Font.Color := clWhite;
        end
        else begin
          case vCust.Status of
          csUnknown : Brush.Color := clSilver;
          csActive : Brush.Color := clLime;
          csInactive: Brush.Color := clFuchsia;
          csBanned : Brush.Color := clRed;
        end;
        Font.Color := clWindowText;
      end;
      FillRect(Rect);
    
      x := Rect.Left+4;
      y := Rect.Top;
    
      for i := 0 to ListView1.Columns.Count-1 do
      begin
        if ListView1.Columns[i].Caption = 'ID' then
          TextOut(x, y, IntToStr(vCust.ID))
        else if ListView1.Columns[i].Caption = 'Name' then
          TextOut(x, y, vCust.Name)
        else if ListView1.Columns[i].Caption = 'Status' then
          TextOut(x, y, CUST_STATUS_NAMES[vCust.Status]);
        Inc(x, ListView1.Columns[i].Width);
      end;
    end;
    
    Demo Project

    In the end of this article you would find link to download demo project I had written to illustrate the points we discussed earlier.

    Running the Demo Project
    • Upon executing the demo project you would be presented with screen like this.

      RunTime_01.png

      Note that in the active tab, we only use the "easier" method of managing the rows.
    • Now click the Populate button. The TListView (named lvStd in the demo project) will be populated with dummy rows like shown below.

      RunTime_02_Populated.png

      The populating codes were as follows.
      procedure TForm1.BitBtn9Click(Sender: TObject);
      var
        vItem: TListItem;
        vCust: TCustomer;
      begin
        lvStd.Clear;
      
        vCust := FStdCustomers.Add;
        with vCust do
        begin
          ID := 1;
          Name := 'One';
          Status := csActive;
      
          vItem := lvStd.Items.Add;
          vItem.Caption := IntToStr(ID);
          vItem.SubItems.Add(Name);
          vItem.SubItems.Add(CUST_STATUS_NAMES[Status]);
          vItem.Data := vCust;
        end;
      
        vCust := FStdCustomers.Add;
        with vCust do
        begin
          ID := 2;
          Name := 'Two';
          Status := csActive;
      
          vItem := lvStd.Items.Add;
          vItem.Caption := IntToStr(ID);
          vItem.SubItems.Add(Name);
          vItem.SubItems.Add(CUST_STATUS_NAMES[Status]);
          vItem.Data := vCust;
        end;
      
        ...
        ...
      end;
      
    • Note how we use the Caption and SubItems properties of TListItem instances?
  • To edit a row, you can click on the row and click on Edit customer button. You will be presented with a form like shown below.

    RunTime_03_EditCustomer_Easy.png

    After you made the changes, click on OK button to apply the changes to the underlying TCustomer instance, and in turn apply the changes to lvStd. The codes were as follows.
    procedure TForm1.BitBtn5Click(Sender: TObject);
    var
      vCust: TCustomer;
    begin
      if lvStd.ItemFocused=nil then
        raise Exception.Create('You have not selected customer. What were you thinking?');
    
      vCust := LvStd.ItemFocused.Data;
    
      with TfrmCustomer.Create(Self) do
      try
        Customer := vCust;
        if ShowModal=mrOK then
        begin
          vCust.Assign(Customer);
    
          lvStd.ItemFocused.Caption := IntToStr(vCust.ID);
          lvStd.ItemFocused.SubItems.Clear;
          lvStd.ItemFocused.SubItems.Add(vCust.Name);
          lvStd.ItemFocused.SubItems.Add(CUST_STATUS_NAMES[vCust.Status]);
    end;
      finally
        Free;
      end;
    end;
    
    Note that we need to clear the content of SubItems prior to adding the current valid information.
  • Now let's see how those would be done in the "harder" way. Activate the Advance tab. Then click on Populate button. You will get something like shown below. Note that the rows were in different colors, according to associated customer's status.

    RunTime_06_Populated_Harder.png

    And the populating codes were:
    procedure TForm1.BitBtn1Click(Sender: TObject);
    var
      vItem: TListItem;
      vCust: TCustomer;
    begin
      ListView1.Clear;
    
      vCust := FAdvCustomers.Add;
      with vCust do
      begin
        ID := 1;
        Name := 'One';
        Status := csActive;
      end;
      vItem := ListView1.Items.Add;
      vItem.Data := vCust;
    
      vCust := FAdvCustomers.Add;
      with vCust do
      begin
        ID := 2;
        Name := 'Two';
        Status := csActive;
      end;
      vItem := ListView1.Items.Add;
      vItem.Data := vCust;
      ...
      ...
    end;
    
    I believe you will agree with me if I say that this codes are shorter and simpler than ones for "easier" way. However you must also note that now we are responsible to "render" customers information in the TListView's (in the demo project was named lvAdvance) canvas. We did it by handling its OnDrawItem event. Like this.
    procedure TForm1.lvAdvanceDrawItem(Sender: TCustomListView;
    Item: TListItem; Rect: TRect; State: TOwnerDrawState);
    var
      i: Integer;
      x, y: Integer;
      vCust: TCustomer;
    begin
      if Item=nil then Exit;
      vCust := Item.Data;
      if vCust=nil then Exit;
    
      with lvAdvance.Canvas do
      begin
        if odSelected in State then
        begin
          Brush.Color := clNavy;
          Font.Color := clWhite;
        end
        else begin
          case vCust.Status of
            csUnknown : Brush.Color := clSilver;
            csActive : Brush.Color := clLime;
            csInactive: Brush.Color := clFuchsia;
            csBanned : Brush.Color := clRed;
          end;
          Font.Color := clWindowText;
        end;
        FillRect(Rect);
    
        x := Rect.Left+4;
        y := Rect.Top;
    
        for i := 0 to lvAdvance.Columns.Count-1 do
        begin
          if lvAdvance.Columns[i].Caption = 'ID' then
            TextOut(x, y, IntToStr(vCust.ID))
          else if lvAdvance.Columns[i].Caption = 'Name' then
            TextOut(x, y, vCust.Name)
          else if lvAdvance.Columns[i].Caption = 'Status' then
            TextOut(x, y, CUST_STATUS_NAMES[vCust.Status]);
          Inc(x, lvAdvance.Columns[i].Width);
        end;
      end;
    end;
    
    Note that this is where we decide the color of the cells of a row according to associated customer's status.
  • ]What about editing? Let's click on the Edit customer button. You will be presented similar form like before. Simply do your changes and click OK to enjoy the changes be reflected in [i]lvAdvance. However, if you inspect the codes behind this, you will find it shorter and simpler than ones in easier way. The codes were:
    procedure TForm1.btnEditClick(Sender: TObject);
    var
    vCust: TCustomer;
    begin
      if lvAdvance.ItemFocused=nil then
        raise Exception.Create('You have not selected customer. What were you thinking?');
    
      vCust := lvAdvance.ItemFocused.Data;
    
      with TfrmCustomer.Create(Self) do
      try
        Customer := vCust;
        if ShowModal=mrOK then
        begin
          vCust.Assign(Customer);
          lvAdvance.Invalidate;
        end;
      finally
        Free;
      end;
    end;
    

Attached Files

  • Attached File  Demo.zip   276.03KB   2259 downloads

Edited by LuthfiHakim, 25 February 2013 - 03:56 AM.

  • 0





Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download