C# – Handle scrolling of a WinForms control manually

.netc++gdi+winforms

I have a control (System.Windows.Forms.ScrollableControl) which can potentially be very large. It has custom OnPaint logic. For that reason, I am using the workaround described here.

public class CustomControl : ScrollableControl
{
public CustomControl()
{
    this.AutoScrollMinSize = new Size(100000, 500);
    this.DoubleBuffered = true;
}

protected override void OnScroll(ScrollEventArgs se)
{
    base.OnScroll(se);
    this.Invalidate();
}

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    var graphics = e.Graphics;
    graphics.Clear(this.BackColor);
    ...
}
}

The painting code mainly draws "normal" things that move when you scroll. The origin of each shape that is drawn is offsetted by this.AutoScrollPosition.

graphics.DrawRectangle(pen, 100 + this.AutoScrollPosition.X, ...);

However, the control also contains "static" elements, which are always drawn at the same position relative to the parent control. For that, I just don't use AutoScrollPosition and draw the shapes directly:

graphics.DrawRectangle(pen, 100, ...);

When the user scrolls, Windows translates the entire visible area in the direction opposite to the scrolling. Usually this makes sense, because then the scrolling seems smooth and responsive (and only the new part has to be redrawn), however the static parts are also affected by this translation (hence the this.Invalidate() in OnScroll). Until the next OnPaint call has successfully redrawn the surface, the static parts are slightly off. This causes a very noticable "shaking" effect when scrolling.

Is there a way I can create a scrollable custom control that does not have this problem with static parts?

Best Solution

You could do this by taking full control of scrolling. At the moment, you're just hooking in to the event to do your logic. I've faced issues with scrolling before, and the only way I've ever managed to get everything to work smoothly is by actually handling the Windows messages by overriding WndProc. For instance, I have this code to synchronize scrolling between several ListBoxes:

protected override void WndProc(ref Message m) {
    base.WndProc(ref m);
    // 0x115 and 0x20a both tell the control to scroll. If either one comes 
    // through, you can handle the scrolling before any repaints take place
    if (m.Msg == 0x115 || m.Msg == 0x20a) 
    {
        //Do you scroll processing
    }
}

Using WndProc will get you the scroll messages before anything gets repainted at all, so you can appropriately handle the static objects. I'd use this to suspend scrolling until an OnPaint occurs. It won't look as smooth, but you won't have issues with the static objects moving.