利用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過去後,就像被開一條純背景色的高速公路一樣,完全不能看啊。
- 更新一:修正類別,使其可支援外框文字(Outline Text)的繪製,在具有背景圖片的表單下,比之前無外框文字更來的實用許多。
- 更新二:修正類別,使其使用CJK相關字形進行大型外框文字繪製時,不至於產生框線暴衝(OverFlow)等問題,測試文字可以使用「輛」這個字。
- 更新三:新增bTimerRunning屬性,可以讓你輕易實作marquee rolling toggle方法。
- 更新四:把整個類別重新改過一次,不直接在Form表單上作畫,而是改動態產生一個PictureBox來作畫,效能會相對好上非常多。
- 更新五:將原本單一字串版本,修正為字串陣列版本,降低執行時期控制跑馬燈起訖的複雜度。