Recent

Author Topic: [New Component] ExtTabCtrl - A custom, feature-rich tab control (LGPL)  (Read 6437 times)

ovidio

  • Jr. Member
  • **
  • Posts: 68
Compiled the demo projects at 96ppi and ran them at 144ppi - the inner dimensions scale correctly, fine!. With one exception: the images do not scale or are scaled by the system to become very pixelated - see screenshot.

Looking at the code of the demo_simple I see that you do not scale the user-provided imagelist. I set ImageList.Scaled to true, added two image resolutions for 24x24 and 32x32 images (in the ImageList editor) and replaced the icons by similar ones from the Lazarus icon collection (folder images/general_purpose). This changed the two user-icons on two tabs but left the internal icons for left/right arrows, close-crosses and add-tab unchanged. I guess these are the ButtonImages properties - but assigning some indices of my imagelist did not change anything. So, I am afraid that something's wrong here in image handling.

The other issue is that the user-provided images do not change their size when the projects runs under 144 ppi. You need some changes to take care of the "real" image size under High-DPI:

* In function TExtTabCtrl.GetTabImageWidth use FImages.WidthForPPI[0, Font.PixelsPerInch] to measure the image width; the "0" indicates that the default width of the images (i.a. at 96ppi) should be assumed to be the Width of the ImageList, but the images can be scaled to any other base width (normally there is an extra property "ImagesWidth" next to the "Images" property in controls using an ImageList). You may also have to apply the result of CanvasScaleFactor, but I don't have experience with it since Windows does not use it (macOS does...)

* Likewise, in GetTabImageHeight use FImages.HeightForPPI[...].

* I did not dive into your drawing code which is more complicated than usual because you are caching rotated images. But basically you should always call the XXXXForPPI methods of the imagelist to get the image at the correct size for the current resolution. Example: Draw the image of index AIndex at position AX, AY - AImageWidthAt96PPI is 0 (or FImagesWidth if you'd provide it), ATargetPPI is usually taken from the font settings (Font.PixelsPerInch), ACanvasFactor is the result of GetCanvasScaleFactor:
Code: Pascal  [Select][+][-]
  1. procedure DrawForPPI(ACanvas: TCanvas; AX, AY, AIndex: Integer;
  2.       AImageWidthAt96PPI, ATargetPPI: Integer; ACanvasFactor: Double; AEnabled: Boolean = True);

* In order to extract an image for caching you should call ImageList.ResolutionForPPI[AImageWidth, APPI: Integer; const ACanvasScaleFactor: Double].GetBitmap(Index: Integer; Image: TCustomBitmap)

In the attachment there's a version of demo_simple where everything should be set up for correct scaling.

Two more remarks:
* I am missing an imageindex entry for the close button in the ButtonImages property.
* You should declare all image index properties as TImageIndex rather than integer. Then the LCL can select the corresponding property editor which displays preview images in a combobox.

I have tried to implement all of your suggestions. I hope to have done it right (I have zero experience with high-DPI and don't have any setup to test it). I had a lot of troubles with GetCanvasScaleFactor (on my macOS it was returning 2, and this resulted in the image stretched to twice its size, extending outside the tab and pixelated), hence, I commented this out. Maybe somebody can tell me what was wrong.

Please, wp, can you give it a try?

ovidio

  • Jr. Member
  • **
  • Posts: 68
I don't know, maybe there are similar one in the TabOptions property: When I turn toShowAddButton off in the Object inspector, the icon at the right does not disappear.
I believe that this behavior is because this image is on a TSpeedButton, and in edit mode components are shown even when Visible=False. Am I right?

Shouldn't the top stripe run across the entire width of the tab? And I am a bit confused about the colored line appearing above the top of the selected tab. Sometimes it is hard to be distinguished. Is it to highlight the selected tab again? Can it be turned off?
Yes, this "shadow" line is to highlight the selected tab (I have disabled it for the active tabs with a stripe color defined). Moreover, now the color stripe extends over all the tab. Can you check that it is OK?

ovidio

  • Jr. Member
  • **
  • Posts: 68
I am a bit confused about the colored line appearing above the top of the selected tab. Sometimes it is hard to be distinguished. Is it to highlight the selected tab again? Can it be turned off?

I'm actually using PageControl combined with the component attabs for such purpose, but i did read this topic with interest.

My 5 pennies: a colored line appearing on top of the selected  a selected or hovered tab as well can be a very useful visual indicator resp. additional supporter to recognize better the selected tab and the tab where the mouse is actually over. Personally i won't miss such. See sample screenshots:

Right now, you can define a color stripe at the top of any tab, but the stripe remains visible even when the tab is not active. Only the Chrome style uses a stripe to indicate the active tab.

Indeed, the color stripes in ExtTabCtrl are inspired by ATTabs, which I have used in some projects. However, I found that ATTabs offers so many configuration options that I decided to create a component that is simpler for users to configure.

EDIT: You can easily get the same effect for any style, by setting/removing the StripeColor property when the tab is activated/deactivated.
« Last Edit: June 08, 2026, 01:46:22 pm by ovidio »

ovidio

  • Jr. Member
  • **
  • Posts: 68
In gtk3 there seems to be some text measuring issue
Was able to reproduce this outside the component and reported as a bug: https://gitlab.com/freepascal.org/lazarus/lazarus/-/work_items/42352

[EDIT]
Has been fixed already. Thank you, zeljko.

Great! Thank you!

ovidio

  • Jr. Member
  • **
  • Posts: 68
I don't know, maybe there are similar one in the TabOptions property: When I turn toShowAddButton off in the Object inspector, the icon at the right does not disappear.
I believe that this behavior is because this image is on a TSpeedButton, and in edit mode components are shown even when Visible=False. Am I right?

You were right, there was a bug. I believe that everything is working now. I mean, the button still remains visible at design time, but the button glyphs were not updated correctly, and now they are.
« Last Edit: June 08, 2026, 05:43:24 pm by ovidio »

wp

  • Hero Member
  • *****
  • Posts: 13631
I have used 'csDesignInteractive' to allow the user selecting the tabs at design time, and this works as expected. However, this has the unintended result that the user cannot drag the component to a new location. In general, this is not a huge problem, since this component should generally be aligned to the Top/Left/..., but it would be nice to have it anyway.
Hmm, even when the control will be aligned to top/left/etc in the end, I did not expect that I cannot drag it. As long as the tabs can be selected in the component tree, I would say that selecting tabs at designtime less import. This is different from a TPageControl where individual pages are attached to the tabs, and clicking on a tab allows to bring the associated page to the front so that it can be edited. But TExtTabCtrl has no pages, the only need to click a tab would be to select that tab in the object inspector. As long as selecting tabs in the component tree is working (and it does) there is no immediate need to click tabs in the designer form. Of course, it would be nice to have, but not so imporant to sacrifice the feature common to all controls], namely the ability to be dragged and resized at designtime.

Searched the Lazarus code base for the word "csDesignInteractive", and I found only the TCDCustomTabControl ("CD"=custom-drawn) where it is added to the ControlStyle. Installed the CustomDrawn components, and the TCDTabControl has the same issue...

I would say that csDesignInteractive is a half-finished feature. Do not use it.

The JvTabBar (in the JVCL library) is a similar component like yours: it allows selection of tabs in the component tree, but the selected tab is not updated in the form designer - like your component when csDesignInteractive is removed.

ATTabs is even worse: it does not even show the tabs in the object tree, you must invoke the component editor (click on the '...' next to "Tabs") to select a tab for editing.

In total, I think your control is as good as these, or better, even without the ability to have the possibility to select the tabs in the form designer.

wp

  • Hero Member
  • *****
  • Posts: 13631
Some testing of the latest commits:

* High-DPI seems to be working, but only with the external images from an imagelist.  [EDIT: When I drag the form to be narrower so that the scrollbutton(s) appear, it can be seen that they are pixelated - something is wrong with scaling of the images for the internal buttons]

* I extended the notebook demo so that high-dpi performance can be checked (I am having another user on the same system for which the screen has been set up for 144 ppi, this is on Windows, don't know how to do this on mac). In this demo the '+' button (add tab) looks too large and kind of asymmetrical at 144 ppi (see screenshot), I think the speed button should be created as a square rather than a narrow rectangle. Or maybe its width is not scaled correctly?

* Collective ImagesWidth property: I'd prefer the default values of all XXXWidth elements to be 0. Then the Width of the ImageList determines the base size of the image. If the user wants some images to be larger he has to provide a full set of them for the larger size, in other words: when TabImagesWidth should be 24 the image list must contain also images at 36x26 and 48x48. When you force it to be 24 the user must be aware to provide these sizes which is not standard, normally he would only provide 16x16, 24x24 and 32x32.

* Internal images: You show them on speedbuttons and assign them to their Glyph property. For proper scaling of these images you must also provide them at the other sizes for 150% and 200% screen resolution, and when an image is selected you must read the pixels-per-inch and decide which image is to be taken. Or you create an internal ImageList for which you set its Scaled to true and register these additional image sizes (ImageList.RegisterResolutions(16, 24, 32) -- not sure: maybe the "16" can be dropped...), and then you add each image from the resource at all the sizes needed (ImageList.AddMultipleResolutions([bitmap16, bitmap24, bitmap32]). This has the advantage that all scaling is done automatically, it also takes care of intermediate image sizes. Set the internal imagelist to Images property of the speedbuttons and assign the corresponding imageindex. If the user assigns its own imagelist to TExtCtrl.Images then, of course, the speedbuttons imagelist and imageindex must be updated.

* Stripe of selected tab in Chrome style when no StripColor is prepared: Can the color be modified? The default blue is fine for me, but other user maybe have other preferences, and the color may not be visible clearly in dark mode.
« Last Edit: June 08, 2026, 08:24:27 pm by wp »

ovidio

  • Jr. Member
  • **
  • Posts: 68
I would say that csDesignInteractive is a half-finished feature. Do not use it.

Ok, you are right. I have removed csDesignInteractive. I tried to achieve the same goal by intercepting LM_LBUTTONDOWN, but this is not working (at design time the mouse clicks are not captured). For now, I left this code, but it is useless. Any idea of how to get this working?

ovidio

  • Jr. Member
  • **
  • Posts: 68
* High-DPI seems to be working, but only with the external images from an imagelist.  [EDIT: When I drag the form to be narrower so that the scrollbutton(s) appear, it can be seen that they are pixelated - something is wrong with scaling of the images for the internal buttons]
I will check what can be producing this effect. Probably is the width of the button, but I am not sure at the moment.

* I extended the notebook demo so that high-dpi performance can be checked (I am having another user on the same system for which the screen has been set up for 144 ppi, this is on Windows, don't know how to do this on mac). In this demo the '+' button (add tab) looks too large and kind of asymmetrical at 144 ppi (see screenshot), I think the speed button should be created as a square rather than a narrow rectangle. Or maybe its width is not scaled correctly?
I have updated the examples, with your modifications. In the Notebook example I have added a button lo load the image because by default the macOS sets the Documents dir, and the direct load fails.

* Collective ImagesWidth property: I'd prefer the default values of all XXXWidth elements to be 0. Then the Width of the ImageList determines the base size of the image. If the user wants some images to be larger he has to provide a full set of them for the larger size, in other words: when TabImagesWidth should be 24 the image list must contain also images at 36x26 and 48x48. When you force it to be 24 the user must be aware to provide these sizes which is not standard, normally he would only provide 16x16, 24x24 and 32x32.
Done.

* Internal images: You show them on speedbuttons and assign them to their Glyph property. For proper scaling of these images you must also provide them at the other sizes for 150% and 200% screen resolution, and when an image is selected you must read the pixels-per-inch and decide which image is to be taken. Or you create an internal ImageList for which you set its Scaled to true and register these additional image sizes (ImageList.RegisterResolutions(16, 24, 32) -- not sure: maybe the "16" can be dropped...), and then you add each image from the resource at all the sizes needed (ImageList.AddMultipleResolutions([bitmap16, bitmap24, bitmap32]). This has the advantage that all scaling is done automatically, it also takes care of intermediate image sizes. Set the internal imagelist to Images property of the speedbuttons and assign the corresponding imageindex. If the user assigns its own imagelist to TExtCtrl.Images then, of course, the speedbuttons imagelist and imageindex must be updated.
I have to think a bit how to do this but, for now, I would prefer not to touch it. The goal of the internal images is to provide a bare minimum, and the user can easily set the component to work at high DPIs (or will be able to do it when all the bugs are fixed).

* Stripe of selected tab in Chrome style when no StripColor is prepared: Can the color be modified? The default blue is fine for me, but other user maybe have other preferences, and the color may not be visible clearly in dark mode.
This stripe uses the color clHighlight, so it should always be visible if the OS is properly configured. This is the only method to change this color. To be honest, I would prefer not to add any configuration option for a feature that only appears on one style.

wp

  • Hero Member
  • *****
  • Posts: 13631
* High-DPI seems to be working, but only with the external images from an imagelist.  [EDIT: When I drag the form to be narrower so that the scrollbutton(s) appear, it can be seen that they are pixelated - something is wrong with scaling of the images for the internal buttons]
I will check what can be producing this effect. Probably is the width of the button, but I am not sure at the moment.
I see at 96ppi that the button is square, and it is unnaturally high at 144 ppi. I guess you scale the button height twice.

I have updated the examples, with your modifications. In the Notebook example I have added a button lo load the image because by default the macOS sets the Documents dir, and the direct load fails.
Sorry, I had forgotten to add the button to my project. I noticed, however, that checking the "Show close buttons" option on the Settings page should display the "cross" icon in all tabs, but this does not happen.

* Internal images...]
I have to think a bit how to do this but, for now, I would prefer not to touch it. The goal of the internal images is to provide a bare minimum, and the user can easily set the component to work at high DPIs (or will be able to do it when all the bugs are fixed).
I can try to implement it on my own when the status of the component has settled. Otherwise, of course, you are the boss.

* Stripe of selected tab in Chrome style...
This stripe uses the color clHighlight [...]
That's fine.

One thing, I don't know whether I already mentioned it: Declare all image index properties as TImageIndex rather than integer. This way the ImageIndex property editor is used by the object inspector, and the user sees the images in the dropdown next to the property name (compare with TMenuItem.ImageIndex).

ovidio

  • Jr. Member
  • **
  • Posts: 68
I see at 96ppi that the button is square, and it is unnaturally high at 144 ppi. I guess you scale the button height twice.
Ok, with this info I know better where to search for the problem.

EDIT 2 (2026/06/10): I have rewritten the 'AnchorButtons' method, and I believe that this should be fixed now. Can you check it?

Sorry, I had forgotten to add the button to my project. I noticed, however, that checking the "Show close buttons" option on the Settings page should display the "cross" icon in all tabs, but this does not happen.
This is my fault. I set "ShowCloseButton=False" in these 3 tabs. For all the other tabs the close button will appear. Would you prefer that it also appears in the 3 main tabs?

I can try to implement it on my own when the status of the component has settled. Otherwise, of course, you are the boss.
It would be great! Obviously, you know far better than me how to deal with high DPIs.

One thing, I don't know whether I already mentioned it: Declare all image index properties as TImageIndex rather than integer. This way the ImageIndex property editor is used by the object inspector, and the user sees the images in the dropdown next to the property name (compare with TMenuItem.ImageIndex).
Yes, you mentioned it (and I believe that I did). Did I miss any index?
EDIT: I had not done it for the image index in the tabs. Now it is fixed. However, it is not working as expected. I will have to check why.
« Last Edit: June 10, 2026, 01:08:02 pm by ovidio »

wp

  • Hero Member
  • *****
  • Posts: 13631
Sorry, I had forgotten to add the button to my project. I noticed, however, that checking the "Show close buttons" option on the Settings page should display the "cross" icon in all tabs, but this does not happen.
This is my fault. I set "ShowCloseButton=False" in these 3 tabs. For all the other tabs the close button will appear. Would you prefer that it also appears in the 3 main tabs?
I don't know, it was meant just as a visual check for high-dpi. But in the logics of the programm the 3 main tabs should not have it - so, probably better to keep the code as it is. Maybe you should change the caption of the "Show Close Buttons" checkbox to "Show Close Buttons in additionally created buttons", or similar. I'll certainly forget this detail otherwise.

One thing, I don't know whether I already mentioned it: Declare all image index properties as TImageIndex rather than integer. This way the ImageIndex property editor is used by the object inspector, and the user sees the images in the dropdown next to the property name (compare with TMenuItem.ImageIndex).
Yes, you mentioned it (and I believe that I did). Did I miss any index?
EDIT: I had not done it for the image index in the tabs. Now it is fixed. However, it is not working as expected. I will have to check why.
Ah, normally the ImageIndex is a property of the class which also has the ImageList. The ImageIndex property editor, in components/ideintf/graphpropedits.pas, uses the method GetImagelist to find the imagelist from which it can take the images to display in the combobox. It is probably the easiest way to extend the TImageIndexPropertyEditor for TExtTabCtlr (--> TExtTabCtrlImageIndexProperty) in a way similar to what is done for the stringgrid (TGridImageIndexPropertyEditor in the same unit): GetComponent(0) is the component which has the imageindex as property (TExtTab, or TButtonImage (?)). For TExtTab you can find the ExtTabCtrl from its GetOwner method, and for TButtonImages you need to provide a field pointing to the TabCtrl. Once the TabCtrl is known you can return the imagelist:
Code: Pascal  [Select][+][-]
  1. //Untested...
  2.  
  3. type
  4.   TExtTabCtrlImageIndexProperty = class(TImageIndexPropertyEditor)
  5.   protected
  6.     function GetImageList: TCustomImageList; override;
  7.   end;
  8.  
  9. function TExtTabCtrlImageIndexProperty.GetImageList: TCustomImageList;
  10. var
  11.   P: TPersistent;
  12.   tc: TExtTabCtrl;
  13. begin
  14.   P := GetComponent(0);
  15.   if P is TExtTab then
  16.     tc := TExtTab(P).GetOwner
  17.   else if P is TButtonImages then
  18.     tc := TButtonImages(P).GetOwner;   // <--- GetOwner to be added
  19.   else
  20.     exit (nil);
  21.   Result := tc.Images;
  22. end;
  23.  
  24. procedure Register;
  25. begin
  26.   ...
  27.   RegisterPropertyEditor(TypeInfo(TImageIndex), TExtTabCtrl, '', TTabCtrlImageIndexPropertyEditor);
  28. end;

ovidio

  • Jr. Member
  • **
  • Posts: 68
Ah, normally the ImageIndex is a property of the class which also has the ImageList. The ImageIndex property editor, in components/ideintf/graphpropedits.pas, uses the method GetImagelist to find the imagelist from which it can take the images to display in the combobox. It is probably the easiest way to extend the TImageIndexPropertyEditor for TExtTabCtlr (--> TExtTabCtrlImageIndexProperty) in a way similar to what is done for the stringgrid (TGridImageIndexPropertyEditor in the same unit): GetComponent(0) is the component which has the imageindex as property (TExtTab, or TButtonImage (?)). For TExtTab you can find the ExtTabCtrl from its GetOwner method, and for TButtonImages you need to provide a field pointing to the TabCtrl. Once the TabCtrl is known you can return the imagelist:
Code: Pascal  [Select][+][-]
  1. //Untested...
  2.  
  3. type
  4.   TExtTabCtrlImageIndexProperty = class(TImageIndexPropertyEditor)
  5.   protected
  6.     function GetImageList: TCustomImageList; override;
  7.   end;
  8.  
  9. function TExtTabCtrlImageIndexProperty.GetImageList: TCustomImageList;
  10. var
  11.   P: TPersistent;
  12.   tc: TExtTabCtrl;
  13. begin
  14.   P := GetComponent(0);
  15.   if P is TExtTab then
  16.     tc := TExtTab(P).GetOwner
  17.   else if P is TButtonImages then
  18.     tc := TButtonImages(P).GetOwner;   // <--- GetOwner to be added
  19.   else
  20.     exit (nil);
  21.   Result := tc.Images;
  22. end;
  23.  
  24. procedure Register;
  25. begin
  26.   ...
  27.   RegisterPropertyEditor(TypeInfo(TImageIndex), TExtTabCtrl, '', TTabCtrlImageIndexPropertyEditor);
  28. end;

I have implemented this (with some modifications). The funny thing is that it works for TButtonImages, but not for TExtTab. Any idea of why?

Edit: Fixed! All the TImageIndex properties are working as expected now.
« Last Edit: June 09, 2026, 06:19:19 pm by ovidio »

wp

  • Hero Member
  • *****
  • Posts: 13631
yeah, on the road to perfection...

Forked your repo to have something to work on for the high-dpi issue. I'll be back...

d7_2_laz

  • Hero Member
  • *****
  • Posts: 696
I find it very promising too. Unfortunately, actually i've not much time left to check more indepth if i potentially could replicate my preferred behaviour with it.
I did a short test using the demo_page_control, by trying not to install packages, but by adding the unit path to the project's search path and setting up the ExtTabCtrl1 object and it's properties in the main form's pas file dynamically at run time.
All fine, but i saw: exttabctrl.pas includes the units ComponentEditors and PropEdits which compile only if the package IDEIntf is added to the project's dependencies.
So my first question would be: do we really need the package IDEIntf for to use the ExtTabCtrl dynamically in an app? (I hope i simply did something wrong)

2) Looking onto the styles, i'd preferred the "Chrome" style, but it looks we cannot set a tab's background color here (differently to the "Button" style). I could imagine the reason why, but isn't it an unneeded restriction to exclude this possibility?

3) Wouldn't it be nice to have something like a TabGotFocus resp. TabLostFocused callback event for to set/reset the stripe color for focus changes too? (= special stripe color for focused tabs)
Edit: alternatively: responsible properties (StripeColor, SelectedStripeColor, FocusedStripeColor) for individual settings. Similar for Color (Color, SelectedColor, FocusedColor).
« Last Edit: June 09, 2026, 10:27:09 pm by d7_2_laz »
Lazarus 4.8  FPC 3.2.2 Win11 64bit

 

TinyPortal © 2005-2018