views:

513

answers:

2

I have a form where controls are dynamically added to a Panel. However, when they do so, they are many times added below the fold (bottom of the container). It's nice that the .NET Framework provides this ScrollControlIntoView method, however, for added usability, it would also be nice if there was an easy way to animate so that it is easy for the user to understand that the Panel was automatically scrolled.

Has anyone ever encountered this or have any ideas as to how to tackle it?

+1  A: 

You could subclass Panel, add a Timer, and override ScrollIntoView() in order to do this all within your new AnimatedScrollPanel. I'm pretty sure you'll have to do it this way in order to use certain protected methods, such as ScrollToControl() which returns a Point (used by ScrollIntoView).

Joel B Fant
A: 

@Joel

Thanks! I've got it, wasn't too difficult to do with a little help from reflector. There might be ways to optimize it, but this is what I've got to start. It just so happens that I needed this functionality in a FlowLayoutPanel, but it would work with anything that inherits from ScrollableControl.

Edit: I changed a few things due to @Joel B Fant pointing out that I was not removing the delegate, keeping the timer object around indefinitely. I believe I've alleviated that concern by assigning the delegate to an EventHandler object so that it can be removed within itself.

using System;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;

public class AnimatedScrollFlowLayoutPanel : FlowLayoutPanel
{
    public new void ScrollControlIntoView(Control activeControl)
    {
        if (((this.IsDescendant(activeControl) && this.AutoScroll) &&
            (this.HScroll || this.VScroll)) && (((activeControl != null) &&
            (ClientRectangle.Width > 0)) && (ClientRectangle.Height > 0)))
        {
            Point point = this.ScrollToControl(activeControl);
            int x = DisplayRectangle.X, y = DisplayRectangle.Y;
            bool scrollUp = x < point.Y;
            bool scrollLeft = y < point.X;

            Timer timer = new Timer();
            EventHandler tickHandler = null;
            tickHandler = delegate {
                int jumpInterval = ClientRectangle.Height / 10;

                if (x != point.X || y != point.Y)
                {
                    y = scrollUp ?
                        Math.Min(point.Y, y + jumpInterval) :
                        Math.Max(point.Y, y - jumpInterval);
                    x = scrollLeft ?
                        Math.Min(point.X, x + jumpInterval) :
                        Math.Max(point.X, x - jumpInterval);

                    this.SetScrollState(8, false);
                    this.SetDisplayRectLocation(x, y);
                    this.SyncScrollbars(true);
                }
                else
                {
                    timer.Stop();
                    timer.Tick -= tickHandler;
                }
            };

            timer.Tick += tickHandler;
            timer.Interval = 5;
            timer.Start();
        }
    }

    internal bool IsDescendant(Control descendant)
    {
        MethodInfo isDescendantMethod = typeof(Control).GetMethod(
            "IsDescendant", BindingFlags.NonPublic | BindingFlags.Instance);
        return (bool)isDescendantMethod.Invoke(this, new object[] { descendant });
    }

    private void SyncScrollbars(bool autoScroll)
    {
        MethodInfo syncScrollbarsMethod = typeof(ScrollableControl).GetMethod(
            "SyncScrollbars", BindingFlags.NonPublic | BindingFlags.Instance);
        syncScrollbarsMethod.Invoke(this, new object[] { autoScroll });
    }
}
Lloyd Cotten
There is one big problem with that. You keep adding the delegate to the Tick event and never remove it when you're done. You need to redesign a bit so that you only have the 1 delegate in the Tick event at a time.
Joel B Fant
Such a redesign will probably involve not using an anonymous delegate. You can't remove an anonymous delegate from the event.
Joel B Fant
Thanks for your comments. I'm not sure I quite understand though: if I were to change to not using an anonymous delegate my code to add the delegate would be something like timer.Tick += new EventHandler(timer_Tick)Where do you suggest I need to remove the delegate from the event?
Lloyd Cotten
I think I would create a nested class that handles the animation. Give it a reference to the panel instance on creation so it can call protected methods. It has the Timer, handler, parameters. IDisposable, with a Finished event to call it. In that and Dispose(), shed all references. Fire and forget.
Joel B Fant