Lazarus

Programming => LCL => Topic started by: d7_2_laz on December 17, 2024, 03:04:34 pm

Title: [SOLVED] Treeview flicker w. Hottrack when running inside NPP Plugin (dark mode)
Post by: d7_2_laz on December 17, 2024, 03:04:34 pm
Made good progress to bring an old 32bit DLL plugin for Notepad++ to Lazarus 64bit.
The code base for the demo i experiment with is from
"Notepad++ Plugin Template for Delphi & Lazarus"
https://bitbucket.org/rdipardo/delphiplugintemplate/src/default/

After some beginner struggle it seems to work quite fine.
Regarding the gui there are only two major things left. The one i'd like to ask here, because i think it might be LCL related too.

Treeview (will need a bit additional work on custom paint here, no problem):
In my own component (based on custom treeview) now i had to notice heavy horrible flicker on mouse move.
Tried to find the reason: it is here:
    TreeviewXY.Hottrack := true.
Without Hottrack no flicker.  And, no flicker in 'normal' mode (light theme) too.

What's going on?
What i do assume is that there is a concurrent painting LCL vs. NPP themeing where both are disturbing and triggering each other, which results in a heavy flicker. Pls. note that i use myself extensively custom drawing without seeing such flicker.

I could reproduce simply by placing a shelltreeview onto a plugin demo docking form.
Hotrack on:    heavy flicker in dark mode
Hotrack off:   no flicker at all

Unfortunately i need the Hottrack for some reasons and won't like to switch it off.
Found the guilty line here:

Code: Pascal  [Select][+][-]
  1. //treeview.inc:
  2. procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  3. begin
  4. ...............
  5. ..............  
  6.   Invalidate;     // OOPS! This will repaint the whole control .....
  7. end;

From my own code for 'mouse hover' i was used for such case:
- repaint the line where the mouse is over
- repaint/refresh the line the mouse pointer had left
That had been ugly enough, because i had been forced to duplicate the whole treeview paint code, only because there is not measure to override/influence the color of the background erasure at the beginning (i had posted a special forum's topic on this times ago). Anyhow.

My question to the LCL experts: is there a more granular approach possible, without engaging the massive cavalry of invalidating all?

If someone is interested to try a test project: i can provide one, which would only require:
Lazarus 3.x (up) Windows 64 bit
A notepad++ 64bit installation recent version (i mainly prefer portable versions)
Select "dark mode" in the NPP options
And adapt two paths in the project settings.

Edit: i assume that the Delphi users of the demo plugin template don't have this problem, as here the treeview is OS-based, not pure VCL object, differently to Lazarus.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 17, 2024, 03:45:45 pm
I just did a little POC, and yeees

It works, but of course it covers only half of the story.

Code: Pascal  [Select][+][-]
  1. procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  2. var aNode: TTreeNode; R: TRect;
  3. begin
  4. .........
  5.    // Invalidate;   // please no brute force here
  6.    aNode := GetNodeAtY(Y);
  7.    If aNode <> nil then begin
  8.        R := ANode.DisplayRect(False);
  9.        InvalidateRect(Handle, @R, True);
  10.    end;

The previous mouse-hovered, now mouse-left node would be needed to be remembered and InvalidateRect'ed too. This memory would be needed to be resetted when another context appears.

Any ideas?

Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 17, 2024, 04:23:07 pm
POC, part 2 - yeees - would work perfectly i think:

Code: Pascal  [Select][+][-]
  1. //comctrls.pp:
  2.   TCustomTreeView = class(TCustomControl)
  3.      FHotTrackedPrevNode: TTreeNode;
  4.  
  5. //treeview.inc:
  6.     constructor TCustomTreeView.Create(AnOwner: TComponent);
  7.         FHotTrackedPrevNode := nil;
  8.  
  9.     procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  10.  
  11.       // Invalidate;    // please no brute force
  12.  
  13.       if Assigned(FHotTrackedPrevNode) then begin
  14.         if FHotTrackedPrevNode.Visible then begin
  15.            R := FHotTrackedPrevNode.DisplayRect(False);
  16.            InvalidateRect(Handle, @R, True);
  17.         end;
  18.         FHotTrackedPrevNode := nil;
  19.       end;
  20.       if Assigned(FNodeUnderCursor) then begin
  21.           R := FNodeUnderCursor.DisplayRect(False);
  22.           InvalidateRect(Handle, @R, True);
  23.           FHotTrackedPrevNode := FNodeUnderCursor;
  24.       end;

Would this make sense?

Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 17, 2024, 04:30:13 pm
For the NPP plugin the horrible flicker fully went away ..

Just retested outside the NPP plugin stuff, with a regular stand-alone exe,
and felt that even this would behave a bit more smoother now ...

Any opinions about?
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 17, 2024, 04:38:24 pm
It is right approach. If I knew about this flicker before, I would do the same.

PS. Welcome to write plugin for _my_ editor, see signature.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 17, 2024, 04:40:40 pm
And pls check - is UpdateHotTrack called when I move mouse inside single tree node? If so you call InvalidateRect twice for the same item.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 17, 2024, 05:10:08 pm
And pls check - is UpdateHotTrack called when I move mouse inside single tree node? If so you call InvalidateRect twice for the same item.

Good point Alex!
Means, something like this ?

Code: Pascal  [Select][+][-]
  1. procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  2. var aNode: TTreeNode; R: TRect; bDoInvalidate: Boolean;
  3. begin
  4. ...........
  5.   // Invalidate;    // Please no brute force here
  6.  
  7.   if Assigned(FHotTrackedPrevNode) then begin
  8.     if FHotTrackedPrevNode.Visible then begin
  9.        bDoInvalidate :=
  10.           ((Not Assigned(FNodeUnderCursor)) Or (FHotTrackedPrevNode.Index <> FNodeUnderCursor.Index));
  11.        if bDoInvalidate then begin
  12.           R := FHotTrackedPrevNode.DisplayRect(False);
  13.           InvalidateRect(Handle, @R, True);
  14.        end;
  15.     end;
  16.     FHotTrackedPrevNode := nil;
  17.   end;
  18.   if Assigned(FNodeUnderCursor) then begin
  19.       R := FNodeUnderCursor.DisplayRect(False);
  20.       InvalidateRect(Handle, @R, True);
  21.       FHotTrackedPrevNode := FNodeUnderCursor;
  22.   end;
  23. end;
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 17, 2024, 05:45:57 pm
Something like this, thanks.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 17, 2024, 06:00:39 pm
If a node is deleted programmatically, you should handle this and maybe Nil the FHotTrackedPrevNode.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 17, 2024, 08:08:04 pm
But if a node is deleted programmatically, eg. triggered by an external system - db, file system or such -, and this should be (randomly) the FHotTrackedPrevNode, then it will be already nil'ed, won't it?

Btw., in an app i did for own usage with same scenario, i didn't use the ttreenode as info about "previous", but the node's index. Don't know if this is a better way ('better a dangling integer than a dangling node'?).
If this should appear as more appropriate, then I'd like to change that of course.

Anyhow i'd need to retrieve back then the ttreenode itself from the index at each mouse move, at least for to ask if it' s visible; hm.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 18, 2024, 07:58:29 am
Yes, better to have dangling index than dangling TTreeNode, so save to Prev index instead, and calculate Node from it.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 18, 2024, 09:46:43 am
Yes Alex, i'd agree and change this.
Before i do, i'd like to share an observation:

Need to go one step back.
I have im my custom painting two differenct scenarios, and both should be preserved by the change for not to be too restrictive.
Note, the InvalidateRects done are the trigger, so to say, that on the customdraw side the correct input is given
(= node xy needs to be repainted). Otherwise the customdraw might code what it wants, but the code won't be called ... No invalidation, no painting possible.

Ok, mouse-wheel. Scrolling within the tree via mouse wheel up and down.
How a "hottracked line" should behave, when one uses the mousewheel ?
2 possible flavours (as option):   when wheeling up,
a) the hottracked line stays statically, where it is (and the contents moves underneath it away, going upwards))
b) the hottracked line moves up resp. down too (MS-like style). Selected line and hovered line do change it's position.

I checked if the change proposal would support both behaviour options.
- The initial proposal (from reply #2) does it; ok. Had no side-effects.
- The last proposal does not reliably. Sometimes it may happen one highlighted line to remain as painting relict. Here, the prev-info had been nil'ed out too early.

With other words: the check, if FHotTrackedPrevNode and FNodeUnderCursor are different, might be too thrifty when using the mousewheel.
Aa due to the movement of the wheel both will cross heavily each other (in flavour a.) and so some needed line-invalidations might go lost.
So imo the easier/first solution (logic) here would be the better, as both flavours mentioned could be done with it fine.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 18, 2024, 02:26:32 pm
I always thought that even for mousewheel, HotTrack must affect the node under cursor. If it makes some difficulty, it's bad.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 18, 2024, 04:35:03 pm
Hi Alex,
not bad, doable.
- Hover node, scroll mousewheel -> the original node under the cursor wanders away, cursor stays at same position as before (flavour 1)
- Hover node, scroll mousewheel -> node under the cursor wanders away, but the hover marker follows him (flavour 2)
In my standalone-exe i implemented both (as option) with custom drawing.
The essential part here is only that the invalidation rules do send the right invalidations for any, so that a programmer can decide.

The proposal now is here as follows
- with using an index instead of a ttreenode for to remember the prev state
- i tried to cover your correct note, avoid "InvalidateRect twice for the same item", by comparing the 'Top's of both InvalidateRects and do only the first one if the Tops are identical.
  But i'm not fully convinced. Sometimes i felt sporadical relicts though, not quite sure. So i show it as comments here only.


Code: Pascal  [Select][+][-]
  1. //comctrls.pp:
  2.   TCustomTreeView = class(TCustomControl)
  3.    private
  4.      FHotTrackedIdxPrevNode: Integer;
  5.  
  6. //treeview.inc:
  7.   constructor TCustomTreeView.Create(AnOwner: TComponent);
  8.      FHotTrackedIdxPrevNode := -1;
  9.  
  10.   procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  11.   var aNode: TTreeNode; R: TRect; // rTop: Integer;
  12.   begin
  13.      .........
  14.      //Invalidate;    // Please no brute force here
  15.  
  16.      //rTop := -1;
  17.      if FHotTrackedIdxPrevNode > -1 then begin
  18.         aNode := Items[FHotTrackedIdxPrevNode];
  19.         if (Assigned(aNode) And aNode.Visible) then begin
  20.            R := aNode.DisplayRect(False);
  21.            InvalidateRect(Handle, @R, True);
  22.            //rTop := R.Top;
  23.         end;
  24.         FHotTrackedIdxPrevNode := -1;
  25.      end;
  26.      if Assigned(FNodeUnderCursor) then begin
  27.         R := FNodeUnderCursor.DisplayRect(False);
  28.         //if R.Top <> rTop then
  29.         InvalidateRect(Handle, @R, True);
  30.         FHotTrackedIdxPrevNode := FNodeUnderCursor.AbsoluteIndex;
  31.      end;

My concern here is mainly, to minimize flicker AND to assure that the TCustomTreeView.UpdateHotTrack would do the _correct_ invalidations for any case of custom painting.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 18, 2024, 04:43:28 pm
This code looks OK, so only the testing (with mousewheel too) will show is it correct or not...

PS. Indent = 3 spaces? weird. 2 spaces better.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 18, 2024, 07:31:42 pm
This code looks OK, so only the testing (with mousewheel too) will show is it correct or not...

PS. Indent = 3 spaces? weird. 2 spaces better.

Did that intensively with my standalone-app, where i had implemented the customdraws for that.
Any other tester would of course be very welcome. -> One or the other should use "HotTrack" in his code, right?

Back to the NPP plugin: flicker is (nearly (*)) fully solved here, as mentioned.

rdipardo had added a treeview demo in his template yesterday (and i could spend a demo test project with that, as noted at the beginning here). But there's no special custom draw dealing with hover herein. It covers only painting of the background, expand sign, and item text correctly, and with the update also the scrollbars themed. So it's no good test area for this special thing 'hovering' here.
In my plugin i'm about to convert however there is this hovering mechanism, and it behaves fine, just as in the stand-alone app.

(*) Let's say flicker shrinks from 90% to 2%, as there is a very very little painting movement - only in the plugin! - one still can see when hovering an item. A bit more than in the stand-alone app. But nothing compared to the original situation.

Indentation: changed to 2 spaces.

I'd like to propose it in the bug-tracker. Where imo it's not a bug at all of course. We're speaking about a bit of more support when entering a still more or less unexplored territory. A little fine-tuning.
What do you think?
Maybe i'll do some more tests about "avoid InvalidateRect twice for the same item" though before; i'll see.
But in any case, I will keep a close eye on it in daily use to see if it proves effective before, for a week.

A word about the origin of the topic: imo a dark theme is a proof that mercilessly exposes any weakness of a code,
any unhandled white areas / erasures within a dark theme are directly visible. And the more if it's in a plugin.
There's a theme code, here by NPP via OS calls, the 'host application's' own drawsings, and the user's own custom drawing on top as a third layer. - Very demanding.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 19, 2024, 10:22:47 am
In my equality query in reply #6 - for not to call InvalidateRect twice for the same item - i did a mistake on the fly, having used "Index" instead of "AbsoluteIndex". My bad ... sorry!
So now it behaves without abnormalies with both mousewheel flavours:

Code: Pascal  [Select][+][-]
  1. procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  2. var aNode: TTreeNode; R: TRect; previdx: Integer;   // rTop: Integer
  3. begin
  4. .....
  5.   //Invalidate;
  6.   previdx := FHotTrackedIdxPrevNode;
  7.   if FHotTrackedIdxPrevNode > -1 then begin
  8.     aNode := Items[FHotTrackedIdxPrevNode];
  9.     if (Assigned(aNode) And aNode.Visible) then begin
  10.       R := aNode.DisplayRect(False);
  11.       InvalidateRect(Handle, @R, True);
  12.     end;
  13.     FHotTrackedIdxPrevNode := -1;
  14.   end;
  15.   if Assigned(FNodeUnderCursor) then begin
  16.     R := FNodeUnderCursor.DisplayRect(False);
  17.     if not (previdx = FNodeUnderCursor.AbsoluteIndex) then // Not call InvalidateRect twice for the same item
  18.       InvalidateRect(Handle, @R, True);
  19.     FHotTrackedIdxPrevNode := FNodeUnderCursor.AbsoluteIndex;
  20.   end;

Now back to the original topic:
as illustration i add two short videos for to show
- the behaviour before
(just added a shelltreeview to rdiparos's demo. Both actually have only a rudimentary painting. Eg. no icons, no hovering. It should only demonstrate the original flicker topic)
- and after the change the work-in-progress component flicker-free within the plugin.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 19, 2024, 10:23:29 am
The second one separately due to size restricion
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 19, 2024, 12:05:12 pm
Thanks. Looks good.
Offtopic: I see issue on Windows 10 with CudaText with dark theme: first painting of UI makes flicker - it shows some white areas, then CudaText dark theme is applied. Flicker of white areas. Did you see it in your project with NPP?
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 19, 2024, 03:47:23 pm
Hello Alex,
no, not especially with this NPP plugin.
But yes, meanwhile it's in general a very well-known terrain to me. Don't know your details here, but there might be a bunch of measures, some also proposed to metadarkstyle (had previously some issues here, see e.g. for flicker in StatusBar):

Basically it' s so that the OS or widgetset might paint the background in light, then comes the dark theme painting, and the component that might paint too. Every light painting that stays a while will be directly noticed as flicker, promised! Especially if a process needs time until it starts.

- Intercept the WM_ERASEBKGND eg. in message loop  resp. WMEraseBackground message, assure to paint dark bg here (instead of let others do white painting) and return True in message loop
- Maybe defer long operations triggered in WM_CREATE to a phase directly afterwards, so that other controls have a chance to build first. Or your control, with somehow deferred generation of it's contents
- Assure that containers(!) of the control, TPanel&Co, will be erased in dark too (easy example on app level below), not focus only the control itself
- Check Invalidates that are too global  ;-)
- I assume that well-known things like property DoubleBuffered, LockWindowUpdate, BeginUpdate/EndUpdate are already checked
- For some base components like TreeView ListView, others might have equivalents, it might be worthy to try out ovrrride possibilities at early creation stage, eg. in CreateWnd, maybe other window styles, SendMessages etc.
  Caution, some measure might speedup in light mode, but might not be good for dark mode, and vice versa
  So one might have something like:
Code: Pascal  [Select][+][-]
  1. procedure <TSomeListView>.CreateWnd;
  2. var Style: Dword;
  3. begin
  4.   inherited CreateWnd;
  5.   ControlStyle := ControlStyle + [csOpaque];   //  Add
  6. ,,.
  7.   if HandleAllocated then begin
  8.      If Not IsDarkModeEnabled then begin
  9.         Style := ListView_GetExtendedListViewStyle(Handle);
  10.         Style := Style or LVS_EX_DOUBLEBUFFER;
  11.         ListView_SetExtendedListViewStyle(Handle, Style);
  12.      end else begin
  13. ...
  14.     end;
  15. ...

--- Easy examples for overriding WMEraseBgnd on app level for standard (not subclassed) controls:

Code: Pascal  [Select][+][-]
  1. // For dark mode flickering
  2.   TSplitter = class(ExtCtrls.TSplitter)
  3.     procedure WMEraseBkgnd(var Message: TLMEraseBkgnd); message LM_ERASEBKGND;
  4.   end;
  5.   TPanel = class(ExtCtrls.TPanel)
  6.     procedure WMEraseBkgnd(var Message: TLMEraseBkgnd); message LM_ERASEBKGND;
  7.   end;
  8.   TEdit = class(StdCtrls.TEdit)     // Fix dark mode white flicker in a very long entry field
  9.    procedure WMEraseBkgnd(var Message: TLMEraseBkgnd); message LM_ERASEBKGND;
  10.   end;
  11.  
  12. // For dark mode flickering
  13. procedure TSplitter.WMEraseBkgnd(var Message: TLMEraseBkgnd);
  14. var vRect: TRect;
  15. begin
  16.   if Not IsDarkModeEnabled then
  17.      exit;
  18.   if Message.DC = 0 then Exit;
  19.   Brush.Color := $00353535;
  20.   vRect := Rect(0, 0, Width, Height);
  21.   FillRect(Message.DC, vRect, Brush.Reference.Handle);
  22. end;
  23.  
  24. // Drawback here: generates flicker at Resize. So, achieve via a flag (in Tag) that this will only be executed in the starting phase
  25. procedure TPanel.WMEraseBkgnd(var Message: TLMEraseBkgnd);
  26. var vRect : TRect;
  27. begin
  28.   if Not IsDarkModeEnabled then
  29.      exit;
  30.   if Not (Self.Tag = -1) then  // Values to be set somewhere else, at me eg. initialized in FormCreate and reset in TFormXYZ.AppIdle
  31.      exit;
  32.   if Message.DC = 0 then Exit;
  33.   Brush.Color := $00353535;
  34.   vRect := Rect(0, 0, Width, Height);
  35.   FillRect(Message.DC, vRect, Brush.Reference.Handle);
  36. end;
  37.  
  38. // Drawback here: generates flicker at Resize. So, achieve via a flag (in Tag) that this will only be executed in the starting phase
  39. procedure TEdit.WMEraseBkgnd(var Message: TLMEraseBkgnd);
  40. var vRect : TRect;
  41. begin
  42.   if Not IsDarkModeEnabled then
  43.      exit;
  44.   if Not (Self.Tag = -1) then  // Values to be set somewhere else, at me eg. initialized in FormCreate and reset in TFormXYZ.AppIdle
  45.      exit;
  46.   if Message.DC = 0 then Exit;
  47.   Brush.Color := $00353535;
  48.   vRect := Rect(0, 0, Width, Height);
  49.   FillRect(Message.DC, vRect, Brush.Reference.Handle);
  50. end;

Does one of those things somehow influences?
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 19, 2024, 03:59:45 pm
Oh, just looked ... in my pagecontrrol unit, where a pagecontrol and your attabs are in, i have:

Code: Pascal  [Select][+][-]
  1.   TPageControl = class(ComCtrls.TPageControl)
  2.       procedure WMEraseBkgnd(var Message: TLMEraseBkgnd); message LM_ERASEBKGND;
  3.   end;
  4.  
  5.   TATTabs = class(attabs.TATTabs)
  6.       procedure WMEraseBkgnd(var Message: TLMEraseBkgnd); message LM_ERASEBKGND;
  7.   end;
  8.  
  9. procedure TPageControl.WMEraseBkgnd(var Message: TLMEraseBkgnd);
  10. var vRect : TRect;
  11. begin
  12.   if Not IsDarkModeEnabled then
  13.      exit;
  14.   if Message.DC = 0 then Exit;
  15.   if FFirsttime then
  16.      FFirsttime := False
  17.   else
  18.      exit;
  19.   Brush.Color := $00353535;
  20.   vRect.Left := 0;
  21.   vRect.Top := 0;
  22.   vRect.Width := Width;
  23.   vRect.Height := Height;
  24.   FillRect(Message.DC, vRect, Brush.Reference.Handle);
  25. end;
  26.  
  27. procedure TATTabs.WMEraseBkgnd(var Message: TLMEraseBkgnd);
  28. var vRect : TRect;
  29. begin
  30.   if Not IsDarkModeEnabled then
  31.      exit;
  32.   if Message.DC = 0 then Exit;
  33.   Brush.Color := $00353535;
  34.   vRect.Left := 0;
  35.   vRect.Top := 0;
  36.   vRect.Width := Width;
  37.   vRect.Height := Height;
  38.   FillRect(Message.DC, vRect, Brush.Reference.Handle);
  39. end;

Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: AlexTP on December 19, 2024, 06:26:32 pm
Erasebkgnd: I am almost suceeded! Added LM_ERASE..... handler to ATSynEdit and I see initial repaint does flicker with green (testing color in handler).
Thanks!

I also modified WM_ERASE.... handler in ATTabs so now you don't need your workaround.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 19, 2024, 07:51:44 pm
Thanks for feedback Alex, please tell if it has good effects at the end.

Good test with color green (case there are some nested containers interfering .. which one might be responsible?)

For the "WMEraseBkgnd" examples, i just added a (needed) code comment above, where i had set the "Self.Tag" flag here. I have:  set in FormCreate, changed in AppIdle. This could be interesting when dealing with problems exclusively in the start phase. In FormActivate is was not always helpful. - Example:

Code: Pascal  [Select][+][-]
  1. procedure TFormXYZ.FormCreate(Sender: TObject);
  2. begin
  3.     if IsDarkModeEnabled then begin
  4.        efDirOut.Tag := -1;  // Flag to process WMEraseBkgnd
  5.        PanelUpper.Tag := -1;
  6.        Application.OnIdle := AppIdle;
  7.     end;
 

And sometimes the procedure was good for the start phase, but counterproductive for Resize, window Maximize, return from the task bar etc. A question of testing ...

And thanks for the info about the change in ATTabs!
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 20, 2024, 06:22:35 am
I just wanted to mention that sometimes it is simpler (and cheaper code-wise) to simply set the background brush (which is used to erase the background) to a NULL_BRUSH.

Basically this setting allows erasing the background  without erasing the background since the NULL_BRUSH has no effect on the background.

disclaimer: I don't know how well this will work in the LCL but works very nicely when using just the WinApi.

HTH.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 20, 2024, 10:18:00 am
Interesting 440bx! But wouldn't that mean that potentially any other unwanted color would remain for a while in the target area? Maybe you could point to some example snippet.

Btw i realized that i had been somehow unprecise that i had not returned a message result in callbacks using LM_ERASEBKGND instead of WM_ERASEBKGND (here it is needed).
In theory this return value (1 resp. true) is a Must (meaning: job done, don't do it again) for the purpose.
Afair in callbacks of 'Message: TLMEraseBkgnd' it hadn't make a difference in practice and so i had negelected it. Not sure yet, would need to re-dig into.
Maybe anybody knows.
Maybe i was wrong and that had been the reason why i had, in a few cases, some Resize flicher issues to handle separately.
Anyhow: to my experience those measures sum up to a better appearance.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 20, 2024, 10:51:17 am
Interesting 440bx! But wouldn't that mean that potentially any other unwanted color would remain for a while in the target area? Maybe you could point to some example snippet.
You'll find fully working example code at
https://forum.lazarus.freepascal.org/index.php/topic,53791.msg398393.html#msg398393
The examples BitBlt, StretchBlt and SelectClipRgn (in the following post) all use a NULL_BRUSH.

Using a NULL_BRUSH should be equivalent to not clearing the background.  IOW, whatever was in the background  remains there after being "erased" with a NULL_BRUSH.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 20, 2024, 11:06:25 am
Thank you! I'll see if i can a test next within the given context.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 20, 2024, 11:13:51 am
You're welcome.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 20, 2024, 09:39:01 pm
... but couldn't bring such easily to work.
The examples are constructed as 'traditional' Win-programs based on WinMain, CreateWindowEx, WndProc etc.
But is it possible, and how would it be possible within a FormCreate to modify the background brush handle of the form, better: a control ?

Code: Pascal  [Select][+][-]
  1. procedure THelloWorldDockingForm.FormCreate(Sender: TObject);
  2. var .......  wnd: Windows.TWNDClassEx; szClassName: String;
  3. begin
  4. ....
  5.   szClassName := Application.ClassName;  // 'TApplication'
  6.   if GetClassInfoEx(hInstance, PChar(szClassName), @wnd) then
  7.   //szClassName := 'THelloWorldDockingForm';
  8.   //if GetClassInfoEx(hInstance, PChar(szClassName), @wnd) then
  9.   //if GetClassInfoEx(Self.Handle, PChar(szClassName), @wnd) then
  10.   //szClassName := 'TTreeView';
  11.   //if GetClassInfoEx(TreeView1.Handle, PChar(szClassName), @wnd) then
  12.   begin
  13.     // Keep most properties untouched, let them as Get'ed
  14.     wnd.hbrBackground := HBRUSH(GetStockObject(NULL_BRUSH));
  15.     Windows.RegisterClassEx(wnd);  // Possible as update? To rewrite slightly modified information?
  16.   end;
  17. ....
 
Doesn't succeed, GetClassInfoEx returns False, for form or control. No right idea yet. Maybe there are more correct approaches ...

Besides that: somebody may wonder, why doesn't he apply the TWMEraseBkgnd interception technique to the treeview?
That's already done. But has no effect here (in difference to a stand-alone exe): the code won't never be called.
I would explain it by the fact that the class is already subclassed ("catched") by the Notepad++ plugin theme that takes over responsibility for (dark) background painting
Code: Pascal  [Select][+][-]
  1.    SendMessage(Npp.NppData.NppHandle, NPPM_DARKMODESUBCLASSANDTHEME, DmFlag, Self.TreeView1.Handle);
And so, for instance, i could, in a WndProc, breakpoint on WM_MOUSEWHEEL, but not on WM_ERASEBKGND. Normally, within other contexts, or the metadarkstyle layer, it has a good effect to proceed so, but not here.
In the NPP plugin it doesn't seem to work.

So:  as of now the most promising candidate seems to be for me the proposed code change in TCustomTreeView.UpdateHotTrack  ...
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 20, 2024, 11:08:58 pm
Now I remember the LCL does some "unusual" things with windows and that is likely the reason getting the class info doesn't work as expected.

The LCL seems to often use a generic class named "Window" to route message processing to a generic window proc.

The following example code gets the class name from any visible window. 
Code: Pascal  [Select][+][-]
  1. {$APPTYPE        GUI}
  2.  
  3. {$LONGSTRINGS    OFF}
  4. {$WRITEABLECONST ON}
  5. {$DESCRIPTION    'Win32 API function - GetClassName example'}
  6.  
  7. // {$R GetClassName.Res}  { left out }
  8.  
  9. program _GetClassName;
  10.   { Win32 API function - GetClassName example                                 }
  11.  
  12. uses Windows, Messages, Resource, SysUtils;
  13.  
  14. const
  15.   AppNameBase  = 'GetClassName Example';
  16.  
  17.   {$ifdef WIN64}
  18.     Bitness64  = ' - 64bit';
  19.     AppName    = AppNameBase + Bitness64;     { NOTE: also used as class name }
  20.   {$else}
  21.     Bitness32  = ' - 32bit';
  22.     AppName    = AppNameBase + Bitness32;
  23.   {$endif}
  24.  
  25.   AboutBox     = 'AboutBox';
  26.   APPICON      = 'APPICON';
  27.   APPMENU      = 'APPMENU';
  28.  
  29. {-----------------------------------------------------------------------------}
  30.  
  31. {$ifdef VER90} { Delphi 2.0 }
  32. type
  33.   ptrint  = longint;
  34.   ptruint = dword;
  35. {$endif}
  36.  
  37. {-----------------------------------------------------------------------------}
  38.  
  39. function About(DlgWnd : hWnd; Msg : UINT; wParam, lParam : ptrint)
  40.          : ptrint; stdcall;
  41. begin
  42.   About := ord(TRUE);
  43.  
  44.   case msg of
  45.  
  46.     WM_INITDIALOG: exit;
  47.  
  48.     WM_COMMAND:
  49.     begin
  50.       if (LOWORD(wParam) = IDOK) or (LOWORD(wParam) = IDCANCEL) then
  51.       begin
  52.         EndDialog(DlgWnd, ord(TRUE));
  53.  
  54.         exit;
  55.       end;
  56.     end;
  57.   end;
  58.  
  59.   About := ord(FALSE);
  60. end;
  61.  
  62. {-----------------------------------------------------------------------------}
  63.  
  64. procedure DrawWindowFrame(Wnd : HWND);
  65.   { Draws a frame around the parameter Wnd                                    }
  66. var
  67.   dc         : HDC;
  68.   WindowRect : TRECT;
  69.  
  70.   Pen        : HPEN;
  71.   OldPen     : HPEN;
  72.  
  73. begin
  74.   { a 5 pixel wide pen is a reasonable choice. Some windows are "tucked" under}
  75.   { other child windows and a thin frame won't be visible because it falls    }
  76.   { in the "tucked" area.                                                     }
  77.  
  78.   Pen := CreatePen(PS_INSIDEFRAME, 5, RGB(255, 0, 255));
  79.  
  80.   GetWindowRect(Wnd, WindowRect);              { the window rectangle         }
  81.  
  82.   {---------------------------------------------------------------------------}
  83.   { convert the coordinates in WindowRect to be relative to the upper left    }
  84.   { corner of the window.  At this time they are relative to the upper left   }
  85.   { corner of the screen.  After the conversion the (Left, Top) coordinate in }
  86.   { WindowRect will be (0, 0) which matches the preset (Left, Top) coordinate }
  87.   { the window dc.                                                            }
  88.  
  89.   with WindowRect do OffsetRect(WindowRect, - Left, - Top);
  90.  
  91.   {---------------------------------------------------------------------------}
  92.   { we need a dc that doesn't clip the output to the client area and that can }
  93.   { be used to update a locked window (the window to be framed is locked).    }
  94.  
  95.   dc :=  GetDCEx(Wnd,
  96.                  0,                      { no region                          }
  97.                  DCX_WINDOW       or
  98.                  DCX_CACHE        or
  99.                  DCX_EXCLUDERGN   or     { excludes nothing because region = 0}
  100.                  DCX_CLIPSIBLINGS or
  101.                  DCX_LOCKWINDOWUPDATE);
  102.  
  103.   { select the pen and the brush used by the Rectangle API                    }
  104.  
  105.   OldPen := SelectObject(dc, Pen);
  106.   SelectObject(dc, GetStockObject(NULL_BRUSH));  { only the frame gets drawn  }
  107.  
  108.   { select a raster op that causes the original pixels to be restored when the}
  109.   { rectangle is drawn the second time.                                       }
  110.  
  111.   SetROP2(dc, R2_NOTXORPEN);
  112.  
  113.   {---------------------------------------------------------------------------}
  114.   { draw a frame around (inside) the window rectangle                         }
  115.  
  116.   with WindowRect do
  117.   begin
  118.     Rectangle(dc, Left, Top, Right, Bottom);
  119.   end;
  120.  
  121.   SelectObject(dc, OldPen);          { restore the original pen               }
  122.   ReleaseDC(Wnd, dc);
  123.   DeleteObject(Pen);                 { get rid of the pen                     }
  124.  
  125.   {---------------------------------------------------------------------------}
  126.   { release the window dc                                                     }
  127.  
  128.   ReleaseDC(dc, Wnd);
  129. end;
  130.  
  131. {-----------------------------------------------------------------------------}
  132.  
  133. function WndProc (Wnd : hWnd; Msg : UINT; wParam, lParam : ptrint)
  134.          : ptrint; stdcall;
  135.   { main application/window handler function                                  }
  136. const
  137.   GetClassName_Call
  138.    = 'GetClassName (Wnd : HWND; NameBuf : pchar; BufSize : integer) : integer;';
  139.  
  140.   FramedWindow         : hWnd = 0;
  141.  
  142.   Tracking             : BOOL = FALSE;
  143.  
  144.   Hint = 'Press the left mouse button - here - then move the mouse around';
  145.  
  146. var
  147.   ps                 : TPAINTSTRUCT;
  148.  
  149.   ClientRect         : TRECT;
  150.  
  151.   Buf                : packed array[0..511] of char;
  152.   ClassName          : packed array[0..255] of char;
  153.  
  154.   TextSize           : TSIZE;
  155.  
  156.   MouseOnWindow      : HWND;
  157.   TopWindow          : HWND;
  158.   MousePt            : TPOINT;
  159.  
  160. begin
  161.   WndProc := 0;
  162.  
  163.   case Msg of
  164.     WM_LBUTTONDOWN:
  165.     begin
  166.       {-----------------------------------------------------------------------}
  167.       { capture the mouse to make sure we always get the button up which      }
  168.       { is the signal to refresh the client area.                             }
  169.  
  170.       SetCapture(Wnd);
  171.  
  172.       {-----------------------------------------------------------------------}
  173.       { if the window is partially covered by another window (like a menu)    }
  174.       { we want to make sure the window is fully uncovered before we draw     }
  175.       { the frame.  We do this using SetWindowPos and UpdateWindow.           }
  176.  
  177.       SetWindowPos(Wnd,
  178.                    HWND_TOPMOST,
  179.                    0, 0, 0, 0,
  180.                    SWP_NOMOVE or SWP_NOSIZE or SWP_DRAWFRAME);
  181.       UpdateWindow(Wnd);
  182.  
  183.       { it should be ok to draw the window frame now                          }
  184.  
  185.       DrawWindowFrame(Wnd);
  186.       FramedWindow := Wnd;
  187.  
  188.       Tracking := TRUE;                   { we are tracking the mouse         }
  189.  
  190.       InvalidateRect(Wnd, nil, TRUE);
  191.       exit;
  192.     end;
  193.  
  194.     WM_LBUTTONUP:
  195.     begin
  196.       {-----------------------------------------------------------------------}
  197.       { Note that using "if GetCapture = Wnd" to find out if we are           }
  198.       { tracking the mouse can be a source of problems.  In some instances    }
  199.       { Windows (thru DefWindowProc) will capture the mouse for us, so        }
  200.       { having the mouse captured does not necessarily mean that we should    }
  201.       { draw or erase a frame.                                                }
  202.  
  203.       if Tracking then
  204.       begin
  205.         ReleaseCapture;                 { let the cat play with it            }
  206.  
  207.         DrawWindowFrame(FramedWindow);  { erase the frame                     }
  208.         FramedWindow := -1;
  209.         Tracking     := FALSE;
  210.         LockWindowUpdate(0);
  211.  
  212.         SetWindowPos(Wnd,
  213.                      HWND_NOTOPMOST,
  214.                      0, 0, 0, 0,
  215.                      SWP_NOMOVE or SWP_NOSIZE or SWP_DRAWFRAME);
  216.  
  217.         InvalidateRect(Wnd, nil, TRUE); { redraw the client area              }
  218.       end;
  219.  
  220.       exit;
  221.     end;
  222.  
  223.     WM_MOUSEMOVE:
  224.     begin
  225.       { if we are not tracking the mouse then there's nothing to do           }
  226.  
  227.       if not Tracking then exit;
  228.  
  229.       { we don't use the coordinates stored in the lParam because they are    }
  230.       { in client coordinates and may not reflect the current position of     }
  231.       { the mouse if the user moved it after this message was received.       }
  232.  
  233.       GetCursorPos(MousePt);
  234.  
  235.       { get the handle of the window under the cursor                         }
  236.  
  237.       MouseOnWindow := WindowFromPoint(MousePt);
  238.  
  239.       if MouseOnWindow = FramedWindow then exit;  { previously framed         }
  240.  
  241.       { The mouse is on a new window. Erase the previous frame                }
  242.  
  243.       DrawWindowFrame(FramedWindow);
  244.       LockWindowUpdate(0);             { unlock it                            }
  245.       UpdateWindow(FramedWindow);      { let any pending updates thru         }
  246.  
  247.       {-----------------------------------------------------------------------}
  248.       { check that the window handle obtained is valid.  Just in case this    }
  249.       { is one of these windows that "come and go" (timed popups and such)    }
  250.  
  251.       { following comment applies to Win9x only:                              }
  252.  
  253.       { NOTE: strictly speaking we should acquire the Win16Mutex to make      }
  254.       {       sure that the window isn't going to disappear before we         }
  255.       {       attempt to frame it.                                            }
  256.  
  257.       if not IsWindow(MouseOnWindow) then
  258.       begin
  259.         FramedWindow := -1;
  260.  
  261.         exit;
  262.       end;
  263.  
  264.       { draw the frame around the window. Because we did not acquire the      }
  265.       { Win16Mutex there is a _very_ slim chance that the window handle       }
  266.       { may no longer be valid.  We'll live with that possibility for this    }
  267.       { example.                                                              }
  268.  
  269.       { tell the window to update itself before we lock it.  This prevents    }
  270.       { framing half painted windows. Unfortunately this produces flicker     }
  271.       { when the mouse is on windows that paint themselves periodically       }
  272.       { such as the System Monitor.  The flicker can be eliminated by         }
  273.       { always locking the Top Level window instead of the child window.      }
  274.  
  275.       TopWindow := MouseOnWindow;
  276.       while GetParent(TopWindow) <> 0 do TopWindow := GetParent(TopWindow);
  277.  
  278.       UpdateWindow(MouseOnWindow);
  279.  
  280.       if MouseOnWindow <> Wnd then LockWindowUpdate(TopWindow);   { lock it   }
  281.       DrawWindowFrame(MouseOnWindow);                             { frame it  }
  282.  
  283.       { we should release the Win16Mutex here if we had obtained it.          }
  284.       {-----------------------------------------------------------------------}
  285.  
  286.       { keep track of the currently framed window.                            }
  287.  
  288.       FramedWindow := MouseOnWindow;
  289.  
  290.       { update our display to reflect the window size of the new window       }
  291.  
  292.       InvalidateRect(Wnd, nil, TRUE);
  293.  
  294.       exit;
  295.     end;
  296.  
  297.     WM_PAINT:
  298.     begin
  299.       BeginPaint(Wnd, ps);
  300.  
  301.       GetClientRect(Wnd, ClientRect);
  302.  
  303.       SetBkMode(ps.hdc, TRANSPARENT);
  304.       SetTextAlign(ps.hdc, TA_CENTER or TA_BOTTOM);
  305.       SelectObject(ps.hdc, GetStockObject(ANSI_VAR_FONT));
  306.  
  307.       if Tracking then
  308.       begin
  309.         {---------------------------------------------------------------------}
  310.         { show the window handle the mouse is currently on                    }
  311.  
  312.         GetClassName(FramedWindow, ClassName, sizeof(ClassName));
  313.         StrFmt(Buf, 'Class Name: "%s"', [ClassName]);
  314.       end
  315.       else
  316.       begin
  317.         {---------------------------------------------------------------------}
  318.         { give the user a hint about what to do next                          }
  319.  
  320.         lstrcpy(Buf, Hint);
  321.       end;
  322.  
  323.       TextOut(ps.hdc,
  324.               ClientRect.Right   div 2,
  325.               ClientRect.Bottom  div 2,
  326.               Buf,
  327.               lstrlen(Buf));
  328.  
  329.       {-----------------------------------------------------------------------}
  330.       { draw the function call                                                }
  331.  
  332.  
  333.       SelectObject(ps.hdc, GetStockObject(DEFAULT_GUI_FONT));
  334.       lstrcpy(Buf, GetClassName_Call);
  335.  
  336.       { calculate the size of the output string                               }
  337.  
  338.       GetTextExtentPoint32(ps.hdc,
  339.                            Buf,
  340.                            lstrlen(Buf), TextSize);
  341.  
  342.       TextOut(ps.hdc,
  343.               ClientRect.Right div 2,
  344.               ClientRect.Bottom - TextSize.cy,
  345.               Buf,
  346.               lstrlen(Buf));
  347.  
  348.       {-----------------------------------------------------------------------}
  349.       { we're done painting                                                   }
  350.  
  351.       EndPaint(Wnd, ps);
  352.  
  353.       exit;
  354.     end;
  355.  
  356.     WM_COMMAND:
  357.     begin
  358.       case LOWORD(wParam) of
  359.         IDM_ABOUT:
  360.         begin
  361.           DialogBox(hInstance, ABOUTBOX, Wnd, @About);
  362.  
  363.           exit;
  364.         end; { IDM_ABOUT }
  365.  
  366.         IDM_EXIT:
  367.         begin
  368.           DestroyWindow(Wnd);
  369.  
  370.           exit;
  371.         end; { IDM_EXIT }
  372.       end; { case LOWORD(wParam) }
  373.     end; { WM_COMMAND }
  374.  
  375.     WM_DESTROY:
  376.     begin
  377.  
  378.       PostQuitMessage(0);
  379.  
  380.       exit;
  381.     end; { WM_DESTROY }
  382.   end; { case msg }
  383.  
  384.   WndProc := DefWindowProc (Wnd, Msg, wParam, lParam);
  385. end;
  386.  
  387. {-----------------------------------------------------------------------------}
  388.  
  389. function InitAppClass: WordBool;
  390.   { registers the application's window classes                                }
  391. var
  392.   cls : TWndClassEx;
  393.  
  394. begin
  395.   cls.cbSize          := sizeof(TWndClassEx);         { must be initialized   }
  396.  
  397.   if not GetClassInfoEx (hInstance, AppName, cls) then
  398.   begin
  399.     with cls do
  400.     begin
  401.       { cbSize has already been initialized as required above                 }
  402.  
  403.       style           := CS_BYTEALIGNCLIENT;
  404.       lpfnWndProc     := @WndProc;                    { window class handler  }
  405.       cbClsExtra      := 0;
  406.       cbWndExtra      := 0;
  407.       hInstance       := system.hInstance;            { qualify instance!     }
  408.       hIcon           := LoadIcon (hInstance, APPICON);
  409.       hCursor         := LoadCursor(0, idc_arrow);
  410.       hbrBackground   := GetSysColorBrush(COLOR_WINDOW);
  411.       lpszMenuName    := APPMENU;                     { Menu name             }
  412.       lpszClassName   := AppName;                     { Window Class name     }
  413.       hIconSm         := LoadImage(hInstance,
  414.                                    APPICON,
  415.                                    IMAGE_ICON,
  416.                                    16,
  417.                                    16,
  418.                                    LR_DEFAULTCOLOR);
  419.     end; { with }
  420.  
  421.     InitAppClass := WordBool(RegisterClassEx(cls));
  422.   end
  423.   else InitAppClass := True;
  424. end;
  425.  
  426. {-----------------------------------------------------------------------------}
  427.  
  428. Function WinMain : integer;
  429.   { application entry point                                                   }
  430. var
  431.   Wnd : hWnd;
  432.   Msg : TMsg;
  433. begin
  434.   if not InitAppClass then Halt (255);  { register application's class        }
  435.  
  436.   { Create the main application window                                        }
  437.  
  438.   Wnd := CreateWindowEx(WS_EX_CLIENTEDGE,
  439.                         AppName,                { class name                  }
  440.                         AppName,                { window caption text         }
  441.                         ws_Overlapped       or  { window style                }
  442.                         ws_SysMenu          or
  443.                         ws_MinimizeBox      or
  444.                         ws_ClipSiblings     or
  445.                         ws_ClipChildren     or  { don't affect children       }
  446.                         ws_visible,             { make showwindow unnecessary }
  447.                         50,                     { x pos on screen             }
  448.                         50,                     { y pos on screen             }
  449.                         400,                    { window width                }
  450.                         300,                    { window height               }
  451.                         0,                      { parent window handle        }
  452.                         0,                      { menu handle 0 = use class   }
  453.                         hInstance,              { instance handle             }
  454.                         Nil);                   { parameter sent to WM_CREATE }
  455.  
  456.   if Wnd = 0 then Halt;                         { could not create the window }
  457.  
  458.   while GetMessage (Msg, 0, 0, 0) do            { wait for message            }
  459.   begin
  460.     TranslateMessage (Msg);                     { key conversions             }
  461.     DispatchMessage  (Msg);                     { send to window procedure    }
  462.   end;
  463.  
  464.   WinMain := Msg.wParam;                        { terminate with return code  }
  465. end;
  466.  
  467. begin
  468.   WinMain;
  469. end.
Note that I left out the resource file (in line 7) but that should not affect how the program works.  With that code you'll at least know what the class name is.
Also to compile this code (GetClassName.lpr) tell Lazarus it is a "simple program" (don't use any other option.)
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 20, 2024, 11:49:20 pm
Oh thanks 440bx. As this did remind me on the old MS tool Spy++, i did a quick check with this, and indeed .. 'Window' (see screenshot) (independent from if form or any control are focused).
So, one step forward. The If-code will be executed. But it hasn't any effect yet.
The current value for hbrBackground is 0, that might possibly not be correct. (?)
For comparison, lpszClassName (before assignment, only for inspecting:) is 'Window', so this matches the really existing value.
Maybe the renewed 'RegisterClassEx' is an inappropriate call for to update a elass information ?
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 21, 2024, 12:33:02 am
It looks like setting the class background brush to a null brush does not "fit" well in the LCL.

Since I know next to nothing about the LCL, I have nothing else to offer.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 21, 2024, 11:11:09 am
Ok 440bx, but i'd like to remark that there might be also some other reasons possible:
- The subclassing NPP theme layer had already picked up the ownership of the drawing
- the null_brush works for the form, but this is not noticeable (no form's flicker before, and not after). But the treeview flicker continues  because it's Invalidates causes control's repaint in light
- RegisterClassEx is not designed for/not able to have an update capability
- Within FormCreate it is done too late
For to come nearer, some checks:
Code: Pascal  [Select][+][-]
  1.   wnd.hCursor := Windows.LoadCursor(0, IDC_WAIT); // This should directly be visible on the form ..
  2.   anAtom := Windows.RegisterClassEx(wnd);
No effect. 'anAtom' is 0. It should contain a unique id for the class, so '0' is probably wrong.
That does mean imo, no modification of the class attributes had happened. So it's not likely it's about the NULL_BRUSH only.

Next checks:
- try to do it at an earlier stage (CreateWnd):

Code: Pascal  [Select][+][-]
  1.   protected
  2.     procedure CreateParams(var Params: TCreateParams); override;
  3.     procedure CreateWnd; override;
  4.  
  5. procedure THelloWorldDockingForm.CreateWnd;
  6. var wnd: Windows.TWNDClassEx; szClassName: String; anAtom: ATOM;
  7. begin
  8.   inherited CreateWnd;
  9.   szClassName := 'Window';
  10.   if GetClassInfoEx(hInstance, PChar(szClassName), @wnd) then
  11.   begin
  12.     wnd.hCursor := Windows.LoadCursor(0, IDC_WAIT);
  13.     anAtom := Windows.RegisterClassEx(wnd); // After that, 'anAtom' is 0.
  14.   end;
  15. end;

No change of the cursor. - Now, an attempt using CreateParams, where one can influence styles:

Code: Pascal  [Select][+][-]
  1. procedure THelloWorldDockingForm.CreateParams(var Params: TCreateParams);
  2. begin
  3.   //inherited CreateParams(Params);
  4.   with Params do begin
  5.        //Style := Style or WS_BORDER;
  6.        //ExStyle := WS_EX_TOOLWINDOW or WS_EX_TOPMOST;
  7.        //WindowClass.Style := CS_SAVEBITS;
  8.        WindowClass.hbrbackground := HBRUSH(GetStockObject(NULL_BRUSH));;
  9.        WindowClass.hCursor := Windows.LoadCursor(0, IDC_WAIT);
  10.   end;
  11.   inherited CreateParams(Params);
  12. end;

No wait cursor for the form, and no reduction of the treeview's flicker.

I'll give up this way and go back to observe if i see side-effects of the proposed code change in TCustomTreeView.UpdateHotTrack next days.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 21, 2024, 12:05:35 pm
I see a problem in the code you showed, the size field of the wndclassex structure has not been initialized as required. That will cause the registration to fail.

see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa for details.

HTH.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 21, 2024, 12:58:13 pm
Ok, good point!, as they say "set this member before calling the GetClassInfoEx function"
So:
Code: Pascal  [Select][+][-]
  1.   szClassName := 'Window';
  2.   wnd.cbSize := SizeOf(TWndClassEx);
  3.   if GetClassInfoEx(hInstance, PChar(szClassName), @wnd) then begin
  4.      wnd.hbrBackground := HBRUSH(GetStockObject(NULL_BRUSH));
  5.      wnd.hCursor := Windows.LoadCursor(0, IDC_WAIT);
  6.      anAtom := Windows.RegisterClassEx(wnd);
  7.   end;
-> anAtom stays '0'; no wait cursor to see  ..

For to see, if the WinApi demo program 'StretchBlt' would allow at all to do a second call of RegisterClassEx, i did the following check - > see screenshot.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: 440bx on December 21, 2024, 01:13:53 pm
I've never tried registering a class more than once.  Windows probably considers that an error, you should use GetLastError to find out why the call failed.

Also, once a class is registered, changing any of its "characteristics" is done using SetClassLong.  Read about it at https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclasslonga

You should use SetClassLongPtr but read the above linked page first.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 21, 2024, 01:53:27 pm
With GetLastError I was just about   :)  -> image.  -  As expected ...

'SetClassLongPtrW' might be a promising pointer, possibly something like:

Code: Pascal  [Select][+][-]
  1.     //SetClassLongPtrW(hInstance, GCL_HBRBACKGROUND, 0);
  2.     //SetClassLongPtrW(Self.handle, GCL_HBRBACKGROUND, 0);
  3.     //SetClassLongPtrW(Treeview1.handle, GCL_HBRBACKGROUND, 0);
  4.     //SetClassLongPtrW(Self.Handle, GCL_HBRBACKGROUND, COLOR_BACKGROUND);
  5.     //SetClassLongPtrW(TreeView1.Handle, GCL_HBRBACKGROUND, COLOR_BACKGROUND);
  6.  
  7.     SetClassLongPtrW(hInstance, GCL_HBRBACKGROUND, HBRUSH(GetStockObject(NULL_BRUSH)));
  8.     SetClassLongPtrW(Self.Handle, GCL_HBRBACKGROUND, HBRUSH(GetStockObject(NULL_BRUSH)));
  9.     SetClassLongPtrW(Treeview1.handle, GCL_HBRBACKGROUND, HBRUSH(GetStockObject(NULL_BRUSH)));
  10.     // or:
  11.     SetClassLongPtrW(Treeview1.handle, GCL_HBRBACKGROUND, LONG_PTR(GetStockObject(NULL_BRUSH)));

(I remember i had used this once, ages ago, but only for the light theme, it hadn't played very well together with metadarkstyle)

Within the plugin demo dockingform, dark, it has no visible effect for me.
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on December 22, 2024, 10:59:36 am
Now back to the code change proposal.
Due to daily practice i've improved it a bit, as that's better for a rare use case where it should painted although the cursor is in a line in a x-pos right after a node's caption. This one would not be fully fetched by:
Code: Pascal  [Select][+][-]
  1.   FNodeUnderCursor := GetNodeAt(X, Y);
If the cursor is after ending of the node's caption, a redraw won't be triggered.
That should happen also when the cursor is within a line, not only within a caption text.
But: principle is: i never wont't touch FNodeUnderCursor itself, for not to touch the used behaviour of drag & drop, no way.
So, a local variable is needed.
Compared with the situation before (Invalidate all) it doesn't play a role at all. It's simply a little bit more where InvalidateRect will be triggered.

Another point: one may argue: and what's about MultiSelect? Ok, then ('for paranoia') do conventional behavior: Invalidate.
Otherwise, please note: possibly only two lines are affected, Where the cursor is, and where he was.

Code: Pascal  [Select][+][-]
  1. procedure TCustomTreeView.UpdateHotTrack(X, Y: Integer);
  2. var aNode, nodeUnderCursorY: TTreeNode; R: TRect; previdx: Integer;
  3. begin
  4. .......
  5.   FNodeUnderCursor := GetNodeAt(X, Y);
  6. .......
  7.   // Invalidate;    // Not so global please - invalidae only the affected lines (at least if not MultiSelect)
  8.  
  9.   if Self.MultiSelect then
  10.     Invalidate
  11.   else begin
  12.     nodeUnderCursorY := GetNodeAtY(Y);  // Instead of GetNodeAt(X, Y) FNodeUnderCursor. Don't want to touch FNodeUnderCursor itself!
  13.     previdx := FHotTrackedIdxPrevNode;
  14.     if FHotTrackedIdxPrevNode > -1 then begin
  15.       aNode := Items[FHotTrackedIdxPrevNode];
  16.       if (Assigned(aNode) And aNode.Visible) then begin
  17.         R := aNode.DisplayRect(False);
  18.         InvalidateRect(Handle, @R, True);
  19.       end;
  20.       FHotTrackedIdxPrevNode := -1;
  21.     end;
  22.     if Assigned(nodeUnderCursorY) then begin
  23.       R := nodeUnderCursorY.DisplayRect(False);
  24.       if not (previdx = nodeUnderCursorY.AbsoluteIndex) then // Not call InvalidateRect twice for the same item
  25.         InvalidateRect(Handle, @R, True);
  26.       FHotTrackedIdxPrevNode := nodeUnderCursorY.AbsoluteIndex;
  27.     end;
  28.   end;
  29.  
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on January 01, 2025, 10:42:19 am
I'd opened an item in the bugtracker
https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/41290
and a lately found little update preventing an additional condition about "not call InvalidateRect twice for the same item".
Title: Re: Heavy flicker treeview w. Hottrack when running inside an NPP Plugin (dark mode)
Post by: d7_2_laz on January 20, 2025, 12:13:44 pm
Fix available in 'main' (and expected for Fixes_4)  :)
https://gitlab.com/freepascal.org/lazarus/lazarus/-/commit/e897d845eafc7406b0a8b901f62aeea046097f9c
Title: Re: [SOLVED] Treeview flicker w. Hottrack when running inside NPP Plugin (dark mode)
Post by: d7_2_laz on January 27, 2025, 10:15:27 pm
It's in Fixes_4 and so either in an 4.0 RC3 or 4.0.
TinyPortal © 2005-2018