在C#界面编程中,通常在大量的绘制和添加控件的过程中出现闪烁的问题,常见的闪烁的原因很容易理解:
当我们添加控件时触发WM_PAINT消息,导致窗体需要重绘。步骤大致如下:
- 使用窗体的背景色擦除窗口表面
- 绘制窗体图像
- 绘制其上的控件和图像
当我们的主窗口的内容或大小改变,都会触发Paint事件重绘。该操作会使画面重新刷新一次,而每次刷新过程中,每一个图元(跟图像显示有关的元器件)都会重绘并显示到窗口,而刷新的时间是有存在差别的,有可能出现闪烁现象。 闪烁大多现象大多出现在大量图像显示的情况,例如我们的LISTVIEW中采用大图模式加载等
如果新添加的控件较多或因为其他原因导致这些操作不在同一时间段完成的话,我们就会先看到背景色然后看到图像。或者是在每次添加控件的时候都触发刷新导致窗体重复刷新重绘,这是就产生了背景和控件的交替显示,也就是我们看到的闪烁问题,这个问题的解决方式有多种,复制一份控件在当前控件加载期间顶替一下的方式没有太大的技术性可言,我们主要说另外两种比较好的方法:双缓冲和虚拟模式。
起始这两种方式的基本原理是一样的,我们知道导致闪烁的原因是内存加载导致顺序出现差别,而解决办法也是尽量让所有的图像都充分准备好,尽量一次性的全部绘制完毕。
双缓冲
ControlStyles是C#中几乎所有的界面控件都支持的枚举样式,其枚举值如下:
成员名称 | 说明 |
---|---|
ContainerControl | 如果为 true,则控件是类似容器的控件。 |
UserPaint | 如果为 true,控件将自行绘制,而不是通过操作系统来绘制。 如果为 false,将不会引发 Paint 事件。 此样式仅适用于派生自 Control 的类。 |
Opaque | 如果为 true,则控件被绘制为不透明的,不绘制背景。 |
ResizeRedraw | 如果为 true,则在调整控件大小时重绘控件。 |
FixedWidth | 如果为 true,则自动缩放时,控件具有固定宽度。 例如,如果布局操作尝试重新缩放控件以适应新的Font,则控件的 Width 将保持不变。 |
FixedHeight | 如果为 true,则自动缩放时,控件具有固定高度。 例如,如果布局操作尝试重新缩放控件以适应新的Font,则控件的 Height 将保持不变。 |
StandardClick | 如果为 true,则控件将实现标准 Click 行为。 |
Selectable | 如果为 true,则控件可以接收焦点。 |
UserMouse | 如果为 true,则控件完成自己的鼠标处理,因而鼠标事件不由操作系统处理。 |
SupportsTransparentBackColor | 如果为 true,控件接受 alpha 组件小于 255 的 BackColor 以模拟透明。 仅在 UserPaint 位设置为 true并且父控件派生自 Control 时才模拟透明。 |
StandardDoubleClick | 如果为 true,则控件将实现标准 DoubleClick 行为。 如果 StandardClick 位未设置为 true,则忽略此样式。 |
AllPaintingInWmPaint | 如果为 true,控件将忽略 WM_ERASEBKGND 窗口消息以减少闪烁。 仅当 UserPaint 位设置为 true 时,才应当应用该样式。 |
CacheText | 如果为 true,控件保留文本的副本,而不是在每次需要时从 Handle 获取文本副本。 此样式默认为false。 此行为提高了性能,但使保持文本同步变得困难。 |
EnableNotifyMessage | 如果为 true,则为发送到控件的 WndProc 的每条消息调用 OnNotifyMessage 方法。 此样式默认为false。 EnableNotifyMessage 在部分可信的情况下不工作。 |
DoubleBuffer | 如果为 true,则绘制在缓冲区中进行,完成后将结果输出到屏幕上。 双重缓冲区可防止由控件重绘引起的闪烁。 如果将 DoubleBuffer 设置为 true,则还应当将 UserPaint 和 AllPaintingInWmPaint 设置为true。 |
OptimizedDoubleBuffer | 如果为 true,则该控件首先在缓冲区中绘制,而不是直接绘制到屏幕上,这样可以减少闪烁。 如果将此属性设置为 true,则还应当将 AllPaintingInWmPaint 设置为 true。 |
UseTextForAccessibility | 指定该控件的 Text 属性的值,如果已设置,则可确定该控件的默认 Active Accessibility 名称和快捷键。 |
我们可以通过调用Control.SetStyle()设置控件的样式,其实最好还是在初始化中设置
//设置双缓冲方式 当然这几个枚举元素也可以单独使用SetStyle绘制 就是得写三句,麻烦
public void MyControl()
{
this.SetStyle(ControlStyles.DoubleBuffer | //双缓冲
ControlStyles.UserPaint | //自绘制
ControlStyles.AllPaintingInWmPaint, //忽略擦出消息减少闪烁
true);
this.UpdateStyles(); //更新
}
双缓冲的目的尽量快的输出图像,使输出在一个刷新周期内完成。双缓冲模式下窗口的绘制不是通过操作系统来绘制,并且采用内存缓冲的方法,把将要输出的内容一起准备好然后一次性输出到窗体上,从而减少了闪烁。
然而问题还是存在的,具体的原因不了解,但是因为双缓冲这个准备的过程,控件在刷新时会出现黑块,没错真的就是一个小的闪烁而过的黑块。可以自己试一下看一看效果。
当我们对双缓冲的原理比较理解的时候,我们也可再自己的代码中实现双缓冲。双缓冲的原理是通过在内存中布局好图像,使得图像尽量一次性加载完毕。那么我们可以有一个控件的画布 MyC,在绘制图像时创建内存画布MemC,所有的绘制操作都在内存中,当绘制完毕之后将图像传输给MyC。
虚拟模式
虚拟模式主要用来应对如LISTVIEW这种多项子元素的控件,大量添加新元素时闪烁非常的严重和凡人,导致此问题出现的原因依旧是绘制的延迟。LISTVIEW的的子项都存储在Items属性中,该属性继承于Collection,绘制时会遍历需要的项进行绘制,由于我们采用动态添加的方式,其中我们的处理或加载导致项与项的添加存在时间差,如果我们在添加新项时没有时间差那么是不会出现闪烁的。
为此依旧是采用内存缓冲的原理,开辟内存空间存储我们所有的项,当所有子项添加完毕之后再调用控件显示,这样子项之间就不会存在时间差,一次性的绘制到了界面上。
通常是采用List<T>方式存储子项,拿LISTVIEW举例子
public List<ListViewItem> CurrentCacheItemsSource;
public InitVirtua() //初始化虚拟模式
{
CurrentCacheItemsSource = new List<ListViewItem>(); //创建虚拟模式需要的列表, 存储所有的子项
listView1.VirtualMode = true; //开启虚拟模式
listView1.VirtualListSize = 0; //设置要加载ListViewItem的总量 不得大于我们的list中项的数目 否则获取元素出错
listView1.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(listView_RetrieveVirtualItem); //绑定项获取函数 从list中获取项
}
//虚拟模式的绘制函数 通过index获取我们列表中的元素进行绘制
private void listView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (this.CurrentCacheItemsSource.Count < e.ItemIndex )
{
return;
}
e.Item = this.CurrentCacheItemsSource[e.ItemIndex];
}
//自己的加载函数 将所有的子项添加到list中
private void Add()
{
listView1.VirtualListSize = 0; //先将虚拟列表大小置0
CurrentCacheItemsSource.Clear(); //清空列表
ListViewItem lvi=null;
for(int i=0;i<100;i++) //添加新数据
{
lvi = new ListViewItem(new string[] { i.ToString(),(i*i).ToString()});
CurrentCacheItemsSource.Add(lvi);
}
//添加完毕 设置虚拟列表大小为缓存列表大小
listView1.VirtualListSize = CurrentCacheItemsSource.Count;
//刷新控件显示
listView1.Invalidate();
}
该方法的关键在于将所有的元素存储在换缓冲区中,然后换取元素时从缓冲区中直接取,省略了元素处理中间的时间差,所以不会产生闪烁。然而该方法依旧存在弊端,当大量加载时我们的数据实际上是不显示的,也就是在界面上没有提示,此时可以考虑分段加载或双缓冲的结合运用。
不难想象,当我们理解这个原理,同样也可以将这个思想运用到我们的编程中,就拿DATAGRIDVIEW来说,我们不是也可以同样的自己实现类似于虚拟模式的功能?当然我们也可以将虚拟模式结合异步的方式进行处理和显示,总之方法总比问题多,多查查多试试总会发现新大陆