Friday, October 08, 2010

Malfunctioning scrollbars in Panel/ScrollableControl with AutoScroll turned on

I have recently been working on a WinForms app that involved a Panel control with dynamically-added child controls.  AutoScroll was turned on.  When you scrolled vertically, the “thumb” changed size and position seemingly at random:  large amounts of white space were added as you scrolled to the bottom, which would then disappear as you scrolled in the other direction.   Sometimes the scrollbar would disappear entirely.  The “thumb” seemed to jump around, as if the panel were somehow changing size every time we scrolled.

Googling turned up a lot of complaints and a few workarounds, but none of them worked for us.  (Our dynamic controls were capable of changing size, and most of the workarounds didn’t work with that.  )

Well, after some investigation with Reflector, I think I’ve figured it out.  There appears to be a bug with ScrollableControl, the base of the Panel control.   Scrolling vertically causes the DisplayRectangle property’s Height to increase (incorrectly), which in turn causes the Maximum property of the VerticalScroll object to increase.

What can you do about it?  Well, you can’t change DisplayRectangle, which is read-only, and if you have AutoScroll turned on, attempts to set the VerticalScroll.Maximum property are ignored.  But you can do the following:

If you’re hosting a Panel in something else, put the following code in your Scroll event handler.  If you’re deriving a control from Panel, you’ll have to adapt it slightly for the OnScroll method  (probably by replacing references to “pnl” with “this”):

System.Windows.Forms.Panel pnl = sender as System.Windows.Forms.Panel;
int maxHeight = pnl.Controls.Cast<Control>().Sum(c => c.Height);
if (pnl.VerticalScroll.Maximum > maxHeight)
    MethodInfo mi = typeof(ScrollableControl).GetMethod("SetDisplayRectangleSize", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Int32), typeof(Int32) }, null);   
    if (mi != null)
        mi.Invoke(pnl, new object[] { pnl.DisplayRectangle.Width, maxHeight });
    mi = typeof(ScrollableControl).GetMethod("SyncScrollbars", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(bool) }, null);
    if (mi != null)
        mi.Invoke(pnl, new object[] { pnl.AutoScroll });

ETA:  Actually, depending on how your child controls are arranged, you might want to calculate maxHeight as Max(c => c.Bottom) rather than Sum(c => c.Height).  Ours were arranged in a single column with no intervening space between them, but your situation might be different. 


Post a Comment

<< Home