利用GDI+圖形雙重緩衝(Double Buffering)來實作圖形跑馬燈

之前實作過文字跑馬燈後,應同事要求再實作出圖形跑馬燈,原因無他,因為客戶要求文字跑馬燈後,可預期的圖形跑馬燈的需求一定接踵而至,話不多說了,直接看程式碼。

在這次的程式碼中,依然動態的調用PictureBox來當作我們的基礎繪製畫布,如此一來可以避免直接在Form表單底圖作畫,產生許多效能上的問題。當然,這個類別也提供了事件供給外部程式碼掛載,可以在動態運行的時期藉以觀察相關的參數(若有需要可以自己調整參數)。此外,我這些類別都是寫給自己用的,所以許多的建構或方法,都沒有詳細地進行初始化的檢查,也就是說,當類別的實例被生成之後,相關的欄位或方法的指定,請依照順序來進行,否則可能會跳出null Exception,這並不是因為程式碼寫的爛,純粹是因為這是自用(程式設計師使用),而非用嚴謹的商業元件角度來撰寫。

MarqueeImage文字跑馬燈類別原始程式碼

程式碼裡面依然有透過System.Reflection反射方法,來調用Windows Forms被保護(protected)的SetStyle、UpdateStyles方法,因此你不需要再去Form建構子中,去設定啟用OptimizedDoubleBuffer。

namespace Slashview
{
  /// <summary>
  /// MarqueeImage事件委派器
  /// </summary>
  /// <param name="sender">MarqueeText實例本體</param>
  /// <param name="e">參數包</param>
  public delegate void MarqueeImageEventHandler(System.Object sender, System.EventArgs e);
  /// <summary>
  /// MarqueeImage主類別
  /// </summary>
  public class MarqueeImage
  {
    //(私有變數)圖片顯示元件
    private System.Windows.Forms.PictureBox _oTargetPictureBox;
    //(私有變數)計時器物件
    private System.Windows.Forms.Form _oTargetForm;
    //(私有變數)計時器物件
    private System.Timers.Timer _oTimer;
    //(私有變數)兩張圖片中間的指標
    private int _iPartitionPoint;
    /// <summary>
    /// 提供螢幕畫面重新繪製時的外部事件通知
    /// </summary>
    public event MarqueeImageEventHandler OnDrawing;
    /// <summary>
    /// 圖片要畫在哪一個表單物件的繪布上
    /// </summary>
    public System.Windows.Forms.Form oTargetForm
    {
      get { return _oTargetForm; }
      set
      {
        _oTargetForm = value;
        //阻擋違法的建構參數
        if (iWidth == 0 || iHeight == 0) { throw new System.Exception("預設圖片的寬度與高度不可能為零。"); }
        //動態產生PictureBox
        if (_oTargetPictureBox == null)
        {
          _oTargetPictureBox = new System.Windows.Forms.PictureBox()
          {
            Left = iPosX,
            Top = iPosY,
            BorderStyle = System.Windows.Forms.BorderStyle.None,
            Size = new System.Drawing.Size(iWidth, iHeight),
            ClientSize = new System.Drawing.Size(iWidth, iHeight)
          };
          _oTargetForm.Controls.Add(_oTargetPictureBox);
          SetDoubleBuffering(_oTargetPictureBox);
          //強制重繪PictureBox
          _oTargetPictureBox.Refresh();
        }
      }
    }
    /// <summary>
    /// 圖片要顯示在哪個X軸上
    /// </summary>
    public int iPosX { get; set; }
    /// <summary>
    /// 圖片要顯示在哪個Y軸上
    /// </summary>
    public int iPosY { get; set; }
    /// <summary>
    /// 圖片預期寬度
    /// </summary>
    public int iWidth { get; set; }
    /// <summary>
    /// 圖片預期高度
    /// </summary>
    public int iHeight { get; set; }
    /// <summary>
    /// 圖片幾千分之一秒移動一次
    /// </summary>
    public int iTimerInterval { get; set; }
    /// <summary>
    /// 圖片每次要移動幾個像素
    /// </summary>
    public int iMovePixel { get; set; }
    /// <summary>
    /// 當前圖片物件
    /// </summary>
    public System.Drawing.Image oCurrentImage { get; set; }
    /// <summary>
    /// 下張圖片物件
    /// </summary>
    public System.Drawing.Image oNextImage { get; set; }
    /// <summary>
    /// 開始移動圖片
    /// </summary>
    public void Start()
    {
      if (iTimerInterval <= 0) { throw new System.Exception("計數器必須被設定,或已經設定的整數值有誤。"); }
      if (_oTimer != null) { _oTimer.Stop(); }
      _oTimer = new System.Timers.Timer() { Interval = iTimerInterval };
      _oTimer.Elapsed += DrawMarquee;
      _oTimer.SynchronizingObject = _oTargetPictureBox;
      _iPartitionPoint = oCurrentImage.Width;  
      _oTimer.Start();
    }
    /// <summary>
    /// 結束移動圖片
    /// </summary>
    public void Stop()
    {
      if (_oTimer == null) { return; }
      _oTimer.Stop();
      _oTimer.Dispose();
    }
    /// <summary>
    /// 繪製圖形跑馬燈
    /// </summary>
    public void DrawMarquee(System.Object sender, System.EventArgs e)
    {
      if (_oTargetForm == null || _oTargetPictureBox == null) { throw new System.Exception("未指定繪製目標表單,或者是建立繪圖元件失敗。"); }
      /* 進行繪圖作業 */
      using (System.Drawing.BufferedGraphics oBuffer = System.Drawing.BufferedGraphicsManager.Current.Allocate(_oTargetPictureBox.CreateGraphics(), _oTargetPictureBox.ClientRectangle))
      {
        using (System.Drawing.Graphics oGraph = oBuffer.Graphics)
        {
          //最佳化繪圖輸出
          oGraph.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
          oGraph.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
          oGraph.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
          oGraph.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
          //在目標重繪所屬表單背景色
          oGraph.Clear(_oTargetForm.BackColor);
          //(匿名函式)運算與繪製父表單背景圖片
          System.Action funcDrawBackgroundImage = () => {
            int iCorpX;             //剪裁座標X
            int iCorpY;             //剪裁座標Y
            int iCorpWidth;         //剪裁目標寬
            int iCorpHeight;        //剪裁目標高
            int iPasteTargetX = 0;  //黏貼目標座標X
            int iPasteTargetY = 0;  //黏貼目標座標Y
            //評估水平座標系統
            if (_oTargetPictureBox.Left < 0)
            {
              if ((_oTargetPictureBox.Left + _oTargetPictureBox.Width) >= 0) { iCorpX = 0; iCorpWidth = _oTargetPictureBox.Left + _oTargetPictureBox.Width; iPasteTargetX = -(_oTargetPictureBox.Left - 1); }
              else { iCorpX = -1; iCorpWidth = -1; }
            }
            else if (_oTargetPictureBox.Left >= 0 && _oTargetPictureBox.Left < _oTargetForm.BackgroundImage.Width)
            {
              if ((_oTargetPictureBox.Left + _oTargetPictureBox.Width) < _oTargetForm.BackgroundImage.Width) { iCorpX = _oTargetPictureBox.Left; iCorpWidth = _oTargetPictureBox.Width; iPasteTargetX = 0; }
              else { iCorpX = _oTargetPictureBox.Left; iCorpWidth = _oTargetForm.BackgroundImage.Width - _oTargetPictureBox.Left; iPasteTargetX = 0; }
            }
            else  /* means oPB.Left >= this.BackgroundImage.Width */
            {
              iCorpX = -1; iCorpWidth = -1;
            }
            //評估垂直座標系統
            if (_oTargetPictureBox.Top < 0)
            {
              if ((_oTargetPictureBox.Top + _oTargetPictureBox.Height) >= 0) { iCorpY = 0; iCorpHeight = _oTargetPictureBox.Top + _oTargetPictureBox.Height; iPasteTargetY = -(_oTargetPictureBox.Top - 1); }
              else { iCorpY = -1; iCorpHeight = -1; }
            }
            else if (_oTargetPictureBox.Top >= 0 && _oTargetPictureBox.Top < _oTargetForm.BackgroundImage.Height)
            {
              if ((_oTargetPictureBox.Top + _oTargetPictureBox.Height) < _oTargetForm.BackgroundImage.Height) { iCorpY = _oTargetPictureBox.Top; iCorpHeight = _oTargetPictureBox.Height; iPasteTargetY = 0; }
              else { iCorpY = _oTargetPictureBox.Top; iCorpHeight = _oTargetForm.BackgroundImage.Height - _oTargetPictureBox.Top; iPasteTargetY = 0; }
            }
            else  /* means oPB.Top >= this.BackgroundImage.Height */
            {
              iCorpY = -1; iCorpHeight = -1;
            }
            //評估需不需要填入任何背景
            bool bNeedDrawBackgroundImage = true;
            if (iCorpX < 0 || iCorpY < 0 || iCorpWidth < 0 || iCorpHeight < 0) { bNeedDrawBackgroundImage = false; }
            //如果有需要填入背景,就切割背景影像,貼到應該貼上的位置
            if (bNeedDrawBackgroundImage)
            {
              oGraph.DrawImage(new System.Drawing.Bitmap(
                _oTargetForm.BackgroundImage).Clone(
                  new System.Drawing.Rectangle(iCorpX, iCorpY, iCorpWidth, iCorpHeight),
                  _oTargetForm.BackgroundImage.PixelFormat
                ),
                new System.Drawing.Point(iPasteTargetX, iPasteTargetY)
              );
            }
          };
          //如果表單有背景圖片的話,就重繪背景圖片(透明效果)
          if (_oTargetForm.BackgroundImage != null) { funcDrawBackgroundImage(); }
          
          /* 繪製雙圖片輪播效果 */
          int iCorpCurrentX = oCurrentImage.Width - _iPartitionPoint;
          int iCorpCurrentWidth = oCurrentImage.Width - iCorpCurrentX;
          int iCorpNextWidth = iCorpCurrentX;
          //丟出事件
          if (OnDrawing != null) { OnDrawing(this, new MarqueeImageArgs() { iCurrentImageWidth = iCorpCurrentWidth, iPartitionPoint = _iPartitionPoint, iNextImageWidth = iCorpNextWidth }); }
          //繪製現在圖片應該在的位址
          if (iCorpCurrentWidth != 0)
          {
            oGraph.DrawImage(new System.Drawing.Bitmap(
              oCurrentImage).Clone(
                new System.Drawing.Rectangle(iCorpCurrentX, 0, iCorpCurrentWidth, oCurrentImage.Height),
                _oTargetForm.BackgroundImage.PixelFormat
              ),
              new System.Drawing.Point(0, 0)
            );
          }
          //繪製下張圖片應該在的位址
          if (iCorpNextWidth != 0)
          {
            oGraph.DrawImage(new System.Drawing.Bitmap(
              oNextImage).Clone(
                new System.Drawing.Rectangle(0, 0, iCorpNextWidth, oNextImage.Height),
                _oTargetForm.BackgroundImage.PixelFormat
              ),
              new System.Drawing.Point(_iPartitionPoint, 0)
            );
          }
          //計算下一次的位移
          if (_iPartitionPoint == 0)
          {
            _oTimer.Stop();
            //修正有時按下螢幕快照截圖時,會產生的繪圖區洗成空白的問題
            _oTargetPictureBox.Image = oNextImage;
            _oTargetPictureBox.Refresh();
          }
          else
          {
            _iPartitionPoint -= iMovePixel;
            if (_iPartitionPoint < 0) { _iPartitionPoint = 0; }
          }
          //將緩衝丟回目標前景
          oBuffer.Render(_oTargetPictureBox.CreateGraphics());
        }
      }
    }
    /// <summary>
    /// 驅動繪圖目標對象使用雙圖形緩衝
    /// </summary>
    /// <param name="oTemp">套用目標對象</param>
    private void SetDoubleBuffering(System.Object oTemp)
    {
      try
      {
        //因為SetStyle、UpdateStyles是被設定存取屬性是protected,因此只能透過Reflection來代理
        System.Reflection.MethodInfo oMethod_1 = oTemp.GetType().GetMethod("SetStyle", (System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance));
        System.Reflection.MethodInfo oMethod_2 = oTemp.GetType().GetMethod("UpdateStyles", (System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance));
        //如果兩個方法都有調用到,那就Invoke它們
        if (oMethod_1 != null && oMethod_2 != null)
        {
          oMethod_1.Invoke(oTemp, new object[] { System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer, true });
          oMethod_2.Invoke(oTemp, new object[] { });
        }
      }
      catch { throw new System.Exception("驅動繪圖對象使用圖形雙重緩衝失敗!"); }
    }
  }
  /// <summary>
  /// MarqueeImageArgs參數包
  /// </summary>
  public class MarqueeImageArgs : System.EventArgs
  {
    /// <summary>
    /// 繪製中前一個圖片的剩餘寬度
    /// </summary>
    public int iCurrentImageWidth { get; set; }
    /// <summary>
    /// 分割兩個圖片的座標X軸
    /// </summary>
    public int iPartitionPoint { get; set; }
    /// <summary>
    /// 繪製中下一個圖片的當前寬度
    /// </summary>
    public int iNextImageWidth { get; set; }
  }
}

MarqueeImage類別使用方法

使用的方法很簡單,可能你是在Form Load事件下呼叫這個可能被設定成全域物件的oMarquee,然後你只要給他一切應該有的參數後,接著調用.Start();即可。當然,你可以選擇掛上某些額外的按鈕,讓這些按鈕來調用.Stop();方法或者是.OnDrawing()事件。

oMarquee = new MarqueeImage()
{
  iPosX = 0,
  iPosY = 0,
  iWidth = 800,
  iHeight = 100,
  iTimerInterval = 50,
  iMovePixel = 20,
  oCurrentImage = System.Drawing.Image.FromFile(@"X:\Slashview\001.png"),
  oNextImage = System.Drawing.Image.FromFile(@"X:\Slashview\002.png"),
  oTargetForm = this
};
oMarquee.OnDrawing += oMarquee_OnDrawing;  //可選擇不掛上事件
oMarquee.Start();

本類別有支援表單(Form)之背景顏色以及背景圖片,因此你可以自由的移動PictureBox到任何表單的可視區域內,類別會自動幫你計算並重新繪製應該繪製的圖片,來產生透明的效果。

Marquee WindowsForms Win32 C# .NetFramework Image Picture System.Drawing.Graphics System.Drawing.BufferedGraphics System.Drawing.BufferedGraphicsManager