公司需要我寫幾個GUI程序,讓虛擬機(guest)內部可以控制虛擬機(host)外部的硬件。

控制外部的硬件的方法就是開一個串口,這樣虛擬機與宿主機就可以相互通訊,此時就可以讓虛擬機發送命令,宿主機執行命令,并返回結果

我需要一行行地展示內容,比如這樣:

image

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

  1. 畫面殘影

image

  1. 畫面出現大量空白

image

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

https://learn.microsoft.com/zh-cn/dotnet/api/system.windows.forms.flowlayoutpanel?view=windowsdesktop-8.0

image

可見微軟也承認自己的東西有這個毛病了,所以微軟我xxx。

我后面和同學聊了下這個問題,他說Qt也有這個毛病,我幾乎沒用過Qt,所以不知道具體是什么樣子的。

查了很多方法,網上的說法清一色的是設置DoubleBuffered,算是有點用處,結果就是“殘影”沒了,大量空白出現了,拖動條也變得卡頓了,真就給抄成習慣了……

然而瀏覽器、文件管理器的拖動條是正常的,這說明這個問題是可以被解決的。

順便一提,拖動界面時可以出現界面刷新率低的問題,刷新率低到10也可以,但絕對不能出現閃爍問題。降低幀率似乎就是瀏覽器的做法,這也提醒我在界面繪制完成之前不要進行下一次繪制。

公司里沒有人會 C#,所以也沒有人可問,沒辦法,只能另尋它路。

想法一:逆向

首先想到的是逆向。瀏覽器不值得看,因為其主要界面大概率不是,文件管理器大概率可以。下載了幾個界面分析軟件,包括 SPY++,GUI-wizard等等,拿到的窗口的類名是沒有見過的,所以放棄了這種做法

想法二:找其他開源框架

問GPT,推薦了ReaLTaiizor,SunnyUI,CSharpSkin之類的,只有ReaLTaiizor可以看到源代碼,就下載它來用了。而它確實沒有出現這個閃爍的問題。

雖然更換到這個開源框架也行,但是發現它自帶的滾動界面不能滿足我的要求,而且目前也已經實現了所有功能了,只差這個問題就可以提交給測試了,我希望快點做完,于是決定參考其代碼做一個簡單的組件。

ReaLTaiizor 對滾動界面的實現是:手寫界面更新邏輯。也就是說重寫了OnPaint方法,自己定義了一套繪制邏輯,要畫線就調用劃線的方法直接操作界面,要繪圖就就調用繪圖的方法直接操作界面……

想法三:參考ReaLTaiizor

我需要的是一個組件而不是一個GUI庫,基于這樣的想法,我確定了這個組件必須做成什么樣子的:

  1. 表格形式,每一行的高度相同,每一列的寬度可以自定義,每一格中存儲的內容可以是圖片或者文字,文字需要支持換行。和本文第一張圖相似的表格就行了。圖片和文字就夠了,拿兩張圖片,再綁定一個回調就可以做一個開關,這樣按鈕也有了
  2. 最重要的一點,絕對不能夠閃爍,且滾動條需要與界面顯示同步
  3. 需要一個回調,回調的參數是點擊到的行,點擊到的列,然后該行綁定到的對象,這一行的內容
  4. 由3引申出來的,即每一行都可以綁定一個對象
  5. 只支持上下滾動,左右滾動不支持
  6. 可以增加行、更新行或刪除行,操作之后界面必須體現出來

所以就開始抄這個源代碼了。因為是公司的代碼所以不好貼出來,總體分為這幾步:

  1. 基本驗證

我不知道手寫界面繪制能否解決問題,所以還是驗證了一下。驗證的方法很簡單,每次鼠標滾輪事件觸發了,就給界面換一個顏色,手機拍攝這整個過程,逐幀播放,檢查是否有閃爍的問題。微軟做的滾動條沒什么問題,可以直接用,所以我就直接用這個滾動條了。

驗證之后,發現正常,所以繼續實現了。

  1. 界面顯示與同步滾動條與界面顯示位置同步

界面顯示就是每一次鼠標滾動滾輪,或者拖動滾動條,都調用界面刷新方法,將界面刷新,手動的把圖片、文字刷新上去。

前文提到的“界面繪制完成之前不要進行下一次繪制”也在代碼中實現了,方法很簡單,添加一個bool isupdating,進入OnPaint之前檢查它的值,為false就設置為true,然后在結束的時候設置回false;為true就直接返回,放棄這一回繪制。雖然可能沒有用吧,但萬一呢?

界面位置與滾動條位置同步問題,其實就是一個比例轉換的問題,總體就是一個公式:

\[\frac{界面顯示的位置}{界面的總高度}=\frac{滾動條位置}{滾動條總長度} \]

關于這個公式,網上有一篇博客講的不錯。

http://www.rzrgm.cn/lesliexin/p/13440927.html

雖然講的不錯,但是他沒有解決閃爍問題,源代碼我下載過來試了,評論里也有人說他沒有解決這個問題

image

  1. 開啟DoubleBuffered

其實手寫了界面繪制之后,發現閃爍問題依然沒有解決。此時我猜想是沒有開啟DoubleBuffered導致的。開啟之后,問題解決了。

到此我猜想,DoubleBuffered的語義確實是微軟描述的那樣:界面先繪制到緩沖區,然后在一口氣把緩沖區的內容顯示到界面上。

但是為什么FlowLayoutPanel開啟了這個功能,效果也沒有多好?下班后我逆向了FlowLayoutPanel的源代碼,發現它壓根沒有走OnPaint的繪制邏輯,所以DoubleBuffered應該是無效的。

到此界面效果就是下圖了:

image

錄制滾動界面的視頻,逐幀播放,效果都與上圖相似。當然,這個界面依然還有問題,見問題4。

  1. 實現回調

沒什么好說的,就是將點擊位置轉換為行坐標與列坐標

  1. 界面模糊與字體鋸齒嚴重

這界面模糊可以見這篇博客。

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,其基本想法是,不在界面以內的不顯示,在界面以內的計算其位置并顯示,繪制的方法用默認的。

效果如下圖。它確實不閃爍了,但是界面變得割裂了!肉眼可見的界面從上面往下面刷新!

image

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