Recent

Author Topic: ListBox MeasureItem  (Read 1362 times)

TheCompanionCube

  • Newbie
  • Posts: 6
ListBox MeasureItem
« on: January 25, 2026, 10:16:44 pm »
Hello!
I'm playing with a ListBox flagged with lbOwnerDrawVariable, so the two events OnDrawItem and OnMeasureItem are fired when items are inserted in the list during the OnCreate event of the form.
To use a bit more data to draw the items, I've created a simple object, like

Code: Pascal  [Select][+][-]
  1. OItemData = class
  2.     Title: String;
  3.     Text: String;
  4.     ID: Integer;
  5.     Status: Integer;
  6.     Color: TColor;
  7. end;
  8.  

So i can do something like

Code: Pascal  [Select][+][-]
  1. ItemData := OItemData.Create;
  2. ItemData.ID := 34;
  3. ItemData.Title := 'Blah blah blah';
  4. ListBox1.Items.InsertObject(0, IntToStr(ItemData.ID), ItemData);

and it works, but only if inside the OnMeasureItem event I don't try to access the object.

Even if I try something very simple like

Code: Pascal  [Select][+][-]
  1. ShowMessage(OItemData(ListBox1.Items.Objects[Index]).Title);

I get a access violation error because simply there is no object to access.
But the object is present, because elsewhere in the code, for example in the OnDrawItem event, I access it perfectly.

I also tryed to add the item using a button, so outside of the OnCreate event, but the result is the same.

Thank you for any suggestion!

Denis





speter

  • Sr. Member
  • ****
  • Posts: 494
Re: ListBox MeasureItem
« Reply #1 on: January 25, 2026, 10:28:01 pm »
Maybe move the code from form.create() to form.activate(); with a boolean set in create() and activate() to make sure the code is only executed once.

cheers
S.
I climbed mighty mountains, and saw that they were actually tiny foothills. :)

TheCompanionCube

  • Newbie
  • Posts: 6
Re: ListBox MeasureItem
« Reply #2 on: January 25, 2026, 10:43:34 pm »
I tryed to move the code as you suggested, but the result is the same, the OnMeasureItem event keeps failing with the access violation error.

jamie

  • Hero Member
  • *****
  • Posts: 7610
Re: ListBox MeasureItem
« Reply #3 on: January 25, 2026, 11:14:38 pm »
can't make it fault.
I am using 4.2 release.
There was some recent work with the ListBox in the trunk, maybe that's it but below is the test code I did.
Code: Pascal  [Select][+][-]
  1. procedure TForm1.ListBox1MeasureItem(Control: TWinControl; Index: Integer;
  2.   var AHeight: Integer);
  3. begin
  4.   AHeight := TListBox(Control).Canvas.TextHeight(TListBox(Control).Items[index])+Random(10);
  5. end;
  6.  
  7. procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  8.   ARect: TRect; State: TOwnerDrawState);
  9. begin
  10.   TListBox(Control).Canvas.FillRect(ARect);
  11.   TListBox(Control).Canvas.TextOut(ARect.Left,ARect.Top,TListBox(Control).Items[Index]+' , '+
  12.   IntToStr(UintPtr(TListBox(Control).Items.Objects[Index])));
  13. end;
  14.  
  15. procedure TForm1.Button1Click(Sender: TObject);
  16. begin
  17.   ListBox1.Items.InsertObject(0,DateTimeToStr(Now),TObject(UintPtr(Now)));
  18. end;
  19.  
  20. procedure TForm1.FormCreate(Sender: TObject);
  21. begin
  22.   ListBox1.Items.Add('Start of List');
  23. end;
  24.                                                            
  25.  

Jamie
The only true wisdom is knowing you know nothing

LeP

  • Full Member
  • ***
  • Posts: 208
Re: ListBox MeasureItem
« Reply #4 on: January 26, 2026, 12:31:18 am »
He tries to access the list of Objects in the OnMeasureItem event during the insertion itself.
The event fires immediately during the insertion operation, and it's very likely that the list hasn't yet been updated with the insertion itself.
In practice, the element with Index 0 (because the insertion is always performed at the beginning of the list) is never present during this operation.
In your example, @Jamie, however, you don't access the "object" in the OnMeasureItem event, and therefore the error isn't raised.

jamie

  • Hero Member
  • *****
  • Posts: 7610
Re: ListBox MeasureItem
« Reply #5 on: January 26, 2026, 12:46:58 am »
No indication of what Widget he is using and if the trunk is being used because that has had recent work done to it.

I did this on Windows 64; it works fine even when inserting such items in the Form.Create.

Time will tell I guess.

Jamie


The only true wisdom is knowing you know nothing

TheCompanionCube

  • Newbie
  • Posts: 6
Re: ListBox MeasureItem
« Reply #6 on: January 26, 2026, 09:24:54 am »
Using Jamie code, I modified it to raise the error.

Code: Pascal  [Select][+][-]
  1. OItemData = class
  2.   Text: String;
  3.   Height: Integer;
  4. end;
  5.  
  6.  procedure TForm1.ListBox1MeasureItem(Control: TWinControl; Index: Integer;
  7.   var AHeight: Integer);
  8. begin
  9.   //AHeight := TListBox(Control).Canvas.TextHeight(TListBox(Control).Items[index])+Random(10);
  10.   AHeight := OItemData(TListBox(Control).Items.Objects[Index]).Height;
  11. end;
  12.  
  13. procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  14.   ARect: TRect; State: TOwnerDrawState);
  15. begin
  16.   TListBox(Control).Canvas.FillRect(ARect);
  17.   TListBox(Control).Canvas.TextOut(ARect.Left,ARect.Top,TListBox(Control).Items[Index]+' , '+
  18.   IntToStr(UintPtr(TListBox(Control).Items.Objects[Index])) + ' - ' + OItemData(TListBox(Control).Items.Objects[Index]).Text);
  19. end;
  20.  
  21. procedure TForm1.Button1Click(Sender: TObject);
  22. var
  23.   Item: OItemData;
  24. begin
  25.   Item := OItemData.Create;
  26.   Item.Heigh := Random(10) + 10;
  27.   Item.Text := 'Hello, world!';
  28.   //ListBox1.Items.InsertObject(0,DateTimeToStr(Now),TObject(UintPtr(Now)));
  29.   ListBox1.Items.InsertObject(0,Item.Text,Item);
  30. end;
  31.  

I'm working on version 4.4 on Windows 64.

VisualLab

  • Hero Member
  • *****
  • Posts: 717
Re: ListBox MeasureItem
« Reply #7 on: January 26, 2026, 11:55:12 am »
You need to check if the object is on the list.

Code: Pascal  [Select][+][-]
  1. procedure TForm1.ListBox1MeasureItem(Control: TWinControl; Index: Integer; var AHeight: Integer);
  2. var
  3.   LListBox: TListBox;
  4. begin
  5.   LListBox := Control as TListBox;
  6.   if LListBox.Items.Objects[Index] = nil // checking if the object is on the list
  7.    then Exit;
  8.   AHeight := (LListBox.Items.Objects[Index] as TMyItemData).Height;
  9. end;

I am attaching a sample project.

TheCompanionCube

  • Newbie
  • Posts: 6
Re: ListBox MeasureItem
« Reply #8 on: January 26, 2026, 05:09:46 pm »
Thank you!
The code works perfectly*, but only if the Style of the ListBox is set to lbStandard.
If you set the Style property to lbOwnerDrawVariable, LListBox.Items.Objects[Index] is always NIL, on every item you add to the list, but only in the ListBox1MeasureItem event.
The ListBox1DrawItem event always works and beacuse the objects in that function are always valid and not NIL.

You need to check if the object is on the list.

Code: Pascal  [Select][+][-]
  1. procedure TForm1.ListBox1MeasureItem(Control: TWinControl; Index: Integer; var AHeight: Integer);
  2. var
  3.   LListBox: TListBox;
  4. begin
  5.   LListBox := Control as TListBox;
  6.   if LListBox.Items.Objects[Index] = nil // checking if the object is on the list
  7.    then Exit;
  8.   AHeight := (LListBox.Items.Objects[Index] as TMyItemData).Height;
  9. end;

I am attaching a sample project.

* Thank you very much for your code, I've learn a lot about clean and effective Pascal code writing!Migrating from other languages is always a bit confusing.

TheCompanionCube

  • Newbie
  • Posts: 6
Re: ListBox MeasureItem
« Reply #9 on: February 05, 2026, 09:48:41 pm »
In the end, the answer, as often happens, is simpler than it might seem.
I write here what I found, maybe it can be useful for someone else.

When an item is created in the ListBox, immediately after the LB_ADDSTRING or LB_INSERTSTRING messages, the control sends a WM_MEASUREITEM to the parent.
 
At this point, the data part of the item is not ready yet, because the LB_SETITEMDATA message is processed only after the WM_MEASUREITEM message, so the only way to calculate the height of an item is before inserting it into the ListBox, and keep the value available outside the data part of the item to use it during the measuring stage.

It's also possible to ignore the WM_MEASUREITEM message, or use a constant value, and send a LB_SETITEMHEIGHT message, with the correct calculated value, during the drawing process of the items. When the WM_DRAWITEM message is sent from the ListBox, the LB_SETITEMDATA has already been processed, so it is possible to calculate all the correct value using every detail of the item.
This method works, but the vertical scroll bar, which values are calculated using the data collected from WM_MEASUREITEM, could not work correctly, unless the user manually scrolls the items some times after the insertion.

jamie

  • Hero Member
  • *****
  • Posts: 7610
Re: ListBox MeasureItem
« Reply #10 on: February 05, 2026, 11:02:02 pm »
Something is wrong here.

The code I posted worked flawlessly without those hiccups, when using the AddObject it should never call the measure item until the call is complete.

I am using Windows, what widget are you using, because I feal something is wrong here?

Jamie
 
The only true wisdom is knowing you know nothing

TheCompanionCube

  • Newbie
  • Posts: 6
Re: ListBox MeasureItem
« Reply #11 on: February 09, 2026, 08:43:49 pm »
I don't know exactly where the problem is, but also Microsoft talks about it:

https://learn.microsoft.com/en-us/windows/win32/winauto/exposing-owner-drawn-list-box-items

Quote
With owner-drawn variable list boxes created with the style LBS_OWNERDRAWVARIABLE, use a global variable or some other mechanism to keep track of when the itemData member of the MEASUREITEMSTRUCT is valid. The global variable is needed because the system sends the WM_MEASUREITEM message as soon as the string is added but before the item data is attached, and at this point the itemData member is not valid.

 

TinyPortal © 2005-2018