9

I've implement a log viewer using a TListBox in virtual mode.

It works fine (for all the code I wrote), displays the content as expected (I even added an horizontal scrollbar easily), but I guess I've reached the some kind of limit of the vertical scrollbar.

That is, when I scroll the vertical bar from the top to the bottom, it will not scroll the content to the end of the list, but only to some limit.

Do you know any possibility to get rid of this limit? I tried with SetScrollInfo, but it didn't work since the limit sounds to be not in the scrollbar, but in the TListBox itself.

I know the solution of creating a dedicated TCustomControl: in this case, the SetScrollInfo will work as expected. But does anyone know about a solution/trick to still use TListBox?

Edit: to make it clear - I don't ask for a (third-party) component solution, but to know if there is some low-level GDI message to send to the standard TListBox to override this limit. If there is none, I'll go to the dedicated TCustomControl solution.

Here is the code using TSCROLLINFO:

procedure ScrollVertHuge(Handle: HWND; count: integer);
var Scroll: TSCROLLINFO;
begin
  Scroll.cbSize:= sizeof(Scroll);
  Scroll.fMask := SIF_DISABLENOSCROLL or SIF_RANGE;
  Scroll.nMin := 0;
  Scroll.nMax := count;
  SetScrollInfo(Handle,SB_VERT,Scroll,false);
end;

To precise the issue: Adding and drawing both work, of course (my tool works as exepected), but what does not work is the vertical scrollbar dragging. I renamed the title of the question, and got rid of the deprecated MSDN articles, which are confusing.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • TVirtualTreeView can do the same UI, but without 32K limits. – LU RD Aug 22 '11 at 08:49
  • @LU RD `TVirtualTreeView` is great, but not an option for me. It's too huge for this little tool, and I don't want to use third-party components. A `TCustomControl` could easily make the trick, but my question is if someone knows about a solution to override the `TListBox` limit (e.g. by some low-level message to send). – Arnaud Bouchez Aug 22 '11 at 08:53
  • What about TListView? Could you show what you tried with SetScrollInfo? I do remember I once did something similar, without deriving a component. – Rudy Velthuis Aug 22 '11 at 09:01
  • @Rudy I've added the SetScrollInfo code, but it seems to be an absolute 8 K row limit here... I could try `TListView`, but I found it slow, even in virtual mode - with it, [the limit is 100,000,000](http://stackoverflow.com/questions/2454942/is-winforms-listview-in-virtualmode-limited-to-100-000-000-rows) so it could be enough for my use. – Arnaud Bouchez Aug 22 '11 at 09:06
  • Not even W95, that article is for 16 bit Windows. I just put 500000 items in a 'lbsOwnerDrawFixed' list box, there's no such limit on the listbox itself. – Sertac Akyuz Aug 22 '11 at 09:12
  • By overriding the GetCells in a TStringGrid, it is possible to have a fast virtual string grid. I use it to present data with millions of rows. – LU RD Aug 22 '11 at 09:18
  • @Sertac You can put 500,000 items in a TListBox. It works, about the adding, and even the display (if you use page up/page down/go to end). My tool has no problem with that - read my question. But what is not working, is the scrollbar dragging. Thanks for trying to reproduce the issue, anyway. – Arnaud Bouchez Aug 22 '11 at 09:30
  • @Arnaud - I can drag the thumb anyway I want - without overriding any scrolling code. But it's possible that I didn't quite understand the problem. Anyway, my point is that, the limits on the linked article are not relevant. – Sertac Akyuz Aug 22 '11 at 09:37
  • @Sertac Yes, I extracted the linked article from my question: it was confusing. About the dragging issue, I just made the test with a plain application, and I didn't have the issue. In my code, in fact, I've the index which is limited to a 16 bit value in the OnDrawItem event: there is a mod 65536 when I scroll the listbox. – Arnaud Bouchez Aug 22 '11 at 09:58
  • I just found out something: if you add a manifest to use Microsoft.Windows.Common-Controls 6.0.0.0 (like XPMan unit) the issue occurs! So it's definitively a Windows bug IMHO - perhaps only under Seven (I didn't test it XP). – Arnaud Bouchez Aug 22 '11 at 10:26
  • how many items do you need in the list box in order to see the problem? Did you try list box in virtual mode? – David Heffernan Aug 22 '11 at 11:09
  • @David The source code is available [from my link above](http://synopse.info/fossil/dir?name=SQLite3/Samples/11+-+Exception+logging). The list box is designed in virtual mode, and the OnDrawItem even receive an Index parameter mod 65535 when you define a XP manifest file... that's the issue... So 65536 items is enough to see it. I think I'll switch to a `TDrawGrid` which does not suffer from this problem. – Arnaud Bouchez Aug 22 '11 at 11:21
  • I'm not surprised by a 16k limit, this code has been around since Windows 1.0 – David Heffernan Aug 22 '11 at 11:45
  • @David What I don't understand is why it works without XP theming, but fails with the common controls revision 6... looks like a regression to me. – Arnaud Bouchez Aug 22 '11 at 12:24
  • So to fix this, I used a `TDrawGrid` instead of the buggy `TListBox`. I spent hours tracking.... a Windows bug... :( Thanks all for your help. – Arnaud Bouchez Aug 22 '11 at 15:59

2 Answers2

10

The below probably should be considered as a work-around for defective OS behavior, since, unless themes are enabled, the default window procedure of a listbox control handles thumb-tracking quite well. For some reason, when themes are enabled (test here shows with Vista and later), the control seems to rely upon the Word sized scroll position data of WM_VSCROLL.

First, a simple project to duplicate the problem, below is an owner draw virtual (lbVirtualOwnerDraw) list box with some 600,000 items (since item data is not cached it doesn't take a moment to populate the box). A tall listbox will be good for easy following the behavior:

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    procedure ListBox1Data(Control: TWinControl; Index: Integer;
      var Data: string);
    procedure ListBox1DrawItem(Control: TWinControl; Index: Integer;
      Rect: TRect; State: TOwnerDrawState);
    procedure FormCreate(Sender: TObject);
  end;

[...]

procedure TForm1.FormCreate(Sender: TObject);
begin
  ListBox1.Count := 600000;
end;

procedure TForm1.ListBox1Data(Control: TWinControl; Index: Integer;
  var Data: string);
begin
  Data := IntToStr(Index) + ' listbox item number';
end;

procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
begin
  // just simple drawing to be able to clearly see the items
  if odSelected in State then begin
    ListBox1.Canvas.Brush.Color := clHighlight;
    ListBox1.Canvas.Font.Color := clHighlightText;
  end;
  ListBox1.Canvas.FillRect(Rect);
  ListBox1.Canvas.TextOut(Rect.Left + 2, Rect.Top + 2, ListBox1.Items[Index]);
end;


To see the problem just thumb-track the scroll bar, you'll notice how the items are wrapped to begin from the start for every 65536 one as described by Arnaud in the comments to the question. And when you release the thumb, it will snap to an item in the top High(Word).


Below workaround intercepts WM_VSCROLL on the control and performs thumb and item positioning manually. The sample uses an interposer class for simplicity, but any other sub-classing method would do:

type
  TListBox = class(stdctrls.TListBox)
  private
    procedure WMVScroll(var Msg: TWMVScroll); message WM_VSCROLL;
  end;

[...]

procedure TListBox.WMVScroll(var Msg: TWMVScroll);
var
  Info: TScrollInfo;
begin
  // do not intervene when themes are disabled
  if ThemeServices.ThemesEnabled then begin
    Msg.Result := 0;

    case Msg.ScrollCode of
      SB_THUMBPOSITION: Exit; // Nothing to do, thumb is already tracked
      SB_THUMBTRACK:
        begin
          ZeroMemory(@Info, SizeOf(Info));
          Info.cbSize := SizeOf(Info);
          Info.fMask := SIF_POS or SIF_TRACKPOS;
          if GetScrollInfo(Handle, SB_VERT, Info) and
              (Info.nTrackPos <> Info.nPos) then
            TopIndex := TopIndex + Info.nTrackPos - Info.nPos;
        end;
      else
        inherited;
    end;
  end else
    inherited;
end;
Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
  • +1 Kind of how TControlScrollBar.ScrollMessage fixes this issue. – NGLN Aug 23 '11 at 05:29
  • To reproduce the issue, just add XPMan unit to the uses clauses, to enable theming. Thanks Sertac for your time and for finding out a workaround. Very nice! – Arnaud Bouchez Aug 23 '11 at 07:06
  • 1
    @Arnaud, you're welcome! Sorry for the previously bloated, over-defensive code. It seems a re-check is not necessary as there's no calculation involved for thumb position, that eliminates two calls since `LB_SETTOPINDEX` itself scrolls the bar. – Sertac Akyuz Aug 23 '11 at 15:33
1

For a custom log viewer I wrote, I use a TListView in virtual mode, not a TListBox. Works great, no 32K limits, no need to fiddle with SetScrollInfo() at all. Just set the Item.Count and the rest is handled automatically. It even has an OnDataHint event that can be used to optimize data access by letting you load only the data that the TListView actually needs. You don't get that with a TListBox.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • The question was about fixing `TListBox` behavior, not using another kind of component. But thanks for your experiment sharing. – Arnaud Bouchez Aug 23 '11 at 07:07
  • If it means avoiding all of the limitations, all of the hacking code, and gaining better functionality, why wouldn't you want to switch to a virtual `TListView`? It is still a standard control. – Remy Lebeau Aug 23 '11 at 19:05
  • 1
    In fact, I switched to a virtual `TDrawGrid`, which offers a better rendering for my log viewer. But the question was about a Windows regression, for which Sertac found out a workaround, just by overriding the `WMVScroll` method. This could happen to an existing application, and having such a workaround could be very useful in itself. – Arnaud Bouchez Aug 24 '11 at 17:23