公司需要我写几个GUI程序,让虚拟机(guest)内部可以控制虚拟机(host)外部的硬件。
控制外部的硬件的方法就是开一个串口,这样虚拟机与宿主机就可以相互通讯,此时就可以让虚拟机发送命令,宿主机执行命令,并返回结果
我需要一行行地展示内容,比如这样:

使用的是 WinForm,很容易找到 FlowLayoutPanel这个组件,结果其效果可谓是大跌眼镜:
- 画面残影

- 画面出现大量空白

这两种现象,微软官方称为flicker,中文翻译为闪烁。可以见官方文档:

可见微软也承认自己的东西有这个毛病了,所以微软我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就直接返回,放弃这一回绘制。虽然可能没有用吧,但万一呢?
界面位置与滚动条位置同步问题,其实就是一个比例转换的问题,总体就是一个公式:
\[\frac{界面显示的位置}{界面的总高度}=\frac{滚动条位置}{滚动条总长度} \]
关于这个公式,网上有一篇博客讲的不错。
https://www.cnblogs.com/lesliexin/p/13440927.html
虽然讲的不错,但是他没有解决闪烁问题,源代码我下载过来试了,评论里也有人说他没有解决这个问题

- 开启DoubleBuffered
其实手写了界面绘制之后,发现闪烁问题依然没有解决。此时我猜想是没有开启DoubleBuffered导致的。开启之后,问题解决了。
到此我猜想,DoubleBuffered的语义确实是微软描述的那样:界面先绘制到缓冲区,然后在一口气把缓冲区的内容显示到界面上。
但是为什么FlowLayoutPanel开启了这个功能,效果也没有多好?下班后我逆向了FlowLayoutPanel的源代码,发现它压根没有走OnPaint的绘制逻辑,所以DoubleBuffered应该是无效的。
到此界面效果就是下图了:

录制滚动界面的视频,逐帧播放,效果都与上图相似。当然,这个界面依然还有问题,见问题4。
- 实现回调
没什么好说的,就是将点击位置转换为行坐标与列坐标
- 界面模糊与字体锯齿严重
这界面模糊可以见这篇博客。
https://www.cnblogs.com/Wonderful-Life/p/10250575.html
字体抗拒齿严重,GPT说这么做就行了,事实证明这确实有效:
C#
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库差不多了。