Recent

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

wp

  • Hero Member
  • *****
  • Posts: 13625
I have already reported the bug: https://gitlab.com/freepascal.org/lazarus/lazarus/-/work_items/42359
Thanks, let's see what happens.

In my fork of your repository (https://github.com/wp-xyz/ExtTabCtrl), I so far completed scaling of the built-in button images. All these images have moved into an imagelist (FInternalImages) which enables scaling for free. As a consequence some of your code had to be dropped (cache).

Before continuing with the external imagelist (Images property) I would like to ask you whether you want to keep the Image property of the tabs. Advantage is that the user can easily assign an icon to a tab (without the need for an imagelist), disadvantage is that these images are not scaled (and that duplicate images may be included in an application when several ExtTabCtrls are used). In my preferences, the icon should only be taken from the Images imagelist and the tab's imageIndex.

And what is the purpose of the CachedTabImage? For the rotated image?

Do I understand correctly that the rotated images come into play only when the tabcontrol is in vertical orientation?

P.S.
When testing my fork please note that it certainly is not in sync with your current development, you'll probably have lots of conflicts when you want to merge it.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 696
Regardless of this: i leave my personal adaption of the page control demo here, reflecting some recent topics.
All needed files are contained inside (no package to be installed), and it has MetaDarkStyle's dark mode too (as by one of the two options in the ini file which is in the bin subdir).

I'd have some more .... hm .. personal preferences, or legitime recommendations? ... don't know, but i think it's not the right place here (better somewhere later in github?) and at least not the right time now and yet where the project in total initially is being intoduced and presented.
Hope that others might look into too - i'd encourage this!
Lazarus 4.8  FPC 3.2.2 Win11 64bit

ovidio

  • Jr. Member
  • **
  • Posts: 68
I have already reported the bug: https://gitlab.com/freepascal.org/lazarus/lazarus/-/work_items/42359
Thanks, let's see what happens.

The bug was already known (https://gitlab.com/freepascal.org/lazarus/lazarus/-/work_items/41825), and included for the upcoming Lazarus 5.0. Now they will also include it on an eventual Lazarus 4.10. If anybody wants to apply the fix "by hand", all what has to be done is to edit designer/designer.pp and replace:
Code: Pascal  [Select][+][-]
  1. TControlAccess(MouseDownComponent).MouseUp(Button, Shift, p.X, p.Y);
  2. Exit;
by:
Code: Pascal  [Select][+][-]
  1. TControlAccess(MouseDownComponent).MouseUp(Button, Shift, p.X, p.Y);
  2. MouseDownComponent:=nil;
  3. MouseDownSender:=nil;
  4. Exit;

In my fork of your repository (https://github.com/wp-xyz/ExtTabCtrl), I so far completed scaling of the built-in button images. All these images have moved into an imagelist (FInternalImages) which enables scaling for free. As a consequence some of your code had to be dropped (cache).

Great! I have created a pull request, to incorporate your changes back into my repository, but when I try to resolve the conflicts, is says that I don't have permissions to do it. Maybe you must resolve them? Or, am I doing something wrong?

Before continuing with the external imagelist (Images property) I would like to ask you whether you want to keep the Image property of the tabs. Advantage is that the user can easily assign an icon to a tab (without the need for an imagelist), disadvantage is that these images are not scaled (and that duplicate images may be included in an application when several ExtTabCtrls are used). In my preferences, the icon should only be taken from the Images imagelist and the tab's imageIndex.

For now, I would prefer to keep the image property, for those users that are not worried by high DPI. As I see it, there is no significant drawback if having it, since any user can just use an ImageList. What do you think?

And what is the purpose of the CachedTabImage? For the rotated image?

Do I understand correctly that the rotated images come into play only when the tabcontrol is in vertical orientation?

Yes, the purpose of the cache images is to store the rotated images, to avoid rotating them again for each repaint.

P.S.
When testing my fork please note that it certainly is not in sync with your current development, you'll probably have lots of conflicts when you want to merge it.

Yes, I have found the conflicts. I don't have much experience with git. Please, can you check if I am doing something wrong.

EDIT: I have managed to make the merging, but it was not clean. Right now the component is broken. I will review it carefully, to make all the changes work together.

EDIT 2: A problem with the new button images is that they are barely seen on a dark mode (see attached file).

EDIT 3: Now I see that you try to recolor the images for a Drak mode, but this is not working on my mac.
« Last Edit: June 13, 2026, 06:06:31 pm by ovidio »

ovidio

  • Jr. Member
  • **
  • Posts: 68
Regardless of this: i leave my personal adaption of the page control demo here, reflecting some recent topics.
All needed files are contained inside (no package to be installed), and it has MetaDarkStyle's dark mode too (as by one of the two options in the ini file which is in the bin subdir).

Thank you very much for sharing your code! Would you mind if I used it as the basis for an example demonstrating component customization?

I've also been thinking about how to let users customize tabs while keeping the configuration options minimal. That led me to an idea inspired by components such as TStringGrid (if I remember correctly), which allow custom drawing. To explore this, I added a new OnDrawTab event that lets users bypass the built-in styles and draw the tab themselves. I also updated demo_simple to show how it can be used. There, I created a custom style that (sort of) resembles the Windows XP taskbar.

What do you think? Do you see yourself using this feature?


I'd have some more .... hm .. personal preferences, or legitime recommendations? ... don't know, but i think it's not the right place here (better somewhere later in github?) and at least not the right time now and yet where the project in total initially is being intoduced and presented.
Hope that others might look into too - i'd encourage this!

Feel free to share any ideas that you have. I cannot promise to implement them all, but at least we can discuss them.

wp

  • Hero Member
  • *****
  • Posts: 13625
Sync'ed my fork with your repository, and surprisingly everything is working correctly. Just the test project was missing which I had forgotten to push.

Tested high-dpi on Windows (ok), Linux gtk2 (ok), gtk3 (ok), qt5 (icons pixelated), qt6 (icons pixelated), cocoa (I only have a VM with macOS Monterey, don't know how to activate high-dpi).

I think I read somewhere that qt5/6 do scaling by means of the CanvasScalingFactor - need to investigate.

Sorry for the "destructive" changes that I made to your code. I switched from lrs to res resources, just because I am accustomed to it and did not want to waste the time how to add my images to an lrs. Along this way, your component icon got lost - it certainly can be extracted from the lrs, or you have the original, and it certainly can be added to the res in the same way as the other png files are added (see the batch file in the images folder; you'll have to rewrite it for mac, and it needs the tool LazRes which you must compile in the (lazarus)/tools folder). And you'll need the component icon in the two larger sizes, too.

I also selected different built-in icons, just because I needed them in three sizes. Of course you can go back to your icons but then you'll have to draw the other sizes yourself. If you prefer to stick with the new icons: I am not very much satisfied with them, except for the < und > icons they look too heavy, the + and x should be redrawn with smaller line width (thats easy: their originals are svg files).

On my mac VM, the recoloring for dark mode is working - see screenshot.

Another thing is missing so far: you had code in CalcLayout and somewhere else which fixed the case when the images are higher than the TabSize. I had to remove this because in high-dpi mode it triggered an endless resizing loop. I think such code belongs to the setters of the TabSize and Images properties.

ovidio

  • Jr. Member
  • **
  • Posts: 68
Sync'ed my fork with your repository, and surprisingly everything is working correctly. Just the test project was missing which I had forgotten to push.

Now I was able to sync automatically, to get your test project.

Sorry for the "destructive" changes that I made to your code. I switched from lrs to res resources, just because I am accustomed to it and did not want to waste the time how to add my images to an lrs. Along this way, your component icon got lost - it certainly can be extracted from the lrs, or you have the original, and it certainly can be added to the res in the same way as the other png files are added (see the batch file in the images folder; you'll have to rewrite it for mac, and it needs the tool LazRes which you must compile in the (lazarus)/tools folder). And you'll need the component icon in the two larger sizes, too.

I also selected different built-in icons, just because I needed them in three sizes. Of course you can go back to your icons but then you'll have to draw the other sizes yourself. If you prefer to stick with the new icons: I am not very much satisfied with them, except for the < und > icons they look too heavy, the + and x should be redrawn with smaller line width (thats easy: their originals are svg files).

No problem. I used .lrs just because I found a tutorial on how to do it. A .res file is just OK. As for the images, I don't care which we use, as long as we provide a reasonable default.

On my mac VM, the recoloring for dark mode is working - see screenshot.

Now I see the problem. If I set the dark mode and then run the app, recoloring works OK. However, if I set the dark mode after the app is running, images are not recolored. Is there any way of knowing when the colors change? I will look into it. On the other hand, isn't it easier to have two sets of images (one for light mode and one for dark mode)?

Another thing is missing so far: you had code in CalcLayout and somewhere else which fixed the case when the images are higher than the TabSize. I had to remove this because in high-dpi mode it triggered an endless resizing loop. I think such code belongs to the setters of the TabSize and Images properties.

Yes, I had realized. Tomorrow I will try to restore this feature, trying not to break high DPI.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 696
Hello ovidio, thank you very much for your response! Yes of course you may take the sample for your demos as you like (but why not use, sometime or other, a real TPageControl / TabSheets as base? It has an option to not show tab headers ("ShowTabs" afaik), and, so, may let those up to another control to take over this role).

Quote
I added a new OnDrawTab event that lets users bypass the built-in styles and draw the tab themselves.
I appreciate the 'opening' of the new event OnDrawTab much, absolutely. The more as elements like stripe, image, caption text (*), close button will be drawn anyway (if i see right), so that the code for such won't be needed to be replicated too. I'll give it a try.

(*) However a little detail besides: i think the text font color clGrayText for the case 'if not IsActive' is not optimal (imo too much dimmed, looks rather as a disabled state than simply passive). Using a variable for the textcolor/passive? Or for: 'DimTabPassiveCaptionText' (yes/no)? Or: blending only a bit down here instead of graytext? Don't know yet what might be the cheapest way.
Edit: Reset: just tried the OnDrawTab mechanism; its not a problem here.

I think there are many possibilities to improve over the time, but ... it's the initial presentation of your component here and now.
« Last Edit: June 13, 2026, 09:17:54 pm by d7_2_laz »
Lazarus 4.8  FPC 3.2.2 Win11 64bit

wp

  • Hero Member
  • *****
  • Posts: 13625
but why not use, sometime or other, a real TPageControl / TabSheets as base?
I think ovidio's motivation for the component was that the original controls do not allow custom-painting, either not at all, or at least not in a consistent way across platforms.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 696
Right wp, but as far as this TPageControl's lack of custom painting possibilities refers solely to the tab headers (indeed it's not possible on Windows, i formerly in the past had suffered on that too), then it shouldn't be a problem - it's possible to use ExtTabCtrl for the tab headers (similar as it's possible with attabs).
PageControl could be simply the container for a top-aligned ExtTabCtrl and the tabsheets (headerless), and ExtTabCtrl should be able to use tabsheets as pages (instead of TFrames like in the current page control demo). *Theoretically*
I only intended to bring this possibility into mind.
Lazarus 4.8  FPC 3.2.2 Win11 64bit

wp

  • Hero Member
  • *****
  • Posts: 13625
If I set the dark mode and then run the app, recoloring works OK. However, if I set the dark mode after the app is running, images are not recolored. Is there any way of knowing when the colors change?
The ThemeServices, unit Themes, have an event OnThemeChange(Sender: TObjecct) which fires when the OS's theme has been changed, e.g. when the system has changed from light to dark mode. I tested it on my macOS VM, and it is working:

- Implement a handler for the OnThemeChange event in the main form which calls ExtTabCtrl.UpdateBtnImages (this is currently protected, but move its declaration into the public section of TExtTabCtrl). If there are several ExtTabCtrls, the handler must run over all of them.
- In the MainForm's OnCreate assign the handler to ThemeServices.OnThemeChange.

Code: Pascal  [Select][+][-]
  1. uses
  2.   ... Themes;
  3.  
  4. procedure TForm1.FormCreate(Sender: TObject);
  5. begin
  6.   ThemeServices.OnThemeChange := @ThemeChangeHandler;
  7. end;
  8.  
  9. procedure TForm1.ThemeChangeHandler(Sender: TObject);
  10. begin
  11.   ExtTabCtrl1.UpdateBtnImages;
  12. end;
  13.  

I tested this with Laz/main. Since there was some work related to themeservices recently, I am not sure whether this code works with release versions.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 696
About the particular topic area 'set colors individually' i tested a bit and can say:
the newly introduced exposure of an 'OnDrawTab' event does cover that fully and easily.
And does need only a very few lines of code. Great!

Lazarus 4.8  FPC 3.2.2 Win11 64bit

ovidio

  • Jr. Member
  • **
  • Posts: 68
Right wp, but as far as this TPageControl's lack of custom painting possibilities refers solely to the tab headers (indeed it's not possible on Windows, i formerly in the past had suffered on that too), then it shouldn't be a problem - it's possible to use ExtTabCtrl for the tab headers (similar as it's possible with attabs).
PageControl could be simply the container for a top-aligned ExtTabCtrl and the tabsheets (headerless), and ExtTabCtrl should be able to use tabsheets as pages (instead of TFrames like in the current page control demo). *Theoretically*
I only intended to bring this possibility into mind.

This approach would be equivalent to the demo with the TNotebook, right? Do you see any advantage in using a TPageControl instead?

ovidio

  • Jr. Member
  • **
  • Posts: 68
If I set the dark mode and then run the app, recoloring works OK. However, if I set the dark mode after the app is running, images are not recolored. Is there any way of knowing when the colors change?
The ThemeServices, unit Themes, have an event OnThemeChange(Sender: TObjecct) which fires when the OS's theme has been changed, e.g. when the system has changed from light to dark mode. I tested it on my macOS VM, and it is working:

- Implement a handler for the OnThemeChange event in the main form which calls ExtTabCtrl.UpdateBtnImages (this is currently protected, but move its declaration into the public section of TExtTabCtrl). If there are several ExtTabCtrls, the handler must run over all of them.
- In the MainForm's OnCreate assign the handler to ThemeServices.OnThemeChange.

Code: Pascal  [Select][+][-]
  1. uses
  2.   ... Themes;
  3.  
  4. procedure TForm1.FormCreate(Sender: TObject);
  5. begin
  6.   ThemeServices.OnThemeChange := @ThemeChangeHandler;
  7. end;
  8.  
  9. procedure TForm1.ThemeChangeHandler(Sender: TObject);
  10. begin
  11.   ExtTabCtrl1.UpdateBtnImages;
  12. end;
  13.  

I tested this with Laz/main. Since there was some work related to themeservices recently, I am not sure whether this code works with release versions.

I tried a different approach (https://forum.lazarus.freepascal.org/index.php?topic=25992.0), using CM_COLORCHANGED, because I would prefer the component to repaint itself rather than making the application responsible for it. Unfortunately, it does not seem to work: CMColorChanged is not called when switching between light and dark modes.

That said, this made me wonder whether we might be overengineering the problem. Our goal is to provide reasonable defaults, allowing users to use their own images for better "eye candy". Also, the images we currently use are intentionally simple (which I like), so there may be easier ways to achieve the same result:
  • We could use characters (+, <, >) for the buttons
  • We could draw the symbols directly on the canvas ourselves, a "cheap" alternative to SVGs; in fact, the code already draws a cross when no "close" image is provided
  • We could provide images that work well in both light and dark themes, such as black symbols with white outlines

My feeling is that any of these approaches would save development time that could be better spent improving other parts of the component.

What do you think?

ovidio

  • Jr. Member
  • **
  • Posts: 68
About the particular topic area 'set colors individually' i tested a bit and can say:
the newly introduced exposure of an 'OnDrawTab' event does cover that fully and easily.
And does need only a very few lines of code. Great!

Good to hear.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 696
Little observation besides: although the (recently updated) procedure 'DrawCloseButton' falls outside the OnDrawTab's job, i see
that the close button cross will be white - in dark mode only, and only when using the OnDrawTab callback. Before it had been red.
No prob, but i simply won't understand yet why, and if/how one could control that.
Attached my current short test (which actually covers horiz. tabdirection only). It has an inifile beyond the bin-subdir.
Lazarus 4.8  FPC 3.2.2 Win11 64bit

 

TinyPortal © 2005-2018