2013-05-11 4 views
3

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

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 вызов успешно перерисовал поверхность, статические части слегка отключаются. Это вызывает очень заметный эффект «встряхивания» при прокрутке.

Есть ли способ создать прокручиваемый пользовательский элемент управления, который не имеет этой проблемы со статическими частями?

+0

возможно дубликат [Как я могу записать событие, что происходит, когда мышь Scrolls в TextBox] (http://stackoverflow.com/questions/26671754/how- can-i-write-an-event-that-happens-when-mouse-scrolls-in-textbox) – drzaus

ответ

2

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

Это обычный Control с двумя ScrollBar контроля над ним, и пользователь переуступке Control в качестве Content. Когда пользователь прокручивает, контейнер устанавливает ContentAutoScrollOffset соответственно. Таким образом, можно использовать элементы управления, которые используют метод AutoScrollOffset для рисования без изменения чего-либо. Фактический размер 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; 
     } 
    } 
} 

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

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

Ваше решение отличное, но у WinForms есть ошибка, поэтому ваше содержимое не меняется при изменении размера формы, когда позиция полосы прокрутки, если нет 0 (т. Е. Появляется полоса прокрутки, прокручивается немного, теперь пытайтесь увеличить размер формы). уменьшается, но содержание остается). Чтобы обойти эту ошибку, вам нужно переопределить OnSizeChanged на пользовательский скроллинге и корректировать свои значения с помощью вызова нативного метода GetScrollInfo: \t недействительного OnSizeChanged (EventArgs е) \t { \t \t INT trackPos = GetScrollBarTrackPos (это); \t \t ScrollEventArgs se = .. \t \t OnScroll (se); \t \t Value = se.NewValue; \t} –

+0

Я думал, что предел 65535 был полностью жестко закодирован? Как именно вы обошли это? – Nyerguds

3

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

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

+0

Вы пробовали это? Что касается ответа [Hans] (http://stackoverflow.com/a/16502234/653473), обработка прокрутки непосредственно в контуре сообщения должна вызвать тот же эффект или нет? – dialer

+0

Зачем вам использовать 0x20a (a.k.a. WM_MOUSELAST)? '0x114' является горизонтальным, а' 0x115' - вертикальной прокруткой. – Bitterblue

+0

Я считаю, 0x20a является mousescroll. – Christian

Смежные вопросы