手动处理WinForms控件的滚动

时间:2013-05-11 20:45:10

标签: c# .net winforms gdi+

我有一个可能非常大的控件(System.Windows.Forms.ScrollableControl)。它具有自定义OnPaint逻辑。出于这个原因,我使用了here所述的解决方法。

public class CustomControl : ScrollableControl
{
public CustomControl()
{
    this.AutoScrollMinSize = new Size(100000, 500);
    this.DoubleBuffered = true;
}

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

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    var graphics = e.Graphics;
    graphics.Clear(this.BackColor);
    ...
}
}

绘画代码主要绘制滚动时移动的“正常”事物。绘制的每个形状的原点都被this.AutoScrollPosition抵消。

graphics.DrawRectangle(pen, 100 + this.AutoScrollPosition.X, ...);

但是,该控件还包含“静态”元素,这些元素始终绘制在相对于父控件的相同位置。为此,我只是不使用AutoScrollPosition并直接绘制形状:

graphics.DrawRectangle(pen, 100, ...);

当用户滚动时,Windows会以与滚动相反的方向翻译整个可见区域。通常这是有道理的,因为滚动似乎是平滑和响应的(并且只有新部分必须重新绘制),但静态部分也会受到此转换的影响(因此this.Invalidate()中的OnScroll) 。在下一个OnPaint调用成功重绘表面之前,静态部分稍微偏离。滚动时,这会产生非常明显的“抖动”效果。

有没有办法可以创建一个可滚动的自定义控件,静态部件没有这个问题?

2 个答案:

答案 0 :(得分:3)

您可以通过完全控制滚动来完成此操作。目前,你只是想参加活动来做你的逻辑。我之前遇到过滚动问题,而且我设法让一切顺利运行的唯一方法是通过覆盖WndProc来实际处理Windows消息。例如,我有这个代码来同步几个ListBox之间的滚动:

protected override void WndProc(ref Message m) {
    base.WndProc(ref m);
    // 0x115 and 0x20a both tell the control to scroll. If either one comes 
    // through, you can handle the scrolling before any repaints take place
    if (m.Msg == 0x115 || m.Msg == 0x20a) 
    {
        //Do you scroll processing
    }
}

使用WndProc会在重新绘制任何内容之前获取滚动消息,因此您可以正确处理静态对象。我会用它来暂停滚动直到出现OnPaint。它看起来不会很平滑,但是静态对象移动时不会出现问题。

答案 1 :(得分:2)

因为我真的需要这个,所以当你在可滚动的表面上有静态图形(其大小可以大于65535)时,我最终专门为这种情况编写了一个控件。

这是一个常规Control,其上有两个ScrollBar控件,用户可分配的ControlContent。当用户滚动时,容器会相应地设置其Content的{​​{1}}。因此,可以使用使用AutoScrollOffset方法进行绘制的控件而不进行任何更改。 AutoScrollOffset的实际大小始终是其可见部分。它可以通过按住shift键来进行水平滚动。

用法:

Content

代码:

它变得有点冗长,但我可以避免丑陋的黑客攻击。应该使用单声道。我认为结果非常合理。

var container = new ManuallyScrollableContainer();
var content = new ExampleContent();
container.Content = content;
container.TotalContentWidth = 150000;
container.TotalContentHeight = 5000;
container.Dock = DockStyle.Fill;
this.Controls.Add(container); // e.g. add to Form

示例内容:

public class ManuallyScrollableContainer : Control
{
    public ManuallyScrollableContainer()
    {
        InitializeControls();
    }

    private class UpdatingHScrollBar : HScrollBar
    {
        protected override void OnValueChanged(EventArgs e)
        {
            base.OnValueChanged(e);
            // setting the scroll position programmatically shall raise Scroll
            this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
        }
    }

    private class UpdatingVScrollBar : VScrollBar
    {
        protected override void OnValueChanged(EventArgs e)
        {
            base.OnValueChanged(e);
            // setting the scroll position programmatically shall raise Scroll
            this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
        }
    }

    private ScrollBar shScrollBar;
    private ScrollBar svScrollBar;

    public ScrollBar HScrollBar
    {
        get { return this.shScrollBar; }
    }

    public ScrollBar VScrollBar
    {
        get { return this.svScrollBar; }
    }

    private void InitializeControls()
    {
        this.Width = 300;
        this.Height = 300;

        this.shScrollBar = new UpdatingHScrollBar();
        this.shScrollBar.Top = this.Height - this.shScrollBar.Height;
        this.shScrollBar.Left = 0;
        this.shScrollBar.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;

        this.svScrollBar = new UpdatingVScrollBar();
        this.svScrollBar.Top = 0;
        this.svScrollBar.Left = this.Width - this.svScrollBar.Width;
        this.svScrollBar.Anchor = AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;

        this.shScrollBar.Width = this.Width - this.svScrollBar.Width;
        this.svScrollBar.Height = this.Height - this.shScrollBar.Height;

        this.Controls.Add(this.shScrollBar);
        this.Controls.Add(this.svScrollBar);

        this.shScrollBar.Scroll += this.HandleScrollBarScroll;
        this.svScrollBar.Scroll += this.HandleScrollBarScroll;
    }

    private Control _content;
    /// <summary>
    /// Specifies the control that should be displayed in this container.
    /// </summary>
    public Control Content
    {
        get { return this._content; }
        set
        {
            if (_content != value)
            {
                RemoveContent();
                this._content = value;
                AddContent();
            }
        }
    }

    private void AddContent()
    {
        if (this.Content != null)
        {
            this.Content.Left = 0;
            this.Content.Top = 0;
            this.Content.Width = this.Width - this.svScrollBar.Width;
            this.Content.Height = this.Height - this.shScrollBar.Height;
            this.Content.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
            this.Controls.Add(this.Content);
            CalculateMinMax();
        }
    }

    private void RemoveContent()
    {
        if (this.Content != null)
        {
            this.Controls.Remove(this.Content);
        }
    }

    protected override void OnParentChanged(EventArgs e)
    {
        // mouse wheel events only arrive at the parent control
        if (this.Parent != null)
        {
            this.Parent.MouseWheel -= this.HandleMouseWheel;
        }
        base.OnParentChanged(e);
        if (this.Parent != null)
        {
            this.Parent.MouseWheel += this.HandleMouseWheel;
        }
    }

    private void HandleMouseWheel(object sender, MouseEventArgs e)
    {
        this.HandleMouseWheel(e);
    }

    /// <summary>
    /// Specifies how the control reacts to mouse wheel events.
    /// Can be overridden to adjust the scroll speed with the mouse wheel.
    /// </summary>
    protected virtual void HandleMouseWheel(MouseEventArgs e)
    {
        // The scroll difference is calculated so that with the default system setting
        // of 3 lines per scroll incremenet,
        // one scroll will offset the scroll bar value by LargeChange / 4
        // i.e. a quarter of the thumb size
        ScrollBar scrollBar;
        if ((Control.ModifierKeys & Keys.Shift) != 0)
        {
            scrollBar = this.HScrollBar;
        }
        else
        {
            scrollBar = this.VScrollBar;
        }
        var minimum = 0;
        var maximum = scrollBar.Maximum - scrollBar.LargeChange;
        if (maximum <= 0)
        {
            // happens when the entire area is visible
            return;
        }
        var value = scrollBar.Value - (int)(e.Delta * scrollBar.LargeChange / (120.0 * 12.0 / SystemInformation.MouseWheelScrollLines));
        scrollBar.Value = Math.Min(Math.Max(value, minimum), maximum);
    }

    public event ScrollEventHandler Scroll;
    protected virtual void OnScroll(ScrollEventArgs e)
    {
        var handler = this.Scroll;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    /// <summary>
    /// Event handler for the Scroll event of either scroll bar.
    /// </summary>
    private void HandleScrollBarScroll(object sender, ScrollEventArgs e)
    {
        OnScroll(e);
        if (this.Content != null)
        {
            this.Content.AutoScrollOffset = new System.Drawing.Point(-this.HScrollBar.Value, -this.VScrollBar.Value);
            this.Content.Invalidate();
        }
    }

    private int _totalContentWidth;
    public int TotalContentWidth
    {
        get { return _totalContentWidth; }
        set
        {
            if (_totalContentWidth != value)
            {
                _totalContentWidth = value;
                CalculateMinMax();
            }
        }
    }

    private int _totalContentHeight;
    public int TotalContentHeight
    {
        get { return _totalContentHeight; }
        set
        {
            if (_totalContentHeight != value)
            {
                _totalContentHeight = value;
                CalculateMinMax();
            }
        }
    }

    protected override void OnResize(EventArgs e)
    {
        base.OnResize(e);
        CalculateMinMax();
    }

    private void CalculateMinMax()
    {
        if (this.Content != null)
        {
            // Reduced formula according to
            // http://msdn.microsoft.com/en-us/library/system.windows.forms.scrollbar.maximum.aspx
            // Note: The original formula is bogus.
            // According to the article, LargeChange has to be known in order to calculate Maximum,
            // however, that is not always possible because LargeChange cannot exceed Maximum.
            // If (LargeChange) == (1 * visible part of control), the formula can be reduced to:

            if (this.TotalContentWidth > this.Content.Width)
            {
                this.shScrollBar.Enabled = true;
                this.shScrollBar.Maximum = this.TotalContentWidth;
            }
            else
            {
                this.shScrollBar.Enabled = false;
            }

            if (this.TotalContentHeight > this.Content.Height)
            {
                this.svScrollBar.Enabled = true;
                this.svScrollBar.Maximum = this.TotalContentHeight;
            }
            else
            {
                this.svScrollBar.Enabled = false;
            }

            // this must be set after the maximum is determined
            this.shScrollBar.LargeChange = this.shScrollBar.Width;
            this.shScrollBar.SmallChange = this.shScrollBar.LargeChange / 10;
            this.svScrollBar.LargeChange = this.svScrollBar.Height;
            this.svScrollBar.SmallChange = this.svScrollBar.LargeChange / 10;
        }
    }
}