13

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:

  • Windows already provides a standard look for the position of scrollbars (it's not trivial to duplicate)
  • that is a lot of functionality to have to reproduce from scratch, when i only want it to InvalidateRect rather than ScrollWindowEx

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.

Ian Boyd
  • 220,884
  • 228
  • 805
  • 1,125
  • 3
    Could you provide screenshots without swear words? – David Heffernan Apr 25 '11 at 18:33
  • 1
    There you go; virgin eyes saved. – Ian Boyd Apr 25 '11 at 18:42
  • 1
    Good question. You've probably tried many of the things I would. I assume you've tried to override WndPrc and intercepting both the scroll message and the paint messages? – Pedery Apr 25 '11 at 18:53
  • 1
    @Pedery i thought about that, but then i would still be faced with having to reimplement a lot of processing. My best bet is to simply override the offending protected `SetDisplayLocation` method, not calling `base`, and copy-pasting the entire guts except for the one line. Downside of that solution is a) i'd prefer to work *with* the .NET `UserControl` (if possible), not against it, b) copying the current implementation i will lose any future .NET enchancements to `ScrollableControl`, and c) although i don't care: it's not legal – Ian Boyd Apr 25 '11 at 19:04
  • Yep, I see. I'd have to play around with this myself to try to come up with a solution. You seem to be very on par with the possibilities and limitations of .Net so I think your attempts are just as good as mine. Good luck! – Pedery Apr 25 '11 at 19:50
  • In typical OO fashion: the protected `SetDisplayLocation` method uses a private `displayRect` member - there's no way to reimplement the method without access to that private member. – Ian Boyd Apr 25 '11 at 20:04
  • @Ian, if I understand correctly, you do the entire inside-painting job yourself (as in you don't place other controls inside the scrollable area), so "reinventing the wheel" here is really not that much to do, and it will give you full control of the areas you want to render. If you do have inner-controls (other than the headers) than it's a bit trickier, but you can only scroll one big container control instead of manipulating the positions of each control. I would choose (and have chosen before...) one of those options. – Itai Bar-Haim Feb 19 '12 at 05:51
  • Very well written, detailed post. I'm having exactly the same problem but I doubt very much if I could have enunciated it as well. It would be nice if Microsoft would provide an option to control this aspect of ScrollableControl. – Jim Balkwill Apr 14 '15 at 14:45

3 Answers3

7

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

Tom
  • 86
  • 1
  • 4
  • Thanks, this works perfectly! You might want to handle WM_MOUSEWHEEL in the same way, also; just add "case 522: // WM_MOUSEWHEEL" to the switch in WndProc() – Ricardo Massaro Nov 21 '14 at 03:45
  • @Ricardo. Nice. I'm using mouse wheel to control image zoom and that's exactly the scenario where I need to avoid ScrollWindowEx. – Jim Balkwill Apr 14 '15 at 18:13
1

Have you tried getting in contact with a programmer from Microsoft? I'm sure if you contact microsoft you could post your question to them, perhaps even get phone support.

Here is a link to support for the .NET framework: click here. It mentions that you can get in contact with .NET support professionals by email, phone, or online.

Veridian
  • 3,278
  • 10
  • 38
  • 73
0

Tom's solution is awesome, but I think there is an opportunity for a small optimization. Without Tom's two methods, when I cause scrolling, for example, by clicking a scroll-bar endpoint, my onPaint sees a single invocation. When I add Tom's two methods, my onPaint starts getting two invocations for the same scroll bar positions. The solution for me seemed to be to ignore the final SB_ENDSCROLL that occurs is the scrolling operations. With this I stopped seeing duplicate paints at the same scroll location.

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
            if ((ushort)m.WParam == 8) // SB_ENDSCROLL ignore scroll bar release
                break;
            sendRedrawMessage(false);
            base.WndProc(ref m);
            sendRedrawMessage(true);
            Refresh(); // Invalidate all
            return;
    }

    base.WndProc(ref m);
}
Cameron
  • 2,324
  • 1
  • 24
  • 26
  • I think if you make that optimization, the the window does not properly repaint after using the buttons on the ends of the scroll bars. At least that seemed to be my experience. – BlueMonkMN Jan 07 '17 at 19:41