利用GDI+圖形雙重緩衝(Double Buffering)來解決文字跑馬燈之繪圖閃爍問題

這篇文章不討論Web-Based,回到傳統的Windows Form來討論,當我們嘗試著在表單上面進行System.Drawing繪圖工作時,會發現在上面進行繪圖的動作極其緩慢,如果再加上不斷的呼叫表單this.Refresh()進行全物件的刷新時,會發現緩慢加上閃爍之問題更是極其嚴重。

這篇文章我試著套用一個跑馬燈的案例,套用上我寫好的一個叫「MarqueeString」類別,試著採用.NET Framework的System.Drawing.BufferedGraphics類別,來操控所謂的圖形雙緩衝繪圖,也就是先在背景記憶體開設一個bitmap,然後對他進行繪圖,等到畫好後再瞬間套用到前景,藉此來消除螢幕重新繪製時期的空白問題,這也就是發生閃爍的主要原因。如果你有興趣的話,可以觀察一下外面比較科技化飲料攤前面的展示螢幕,上面如果有最新消息跑馬燈的功能,大部分的文字都會出現跑幾個Pixel就閃爍的問題。

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

基本上所有應該講的事情,我都已經在程式註解裏面說清楚了,所以我就不在這裏多廢話了。此外我有在這個類別裡面提供一個OnDrawing事件,如果有需要再進行重繪時觸發事件用來進行某些延伸的工作,就可以善用掛載這個事件,不需要再浪費一個Timer了。

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

namespace Slashview
{
  /// <summary>
  /// MarqueeText事件委派器
  /// </summary>
  /// <param name="sender">MarqueeText實例本體</param>
  /// <param name="e">參數包</param>
  public delegate void MarqueeTextEventHandler(System.Object sender, System.EventArgs e);
  /// <summary>
  /// MarqueeText主類別
  /// </summary>
  public class MarqueeText
  {
    //(私有變數)存放要顯示的文字內容
    private string[] _aryContent;
    //(私有變數)存放目前顯示文字內容陣列的第幾筆
    private int _iNowContentPoint = 0;
    //(私有變數)存放要顯示在哪個表單物件
    private System.Windows.Forms.Form _oTargetForm;
    //(私有變數)圖片顯示元件
    private System.Windows.Forms.PictureBox _oTargetPictureBox;
    //(私有變數)計時器物件
    private System.Timers.Timer _oTimer;
    //(私有變數)目前繪製文字的指標
    private int _iDrawPoint;
    //(私有變數)字形真實大小
    private float _fRealFontSize;
    /// <summary>
    /// 提供螢幕畫面重新繪製時的外部事件通知
    /// </summary>
    public event MarqueeTextEventHandler OnDrawing;    
    /// <summary>
    /// 強制計數器停止之旗標
    /// </summary>
    public bool bForceStop { get; private set; }
    /// <summary>
    /// 文字要畫在哪一個表單物件的繪布上
    /// </summary>
    public System.Windows.Forms.Form oTargetForm
    {
      get { return _oTargetForm; }
      set
      {
        _oTargetForm = value;
        //阻擋違法的建構參數
        if (iWidth <= 0 || iHeight <= 0) { throw new System.Exception("預設畫布元件的寬度與高度的值錯誤。"); }
        //修正圖片框高度,以符合文字大小之真實高度
        if (iContentHeight > 0) { iHeight = iContentHeight; }
        //動態產生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();
          //初始化文字滾動的起點
          _iDrawPoint = _oTargetPictureBox.ClientSize.Width;
        }
      }
    }
    /// <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 iMovePixel { get; set; }
    /// <summary>
    /// 文字幾千分之一秒移動一次
    /// </summary>
    public int iTimerInterval { get; set; }
    /// <summary>
    /// 文字的字型格式
    /// </summary>
    public System.Drawing.Font oFont { get; set; }
    /// <summary>
    /// 要顯示的文字陣列
    /// </summary>
    public string[] aryContent
    {
      get { return _aryContent; }
      set
      {
        if (value == null || value.Length == 0) { throw new System.Exception("沒有指定任何要顯示的文字內容。"); }
        foreach (string oItem in value) { if (oItem.Length == 0) { throw new System.Exception("內容陣列內,存在著沒有指定任何要顯示的文字內容。"); } }
        _aryContent = value;
        _iNowContentPoint = 0;
        StringSizeEvaluation(); //先評估一下當前的文字內容長度
      }
    }
    /// <summary>
    /// 文字經過繪製後的寬度
    /// </summary>
    public int iContentWidth { get; private set; }
    /// <summary>
    /// 文字經過繪製後的高度
    /// </summary>
    public int iContentHeight { get; private set; }
    /// <summary>
    /// 文字前景刷子(顏色)
    /// </summary>
    public System.Drawing.SolidBrush oBrushFront { get; set; }
    /// <summary>
    /// 文字背景刷子(顏色)
    /// </summary>
    public System.Drawing.SolidBrush oBrushBack { get; set; }
    /// <summary>
    /// 文字邊框刷子(顏色)
    /// </summary>
    public System.Drawing.SolidBrush oBrushBorder { get; set; }
    /// <summary>
    /// 文字邊框的寬度
    /// </summary>
    public int iBrushBorderWidth { 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;
      //取消強制停止計數旗標
      bForceStop = false;
      _oTimer.Start();
    }
    /// <summary>
    /// 跑馬燈停止
    /// </summary>
    public void Stop()
    {
      if (_oTimer == null) { return; }
      bForceStop = true;  //設定強制停止計數旗標
      _oTimer.Stop();
    }
    /// <summary>
    /// 取得跑馬燈現在的運行狀態
    /// </summary>
    public bool bTimerRunning { get { return _oTimer.Enabled; }}
    /// <summary>
    /// 繪製跑馬燈文字(由內部計數器觸發)
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void DrawMarquee(System.Object sender, System.EventArgs e)
    {
      //停止計數器
      _oTimer.Stop();
      //先看一下是否具備文字內容
      if (_aryContent.Length < 1) { throw new System.Exception("沒有指定任何要顯示的文字內容。"); }
      //觸發外部事件(先確認外部已經有指定觸發對象函式)
      if (OnDrawing != null) { OnDrawing(this, new MarqueeTextArgs() { iCurrentPosX = _iDrawPoint, iCurrentHeight = iContentHeight }); }
      //開始繪圖
      using (System.Drawing.BufferedGraphics oBuffer = System.Drawing.BufferedGraphicsManager.Current.Allocate(_oTargetPictureBox.CreateGraphics(), _oTargetPictureBox.DisplayRectangle))
      {
        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(); }
          //繪製外框文字
          using (System.Drawing.Drawing2D.GraphicsPath oGPath = new System.Drawing.Drawing2D.GraphicsPath())
          {
            oGPath.AddString(_aryContent[_iNowContentPoint], oFont.FontFamily, (int)oFont.Style, _fRealFontSize, new System.Drawing.Point(_iDrawPoint, 0), new System.Drawing.StringFormat() { Alignment = System.Drawing.StringAlignment.Near, LineAlignment = System.Drawing.StringAlignment.Near });
            oGraph.DrawPath(new System.Drawing.Pen(oBrushBorder, iBrushBorderWidth) { LineJoin = System.Drawing.Drawing2D.LineJoin.Round }, oGPath);
            oGraph.FillPath(oBrushFront, oGPath);
            /* For normal text drawing */
            //oGraph.DrawString(cContent, oFont, oBrushFront, iPosX, iPosY);
          }
          //將緩衝丟回目標前景
          oBuffer.Render(_oTargetPictureBox.CreateGraphics());
        }
      }
      //重新評估下一階段的繪製位址與展示字串
      if (_iDrawPoint <= -iContentWidth)
      {
        //應該換文字了
        if ((_iNowContentPoint + 1) >= _aryContent.Length)
        { _iNowContentPoint = 0; }
        else
        { _iNowContentPoint++; StringSizeEvaluation(); }
        //重設起始繪製點
        _iDrawPoint = _oTargetPictureBox.ClientSize.Width;
      }
      else { _iDrawPoint -= iMovePixel; }
      //如果計數器已經被外部下強制停止的指令,那就不要再啟動了
      if (!bForceStop) { _oTimer.Start(); }
    }
    /// <summary>
    /// (私有方法)評估繪製後文字圖形之寬高
    /// </summary>
    private void StringSizeEvaluation()
    {
      if (string.IsNullOrWhiteSpace(_aryContent[_iNowContentPoint]) || oFont == null) { throw new System.Exception("請先設定文字內容、字型物件。"); }
      //取得字形真實大小
      System.Func<float, float> funRealSize= (iFake) => {
        using (System.Drawing.Graphics oGraph = System.Drawing.Graphics.FromImage(new System.Drawing.Bitmap(1, 1)))
        {  return oGraph.DpiY * iFake / 72; }
      };
      //繪製路徑並取得長寬
      using (System.Drawing.Drawing2D.GraphicsPath oGPath = new System.Drawing.Drawing2D.GraphicsPath())
      {
        _fRealFontSize = funRealSize(oFont.Size);
        oGPath.AddString(_aryContent[_iNowContentPoint], oFont.FontFamily, (int)oFont.Style, _fRealFontSize, new System.Drawing.Point(0, 0), new System.Drawing.StringFormat() { Alignment = System.Drawing.StringAlignment.Near, LineAlignment = System.Drawing.StringAlignment.Near });
        iContentWidth = (int)oGPath.GetBounds().Size.Width + iBrushBorderWidth;
        iContentHeight = (int)funRealSize(oGPath.GetBounds().Size.Height) + iBrushBorderWidth;
        oGPath.Reset();
      }
      /* For normal text drawing */
      //System.Drawing.SizeF oStringSize = oGraph.MeasureString(_cContent, oFont);
      //iContentWidth = (int)oStringSize.Width;
      //iContentHeight = (int)oStringSize.Height;
    }
    /// <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>
  /// MarqueeText事件參數包
  /// </summary>
  public class MarqueeTextArgs : System.EventArgs
  {
    /// <summary>
    /// 準備繪製的X軸座標
    /// </summary>
    public int iCurrentPosX { get; set; }
    /// <summary>
    /// 準備繪製的Y軸座標
    /// </summary>
    public int iCurrentHeight { get; set; }
  }
}

MarqueeString類別使用方法

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

private void Form1_Load(object sender, EventArgs e)
{
  oMarquee = new MarqueeText()
  {
    iPosX = 50,
    iPosY = 120,
    iWidth = 700,
    iHeight = 10,
    iMovePixel = 5,
    iTimerInterval = 50,
    iBrushBorderWidth = 10,
    oFont = new System.Drawing.Font("微軟正黑體", 48, System.Drawing.FontStyle.Bold),
    cContent = new string[] { "歡迎光臨 Slashview 官方網站...", "Marquee testing string..." },
    oBrushBack = new System.Drawing.SolidBrush(System.Drawing.Color.Transparent),
    oBrushFront = new System.Drawing.SolidBrush(System.Drawing.ColorTranslator.FromHtml("#78FFDD")),
    oBrushBorder = new System.Drawing.SolidBrush(System.Drawing.ColorTranslator.FromHtml("#333")),
    oTarget = this
  };
  oMarquee.Start();
}

對了,忘了說明一件事情,這個類別有支援背景圖片的透通(如果你的Form有設定背景圖片的話),不像坊間有一些範例全部都沒有考慮到文字去背的問題,一直DrawString過去後,就像被開一條純背景色的高速公路一樣,完全不能看啊。

Marquee WindowsForms Win32 C# .NetFramework String OutlineText System.Drawing.Graphics System.Drawing.BufferedGraphics System.Drawing.BufferedGraphicsManager