【问题标题】:How to stop a UserControl (nee ScrollableControl) from calling ScrollWindow?如何阻止 UserControl (nee ScrollableControl) 调用 ScrollWindow?
【发布时间】:2011-04-25 18:31:10
【问题描述】:

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

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

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

注意:UserControl(即ScrollableControl)使用指定WS_HSCROLLWS_VSCROLL 窗口样式的Windows 标准机制来显示滚动条。也就是说:它们不会创建单独的 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(即ScrollableControlcallsScrollWindow 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 的实现,修复一个问题意味着我还必须复制粘贴整个继承链到 UserControl

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

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

【问题讨论】:

  • 能否提供不带脏话的截图?
  • 你去吧;处女的眼睛得救了。
  • 好问题。你可能已经尝试了很多我会做的事情。我假设您已尝试覆盖 WndPrc 并拦截滚动消息和绘制消息?
  • @Pedery 我想过这个问题,但是我仍然需要重新实现很多处理。我最好的选择是简单地覆盖有问题的受保护的SetDisplayLocation 方法,而不是调用base,并复制粘贴除一行之外的整个内容。该解决方案的缺点是 a) 我更愿意使用 .NET UserControl(如果可能的话),而不是反对它,b) 复制当前的实现我将失去任何未来的 .NET 增强功能到ScrollableControl 和 c) 虽然我不在乎:这是不合法的
  • 是的,我明白了。我必须自己解决这个问题才能找到解决方案。您似乎与 .Net 的可能性和局限性非常一致,所以我认为您的尝试和我的一样好。祝你好运!

标签: .net scroll scrollable


【解决方案1】:

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

希望这会有所帮助。

汤姆

【讨论】:

  • 谢谢,这完美!您可能还想以相同的方式处理 WM_MOUSEWHEEL;只需将“case 522: // WM_MOUSEWHEEL”添加到 WndProc() 中的开关
  • @Ricardo。好的。我正在使用鼠标滚轮来控制图像缩放,而这正是我需要避免使用 ScrollWindowEx 的场景。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 我认为,如果您进行优化,则在使用滚动条末端的按钮后,窗口不会正确重绘。至少这似乎是我的经验。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-09
    • 2011-08-18
    • 1970-01-01
    • 2012-01-13
    • 2011-10-13
    • 1970-01-01
    相关资源
    最近更新 更多