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

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

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

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

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

namespace SlashLook
{
	/// <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:\SlashLook\001.png"),
	oNextImage = System.Drawing.Image.FromFile(@"X:\SlashLook\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