.netscrollscrollable

How to stop a UserControl (nee ScrollableControl) from calling ScrollWindow?


The .NET UserControl (which descends from ScrollableControl) has to ability to display horizontal and vertical scrollbars.

The caller can set the visibility, and range, of these horizontal and vertical scrollbars:

UserControl.AutoScroll = true;
UserControl.AutoScrollMinSize = new Size(1000, 4000); //1000x4000 scroll area

Note: The UserControl (i.e. ScrollableControl) uses the Windows standard mechanism of specifying WS_HSCROLL and WS_VSCROLL window styles to make scrollbars appear. That is: they do not create separate Windows or .NET scroll controls, positioning them at the right/bottom of the window. Windows has a standard mechanism for displaying one, or both, scrollbars.

If the user scrolls the control, the UserControl is sent a WM_HSCROLL or WM_VSCROLL message. In response to these messages i want the ScrollableControl to invalidate the client area, which is what would happen in native Win32:

switch (uMsg) 
{ 
   case WM_VSCROLL:
       ...
       GetScrollInfo(...);
       ...
       SetScrollInfo(...);
       ...

       InvalidateRect(g_hWnd, 
              null, //erase entire client area
              true, //background needs erasing too (trigger WM_ERASEBKGND));
       break;
 }

i need the entire client area invalidated. The problem is that UserControl (i.e. ScrollableControl) calls the ScrollWindow API function:

protected void SetDisplayRectLocation(int x, int y)
{
    ...
    if ((nXAmount != 0) || ((nYAmount != 0) && base.IsHandleCreated))
    {
        ...
        SafeNativeMethods.ScrollWindowEx(new HandleRef(this, base.Handle), nXAmount, nYAmount, null, ref rectClip, NativeMethods.NullHandleRef, ref prcUpdate, 7);
    }
    ...
}

Rather than triggering an InvalidateRect on the entire client rectangle, ScrollableControl tries to "salvage" the existing content in the client area. For example, the user scrolls up, the current client content is pushed down by ScrollWindowEx, and then only the newly uncovered area is invalidated, triggering a WM_PAINT:

enter image description here

In the above diagram, the checkerboard area is the content that is invalid and will have to be painted during the next WM_PAINT.

In my case this is no good; the top of my control contains a "header" (e.g. listview column headers). Scrolling this content further down is incorrect:

enter image description here

and it causes visual corruption.

i want the ScrollableControl to not use ScrollWindowEx, but instead just invalidate the entire client area.

i tried overriding OnScroll protected method:

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

   this.Invalidate();
}

But it causes an double-draw.

Note: i could use double-buffering to mask the problem, but that's not a real solution

  • double buffering should not be used under remote desktop/terminal session
  • it's wasteful of CPU resources
  • it's not the question i'm asking

i considered using a Control instead of UserControl (i.e. before ScrollableControl in the inheritance chain) and manually add a HScroll or VScroll .NET control - but that's not desirable either:

Since i can see, and posted, the code internal to ScrollableControl i know there is no property to disable use of ScrollWindow, but is there a property to disable the use of ScrollWindow?


Update:

i tried overriding the offending method, and using reflector to steal all the code:

protected override void SetDisplayRectLocation(int x, int y)
{
    ...
    Rectangle displayRect = this.displayRect;
    ...
    this.displayRect.X = x;
    this.displayRect.Y = y;
    if ((nXAmount != 0) || ((nYAmount != 0) && base.IsHandleCreated))
    {
        ...
        SafeNativeMethods.ScrollWindowEx(new HandleRef(this, base.Handle), nXAmount, nYAmount, null, ref rectClip, NativeMethods.NullHandleRef, ref prcUpdate, 7);
    }
    ...
}

The problem is that SetDisplayRectLocation reads and writes to a private member variable (displayRect). Unless Microsoft changes C# to allow descendants access to private members: i cannot do that.


Update Two

i realized that copy-pasting the implementation of ScrollableControl, fixing the one issue means i will also have to copy-n-paste the entire inheritance chain down to UserControl

...
   ScrollableControl2 : Control, IArrangedElement, IComponent, IDisposable
      ContainerControl2 : ScrollableControl2, IContainerControl
         UserControl2 : ContainerControl2

i'd really prefer to work with object-oriented design, rather than against it.


Solution

  • I had the same problem, thanks for posting this. I may have found a solution to your problem. My solution is to overload WndProc in order to handle the scroll messages, turn off redraw while calling the base class handler, then force a redraw of the entire window after the message has been handled. This solution appears to work ok:

        private void sendRedrawMessage( bool redrawFlag )
        {
            const int WM_SETREDRAW = 0x000B;
    
            IntPtr wparam = new IntPtr( redrawFlag ? 1 : 0 );
            Message msg = Message.Create( Handle, WM_SETREDRAW, wparam, IntPtr.Zero );
            NativeWindow.FromHandle( Handle ).DefWndProc( ref msg );
        }
    
        protected override void WndProc( ref Message m )
        {
            switch ( m.Msg )
            {
                case 276: // WM_HSCROLL
                case 277: // WM_VSCROLL
                    sendRedrawMessage( false );
                    base.WndProc( ref m );
                    sendRedrawMessage( true );
                    Refresh(); // Invalidate all
                    return;
            }
    
            base.WndProc( ref m );
        }
    

    I thought of trying this because of the suggestion to overload WndProc combined with your observation that you can't overload SetDisplayRectLocation. I thought that disabling WM_PAINT during the UserControl's handling of the scroll event might work.

    Hope this helps.

    Tom