Windows使用者介面程式設計中的介面閃爍問題
在Windows圖形化使用者介面程式設計中,若程式自己繪製使用者介面時,會經常碰到介面閃爍,比如其他視窗在上面移動,使用者介面滾動,這些都有可能導致閃爍。在一個容器中繪製特定的文件,需要相應作為繪圖容器的控制元件的OnPaint事件,需要在OnPaint事件處理中重新繪製文件,而Windows作業系統一般會在兩種情況下觸發OnPaint事件:容器控制元件被其他窗體覆蓋後又顯示,還有就是容器控制元件的滾動處理。在這些情況下,Windows作業系統會頻繁的觸發OnPaint事件,而應用程式會頻繁的在繪圖容器中重新繪製圖形,若應用程式沒有進行很好的優化,則很有可能導致使用者介面閃爍。
使用者介面出現閃爍自然害處多多,首先它使得你的程式看起來不專業,甚至有不穩定的嫌疑,對於追求完美的你這麼會容許它的存在呢;其次閃爍會損害使用者的視力,容易讓使用者產生視覺疲勞。
好了,廢話我不多說了,我們就來發現問題,分析問題,解決問題。
首先說說閃爍的本質,說到本質,就不得不提一些計算機系統結構和Windows圖形使用者子系統的一些知識。我們知道,在計算機記憶體中有一個區域叫做視訊記憶體,而顯示卡則每過一些毫秒就從掃描視訊記憶體,然後根據操作顯示器來繪製一個個象素,因此每過一些毫秒顯示器顯示的內容就會重新設定一遍,由於這是硬體操作,非常快,若畫面內容沒有變化,則人類肉眼是看不到這個重新整理的,此時使用者介面是沒有任何閃爍。
右圖就是應用程式繪製使用者介面的原理,應用程式在CPU的支援下向視訊記憶體填充資料,而以此同時顯示卡也從視訊記憶體載入資料操作顯示器繪製圖形,(筆者想若應用程式能直接訪問顯示器則繪製速度不要太快哦),而使用者介面閃爍也就根源於這種顯示結構。前面提到,顯示卡每過一些毫秒就會掃描視訊記憶體,重新整理顯示器的顯示。假設有個顯示卡,設定其重新整理頻率為50赫茲,則它每20毫秒就掃描視訊記憶體重新整理顯示器,而顯示卡的操作和應用程式的操作和應用程式的操作之間沒有任何關係,顯示卡是自帶處理器的,於是應用程式和顯示卡這兩個物件同時操作視訊記憶體,顯示卡只讀取視訊記憶體,而應用程式則修改視訊記憶體,這就導致了類似多執行緒程式的資料同步的問題了。但這時硬體結構決定此時沒有什麼鎖定機制可使用。顯示卡每20毫秒就進行重新整理操作,連作業系統也擋不住,而且應用程式根本不知道顯示卡會何時進行重新整理操作。
某個時刻,應用程式需要繪製使用者介面,首先需要清空繪製容器,因此將視訊記憶體一大片區域設定為白色,應用程式剛完成了清空操作,還每來得及繪製內容時,顯示卡就重新整理了,很快顯示器上顯示了一大片白色。同時,應用程式開始繪製內容,應用程式執行緩慢,它化了20毫秒繪製了文件的上半身,文件上半身主要為紅色,剛繪製了上半身,顯示卡就好不留情的進行重新整理,很快顯示器上顯示了一半的文件,剛才一半的白色大半變成了紅色,此時使用者看來,顯示器一下變成一片白,然後很快一半變成紅色,此時顯示器內容產生了兩次大面積的內容變幻,然後應用程式又化了20毫秒顯示了文件的下半身,文件下半身主要為綠色,此時顯示卡進行重新整理,顯示器上另一半還殘存的白色又變成綠色。由於應用程式繪製文件完畢,因此不再修改視訊記憶體,此時顯示器的顯示的內容不再發生改變。
在上面的描述中,顯示器首先從花花綠綠變成一片白,20毫秒後一半變成紅色,又20毫秒後另一半變成綠色,如此大面積的顯示內容短期的改變就是所謂的閃爍,此時計算機顯示器就是在折磨使用者的眼睛。
其實從廣義上說,計算機顯示器顯示的內容頻繁的發生大面積的改變都是考驗人類的眼睛。有些人玩一些激烈的3D遊戲,比如雷神CS之類的,若玩的時間長點眼睛就受不了。就是因為這些遊戲程式導致顯示器顯示的內容頻繁的發生大面積的改變。因此遊戲程式也算導致使用者介面閃爍。只不過這種閃爍不算難受。
通過上面的討論,知道了閃爍的根源,於是我們發現了問題,並分析了問題,現在解決問題。在目前的計算機結構中,我們的應用程式只能通過填充視訊記憶體來繪製使用者介面,在這種情況下,對付閃爍的不二法則就是快,應用程式要非常快的修改視訊記憶體。對於每20毫秒進行重新整理操作的顯示卡,若我們的應用程式能在20毫秒內修改視訊記憶體完畢,則在很多情況下顯示卡就只會導致顯示器顯示的內容發生改變,減少閃爍,應用程式填充視訊記憶體前後視訊記憶體的資料進行對比,若資料前後不一致的位元組數越少,顯示器中重新整理操作前後顏色發生改變的象素數就越少,這使用者介面的閃爍就越小。
提高應用程式修改視訊記憶體的速度的方法有很多中,而且Windows作業系統為我們做了許多底層的操作。我們知道若一個窗體被覆蓋後又顯示了,則Windows作業系統就會向該窗體傳送重繪訊息,而且還會傳一個矩形資料,該矩形表示窗體中需要重新整理的區域,應用程式可以根據這個矩形來重新繪製文件的某個部分,這樣就不必要繪製所有的內容,提高繪製速度,減少繪製時間,這就需要進行繪圖程式碼的優化。
在某些情況下,繪圖速度很難優化起來,此時可以採用所謂“雙緩衝”的技術來減少閃爍,應用程式可以在記憶體中建立一個和螢幕相相容的圖形裝置上下文。該上下文實際上處理一個儲存在記憶體中的BMP圖片物件,這樣就容許應用程式緩慢地在這個BMP上繪製圖形。應用程式繪製完畢,就用WIN32API函式BitBlt來將BMP圖片填充到視訊記憶體中,BitBlt函式速度非常快,足以在螢幕的重新整理週期內完成繪圖,這樣能基本上避免閃爍。
俗話說,說得容量做到難,程式設計也一樣,本文中對付閃爍的說的輕巧,但在實際程式設計中,閃爍一直是圖形化使用者介面程式設計的老大難問題,需要精心的設計程式結構,優化程式碼,提高速度。這需要靠很多的理論知識和深厚的程式設計功底,這些需要長期的程式設計實踐。
本文免費附送個人簡歷
袁永福,男,170釐米,體重66千克,江西省九江市都昌縣人,於2001年從南京東南大學動力工程系本科畢業,畢業後一直在南京從事計算機軟體開發,主要使用微軟技術,不敢言精通,但也能拍著胸脯說相當熟悉,其看家語言為VB,VB.NET和C#,此外還熟悉XML,XSL。能比較瞭解和使用Javascript和VBA。進行過Win32API程式設計和GUI程式設計。有比較多的圖形化使用者介面程式設計經驗。在目前工作的單位中擔當專案經理,獨立負責過一些有相當技術難度的專案開發。工作4年中完成的專案有
- 2002年,某小程式,獨立VB編寫,3000行程式碼,使用控制元件來實現WindowsNT的服務程式。
- 2002年,某公司資訊系統,為ASP.NET應用,C#編寫,15000行程式碼,使用MS SQL Server 資料庫。本工程是其他人員開發,但由於種種原因無法完工,由我接手進行強制收尾,現該系統比較穩定的運行了2年
- 2002年,某模板編制程式,獨立VB.NET編寫的圖形化使用者介面程式,50000行程式碼,實現了類似VS.NET的窗體設計器的功能,並能自動生成和設計樣式非常接近的HTML文件和配套的XSL文件,
- 2003年,某列印模板編制程式,獨立用VB編寫,60000行程式碼,實現了列印模板的編制,包括列印樣式和資料來源的設計,並在進行列印時使用XSL轉換來生成列印文件
- 2004年,某文字編輯器,獨立用C#編寫,64000行程式碼,從底層做起實現了一個帶格式的文字編輯器,並在文字編輯器中添加了很多特性。
- 此外還編寫了一些小程式,掃在一起也有不少程式碼。
本人對計算機軟體開發有著相當的興趣,並比較看好中國軟體行業的大前景。本人學習能力強,思維敏捷,比較牢固的掌握面向物件的程式設計思想,並有著相當的創新精神,喜歡研究性的軟體開發。雖然本人主要使用微軟技術,但對JAVA技術也保持著相當的學習態度,對MVC模式,J2EE框架有所瞭解,能修改已有的JSP頁面或JAVA程式。
本人現和當前公司的合同快到期,因此現尋求新的僱主,希望能在南京工作,待遇可。有興趣者可聯絡我,聯絡方法為
電子郵件: [email protected] , QQ號:28348092
最後附送一個C#程式,這段程式碼能演示圖形化使用者介面編輯中的一種優化手段,使用VS.NET建立一個C#的標準Win32程式
using System; using System.Drawing; using System.ComponentModel; using System.Windows.Forms; namespace ViewPicture { public class frmViewImg : System.Windows.Forms.Form { private System.Windows.Forms.Button cmdLoad; private System.Windows.Forms.Label label1; private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Label label2; private System.Windows.Forms.Panel panel2; private System.ComponentModel.Container components = null; public frmViewImg() { InitializeComponent(); } protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose(); } } base.Dispose( disposing ); } private void InitializeComponent() { this.cmdLoad = new System.Windows.Forms.Button(); this.label1 = new System.Windows.Forms.Label(); this.panel1 = new System.Windows.Forms.Panel(); this.label2 = new System.Windows.Forms.Label(); this.panel2 = new System.Windows.Forms.Panel(); this.SuspendLayout(); this.cmdLoad.Location = new System.Drawing.Point(16, 8); this.cmdLoad.Name = "cmdLoad"; this.cmdLoad.Size = new System.Drawing.Size(208, 32); this.cmdLoad.TabIndex = 0; this.cmdLoad.Text = "開啟圖片檔案(最好圖片要大)"; this.cmdLoad.Click += new System.EventHandler(this.cmdLoad_Click); this.label1.AutoSize = true; this.label1.Location = new System.Drawing.Point(16, 48); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(42, 17); this.label1.TabIndex = 1; this.label1.Text = "未優化"; this.panel1.AutoScroll = true; this.panel1.BackColor = System.Drawing.SystemColors.Window; this.panel1.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; this.panel1.Location = new System.Drawing.Point(8, 72); this.panel1.Name = "panel1"; this.panel1.Size = new System.Drawing.Size(696, 224); this.panel1.TabIndex = 2; this.panel1.Paint += new System.Windows.Forms.PaintEventHandler(this.panel1_Paint); this.label2.AutoSize = true; this.label2.Location = new System.Drawing.Point(16, 312); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(29, 17); this.label2.TabIndex = 1; this.label2.Text = "優化"; this.panel2.AutoScroll = true; this.panel2.BackColor = System.Drawing.SystemColors.Window; this.panel2.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; this.panel2.Location = new System.Drawing.Point(8, 336); this.panel2.Name = "panel2"; this.panel2.Size = new System.Drawing.Size(696, 224); this.panel2.TabIndex = 2; this.panel2.Paint += new System.Windows.Forms.PaintEventHandler(this.panel2_Paint); this.AutoScaleBaseSize = new System.Drawing.Size(6, 14); this.ClientSize = new System.Drawing.Size(720, 573); this.Controls.Add(this.panel1); this.Controls.Add(this.label1); this.Controls.Add(this.cmdLoad); this.Controls.Add(this.label2); this.Controls.Add(this.panel2); this.Name = "frmViewImg"; this.Text = "顯示圖片"; this.Resize += new System.EventHandler(this.frmViewImg_Resize); this.ResumeLayout(false); } [STAThread] static void Main() { Application.Run(new frmViewImg()); } private void frmViewImg_Resize(object sender, System.EventArgs e) { panel1.Width = this.Width - 30 ; panel1.Height = (this.ClientSize.Height - 48 ) /2 - 26; label2.Top = panel1.Bottom + 2 ; panel2.Top = label2.Bottom + 2 ; panel2.Width = panel1.Width ; panel2.Height = panel1.Height ; } private System.Drawing.Image myImage ; private void cmdLoad_Click(object sender, System.EventArgs e) { using( System.Windows.Forms.OpenFileDialog dlg = new OpenFileDialog()) { dlg.CheckFileExists = true; dlg.Filter = "BMP,JPG,GIF,PNG圖片|*.bmp;*.jpg;*.jpeg;*.png"; if( dlg.ShowDialog( this ) == System.Windows.Forms.DialogResult.OK ) { myImage = System.Drawing.Image.FromFile( dlg.FileName ); panel1.AutoScrollMinSize = myImage.Size ; panel2.AutoScrollMinSize = myImage.Size ; this.Refresh(); } } } // 未優化的繪製圖片 private void panel1_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { if( myImage != null) { e.Graphics.DrawImage( myImage , panel1.AutoScrollPosition.X , panel1.AutoScrollPosition.Y , myImage.Size.Width , myImage.Size.Height ); } } // 優化的繪製圖片 private void panel2_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { if( myImage != null) { e.Graphics.DrawImage( myImage , e.ClipRectangle , new System.Drawing.Rectangle( e.ClipRectangle.X - panel2.AutoScrollPosition.X , e.ClipRectangle.Y - panel2.AutoScrollPosition.Y , e.ClipRectangle.Width , e.ClipRectangle.Height ) , System.Drawing.GraphicsUnit.Pixel ); } } }//public class frmViewImg : System.Windows.Forms.Form }