How to properly set the size of a ListView column according to its content?

2.7k views Asked by At

I have a number of list view controls (TListView) that are used to display data. All these list view are set to "Detail" mode and all have TImageList assigned to their "SmallIcons" properties.

I'm trying to set the width of these column based on their contents exactly in the same way as if the user double-clicked on the separator slider at the end of each of the column headers.

First, I tried to set the column width to "-1" and "-2" for auto-sizing them: not only did that fail to work perfectly (some columns containing local characters - I'm using D6 and that means ANSI strings - are too low) but it also made the display of the column extremely slow (up to 30 seconds to display a list view with 6 column and 150 items when it's instantaneous with fixed width).

I have tried to use GetTextExtent on each cell to obtain the expected width of the text, adding some margin (from 2 to 10 pixels) and the expand the width of the column if it is lower than the calculated text width. Special treatment is applied to the first column (Items.caption) to take into account the display of the icon (I add the width of the icon, plus margin, to the width of the cell's text).

That didn't work either: in many cases (for instance, displaying the date in "yyyy/mm/dd hh:nn:ss" format results in a text too large to fit in the column).

Thinking that the issue could come from the window theme engine, I've switched to use GetThemeTextExtent instead of GetTextExtent but obtained the same result.

The only thing that seems to work is to add an arbitrary large margin (20 pixels) to each column width but, of course, that produces columns that are larger than they should be.

So, is there any alternative strategy ? I don't need anything but something that will calculate the correct width once: when the list is first populated. The code behind "clicking the column separator" works just fine but I can't find how to trigger it by code (well, I guess I could send the double click messages to the header directly as a hack)

For clarification, here are the things I tried the following code:

(in call case, there is a call made to ListView.canvas.Font.Assign(ListView.font). It is not in theses functions because a single assignment is enough but the code loops on all non-autosized columns of the listview).

Edit

My initial attempt using Windows Theme API:

function _GetTextWidth1(AText: widestring; IsHeader: boolean = false): Integer;
var
  ATheme: HTheme;
  rValue: TRect;
  iPartID: integer;
  AWidetext: WideString;
const
  LVP_GROUPHEADER  = 6;
begin
  // try to get text width using theme API
  ZeroMemory(@rValue, SizeOf(rValue));
  ATheme := OpenThemeData(ListView.Handle, 'LISTVIEW');
  try
    if not IsHeader then
      iPartID := LVP_LISTITEM
    else
      iPartID := LVP_GROUPHEADER;
    AWidetext := AText;
    GetThemeTextExtent( ATheme,
                        ListView.Canvas.Handle,
                        iPartID,
                        LIS_NORMAL,
                        PWideChar(AWidetext),
                        -1,
                        DT_LEFT or DT_SINGLELINE or DT_CALCRECT,
                        nil,
                        rValue
                        );
  finally // wrap up
    CloseThemeData(ATheme);
  end;    // try/finally
  result := rValue.Right;
end;

next attempt using DrawText/DrawTextW:

function _GetTextWidth2(AText: widestring; IsHeader: boolean = false): Integer;
var
  rValue: TRect;
  lFlags: Integer;
begin
  // try to get text width using DrawText/DrawTextW
  rValue := Rect(0, 0, 0, 0);
  lFlags := DT_CALCRECT or DT_EXPANDTABS or DT_NOPREFIX or DT_LEFT or DT_EXTERNALLEADING;
  DrawText(ListView.canvas.Handle, PChar(AText), Length(AText), rValue, lFlags);
  //DrawTextW(ListView.canvas.Handle, PWideChar(AText), Length(AText), rValue, lFlags);
  result := rValue.Right;
end;

Third attempt using delphi's TextWidth function

function _GetTextWidth3(AText: widestring; IsHeader: boolean = false): Integer;
begin
  // try to get text width using delphi wrapped around GetTextExtentPoint32
  result := ListView.canvas.TextWidth(Atext);
end;

In all cases, I add a margin to the resulting width: I tried values as high as 20 pixels. I also take into account the possibility that the view use icons (in which case I add the width of the icon plus the margin again to the first column).

1

There are 1 answers

5
GAD ZombiE On

You could use canvas.TextWidth method. But be sure to use TListView canvas (not other, i.e. TForm) and first assign a font to canvas from TListView. For example:

var
  s: integer;
begin
  ListView1.AddItem('test example item', nil);
  ListView1.canvas.Font.Assign(ListView1.font);
  s := ListView1.canvas.TextWidth(ListView1.Items[0].Caption) + 10; //this "+10" is a small additional margin
  if s > ListView1.Columns[0].Width then
    ListView1.Columns[0].Width := s;

It works fine for me.