Recent

Author Topic: [SOLVED] Treeview: full-width background coloring of nodes ... easy?  (Read 5752 times)

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
For a treeview (windows x64) i'm trying a bit of customization that looks like
a.- Selected tree node looks "full-width" backgrounded (like when using the RowSelect property, --> but not changing the background color to lightgray when the control is loosing the focus, for certian reasons)
b.- Nodes hovered with the mouse do look "full-width" backgrounded as well (using a slightly differing backgorund color).
    Plus, keeping (remembering) the hovered line at mouse wheel scroll. (You may see this in recent apps, MS Explorer, Thunderbird, ..) 

So it appears to be a simple thing (changing a background color only) and one might expect that doing somothing like that could succeed:
Code: Pascal  [Select][+][-]
  1.    if cdsSelected in State then
  2.       if stage = cdPrePaint then begin
  3.           Sender.Canvas.Brush.Color := $00FFE8CC;
  4.           DefaultDraw := False;
  5.           RFull := Node.DisplayRect(false);
  6.           Sender.Canvas.FillRect(RFull);
  7.           exit;
  8.       end;

Apparently, for to have any effect that would need DefaultDraw := False (within AdvancedCustomDrawItem).
And now the elements from "ShowLines", expand sign, icon and text would be needed to be redrawn afterwards by hand.
But the latter (at least the lines, expand sign etc.) is a lot of stuff within treeview.inc (DoiPaintNode)  and the app would need to reinvent this wheel (integrate a lot of code).
It works fine (for both cases), but requires a lot of code duplication from the treeview.inc within an app (may easily grow by 300 lines. Too much to incest for a paricular visual customization).

So i'm wondering whether for this "simple task" (full row background color for nodes if wished) somebody already tried simiiar and got an easier clue or trick?
Does mean: changing the row's background color before the DefaultDraw does it's job for lines, expand sign, icon etc.
Is that possible somehow instead of doing a lot of code cloning?
« Last Edit: February 19, 2025, 10:14:31 pm by d7_2_laz »
Lazarus 3.8  FPC 3.2.2 Win10 64bit

jamie

  • Hero Member
  • *****
  • Posts: 6834
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #1 on: August 13, 2022, 04:43:50 pm »
Getting the display rectangle should be enough.

You need to offset the rectangle's X axes to the width of the display panel of the Treeview or what ever you wish.



The only true wisdom is knowing you know nothing

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #2 on: August 13, 2022, 07:51:07 pm »
Yes, but the full width rectangle is already provided: via Node.DisplayRect with parameter 'false'.
Only, using this Rect for FillRect now - Sender.Canvas.FillRect(RFull); - simply will result in a colored bar of course (no icon, no text etc.), understandable.

Restricting this FillRect to  'if stage = cdPrePaint', then it won't have any effect at all.
Unless when saying: DefaultDraw := False. Now the fillRect comes into effect, but would require to paint expand sign, lines, icon, text afterwards too..

The situation (not a problem, and not an issue at all of course) seems to be: you want to change the backcolor for the whole line? Then please do all of the stuff and implement the whole code needed for that (for lines, expand sign &  co)  by yourself.
Does work flawlessly (tested), but is somehow unsatisfying to see the large overhead. So the question if i may have missed a way around here to access the background color.
Lazarus 3.8  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #3 on: August 16, 2022, 03:32:18 pm »
Don't know if there are few responses because the topic is not of interest or because most are thinking: not possible.
As demo i add a small gif sample for to show what i ssucceeded to try out because i found it to be quite nice.

The chanllenge is only, why such a big overhead described, approx. 300 lines of code cloning in the app (for to paint lines, expand sign etc., which already does exist), plus the need for to include themes.pas, only for to change a full line backcolor?

I thought about further and had an idea.
A solution could be a simple one liner within treeview.inc, DoPaintNode.

The idea is: let me (the app) draw the line backcolor and node text (both easy) and else do your DefaultDraw .... but without painting the line background.
That means: please let me draw the item background, without either overwriting it afterwards (DefaultDraw true), or (DefaultDraw false:) force me to do a lot of things redundantly.
The idea simply is: please skip the background draw optionally by demand for certain situations, the app wants to do that itself and not been overridden by the DefaultDraw.
This is the one liner (a simple if). I tried it out and it worked fine:

Code: Pascal  [Select][+][-]
  1.     // draw background
  2.     if not FSkipDrawLineBackground then     // <=== added line. requires def and init of the public variable within comctrls.pp though
  3.        DrawBackground(NodeSelected, NodeRect);

In my use cases, i would set this veriable to true just if needed and now simply spend some very few code lines for background and node text, that's all.
One could care about the line backcolor and text paint, but not to struggle with lines, icon, expand sign.
Example (for highlight line at "Selected", at treeview focused or unfocused), the AdvancedCustomDrawItem could look like as:

Code: Pascal  [Select][+][-]
  1. procedure TForm1.TreeView1AdvancedCustomDrawItem(Sender: TCustomTreeView;
  2.             Node: TTreeNode; State: TCustomDrawState; Stage: TCustomDrawStage;
  3.             var PaintImages, DefaultDraw: Boolean);
  4. var RFull, RText: Trect;
  5. begin
  6.      DefaultDraw := True;
  7.      Sender.FSkipDrawLineBackground := False;
  8.      if cdsSelected In State then begin
  9.         Sender.FSkipDrawLineBackground := True;
  10.         if stage = cdPrePaint then begin
  11.             Sender.Canvas.Brush.Color := $00FFE8CC;
  12.             RFull := Node.DisplayRect(false);
  13.             Sender.Canvas.FillRect(RFull);
  14.             exit;
  15.         end;
  16.         DefaultDraw := False;
  17.         with Sender.Canvas do begin
  18.            RText := Node.DisplayRect(True);
  19.            Sender.Canvas.Brush.Color := $00FFE8CC;
  20.            Sender.Canvas.FillRect(RText);
  21.            inc(RText.Top, 2);
  22.            inc(RText.Left, 4);
  23.            inc(RText.Right, 4);
  24.            DrawText(Handle, PChar(Node.Text), -1, RText, DT_LEFT or DT_VCENTER);
  25.         end;
  26.     end;
  27. end;

Test project attached )which does assume the change mentioned). It shows a full line background  (just as with RowSelect, but without graying it at focus change).
For he "highlight line at mouse over" the principle is juse the same, with a bit more logic around (--> not covered in the test project).

So this does allow much more flexibility for certain vertain customization.
What do you think? Would it make sense to provide this possibility?

(With an additional var, alternatives are possible. But i think this one is the simplest).
Lazarus 3.8  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #4 on: February 05, 2025, 03:08:11 pm »
Quote
For the "highlight line at mouse over" the principle is just the same, with a bit more logic around (--> not covered in the test project).
For to give the full picture, by a special occasion i now tried to do a poc for to really proove that.
Attached a very simple test project.
Note that it would require to add a new property likr "SkipDrawLineBackground" to the treeview.inc resp. comctrls.pas, that simply allows an app to do the background drawing in the "PrePaint" (!!) phase by itself.
By this simple property the AdvancedCustomDraw now could fully act really 'custom' by itself, in so far as this last and only hurdle is acessible to an app.
See the diff report for that, on branch 'main'. 1 property, 1 "if"-condition added for test.

I do not intend to request such officially (i see the risc when implying more and more properties ... the treeview is already complex enough).
But i would be interested to see how others might think about these or similar matters.
Lazarus 3.8  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12695
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #5 on: February 07, 2025, 12:06:20 am »
The idea is great, but I don't like introducing a special property which is dedicated to a very special use case and may be harmful for other cases (no background painting at all).

I took the freedom to modify treeview sources. Because they do not respect the idea behind the CustomDraw events (at least in my opinion): These are events which fire immediately before the nodes are painted, the canvas has already been set up, and the user here has a last chance to modify it according to his wishes. This is very nicely implemented in the PrepareCanvas of the Lazarus grids. But in the treeview it is broken because the canvas properties are changed AFTER the CustomDraw events, overriding the user selections. And this finally makes it very difficult to owner-draw the tree.

I am attaching the modified TCustomTreeView.DoPaintNode method in which this faulty behaviour has been fixed (file treeview_dopaintnode.txt). For testing, just copy & paste the procedure over the original code in lcl/include/treeview.inc. My code is relative to Lazarus/main, but I think there is a good chance that it might work also in Laz 4.0 or older versions (absolutely make a backup copy of the original file).

Another attachment is a small ShellTreeview demo project with checkboxes to toggle RowSelect, HotTrack, Themed and CustomDrawing. For CustomDrawing a handler for the OnCustomDrawItem event is provided (it could also go into the OnAdvancedCustomDrawItem event for the PrePaint stage). For Windows users interested in dark mode I am including the relevant files of the MetaDarkStyle package - just run the binary with parameter "dark".

There are still some issues (alternating row colors, disabled nodes, ...?), but I think they can be solved.

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #6 on: February 07, 2025, 09:57:32 am »
Quote
... to modify treeview sources. Because they do not respect the idea behind the CustomDraw events
Yes, that's it! I.e. relevant stuff like the canvas brush color is an restricted area for it ...

Quote
I don't like introducing a special property which is dedicated to a very special use case
Same opinion, absolutely ... so i didn't really want to propose it.

This morning i had only the time for a quick test of the example. I think it looks very promising!

The only little thing i just noticed so far is:
setting: Row select, hottrack, Not themed, customdraw
then:    for selected node: brush color blue (at least some darker color), font color white (better readable here)   --> the expand sign stays black

I'd have more time later the day ...

EDIT:  Sorry, had forgotten in a hurry: with another ExpandSignType than tvestTheme the ExpandSignColor can be set. No problem!
« Last Edit: February 07, 2025, 03:05:59 pm by d7_2_laz »
Lazarus 3.8  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12695
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #7 on: February 07, 2025, 07:15:37 pm »
Some improvements:
In yesterday's version it was not possible to have a striped background over the full tree width together with ownerdrawing. This works now. It is a bit complicated, though, because the CustomDraw events can set the background color only in the canvas.Brush. But in this use case two color would have to be provided: the color of the background stripes and the color of the selection or hot track which, in case of RowSelect=false, does not extend over the entire tree width. In order to fix this, I activated the preErase and postErase stages which were not supported so far (and not even are in Delphi). Let me describe it as follows
  • cdPrePaint: This is the only stage accessible in the OnCustomDrawItem event; in OnAdvancedCustomDrawItem it is the first stage executed. The event handler should either return DefaultDraw as false - this means that all predefined drawing will be stopped, and the user must do it on his own (including node caption, icons, tree decorations). Or, when Defaultdraw is true, the user can provide settings for the canvas to be applied for painting the specified node. The state in which the node is (selected, hottracked, disabled, ...) is available in the State parameter. If no other stages are handled in the OnAdvancedCustomDrawItem event, painting proceeds similarly to my previous event.
  • cdPreErase: In this case the OnAdvancedCustomDrawItem handler should provide only the background color of the node. This is where a striped background can be made available. This color is applied when the handler exits with DefaultDraw = true. Setting DefaultDraw = false means that the user must draw the node background himself. He could, for example draw a gradient background this way.
  • cdPostErase: Here the user can provide the background color for selected and hot nodes. The background color for the normal nodes must be repeated, otherwise some drawing glitches will appear in the text part of the node. Again, depending on DefaultDraw, either the default background rectangle is paint or the user can do the painting himself.
  • Now the "real" node is painted: the caption, the icon, the tree buttons, the tree lines. The background of the text is drawn in the previously used brush color.
  • cdPostPaint: This happens after drawing of the node is complete. I have no good idea what this could be good for...

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #8 on: February 07, 2025, 09:30:43 pm »
Oh!
I'm just trying to mimic an own test project you already know from another occasion (that with the couple of 'styles'. Will post it here if ready). The most usual styles i can cover easily using your PaintNode change - that's very impressive!

I'm just puzzling with the (admittedly very special!) intermediate styles. Where i paint a 'half row' of a hot-tracked node beginning from the node's text (which is normally trivial using the custom draw). See images.

It seems that 'tvoHotTrack' and full-row painting are now bound very close together. So that one cannot have (if RowSelect) hottrack without full-line painting (and vice versa). But i haven't gotten that far yet to know if it's possible with the previous change or not.

With a few words: i would never had thought about a 'striped background' for a tree. But it reminds me closely to such 'half-line' scenario.
It might the case that the newly activated stages are useful resp. needed here, eg. cdPreErase.

I can study the new change and demo starting tomorrow afternoon. So for now only the question: would you see a fundamental limit to do so?
Lazarus 3.8  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #9 on: February 07, 2025, 10:00:47 pm »
Could do just a quick test -- it seems so that with cdPostErase  ('Draw normal node background only') the background color can be overwritten resp. set as desired so that one could start painting upfrom the node text ...
Lazarus 3.8  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12695
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #10 on: February 07, 2025, 10:19:27 pm »
I'm just puzzling with the (admittedly very special!) intermediate styles. Where i paint a 'half row' of a hot-tracked node beginning from the node's text (which is normally trivial using the custom draw).
You can custom-draw the in the OnAdvancedCustomDrawItem handler when in the postErase stage: this is when the selection is drawn. Just recalculate the paint rect to begin with the node text:
Code: Pascal  [Select][+][-]
  1. procedure TForm1.ShellTreeView1AdvancedCustomDrawItem(Sender: TCustomTreeView;
  2.   Node: TTreeNode; State: TCustomDrawState; Stage: TCustomDrawStage;
  3.   var PaintImages, DefaultDraw: Boolean);
  4. var
  5.   R: TRect;
  6. begin
  7.   case Stage of
  8.     ...
  9.   end;
  10.   if (Stage = cdPostErase) and ([cdsFocused, cdsSelected, cdsHot] * State <> []) then
  11.   begin
  12.     R := Node.DisplayRect(false);
  13.     R.Left := Node.DisplayTextLeft + 2 + Scale96ToFont(2);
  14.     Sender.Canvas.FillRect(R);
  15.     DefaultDraw := false;
  16.   end else
  17.     DefaultDraw := true;
  18. end;
     
The value for R.Left is corrected for the same offsets applied during the paint procedure.

This custom-painting looks the same, no matter whether RowSelect is active or not. The only difference is that when RowSelect is off you must click on the node text to select another node, otherwise you can click anywhere in the line
« Last Edit: February 07, 2025, 10:31:06 pm by wp »

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #11 on: February 07, 2025, 11:27:25 pm »
True - i just was successful with similar code for attempts like the first image above (without RowSelect),
but not like in the second image above (with RowSelect). That's the last missing piece yet ...

PS: the new stage cdPostErase is a real game-changer, most does work so easy now!  :)

Edit:  But has setting a brush color in cdPostErase no effect if RowSelect is defined? (Admittedly a very special approach of course)

Code: Pascal  [Select][+][-]
  1. procedure TForm1.ShellTreeView1AdvancedCustomDrawItem(Sender: TCustomTreeView;
  2.       Node: TTreeNode; State: TCustomDrawState; Stage: TCustomDrawStage;
  3.       var PaintImages, DefaultDraw: Boolean);
  4. var RHalf, RText: TRect;
  5. begin
  6.     DefaultDraw := True;
  7.     case Stage of
  8.       cdPostErase:   // Draw normal node background only
  9.         begin
  10.           if [cdsFocused, cdsSelected] * State <> [] then begin
  11.              Sender.Canvas.Brush.Color := cFocusedFillColor;
  12.              Sender.Canvas.Font.Color := clWindowText;
  13.              Sender.Canvas.Font.Style := [fsBold];
  14.           end else
  15.           if (cdsHot in State) then begin
  16.              if (FSelectStyle = 0) Or (FSelectStyle = 4) then
  17.                    Sender.Canvas.Brush.Color := cHotTrackColor
  18.              else
  19.                    Sender.Canvas.Brush.Color := clWindow;  // --- This would have no effect with RowSelect (style 3)
  20.           end else begin
  21.             Sender.Canvas.Brush.Color := clWindow;
  22.             Sender.Canvas.Font.Color := clWindowText;
  23.             Sender.Canvas.Font.style := [];
  24.           end;
  25.         end;
  26.     end;
  27.    // .....
  28.    // .....
  29.     if  Not (FSelectStyle = 0) Or (FSelectStyle = 4) then begin
  30.        if Stage = cdPostErase then begin    // .... Resp.: in which stage best?
  31.           RHalf := Node.DisplayRect(False);
  32.           RText := Node.DisplayRect(True);
  33.           RHalf.Left := RText.Left;
  34.           with Sender.Canvas do begin
  35.              Brush.Color := cHotTrackColor;;
  36.              FillRect(RHalf);
  37.             // Painting the node text will follow later ...
  38.           end;
  39.        end;
  40.     end;
  41. end;
« Last Edit: February 08, 2025, 12:19:23 am by d7_2_laz »
Lazarus 3.8  FPC 3.2.2 Win10 64bit

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #12 on: February 08, 2025, 12:41:34 am »
'Problem' solved, there was an error in the code.
The last code sequence responsible for to over-paint the half line has to read:

Code: Pascal  [Select][+][-]
  1.     if  Not(FSelectStyle = 0) Or (FSelectStyle = 4) then begin
  2.        if Stage = cdPostPaint then begin
  3.           if (cdsHot in State) then
  4.             if  Not ( [cdsFocused, cdsSelected] * State <> [] ) then begin
  5.                 RHalf := Node.DisplayRect(False);
  6.                 RText := Node.DisplayRect(True);
  7.                 RHalf.Left := RText.Left;
  8.                 with Sender.Canvas do begin
  9.                    Brush.Color := cHotTrackColor;   // Edited
  10.                    FillRect(RHalf);
  11.                   // ..... Node text has to follow later .....
  12.                 end;
  13.                 DefaultDraw := False; //  - Edit: added.  Would work without, but it's
  14.                                             // more correct here
  15.           end;
  16.        end;
  17.     end;
« Last Edit: February 08, 2025, 09:35:41 am by d7_2_laz »
Lazarus 3.8  FPC 3.2.2 Win10 64bit

wp

  • Hero Member
  • *****
  • Posts: 12695
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #13 on: February 08, 2025, 01:36:04 pm »
Anything missing before committing to the Lazarus repository?

d7_2_laz

  • Hero Member
  • *****
  • Posts: 631
Re: Treeview: full-width background coloring of nodes ... easy?
« Reply #14 on: February 08, 2025, 03:25:13 pm »
Something missing? Yes, some test, test. I'd like to finish my own demo project and post it here (maybe this evening, I'd been out today), and at least to probe it against the NPP plugin DLL for to be more sure that i find no side-effects (probably tomorrow).

Then i'd like to tell at least the - purely theoretical - thought: are there riscs for others that the number of calls of AdvanceCustomDraw will increase; possible risc that the chance of wrong usage might increase by applying a code multiple times for the wrong stages? The user needs to be aware that actions within this event callback need to be bound to the correct stage(s).
For me absolutely no problem. And: it's actually already the case anyway! - Only for to have said this.

Last not least: i'd to express great thanks for your understanding and the possibility to discuss!
I find your demo to be excellent, and i hope it could be a bit interesting for you to be checked from another perspective (like those styling gimmicks).
Lazarus 3.8  FPC 3.2.2 Win10 64bit

 

TinyPortal © 2005-2018