公司需要我寫幾個GUI程序,讓虛擬機(guest)內部可以控制虛擬機(host)外部的硬件。
控制外部的硬件的方法就是開一個串口,這樣虛擬機與宿主機就可以相互通訊,此時就可以讓虛擬機發送命令,宿主機執行命令,并返回結果
我需要一行行地展示內容,比如這樣:

使用的是 WinForm,很容易找到 FlowLayoutPanel這個組件,結果其效果可謂是大跌眼鏡:
- 畫面殘影

- 畫面出現大量空白

這兩種現象,微軟官方稱為flicker,中文翻譯為閃爍??梢砸姽俜轿臋n:

可見微軟也承認自己的東西有這個毛病了,所以微軟我xxx。
我后面和同學聊了下這個問題,他說Qt也有這個毛病,我幾乎沒用過Qt,所以不知道具體是什么樣子的。
查了很多方法,網上的說法清一色的是設置DoubleBuffered,算是有點用處,結果就是“殘影”沒了,大量空白出現了,拖動條也變得卡頓了,真就給抄成習慣了……
然而瀏覽器、文件管理器的拖動條是正常的,這說明這個問題是可以被解決的。
順便一提,拖動界面時可以出現界面刷新率低的問題,刷新率低到10也可以,但絕對不能出現閃爍問題。降低幀率似乎就是瀏覽器的做法,這也提醒我在界面繪制完成之前不要進行下一次繪制。
公司里沒有人會 C#,所以也沒有人可問,沒辦法,只能另尋它路。
想法一:逆向
首先想到的是逆向。瀏覽器不值得看,因為其主要界面大概率不是,文件管理器大概率可以。下載了幾個界面分析軟件,包括 SPY++,GUI-wizard等等,拿到的窗口的類名是沒有見過的,所以放棄了這種做法
想法二:找其他開源框架
問GPT,推薦了ReaLTaiizor,SunnyUI,CSharpSkin之類的,只有ReaLTaiizor可以看到源代碼,就下載它來用了。而它確實沒有出現這個閃爍的問題。
雖然更換到這個開源框架也行,但是發現它自帶的滾動界面不能滿足我的要求,而且目前也已經實現了所有功能了,只差這個問題就可以提交給測試了,我希望快點做完,于是決定參考其代碼做一個簡單的組件。
ReaLTaiizor 對滾動界面的實現是:手寫界面更新邏輯。也就是說重寫了OnPaint方法,自己定義了一套繪制邏輯,要畫線就調用劃線的方法直接操作界面,要繪圖就就調用繪圖的方法直接操作界面……
想法三:參考ReaLTaiizor
我需要的是一個組件而不是一個GUI庫,基于這樣的想法,我確定了這個組件必須做成什么樣子的:
- 表格形式,每一行的高度相同,每一列的寬度可以自定義,每一格中存儲的內容可以是圖片或者文字,文字需要支持換行。和本文第一張圖相似的表格就行了。圖片和文字就夠了,拿兩張圖片,再綁定一個回調就可以做一個開關,這樣按鈕也有了
- 最重要的一點,絕對不能夠閃爍,且滾動條需要與界面顯示同步
- 需要一個回調,回調的參數是點擊到的行,點擊到的列,然后該行綁定到的對象,這一行的內容
- 由3引申出來的,即每一行都可以綁定一個對象
- 只支持上下滾動,左右滾動不支持
- 可以增加行、更新行或刪除行,操作之后界面必須體現出來
所以就開始抄這個源代碼了。因為是公司的代碼所以不好貼出來,總體分為這幾步:
- 基本驗證
我不知道手寫界面繪制能否解決問題,所以還是驗證了一下。驗證的方法很簡單,每次鼠標滾輪事件觸發了,就給界面換一個顏色,手機拍攝這整個過程,逐幀播放,檢查是否有閃爍的問題。微軟做的滾動條沒什么問題,可以直接用,所以我就直接用這個滾動條了。
驗證之后,發現正常,所以繼續實現了。
- 界面顯示與同步滾動條與界面顯示位置同步
界面顯示就是每一次鼠標滾動滾輪,或者拖動滾動條,都調用界面刷新方法,將界面刷新,手動的把圖片、文字刷新上去。
前文提到的“界面繪制完成之前不要進行下一次繪制”也在代碼中實現了,方法很簡單,添加一個bool isupdating,進入OnPaint之前檢查它的值,為false就設置為true,然后在結束的時候設置回false;為true就直接返回,放棄這一回繪制。雖然可能沒有用吧,但萬一呢?
界面位置與滾動條位置同步問題,其實就是一個比例轉換的問題,總體就是一個公式:
關于這個公式,網上有一篇博客講的不錯。
http://www.rzrgm.cn/lesliexin/p/13440927.html
雖然講的不錯,但是他沒有解決閃爍問題,源代碼我下載過來試了,評論里也有人說他沒有解決這個問題

- 開啟DoubleBuffered
其實手寫了界面繪制之后,發現閃爍問題依然沒有解決。此時我猜想是沒有開啟DoubleBuffered導致的。開啟之后,問題解決了。
到此我猜想,DoubleBuffered的語義確實是微軟描述的那樣:界面先繪制到緩沖區,然后在一口氣把緩沖區的內容顯示到界面上。
但是為什么FlowLayoutPanel開啟了這個功能,效果也沒有多好?下班后我逆向了FlowLayoutPanel的源代碼,發現它壓根沒有走OnPaint的繪制邏輯,所以DoubleBuffered應該是無效的。
到此界面效果就是下圖了:

錄制滾動界面的視頻,逐幀播放,效果都與上圖相似。當然,這個界面依然還有問題,見問題4。
- 實現回調
沒什么好說的,就是將點擊位置轉換為行坐標與列坐標
- 界面模糊與字體鋸齒嚴重
這界面模糊可以見這篇博客。
http://www.rzrgm.cn/Wonderful-Life/p/10250575.html
字體抗拒齒嚴重,GPT說這么做就行了,事實證明這確實有效:
protected override void OnPaint(PaintEventArgs e)
{
Debug.Assert(rows.Count == bindObj.Count);
base.OnPaint(e);
if (rows.Count == 0) return;
double bar_position = GetScrollBarPosition(vsb.Value);
Graphics graphics = e.Graphics;
// graphics.TextRenderingHint = System.Drawing.Text.TextRendering
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
Rectangle rect = new Rectangle(0, 0, base.Width, base.Height);
另一個嘗試
這個組件只支持表格形式,我后面想試試能不能做一個更加通用的組件,即每行一個,但這一行的內容可以是WinForm中任何一種組件。
簡單寫了一個Demo,其基本想法是,不在界面以內的不顯示,在界面以內的計算其位置并顯示,繪制的方法用默認的。
效果如下圖。它確實不閃爍了,但是界面變得割裂了!肉眼可見的界面從上面往下面刷新!

所以如果WinForm下想要徹底解決閃爍問題,其工作量估計和做一個GUI庫差不多了。
浙公網安備 33010602011771號