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

心不动则不痛 提交于 2019-12-18 12:27:07

问题


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:

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:

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.


回答1:


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




回答2:


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.




回答3:


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);
}


来源:https://stackoverflow.com/questions/5781709/how-to-stop-a-usercontrol-nee-scrollablecontrol-from-calling-scrollwindow

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!