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程序。
本人现和当前公司的合同快到期,因此现寻求新的雇主,希望能在南京工作,待遇可。有兴趣者可联系我,联系方法为
电子邮件: yyf9989@hotmail.com , 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
}