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 specifyingWS_HSCROLL
andWS_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(i开发者_运维技巧nt 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.
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
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.
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);
}
精彩评论