13

.NET UserControl(源自ScrollableControl)必须能够显示水平和垂直滚动条。

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

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

注意: ( UserControlie 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(即ScrollableControl调用ScrollWindowAPI 函数:

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资源
  • 这不是我要问的问题

我考虑使用 aControl而不是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,修复一个问题意味着我还必须复制粘贴整个继承链到UserControl

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

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

4

3 回答 3

7

我遇到了同样的问题,谢谢发布这个。我可能已经找到了解决您问题的方法。我的解决方案是重载 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 可能会起作用。

希望这可以帮助。

汤姆

于 2013-07-06T02:56:10.307 回答
1

您是否尝试过与 Microsoft 的程序员取得联系?我敢肯定,如果您联系微软,您可以将您的问题发布给他们,甚至可以获得电话支持。

这是支持 .NET 框架的链接:单击此处。它提到您可以通过电子邮件、电话或在线与 .NET 支持专业人员取得联系。

于 2012-03-03T19:51:23.183 回答
0

Tom 的解决方案很棒,但我认为有机会进行小优化。如果没有 Tom 的两种方法,当我导致滚动时,例如,通过单击滚动条端点,我的 onPaint 会看到一个调用。当我添加 Tom 的两个方法时,我的 onPaint 开始为相同的滚动条位置获得两次调用。对我来说,解决方案似乎是忽略最后发生的 SB_ENDSCROLL 是滚动操作。有了这个,我不再在同一个滚动位置看到重复的油漆。

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);
}
于 2015-08-01T21:28:59.867 回答