Обрабатывать прокрутку элемента управления WinForms вручную


У меня есть контроль (System.Windows.Forms.ScrollableControl), который потенциально может быть очень большим. Он имеет обычную логику OnPaint. По этой причине я использую обходной путь, описанный здесь .

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 3

2 ответа:

Вы можете сделать это, взяв полный контроль над прокруткой. В данный момент Вы просто подключаетесь к событию, чтобы сделать свою логику. Я сталкивался с проблемами прокрутки раньше, и единственный способ, которым мне когда-либо удавалось заставить все работать гладко, - это фактически обрабатывать сообщения Windows, переопределяя WndProc. Например, у меня есть этот код для синхронизации прокрутки между несколькими списками:

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. Это не будет выглядеть так гладко, но у вас не будет проблем с перемещением статических объектов.

Так как я действительно нуждался в этом, я написал элемент управления специально для случая, когда у вас есть статическая графика на прокручиваемой поверхности (размер которой может быть больше 65535).

Это обычный Control с двумя ScrollBar элементами управления на нем, и назначаемый пользователем Control как его Content. Когда пользователь прокручивает, контейнер устанавливает свои Content'S AutoScrollOffset соответственно. Поэтому можно использовать элементы управления, которые используют метод AutoScrollOffset для рисования, ничего не меняя. Фактический размер Content является именно видимой его частью во все времена. Он позволяет выполнять горизонтальную прокрутку, удерживая нажатой клавишу shift.

Использование:

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

Пример содержания:

public class ExampleContent : Control
{
    public ExampleContent()
    {
        this.DoubleBuffered = true;
    }

    static Random random = new Random();

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

        // random color to make the clip rectangle visible in an unobtrusive way
        var color = Color.FromArgb(random.Next(160, 180), random.Next(160, 180), random.Next(160, 180));
        graphics.Clear(color);

        Debug.WriteLine(this.AutoScrollOffset.X.ToString() + ", " + this.AutoScrollOffset.Y.ToString());

        CheckerboardRenderer.DrawCheckerboard(
            graphics, 
            this.AutoScrollOffset,
            e.ClipRectangle,
            new Size(50, 50)
            );

        StaticBoxRenderer.DrawBoxes(graphics, new Point(0, this.AutoScrollOffset.Y), 100, 30);
    }
}

public static class CheckerboardRenderer
{
    public static void DrawCheckerboard(Graphics g, Point origin, Rectangle bounds, Size squareSize)
    {
        var numSquaresH = (bounds.Width + squareSize.Width - 1) / squareSize.Width + 1;
        var numSquaresV = (bounds.Height + squareSize.Height - 1) / squareSize.Height + 1;

        var startBoxH = (bounds.X - origin.X) / squareSize.Width;
        var startBoxV = (bounds.Y - origin.Y) / squareSize.Height;

        for (int i = startBoxH; i < startBoxH + numSquaresH; i++)
        {
            for (int j = startBoxV; j < startBoxV + numSquaresV; j++)
            {
                if ((i + j) % 2 == 0)
                {
                    Random random = new Random(i * j);
                    var color = Color.FromArgb(random.Next(70, 95), random.Next(70, 95), random.Next(70, 95));
                    var brush = new SolidBrush(color);
                    g.FillRectangle(brush, i * squareSize.Width + origin.X, j * squareSize.Height + origin.Y, squareSize.Width, squareSize.Height);
                    brush.Dispose();
                }
            }
        }
    }
}

public static class StaticBoxRenderer
{
    public static void DrawBoxes(Graphics g, Point origin, int boxWidth, int boxHeight)
    {
        int height = origin.Y;
        int left = origin.X;
        for (int i = 0; i < 25; i++)
        {
            Rectangle r = new Rectangle(left, height, boxWidth, boxHeight);
            g.FillRectangle(Brushes.White, r);
            g.DrawRectangle(Pens.Black, r);
            height += boxHeight;
        }
    }
}