2013-05-11 70 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轉換的方向的整個可見區域對面的滾動。通常這是有道理的,因爲那麼滾動看起來平滑和響應(並且只有新的部分必須重新繪製),然而靜態部分也受到該翻譯的影響(因此,在OnScroll中)。在下一個OnPaint調用成功重繪曲面之前,靜態部分略微關閉。這在滾動時會引起非常明顯的「抖動」效果。

有沒有一種方法可以創建一個沒有靜態部分這個問題的可滾動自定義控件?

+0

可能重複[?我怎麼能寫這種情況發生的事件,當鼠標在滾動文本框(http://stackoverflow.com/questions/26671754/how-我可以寫一個事件,發生時,當鼠標滾動在文本框) – drzaus 2015-08-12 18:39:07

回答

2

因爲我真的需要這個,所以我最終編寫了一個控件,專門用於在可滾動表面(其大小可能大於65535)上有靜態圖形的情況。

這是一個正常的Control,其上有兩個控件ScrollBar,用戶可指定Control作爲其Content。當用戶滾動時,容器會相應地設置其ContentAutoScrollOffset。因此,可以使用使用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; 
     } 
    } 
} 
+0

您的解決方案是偉大的,但WinForms的有個bug,因此當你調整表單時滾動條的位置,如果不爲0(即使滾動條出現,滾動了一下,現在儘量增加你的窗體大小的內容配置是不會改變。滾動條在視覺上正在減少,但內容配置仍然存在)。要解決你需要重寫OnSizeChanged上的自定義滾動條,並通過調用糾正其值的本地方法GetScrollInfo的bug: \t無效OnSizeChanged(EventArgs的五) \t { \t \t INT trackPos = GetScrollBarTrackPos(本); \t \t ScrollEventArgs SE = .. \t \t OnScroll(SE); \t \t Value = se.NewValue; \t} – 2016-02-05 23:04:28

+0

我認爲65535的限制是完全硬編碼的?你究竟如何解決這個問題? – Nyerguds 2017-04-05 18:55:21

3

你可以通過完全控制滾動來做到這一點。目前,你只是在參加這個活動來完成你的邏輯。我之前遇到過滾動問題,而且我唯一能夠讓所有的工作順利進行的方法是通過重寫WndProc來實際處理Windows消息。舉例來說,我有這樣的代碼來幾個列表框之間同步滾動:

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 2013-05-12 09:03:00

+0

爲什麼要使用0x20a(又名WM_MOUSELAST)? 「0x114」是水平的,「0x115」是垂直滾動的。 – Bitterblue 2013-12-03 10:48:11

+0

我相信0x20a是mousescroll。 – Christian 2014-04-10 12:33:23