.NET UserControl(从ScrollableControl衍生而来)必须能够显示水平和垂直滚动条。

调用者可以设置这些水平和垂直滚动条的可见性和范围:

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



注意:UserControl(即ScrollableControl)使用Windows标准机制指定WS_HSCROLLWS_VSCROLL窗口样式以使滚动条出现。也就是说:它们不会创建单独的Windows或.NET滚动控件,而是将其放置在窗口的右侧/底部。 Windows具有显示一个或两个滚动条的标准机制。


如果用户滚动控件,则会向UserControl发送WM_HSCROLLWM_VSCROLL消息。作为对这些消息的响应,我希望ScrollableControl使工作区无效,这将在本机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;
 }


我需要整个客户区都无效。问题是UserControl(即ScrollableControlcalls ScrollWindow API函数:

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


ScrollableControl不会在整个客户矩形上触发InvalidateRect,而是尝试“抢救”客户区域中的现有内容。例如,用户向上滚动,当前客户端内容被ScrollWindowEx按下,然后只有新发现的区域无效,从而触发WM_PAINT



在上图中,棋盘区域是无效的内容,必须在下一个WM_PAINT期间进行绘制。

就我而言,这不好。我控件的顶部包含一个“标题”(例如listview列标题)。向下滚动此内容是不正确的:



并导致视觉损坏。

我希望ScrollableControl不使用ScrollWindowEx,而只是使整个工作区无效。

我尝试覆盖OnScroll受保护的方法:

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

   this.Invalidate();
}


但这会导致两次抽奖。


注意:我可以使用双缓冲来掩盖问题,但这不是真正的解决方案


远程桌面/终端会话下不应使用双缓冲
浪费CPU资源
这不是我要问的问题



我考虑使用Control而不是UserControl(即在继承链中ScrollableControl之前)并手动添加HScroll或VScroll .NET控件-但这也不可取:


Windows已经为滚动条的位置提供了标准外观(复制并不容易)
当我只希望它为InvalidateRect而不是ScrollWindowEx时,必须从头开始复制很多功能


因为我可以看到并发布ScrollableControl内部的代码,所以我知道没有属性可以禁止使用ScrollWindow,但是有没有属性可以禁止使用ScrollWindow



更新:

我尝试覆盖有问题的方法,并使用反射器窃取所有代码:

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


问题是SetDisplayRectLocation读取和写入私有成员变量(displayRect)。除非Microsoft更改C#以允许后代访问私有成员:否则我将无法做到。



更新二

我意识到复制粘贴ScrollableControl的实现,解决了一个问题,这意味着我还必须将整个继承链复制n粘贴到UserControl

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


我真的更喜欢使用面向对象的设计,而不是反对它。

最佳答案

我也遇到了同样的问题,谢谢发布。我可能已经找到解决您问题的方法。我的解决方案是重载WndProc以处理滚动消息,在调用基类处理程序时关闭重绘,然后在处理完消息后强制重绘整个窗口。此解决方案似乎可以正常工作:

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


我之所以考虑尝试此操作,是因为建议重载WndProc,并结合您无法重载SetDisplayRectLocation的观察。我认为在UserControl处理滚动事件期间禁用WM_PAINT可能有效。

希望这可以帮助。

汤姆

08-26 09:58