Lazarus

Programming => LCL => Topic started by: MarkMLl on September 12, 2019, 10:28:05 pm

Title: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 12, 2019, 10:28:05 pm
I'm sure this is an FAQ but so far I've not found the magic search term.

I'm trying to restore the size of a form and the position of a couple of TPairSplitters (containing several layers of nested panels etc.) as preparation for adjusting things so that one pane has the right size for a cut-and-paste.

Restoring the form to the size it had at program startup is trivial, but how do I adjust the pairsplitters? The description of the position property implies that whilst it's not read-only the way to change position is by a drag operation... surely it can also be done by code?

MarkMLl

Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 12, 2019, 11:17:24 pm
Look into the sources (CTRL-click on TPairSplitter to open the source file in which TPairSplitter is implemented). You'll see a property "Position" there, it is read-write and seems to be what your are looking for.

Try attached demo to see that it is working.

However, I do not use use TPairSplitter normally, only regular splitters because I know them from Delphi. In a recent project I used Ondrej's TOMultiPanel (http://www.kluug.net/omultipanel.php, recent source at https://sourceforge.net/p/omultipanel/code/HEAD/tree/): it is similar to a TPairSplitter but much more versatile. Highly recommended for complex flexible layouts!
Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 12, 2019, 11:40:29 pm
I like using the anchor editor for splitters, they seem to work better..
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 13, 2019, 12:22:43 am
I never managed to get a functional splitter using the AnchorEditor (i.e. not using the Align property). Can you explain?
Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 13, 2019, 01:26:10 am
all alignment for the splitter must be off, it does not work well at all.

Personally I think it should be off by default in the OI because it causes design issues.

 That being said, I then use the anchor editor where as I have for example the controls beside it anchor to the splitter and the controls also need to have align off for this to work and so all you need to do is position the splitter and the adjacent controls will follow it as long as you have them anchored to the splitter.
 
 So I've found that it is required to disable the alignment on controls you want to stay with the splitter, this goes for the splitter too.

 When refreshing the form the controls follow the splitter.
The Controls also need to be anchored to the Form or what ever is on the other side of them.
 
 It works well for using the anchor editor..

 years ago before this option came along it was very tricky to get this working because I had to do all of this in the ONSize event which then would retrigger other events and so on.

Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 13, 2019, 01:59:51 am
I wanted to add, if you are trying this with the TpairSplitter, this is slightly a different animal..

 It's tricking getting at the anchor editor, I have to use the OI property panel and click there to get the dialog to come up on the correct part of the control.

 It gets messy especially when you have controls inside the pair splitter, that is when you can use the anchor editor directly to keep this aligned also its hard to use the form designer to move it around I need to use the property OI and set the values because you can't simply get the mouse to grab onto the base of the control.

 I normally use the standard Tsplitter.
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 13, 2019, 10:47:44 am
Thanks for your response, jamie. Got it working finally. For others who had the same problem I am writing down step-by-step instructions (see also attached demo):
Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 13, 2019, 06:36:46 pm
That is a very clean layout thanks for the writeup...

 Also, I think it would be prudent if maybe someone  could get the default alignment set to alNone on the next release because this really bites it hard..
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 13, 2019, 06:53:49 pm
I guess it conflicts with Delphi compatibility. Delphi has TSplitter.Align = alLeft by default. Thus, when a Delphi form with a splitter is loaded into Lazarus the layout will be broken because the default value is not contained in the dfm/lfm file.

I added the steps to the wiki article (https://wiki.freepascal.org/Anchor_Sides#Example_4).
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 13, 2019, 07:26:57 pm
Try attached demo to see that it is working.

You, sir, have the luck of the Devil :-)

Change your code to look like

Code: [Select]
procedure TForm1.ReadFromIni;
...
    PairSplitter1.Position := ini.ReadInteger('PairSplitter', 'Position', PairSplitter1.Width div 2);

and it won't work any more.

What appears to be happening is that manually dragging the PairSplitter doesn't update its Position property until the UpdatePosition method is called. In addition, the first thing that happens when Position is written is that the existing and new values are compared, and if they match the method exits.

Your ini.ReadInteger() was reading Position to get a default, and this was invisibly calling UpdatePosition to set FPosition to the actual position. If you don't do that then Position still has the value from the last time the property was read, and trying to set it will do nothing. Or words to that effect :-)

MarkMLl

Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 13, 2019, 07:49:41 pm
Change your code to look like

Code: [Select]
procedure TForm1.ReadFromIni;
...
    PairSplitter1.Position := ini.ReadInteger('PairSplitter', 'Position', PairSplitter1.Width div 2);

and it won't work any more.
Well, it is working for me also with your modification...

Since widgetset-dependent code is involved in the TPairSplitter: What is your widgetset? I am on Windows.
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 13, 2019, 08:01:11 pm
Well, it is working for me also with your modification...

Since widgetset-dependent code is involved in the TPairSplitter: What is your widgetset? I am on Windows.

Gtk2. Windows might not have the "optimisation" that rejects apparently-useless updates, or might update the Position property without having to be reminded... either of those would fix it.

MarkMLl
Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 13, 2019, 08:47:38 pm
That makes no sense really, did you call the UPDATE or repaint after setting from INiFile?
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 13, 2019, 09:55:07 pm
That makes no sense really, did you call the UPDATE or repaint after setting from INiFile?

What I said: UpdatePosition. The FPosition field (hence Position property) appears to not track manual changes, at least on gtk2.

MarkMLl
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 13, 2019, 10:30:34 pm
Now I also tried gtk2 and qt: No problems either (it's a bit hard here to hit the splitter, though).
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 14, 2019, 12:51:40 am
Landslyde, this was not my question. It was about getting the same effect using the AnchorEditor (which offers more flexibility than using the Align property).
Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 14, 2019, 01:34:02 am
That makes no sense really, did you call the UPDATE or repaint after setting from INiFile?

What I said: UpdatePosition. The FPosition field (hence Position property) appears to not track manual changes, at least on gtk2.

MarkMLl

 When I said UPDATE or REPAINT, I wasn't talking about the POSTION property, I was referring to the UPDATE/REPAINT of the control
 MySplitter.Update or Repaint;

 This is after you set your new position
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 14, 2019, 09:28:58 pm
Well, all I can say is that it's what fixed it for me but it could be specific to Debian "Buster" (i.e. current stable version).

Are you seeing the Position property track manual movement of the splitter?

MarkMLl

Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 15, 2019, 01:25:11 am
Are you seeing the Position property track manual movement of the splitter?
After calling a "Label1.Caption := IntToStr(PairSplitter1.Position)" in the OnResize event of a TPairSplitterSize I see the label text varying according to the splitter position during dragging. Moreover, setting "PairSplitter1.Position := 100" in the OnClick event of a button is executed immediately, and the new position value is displayed in the label.

So, the answer is: Yes.

This is on Lubuntu 18.10 / gtk2.
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 15, 2019, 09:07:26 am
Are you seeing the Position property track manual movement of the splitter?
After calling a "Label1.Caption := IntToStr(PairSplitter1.Position)" in the OnResize event of a TPairSplitterSize I see the label text varying according to the splitter position during dragging. Moreover, setting "PairSplitter1.Position := 100" in the OnClick event of a button is executed immediately, and the new position value is displayed in the label.

So, the answer is: Yes.

This is on Lubuntu 18.10 / gtk2.

I phrased that badly. If you single-step into this code

Code: [Select]
function TCustomPairSplitter.GetPosition: integer;
begin
  if HandleAllocated and (not (csLoading in ComponentState)) then
    UpdatePosition;
  Result := FPosition;
end;

(which is what you called when you read the position) you might see that FPosition is not updated until after UpdatePosition is called.

If you look at the setter you will see that nothing happens if the parameter is the same as the current value of FPosition:

Code: [Select]
procedure TCustomPairSplitter.SetPosition(const AValue: integer);
begin
  if FPosition = AValue then
    Exit;
...

So a manual adjustment of position followed by a change of position under program control will do noting if the manual change didn't update FPosition- which I think is what caused my original question. The fix is either to call UpdatePosition, or to read the property.

MarkMLl




Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 15, 2019, 10:28:46 am
I probably don't understand the issue since the PairSplitter is working correctly for me...

My demo reads the splitter position from the ini file and sets it accordingly. This happens in the OnCreate event of the form where the splitter does not have a handle yet. Therefore the setter writes the ini value to FPosition only but cannot apply it to the widths of the sides, yet. Later, when handles are created, the splitter's CreateWnd is called which finally sets the widths of the sides.

Instead of using the Position property you can also write directly to the Sides[0].Width property of the TPairSplitterSide. This way the FPosition property is bypassed, and there is a discrepancy between the real width of the left splitter and the FPosition value internally. But this is irrelevant because you can detect this only by reading the Position property which calls UpdatePosition and brings them back into agreement.

Note that setting Sides[1].Width does not call UpdatePosition which might be considered a bug. At least the user should know...

If you look at the setter you will see that nothing happens if the parameter is the same as the current value of FPosition:
Code: [Select]
procedure TCustomPairSplitter.SetPosition(const AValue: integer);
begin
  if FPosition = AValue then
    Exit;
...
So a manual adjustment of position followed by a change of position under program control will do noting if the manual change didn't update FPosition
This is standard behaviour of most property setters: when the new property value already is identical to the old value there's nothing else to be done.
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 15, 2019, 10:36:51 am
Note that setting Sides[1].Width does not call UpdatePosition which might be considered a bug. At least the user should know...

Neither, it appears, does moving the splitter using the mouse. FPosition is not updated until the property is read or UpdatePosition is called, so writing to the property before this is done will not work as expected.
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 15, 2019, 11:06:44 am
So, the problem is that you cannot set the splitter position by writing to Sides[1].Width? I said that this is possibly a bug - write a bug report to get it fixed (or better: fix it yourself and submit a patch). But there are two ways to work around this issue: set Sides[0].Width or Position to the complementary value.
Title: Re: Setting TPairSplitter position at runtime
Post by: jamie on September 15, 2019, 03:30:24 pm
@MarkMLI
 
  Just a side note about updating properties.

 If you use a "WITH" statement on a structural property, like a record or something that contains sub members and you alter the values of these members within the WITH block, the Setter of the property will not get called.

 There are many cases where a property can return a class or record for example of some conditions taking place and using this approach does not work via the "WITH" block, which I love using because it makes code for me compact looking.

 I did suggest a fix for this in another thread but I am sure it will get shot down as it has already received a single negative comment, only one that is. To me I think it's a great expansion for the compiler..
 
 SomePropery := With AProperty do Begin ….. End;

 With that, the finalizing "END" of the block will perform a Setter call to the property using the background, otherwise wasted entity to call the Setter.

 Just bare in mind that using a WITH on a complex property and expecting the change of a member to take place isn't going to happen.
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 15, 2019, 03:59:22 pm
OK, I found a situation now which is similar to what you, MarkMLI, describe - see attached demo.
MarkMLI, is this what you mean?

A possible solution would be to add a Resize method to TPairSplitterSide which calls UpdatePosition:
Code: Pascal  [Select][+][-]
  1. procedure TPairSplitterSide.Resize;
  2. begin
  3.   inherited;
  4.   if Self = Splitter.Sides[0] then Splitter.UpdatePosition;
  5. end;
However, explicitly using Splitter.Sides[0] here makes me doubt that this is correct because the widgetset implementation is very general (see comment in TWSCustomPairSplitter.AddSide (in lcl/widgetset/wspairsplitter.pp).
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 15, 2019, 08:31:56 pm
Yes, except what I was doing was saving (form size and) splitter positions immediately after form creation, and then later on- possibly after the splitter(s) had beed dragged manually- restoring them. I think the way it interacted with the properties was the same as you've got though.
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 15, 2019, 08:43:31 pm
If you use a "WITH" statement on a structural property, like a record or something that contains sub members and you alter the values of these members within the WITH block, the Setter of the property will not get called.

Thanks for that, extremely interesting and yet another reason why  with  should be approached with caution- particularly for code which isn't ones own. However I'm confident that it's not relevant in this case, since the only places I'm referencing the property are in the save and restore code and I've single-stepped in to see what's happening.

I've suggested in the mailing lists in the past that  with  define a temporary alias i.e. something like

with const foo = bar^ do begin
  foo.bletch :=...

but needless to say the idea didn't get anywhere: I think the whole idea of in-block declarations is too much like C (and ALGOL-60) for people to stomach :-)

MarkMLl

Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 15, 2019, 08:53:26 pm
Since still your observations are different from mine (reading from ini file): Could you please check these changes whether they fix the issue, too? I think this version is more conformal to the widgetset implementation used:

- in Unit wspairsplitter.pp (in lcl/widgetset) add the method GetPosition to the interface declaration of TWSCustomPairSplitter:
Code: Pascal  [Select][+][-]
  1. type
  2.   TWSCustomPairSplitter = class(TWSWinControl)
  3.   published
  4.     class function AddSide(ASplitter: TCustomPairSplitter; ASide: TPairSplitterSide; Side: integer): Boolean; virtual;
  5.     class function RemoveSide(ASplitter: TCustomPairSplitter; ASide: TPairSplitterSide; Side: integer): Boolean; virtual;
  6.     class function GetPosition(ASplitter: TCustomPairSplitter): Integer; virtual;    // <--------------- ADDED
  7.     class function SetPosition(ASplitter: TCustomPairSplitter; var NewPosition: integer): Boolean; virtual;
  8. ...

- Somewhere in the implementation part of the unit add this code:
Code: Pascal  [Select][+][-]
  1. class function TWSCustomPairSplitter.GetPosition(ASplitter: TCustomPairSplitter): Integer;
  2. begin
  3.   if WSCheckHandleAllocated(ASplitter, 'GetPosition') then
  4.   begin
  5.     if ASplitter.SplitterType = pstHorizontal then
  6.       Result := ASplitter.Sides[0].Width
  7.     else
  8.       Result := ASplitter.Sides[0].Height;
  9.   end else
  10.     Result := ASplitter.Position;
  11. end;

- in unit pairsplitter.pas (in lcl) replace TCustomPairSplitter.SetPosition by this code:
Code: Pascal  [Select][+][-]
  1. procedure TCustomPairSplitter.SetPosition(const AValue: integer);
  2. begin
  3.   if (FPosition = AValue) and
  4.     (TWSCustomPairSplitterClass(WidgetSetClass).GetPosition(Self) = FPosition)   // <---- ADDED
  5.   then
  6.     Exit;
  7.  
  8.   FPosition := AValue;
  9.   if FPosition < 0 then
  10.     FPosition := 0;
  11.   if HandleAllocated and (not (csLoading in ComponentState)) then
  12.     TWSCustomPairSplitterClass(WidgetSetClass).SetPosition(Self, FPosition);
  13. end;

If the issue is fixed I can add the modification to trunk.
   
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 21, 2019, 09:48:29 pm
Sorry about the delay. Yes, I think that fixes it.

MarkMLl
Title: Re: Setting TPairSplitter position at runtime
Post by: wp on September 21, 2019, 10:51:19 pm
Comitted to trunk (r61909) and requested back-porting to fixes.
Title: Re: Setting TPairSplitter position at runtime
Post by: MarkMLl on September 22, 2019, 10:32:20 am
Comitted to trunk (r61909) and requested back-porting to fixes.

Thanks for that. Revision number noted next to my initial fix in my own program :-)

MarkMLl
TinyPortal © 2005-2018