利用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 SlashLook
{
	/// <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[] { "歡迎光臨 SlashLook 官方網站...", "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過去後,就像被開一條純背景色的高速公路一樣,完全不能看啊。

※ 更新一:修正類別,使其可支援外框文字(Outline Text)的繪製,在具有背景圖片的表單下,比之前無外框文字更來的實用許多。

※ 更新二:修正類別,使其使用CJK相關字形進行大型外框文字繪製時,不至於產生框線暴衝(OverFlow)等問題,測試文字可以使用「輛」這個字。

※ 更新三:新增bTimerRunning屬性,可以讓你輕易實作marquee rolling toggle方法。

※ 更新四:把整個類別重新改過一次,不直接在Form表單上作畫,而是改動態產生一個PictureBox來作畫,效能會相對好上非常多。

※ 更新五:將原本單一字串版本,修正為字串陣列版本,降低執行時期控制跑馬燈起訖的複雜度。

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