Jump to content
David Schwartz

best way to display a list of panels?

Recommended Posts

I need something like a TListView but each Item is a fixed-height panel with other components on it. Each one is the same structurally, but with different values. (The same way that list items are all strings, but with different values.) It's one column wide and can have zero to a dozen or two "rows". I need it to behave like a typical List View would.

 

I have a list of panels, and I added a helper function to the base panel to let me add the list to it, and it works fine (more or less).

 

But I've got a splitter and the base panel sits on the top; when it resizes (to make the base panel bigger), items that are initially created "below the horizon" get reordered when the panel exposes them and I don't know why. I need the resizing to not reorder things! Something else does reordering.

 

(FWIW, the splitter and panels are both Raize components: TRzSplitter and TRzPanel.)

 

I guess I have two options: one is when the splitter stops (or the panel stops getting resized), clear the base panel and re-display the list of subpanels on the it. If I could freeze the base panel so nothing updates until you stop adjusting the splitter, that would be really nice, as it causes a lot of screen flickering. Is there a good way to do that?

 

Or maybe just add subpanels incrementally, so you see a gap expand until it's large enough for another subpanel which is then displayed. Or when shrinking the panel, then subpanels disappear when there's not enough height for a full one.

 

The other option would be to find a better control to use. There are several kinds of new panels that do things along this line, but it's unclear which might be best. Suggestions are welcome.  (TFlowPanel, TGridPanel, TRelativePanel, TStackPanel, TCardPanel. And let's not forget TScrollBox!)

 

Jedi has a TJvItemsPanel and TJvComponentPanel. 

 

Also, having a scrollbar would be handy if there are just too many to display no matter what.

 

What's the best way to solve this?

 

 

Edited by David Schwartz

Share this post


Link to post

My solution to something like this was to create a frame which contains only one row. Add basic functionality, like a grip and the resize code, initialization method, validation method, etc. Create as many variants as descendants of said frame as needed to handle different input / visual needs.

Then, create one form with an alClient scrollbox and an alBottom panel with an OK and a cancel button. Give an array of data as an input to this form so it knows how many and what kind of frames it has to create in the ScrollBox.

 

When the user clicks OK, you can see if all data is valid (because the validator is in the parent frame) so you can deny exiting.

 

It ended up like this:

 

image.thumb.png.fd10a6a4b786d54eaf17b1d145cf2a64.png

 

I still need the resizing logic, though, it was a recent request 🙂

  • Like 2
  • Thanks 1

Share this post


Link to post

The question I have is, how can I get something like a TListBox or TListView where the Items is a list of TPanel rather than strings or TListItems, with analogous behavior? I don't need what a TFrame offers.

 

I'd imagine you're going to have the same problem adding either panels or frames to a larger container object if you want them stacked nicely in a list.

Share this post


Link to post

Well, I mean, this is an option. If you don't want to hassle with "problems", look into DevExpress. I'm sure one of their components can do what you want.

  • Thanks 1

Share this post


Link to post
17 minutes ago, David Schwartz said:

The question I have is, how can I get something like a TListBox or TListView where the Items is a list of TPanel rather than strings or TListItems, with analogous behavior? I don't need what a TFrame offers.

 

I'd imagine you're going to have the same problem adding either panels or frames to a larger container object if you want them stacked nicely in a list.

I was in the same shoe with yours and I ended up using a similar approach like what @aehimself described, and actually his approach solves the issue you described. I don't see any other approach better than this in terms of flexibility with the current implementation of VCL.

 

I think the keys are:

  • use a client-aligned TScrollbox to get unlimited amount of rows.
  • Add the frames one after one.
  • To solve the splitter mis-position issue, use the Tag property of the frames to map a pair of frame and splitter, and correct the position error by using something like `TSplitter(frames.Tag).Top = frames.Top + frame.Height`. Note, the standard TSplitter has the same issue you described. Not sure if there is another way to solve it.

 

  • Thanks 1

Share this post


Link to post
On 8/30/2020 at 3:00 AM, aehimself said:

Well, I mean, this is an option. If you don't want to hassle with "problems", look into DevExpress. I'm sure one of their components can do what you want.

I think I forgot to save a reply I made to this ... unfortunately, we don't have either DevExpress or TMS here, and I know they've both got components that would be handy.

 

On 8/30/2020 at 3:06 AM, Edwin Yip said:

 

I think the keys are:

  • use a client-aligned TScrollbox to get unlimited amount of rows.
  • Add the frames one after one.
  • To solve the splitter mis-position issue, use the Tag property of the frames to map a pair of frame and splitter, and correct the position error by using something like `TSplitter(frames.Tag).Top = frames.Top + frame.Height`. Note, the standard TSplitter has the same issue you described. Not sure if there is another way to solve it.

I got something working based on a TScrollBox, which is surprising to me because I think it's the first time I've ever gotten one to work right! 

 

The 3rd item reflects a problem I had that is probably due to rendering the subpanels directly on the panel controlled by the splitter. 

 

I changed the implementation so I have a TObjectList<TPanel> to hold the subpanels. Actually, these are not TPanels but a helper class for them to provide access to the objects added to the panels. Again, I find this a much simpler approach than using TFrames.

 

I also added a helper class to the scrollbox that adds a method to display the list in the scrollbox' display area. I realized it's probably easier to add a way to move items up and down in the display if they're in a list and I can move them relative to each other in the list versus on a panel or something, where they can get rearranged due to logic of the thing they're sitting on. With the list, I can rearrange elements, then just tell the scrollbox to refresh itself from the list. It works great.

 

I implemented the scroll wheel with an increment = the height of each subpanel (they're all the same). You can adjust the splitter up and down, and it smoothly shows more or less of each row with no flicker at all.

 

One little trick I did was put the mouseEnter/Exit and double-click event hooks in the scrollbox even though it doesn't use or need them. But when I tell the scrollbox method to set or refresh the list, it goes through each item and copies these hooks from itself to each of the panels, so I don't have to tell the list or the panels this information. They're informed by their host. (The panels have the same hooks as the host, so it's a simple matter for the host to copy them when it attaches them to its back, so to speak.)

 

The only quirk in this whole approach was that because I used some helper classes, I couldn't add any data members anywhere. So there's no way to inject the list itself anywhere. (I'd prefer to add it to the scrollbox helper, actually, but I can't.) Because the scrollbox is a visual control on the form, creating a child class from it can pose challenges with the form (in my experience). So I resorted to setting the list var up as a global var in the implementation section. This just seems so much simpler than other approaches I could think of at the time. Luckily, I've only got one, and its lifetime is equal to the life of the form. So ... meh. It works. 🙂 

 

Edited by David Schwartz

Share this post


Link to post

Just a small update, I managed to implement the resizing logic and it was really, really easy. Most of my time went away by drawing the transparent, themed, system default resize gripper...

...which almost can not be seen on dark styles, so totally with it! 👍

 

image.thumb.png.e1cf11f12c0ef3bd1afbd86f396eddc1.png

 

image.thumb.png.1b948d5a569678917bf3e914e7e6721f.png

 

Frames above and below are adjusting properly, not changing places or jumping around. Overflow is handled correctly by the alClient scrollbox, if the contents grow too large for the window.

The only thing is that I did not use splitters, I wrote the resizing logic myself (which is awfully long, like 10 lines of code?)

 

Procedure TMultiLineParamFrame.ResizeHandleImageMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
Begin
 _resizing := True;
 SetCapture(Self.Handle);
End;

Procedure TMultiLineParamFrame.ResizeHandleImageMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
 relative: TPoint;
begin
 If Not _resizing Then Exit;

 relative := ScreenToClient(Mouse.CursorPos);
 If relative.Y > 47 Then Height := relative.Y; // Burned in magic number, because we all love them!
End;

Procedure TMultiLineParamFrame.ResizeHandleImageMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
Begin
 ReleaseCapture;
 _resizing := False;
End;

 

Drawing the gripping handle:

Constructor TMultiLineParamFrame.Create(AOwner: TComponent);
Var
 bmp: TBitmap;
Begin
 inherited;
 _resizing := False;
 bmp := TBitmap.Create;
 Try
  bmp.Height := 16;
  bmp.Width := 16;
  bmp.TransparentColor := clYellow;
  bmp.Canvas.Brush.Color := bmp.TransparentColor;
  bmp.Canvas.FillRect(Rect(0, 0, bmp.Width, bmp.Height));

  StyleServices.DrawElement(bmp.Canvas.Handle, StyleServices.GetElementDetails(tsGripper), Rect(0, 0, bmp.Width, bmp.Height));
  ResizeHandleImage.Picture.Assign(bmp);
 Finally
  FreeAndNil(bmp);
 End;
End;

 

  • Like 2

Share this post


Link to post
3 hours ago, Edwin Yip said:

I think you can also implement the 're-order handles' like that~

Possible, however there are no requests like that for the time being.

 

And this is why I hate to implement something new. The next hour when I published the resizable multi-line editor an other request came in, that "it would be nice" if the editor would resize with the form itself, instead of showing a scrollbar 😄

Share this post


Link to post
2 minutes ago, aehimself said:

Possible, however there are no requests like that for the time being.

 

And this is why I hate to implement something new. The next hour when I published the resizable multi-line editor an other request came in, that "it would be nice" if the editor would resize with the form itself, instead of showing a scrollbar 😄

Well, that's how a software is getting better and better :D

  • Like 1

Share this post


Link to post
On 9/11/2020 at 9:01 PM, Edwin Yip said:

@aehimself, Great!

I think you can also implement the 're-order handles' like that~

That is something I want to do. In fact, I added right-click menu options for "Move up ^^^" and "Move down vvv". It's simple to move them on the list and then refresh the display. A drag-n-drop approach would be nice, but it's not worth the trouble for what I need.

Share this post


Link to post

@David Schwartz

 

would be some like this:

of couse, you will needs create your "class" to better usage this. Look that it's not necessary hack any class. For FMX you can do it with same idea ( I think that is more easy than VCL)

 

NOTE: all panels is aligned on "TOP"

 

{$R *.dfm}

var
  LFormHeight: integer = 0;

function MyGetAllChildsHeight(const AControl: TWinControl): integer;
begin
  result := 0;
  //
  for var i: integer := 0 to (AControl.ControlCount - 1) do
    if (AControl.Controls is TPanel) then
      result := result + TPanel(AControl.Controls).Height;
end;

procedure MyReSizeAllChilds(const AControl: TWinControl; const APerc: integer);
begin
  for var i: integer := 0 to (AControl.ControlCount - 1) do
    if (AControl.Controls is TPanel) and (TPanel(AControl.Controls).ShowCaption) then
      begin
        AControl.Controls.Height := Trunc(AControl.Controls.Tag * (APerc / 100));  // <----
        // for test...
        TPanel(AControl.Controls).Caption := AControl.Controls.Height.ToString;
      end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  // I'm using "Panel2.ShowCaption" just for test [resize it or not in "MyReSizeAllChilds()"]
  // you'll should have a property for this usage in special!!!
  //
  LFormHeight := Self.Height;
  // ******************************************* preparing childs setup:
  // *********** panel 1
  Panel1.Tag         := Panel1.Height;   // <---
  Panel1.ShowCaption := false;
  //
  Edit1.Anchors               := [akLeft, akTop, akBottom];
  Edit1.Constraints.MinHeight := Edit1.Height;
  Edit1.Constraints.MaxHeight := Edit1.Height;
  //
  ComboBox1.Anchors               := [akLeft, akTop, akBottom];
  ComboBox1.Constraints.MinHeight := ComboBox1.Height;
  ComboBox1.Constraints.MaxHeight := ComboBox1.Height;
  //
  ListBox1.Anchors               := [akLeft, akTop, akBottom];
  ListBox1.Constraints.MinHeight := ListBox1.Height;
  ListBox1.Constraints.MaxHeight := ListBox1.Height;
  //
  Memo1.Align := alRight;
  //
  // *********** panel 2
  Panel2.Tag                   := Panel2.Height; // <---
  Panel2.ShowCaption           := true;
  Panel2.Constraints.MinHeight := 150;
  //
  Edit2.Anchors               := [akLeft, akTop, akBottom];
  Edit2.Constraints.MinHeight := Edit2.Height;
  Edit2.Constraints.MaxHeight := Edit2.Height;
  //
  ComboBox2.Anchors               := [akLeft, akTop, akBottom];
  ComboBox2.Constraints.MinHeight := ComboBox2.Height;
  ComboBox2.Constraints.MaxHeight := ComboBox2.Height;
  //
  ListBox2.Anchors               := [akLeft, akTop, akBottom];
  ListBox2.Constraints.MinHeight := ListBox2.Height;
  //
  Memo2.Anchors               := [akLeft, akTop, akBottom];
  Memo2.Constraints.MinHeight := 160;
  //
  Self.Constraints.MinHeight := 450; // MyGetAllChildsHeight(Self);
end;

procedure TForm1.FormResize(Sender: TObject);
var
  LPerc           : integer;
begin

  // ******************************************* all logic is here! no needs hack any class
  // Needs verify:
  // (n div 0 ) = exception!!!
  // If "LPerc value it's ok for usage on Height's"
  //
  LPerc := Trunc((LFormHeight / Self.Height) * 100);
  //
  if (LPerc > 100) then { form is shrinking in size... }
    LPerc := (100 - (LPerc mod 100))
  else
    if (LPerc < 100) then { ...now it's growing }
      LPerc := (100 + (100 - LPerc))
    else
      LPerc := 100;
  //
  MyReSizeAllChilds(Self, LPerc);
end;
 

 

Sem título.png

Edited by programmerdelphi2k
  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×