4

I'm trying to optimize populating and scrolling a FlowLayoutPanel, but I've had issues with similar controls before, where if they have too many controls inside, it takes a really long while for the container to populate and get ready for use (and the scroller gets shorter and shorter, you might be familiar with that).

I've read that you can use a pool of just the controls within visible boundaries of the container rectangle and simulate scrolling by repopulating them with corresponding contents, as if they would be without this optimization. So you scroll as usual but the population doesn't take nearly as long. But how do I implement that for a general case?

I'm using custom controls to populate the FlowLayoutPanel container, so I'm looking for a generic enough solution that can be applied to both my control and the standard .Net controls.

user1306322
  • 8,561
  • 18
  • 61
  • 122
  • Put `SuspendLayout()` and `ResumeLayout()` outside the setup loop and see if that helps enough. Also tell us the number of controls you eventually add! As for virtual scrolling you need to be very clear about what ui features you want to work besides paging: scrolling in small steps, drgging the lift quickly over all rows?.. – TaW Oct 01 '16 at 17:03
  • No that doesn't help, and I've know about that trick. It still has to do the layout after resuming. I'm using several hundred controls. What I'm after is the full scrollbar functionality. I've developed a similar solution before for a game once but it didn't use any winforms, and the whole window management logic was custom, so that experience doesn't translate very well, but at least I know it should be possible. – user1306322 Oct 01 '16 at 17:45
  • My sympathies. I've done this in C, C++, and C# (and VB and CLisp) using [*differential execution*](http://stackoverflow.com/a/24875909/23771). – Mike Dunlavey Oct 01 '16 at 22:24

1 Answers1

7

Display and scrolling performance are good reasons to try a virtual paging, although they can be overcome by replacing the Controls.Add with a Controls.AddRange call and a double-buffered container..

..but there is another: Any Winforms control is limited to 32k pixels in its display dimensions. Even if you make it larger nothing will be displayed beyond this limit.

Here is a quick list of things to do, when implementing virtual paging:

  • Use a double-buffered FlowLayoutPanel subclass to simplify the layout and make it flicker-free.
  • Turn off AutoSize and AutoScroll
  • Add a VScrollBar to the right of the FLP and keep its Height the same as the FLP's
  • Calculate the Height (plus Margins) of your UserControl. I assume you add your control wrapped up in a UC, to make things easier.
  • Calculate the paging numbers
  • Create a List<yourUserControlClass> theUCs
  • Now create your UCs but add them only to the list theUCs
  • Code a scrollTo(int ucIndex) function, which clears the FLP's controls and adds the right range from the list.
  • code KeyPreview for the FLP to allow scrolling with the keyboard.

Setting the right values for the VScrollBar's properties, i.e. Minimum, Maximum, Value, SmallChange, LargeChange is a little tricky and setting the page size must be done whenever the FLP is resized or elements are added to or removed from the list.

In my test the setting up and the scrolling results were instantaneous. Only complete UCs are visible from the top, which is fine with me. I have added 1000 UCs with a bitmap in a Panel, a Label and a CheckedListBox.

Here is how I calculate the setting for Maximum:

float pageSize =  flowLayoutPanel2.ClientSize.Height / 
                  (uc1.Height + uc1.Margin.Top + uc1.Margin.Bottom);
vScrollBar1.Maximum = (int)( 1f * theUCs.Count / (pageSize)) + 9;

The extra 9 is a workaround for the quirky offset of a ScrollBar's theoretical and actual Maximum values.

In the ValueChanged event I wrote:

private void vScrollBar1_ValueChanged(object sender, EventArgs e)
{
    int pageSize = flowLayoutPanel1.ClientSize.Height / theUCs.First().Height;
    int v = Math.Min(theUCs.Count, vScrollBar1.Value);

    flowLayoutPanel1.SuspendLayout();
    flowLayoutPanel1.Controls.Clear();
    flowLayoutPanel1.Controls.AddRange(theUCs.Skip( (v- 1) * pageSize)
                                             .Take(pageSize + 1).ToArray());
    flowLayoutPanel1.ResumeLayout();
}

This scrolls to a certain item:

void scrollTo(int item)
{
    int pageSize = flowLayoutPanel1.ClientSize.Height / theUCs.First().Height;
    int p = item / pageSize + 1;
    vScrollBar1.Value = p;
}

For even smoother scrolling use a DoubleBuffered subclass:

class DrawFLP : FlowLayoutPanel
{
    public DrawFLP() { DoubleBuffered = true; }
}

This is probably a bit rough at the edges, but I hope it'll get you on the right track.

enter image description here

TaW
  • 53,122
  • 8
  • 69
  • 111
  • 1
    I've been playing around with SuspendLayout and it doesn't seem to do anything. Not initializing controls, not adding them to the container, but it still takes the same amount of time. Then I started profiling and it turned out it's the debugging build type that's messing up the speed of things. Something to be wary of. – user1306322 Oct 01 '16 at 23:32