Recent

Author Topic: TCheckListBox — variable height for items  (Read 412 times)

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
TCheckListBox — variable height for items
« on: July 16, 2019, 11:27:02 pm »
Many years ago, I created in Delphi 7 a small application with TCheckListBox component, which had items of variable height (has a lbOwnerDrawVariable style). It looked like it was on the screenshot in the attachment.

Today I need to create something like this. And WTF — TCheckListBox does not have a published OnMeasureItem event (is in the public section)… I added this event to the published section of the TCheckListBox class, rebuilt the IDE and used it in my project. And WTF again — this event is not called per each item, is called only once… As a result, all component items have the same height…

As it stands in the documentation:

Code: Pascal  [Select]
  1. type TListBoxStyle = (
  2.   lbStandard,          // Items drawn by the widget.
  3.   lbOwnerDrawFixed,    // Items drawn by user code, all items have the same height.
  4.   lbOwnerDrawVariable, // Items drawn by user code, every item can have a different height.
  5.   lbVirtual
  6. );

So try this:

Code: Pascal  [Select]
  1. procedure TMyForm.MyListBoxMeasureItem(AControl: TWinControl; AIndex: Integer; var AHeight: Integer);
  2. begin
  3.   AHeight := 20 + AIndex * 10;
  4. end;

Each next item should be higher, but all have the same height.


So how can I make items with variable heights, as it appears in a screenshot? I'm so desperated…
« Last Edit: July 16, 2019, 11:46:35 pm by furious programming »
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)

jamie

  • Hero Member
  • *****
  • Posts: 1735
Re: TCheckListBox — variable height for items
« Reply #1 on: July 17, 2019, 01:21:40 am »
You may also need to set the class of the window using SetWindLong to address that behavior..

 Look into the control deeper its possible the message is getting rerouted after the first one..

Also remember that windows only sends OnMessageItem message once per entry and remembers the size  you gave it afterwards. Only when you make a change to an entry even if its the same value, is when the event will start the cycle of MessureItem messaging..

Btw, you can type class the class with the same name in the unit you are using it for and add what ever
you need to it.

Code: Pascal  [Select]
  1. Type
  2.  TCheckListBox = Class(Controls.TCheckListBox)
  3.   Begin
  4.     // add all of your overridies etc here..
  5.   End;
  6.  

 The idea is to get it working here and then building permanently later.

 Also I don't rebuilt the IDE, just the files until I know its working correctly.
« Last Edit: July 17, 2019, 01:25:00 am by jamie »

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
Re: TCheckListBox — variable height for items
« Reply #2 on: July 17, 2019, 02:30:25 am »
Look into the control deeper its possible the message is getting rerouted after the first one..

Hmm… I don't even know where to start, I don't know WinAPI very well.

Quote
Also remember that windows only sends OnMessageItem message once per entry and remembers the size  you gave it afterwards.

Yep, and this behavior is expected for me. However, if I could dynamically (at runtime) change the height of items, it would be very helpful.

Quote
Btw, you can type class the class with the same name in the unit you are using it for and add what ever
you need to it.

I know — this is subclassing. But I don't want to use this technique, because I have multiple TCheckListBoxes on the main form and few more in the other forms, and the content of each of them is rendered differently. What else about a small test program.

Quote
Also I don't rebuilt the IDE, just the files until I know its working correctly.

I wanted the OnMeasureItem event be available in the Object Inspector window, that's why I rebuilt the IDE.
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)

jamie

  • Hero Member
  • *****
  • Posts: 1735
Re: TCheckListBox — variable height for items
« Reply #3 on: July 17, 2019, 02:57:18 am »
Looking at the control it appears there could be an issue, however, I See that you can do what you need using the TlistBox instead..

The TListCheckBox is the basically the same control but if you use the TlistBox you'll simply need to draw the check mark yourself along with other items..

 I'll look tomorrow to see why its like that.

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
Re: TCheckListBox — variable height for items
« Reply #4 on: July 17, 2019, 03:21:34 am »
Looking at the control it appears there could be an issue, however, I See that you can do what you need using the TlistBox instead..

No, I need checkboxes with each item.

Quote
The TListCheckBox is the basically the same control but if you use the TlistBox you'll simply need to draw the check mark yourself along with other items..

Yes, in the end, I'm rendering items myself. But in addition to the visual checkboxes, I also need methods and properties that are not in the TListBox class.


Ehh… I found something…:

Code: Pascal  [Select]
  1. procedure TCustomCheckListBox.CreateParams(var Params: TCreateParams);
  2. begin
  3.   inherited CreateParams(Params);
  4.   Params.Style := (Params.Style and not LBS_OWNERDRAWVARIABLE) or LBS_OWNERDRAWFIXED;
  5. end;

Fixed flags that effectively block the ability to set different item heights. Using subclassing, I can invert this flags and enable needed functionality:

Code: Pascal  [Select]
  1. type
  2.   TCheckListBox = class(CheckLst.TCheckListBox)
  3.   protected
  4.     procedure CreateParams(var AParams: TCreateParams); override;
  5.   end;
  6.  
  7. {..}
  8.  
  9. procedure TCheckListBox.CreateParams(var AParams: TCreateParams);
  10. begin
  11.   inherited CreateParams(AParams);
  12.  
  13.   if Style = lbOwnerDrawVariable then
  14.     AParams.Style := (AParams.Style and not LBS_OWNERDRAWFIXED) or LBS_OWNERDRAWVARIABLE;
  15. end;

See the screenshot — blue item is selected (no text with items, I'm working on it now).

But there is a turbo bug with the checkbox area detection while clicking. The higher the item is, the wider the area is for changing the status of the checkbox with the mouse. See the second attachment — red frames show the areas in which clicking causes checkbox to be checked or unchecked. Checkbox areas are practically squared…
« Last Edit: July 17, 2019, 04:06:43 pm by furious programming »
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
Re: TCheckListBox — variable height for items
« Reply #5 on: July 17, 2019, 03:57:50 am »
But there is a turbo bug with the checkbox area detection while clicking. The higher the item is, the wider the area is for changing the status of the checkbox with the mouse. […] Checkbox areas are practically squared…

I found the code responsible for this behavior. It is in the Win32WSCheckLst unit, inside the CheckListBoxWndProc:

Code: Pascal  [Select]
  1. function CheckListBoxWndProc(Window: HWnd; Msg: UInt; WParam: Windows.WParam;
  2.     LParam: Windows.LParam): LResult; stdcall;
  3. var
  4.   WindowInfo: PWin32WindowInfo;
  5.  
  6.   procedure CheckListBoxLButtonDown;
  7.   var
  8.     I: Integer;
  9.     ItemRect: Windows.Rect;
  10.     MousePos: Windows.Point;
  11.     Message: TLMessage;
  12.   begin
  13.     MousePos.X := GET_X_LPARAM(LParam);
  14.     MousePos.Y := GET_Y_LPARAM(LParam);
  15.     for I := 0 to Windows.SendMessage(Window, LB_GETCOUNT, 0, 0) - 1 do
  16.     begin
  17.       Windows.SendMessage(Window, LB_GETITEMRECT, I, PtrInt(@ItemRect));
  18.       if TCheckListbox(WindowInfo^.WinControl).UseRightToLeftAlignment then
  19.         ItemRect.Left := ItemRect.Right - ItemRect.Bottom + ItemRect.Top
  20.       else
  21.         ItemRect.Right := ItemRect.Left + ItemRect.Bottom - ItemRect.Top;
  22.       if Windows.PtInRect(ItemRect, MousePos) then
  23.       begin
  24.         // item clicked: toggle
  25.         if I < TCheckListBox(WindowInfo^.WinControl).Items.Count then
  26.         begin
  27.         {..}

The selected lines contain instructions that calculate a strange, big rectangle instead of taking the physical area occupied by the button. That's why on a very high item, almost half of its width allows to change the state of the checkbox.

Good job, guys.
« Last Edit: July 17, 2019, 04:08:55 pm by furious programming »
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)

lucamar

  • Hero Member
  • *****
  • Posts: 1942
Re: TCheckListBox — variable height for items
« Reply #6 on: July 17, 2019, 08:42:39 am »
You found it before I could comment :)

Yes, it's getting the height of the whole box and calculating a square with that height.

Probably it's done to allow for changes in the size of the font but it will only work more or less "right" with single-line items, as your test show.
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus 2.0.2/2.0.4  - FPC 3.0.4 on:
(K|L)Ubuntu 12..16, Windows XP SP3, various DOSes.

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
Re: TCheckListBox — variable height for items
« Reply #7 on: July 17, 2019, 03:51:03 pm »
For me, something like that is inconceivable. The more so because the code responsible for determining the area of checkboxes is in the same unit! Therefore, why do not take advantage of it? Why is the area that allows to change the selection of a checkbox with the mouse is much larger than its actual size? This is nonsense.

Now I have to override the control WndProc method and redefine the entire LM_LBUTTONDOWN message hendler part so that the user can use my program normally. Dirty job with subclassing and Windows API. Another day lost, because of such trivial design mistakes.

Probably it's done to allow for changes in the size of the font but it will only work more or less "right" with single-line items, as your test show.

Unfortunately, this code does not work properly even with single-line items. All you have to do is use a different item height than the standard, and calculating the clickable area of the checkbox becomes incorrect. The higher item, the more incorrect.

In the case of a square item, its entire area allows you to change the state of the checkbox with the mouse, and this makes the component completely useless (it can not be properly handled with the mouse).
« Last Edit: July 17, 2019, 04:04:47 pm by furious programming »
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
Re: TCheckListBox — variable height for items
« Reply #8 on: July 17, 2019, 06:58:45 pm »
I used subclassing to redefine the WndProc method so that it can handle mouse messages. Generally, this is about:

Code: Pascal  [Select]
  1. procedure TCheckListBox.WndProc(var AMessage: TLMessage);
  2. begin
  3.   if (AMessage.msg = LM_LBUTTONDOWN) or (AMessage.msg = LM_LBUTTONDBLCLK) then
  4.   begin
  5.     // my code
  6.   end
  7.   else
  8.     inherited WndProc(AMessage);
  9. end;

Even though my code is executing, the one from the default handler is also executed. So my fix do nothing — I can still check and uncheck checkboxes by clicking inside items.

What should I do to completely override the default handler for this messages?
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)

lucamar

  • Hero Member
  • *****
  • Posts: 1942
Re: TCheckListBox — variable height for items
« Reply #9 on: July 18, 2019, 12:46:07 am »
For me, something like that is inconceivable. The more so because the code responsible for determining the area of checkboxes is in the same unit! Therefore, why do not take advantage of it? Why is the area that allows to change the selection of a checkbox with the mouse is much larger than its actual size? This is nonsense.

Now I have to override the control WndProc method and redefine the entire LM_LBUTTONDOWN message hendler part so that the user can use my program normally. Dirty job with subclassing and Windows API. Another day lost, because of such trivial design mistakes.

Have you considered correcting the original source and submitting a patch to the bug-tracker? Or at least asking in the mailing list WTH this was done so? There may be some other reason we know nothing about ... although I can't see it.

As to why ... well, just a wild guess but maybe someone thought that this being a quasi-tabular stle of control the user should be able to "check" the box by clicking on the text or something like that? The road to hell and all that ... :)

I used subclassing to redefine the WndProc method so that it can handle mouse messages. Generally, this is about:

Code: Pascal  [Select]
  1. ....

Even though my code is executing, the one from the default handler is also executed. So my fix do nothing — I can still check and uncheck checkboxes by clicking inside items.

What should I do to completely override the default handler for this messages?

Strange. Have you tried following the message path to see what it's doing and where? I would help you more but this sounds like a little time-consuming and time is something I can't realy spare now. Sorry.
« Last Edit: July 18, 2019, 12:50:11 am by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus 2.0.2/2.0.4  - FPC 3.0.4 on:
(K|L)Ubuntu 12..16, Windows XP SP3, various DOSes.

furious programming

  • Sr. Member
  • ****
  • Posts: 354
  • I click a little.
Re: TCheckListBox — variable height for items
« Reply #10 on: July 18, 2019, 04:00:14 am »
Have you considered correcting the original source and submitting a patch to the bug-tracker? Or at least asking in the mailing list WTH this was done so?

Currently no — I tested my solution only on WinXP. ”Tomorrow” I will check on Win7 and Win10.

Quote
There may be some other reason we know nothing about ... although I can't see it.

Probably there is no reason to do this ”strange” calculations. Items can have any height, not only that calculated by the component itself. And the items can have different height, one bigger than other. Somebody who wrote this code didn't know that, or just implement ”dirty” solution for now.

The size of the checkbox is known — it is retrieved using the method from the ThemeServices object. On the other hand, checkbox offsets in the item are hardcoded in the source code. No problem to calculate the correct area occupied by the checkbox graphic and use it in several widgetset methods (in the painting method and in the message handling method).

Quote
As to why ... well, just a wild guess but maybe someone thought that this being a quasi-tabular stle of control the user should be able to "check" the box by clicking on the text or something like that?

You have a good imagination. 8)

But IMO this is a bug, not a feature. The strange is, that the entire area on the left of the area for text is used to toggle the checkbox. Not the area of the visible checkbox (painted by theThemeServices), but entire area (high as whole item).

I just used the subclassing, overriden the WndProc method and commented out the code in the widgetset, which is responsible for handling left button messages (and toggling the checkbox). But I wrote my code in the way that the user can toggle the checkbox only by clicking in the visual checkbox area — not above, not below and not next to it, just directly on the button graphic. This is intuitive — the user will be happy.

It was the only (and fast) way to completely cover the default handler of such a messages. I don't have time to look for bugs and come up with solutions to eliminate them — the deadline is approaching…

Quote
Strange. Have you tried following the message path to see what it's doing and where?

No, currently I have no time for this. Especially that the message callback for TCheckListBox is overriden in the widgetset itself anyway. There's some magic going on.
« Last Edit: July 18, 2019, 04:12:23 am by furious programming »
Lazarus 2.0.4 with FPC 3.0.4, Windows XP (all 32-bit)