C#中,有什么技术可以将多线程应用在界面绘制上?
因为.net中提供的gui库不允许跨线程操作,所以当用户界面上的控件一多起来之后,gui反映速度就会急剧下降,双缓冲等一般手段完全不起作用。现在有2个问题请教各位:
当界面上会有很多控件时,
1. 如果这些控件有部分分布在可见区域之外,如何取消它们的绘制?(因为不在可见区域内,画出来也看不见)
2. 如果这些控件全部都处于可见状态,如何提高绘制速度?
另外,提供一个比较具体的例子,属于第一种情况:
class 复合控件 : UserControl
{
private System.Windows.Forms.许多控件;
private void InitializeComponent()
{
许多控件 = new 许多控件();
许多控件.许多属性 = 各自的属性设置;
this.Controls.Add( 各种容器 );
各种容器.Controls.Add( 许多控件 );
}
public 复合控件()
{
InitializeComponent(); // 这个过程相当耗时间
}
}
class Form1 : Form
{
private System.Windows.Forms.Form里的许多控件;
private void InitializeComponent() // 这个过程相当耗时间
{
Form里的许多控件 = new Form里的许多控件();
Form里的许多控件.许多属性 = 各自的属性设置;
this.Controls.Add( Form里的各种容器 );
Form里的各种容器.Controls.Add( Form里的许多控件 );
}
public Form1()
{
InitializeComponent();
this.某容器控件.SuspendLayout();
for (int i=0; i <某个数; ++i)
{
复合控件 c = new 复合控件();
c.Location = new Point( i * ( c.Width + 3 ), 0 );
// 这里是横向排列扩展。
// 根据需要也可能会是纵向排列的控件,
// 但是一定都会使后面加入的控件超出可见区域,
// 这时候外面的容器控件会出现滚动条。
this.某容器控件.Controls.Add( c );
}
this.某容器控件.ResumeLayout();
// 前面这个循环相当耗时,当 Form1 还没有显示出来的时候(也就是说
// 类似的工作是在 Form1 的构造函数或者 Load 事件中进行),可以用
// Splash 之类的东西挡一挡,但是这个过程太长的话用户会产生反感。
// 而且,最重要的是,当主界面的 gui 上加入了这么多控件之后,程序
// 的 gui 重绘明显会变得很慢,即使开了双缓冲也起不到明显的效果,
// 甚至是完全看不出效果。但是这个过程又不能用 BackgroundWorker
// 之类的手段放到后台线程完成。
// 请问诸位高手有什么好的办法可以解决吗?
}
}
[解决办法]
因为.net中提供的gui库不允许跨线程操作
------------------
跨线程更新ui在2.0中需要用invoke方法
private void Form1_Load(object sender, EventArgs e)
{
new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(this.DoThread)).Start();
}
private void DoThread(object obj)
{
string[] str=new string[]{ "aaa ", "bbb "};
if (this.label1.InvokeRequired)
{
this.label1.Invoke(new SetLableText(DoInvoke), new object[] { str[0], str[1] });
}
else
this.label1.Text = str[0] + str[1];
}
private delegate void SetLableText(string str1, string str2);
private void DoInvoke(string str1,string str2)
{
this.label1.Text = str1 + str2;
}
[解决办法]
jinjazz(近身剪) 的方法是其一
其二:在线程中激发事件。在用户界面线程下绘制。
[解决办法]
因为不知道你具体的设计是什么,所以无法给出更详细的方案,不过有以下几点可以参考
一、将过多的控件分散到多个页上,而不是在一个页上选择所有的设置
二、如果是涉及到必须放到一个页上,而且很多不一定需要使用控件就可以搞定的东东,应该可以使用自已绘制的方式。象股票软件之类的实时绘制,用控件肯定是不行的
三、所谓的工作线程并不要去用于绘制,因为不但帮不了忙,而且的确如楼上所言会帮倒忙。工作线程一般用于后台计算大量数据时。
四、从你的代码来看绝对不是一个好设计,你可以把你的具体问题拿出来给大家看看吧
[解决办法]
1.在不改变设计的情况下——我指不减少控件数量——有没有方法可以优化这个过程?
没有办法,因为一个控件对应一个窗体,操作系统总要搞很多东东去实现,没办法的
2.以我目前的理解,就算按照楼上的建议将众多控件分散到多个页面也只能解决重画时的效率问题,而无法避免这么多控件同时被集中地创建在主线程产生的瓶颈,是这样吗?
对,也不对,关键看你如何实现,你可以实现成页面加载时再动态加载控件,如果你一定要一次加载完,但的确是会有问题
3.因为集中创建这么多控件都是在 Form1 的构造函数里进行的,这时 Form1 的实例还没有显示出来,那么这时候创建的控件理论上是不会立刻执行它们的绘制代码的对吗?但是实际上会不会呢?
绘制代码当然不会去绘制.你可以去看看windows机制.
4.也就是说这时候占用的时间基本上都是集中的内存等资源的分配上了,这样的理解正确吗?
可以这么说
5.类似这样的一个过程中,在不减少控件数量的情况下,大量创建控件造成的主线程反应迟缓是否是不可避免的?
对
如何你想改良客户体验,可以加一个splash窗体,而且即然是报表,就不应该用控件来做,而是自行绘制,否则当你数据量很大时,那不死掉了呀,而且也只是绘制当前页就好了
[解决办法]
如果许多控件要绘制,这个问题已经与线程没有什么关系了,因为绘这些东西本身就需要这么多的时间。
线程并不是帮助你让计算机加快运行速度的,所以解决问题的思路还需要重新考虑。
[解决办法]
《程序员秘书》让所有人轻轻松松搞开发,详见:http://www.psec.net.cn
上面的文字太多没看完,感觉你的控件是不是出现了嵌套,你自己还没发现
我作了一个电脑监视程序,界面上可以同时显示80台远程电脑的桌面图像,图像是实时动态的。
也就是说,至少用了80个控件,80个线程,但我这个监视程序一点都没有显示慢的感觉,CPU占用才百分之几(这台电脑CPU是2.4G的老电脑)
所以,我感觉你算法是不是有问题。
[解决办法]
“我的目的是想知道有没有办法在·创·建·很多控件的时候不要让主界面失去响应。”
都不要在主线程中创建,在主线程操作确定会“主界面失去响应”的现象
我刚才又查了一下我的监视程序
我程序主界面上有80个pictureBox控件、80个Label控件和80个线程都在一个线程中创建和删除的(不是在主线程中,创建和删除时间可以很短),这么多控件算一屏,一屏与一屏之间在1-30秒任何选择,都没影响。
《程序员秘书》让所有人轻轻松松搞开发,详见:http://www.psec.net.cn
[解决办法]
如果楼主确认如下两点,那么我有个解决办法:
1 根据你说的,你的程序的主窗口有许多控件,且这些控件仅每次程序启动时初始化一次。
2 在程序运行过程中,这些控件进部分需要重绘,不用全部绘制。
如果以上两点成立,那么我就有办法解决。
使用“欢迎窗口”。
实际上,欢迎窗口正式名称是“初始化窗口”
首先你建立一个Form,里面放一个PictureBox,里面放一个你自己认为漂亮的图。
此PictureBox的Dock设置为Fill,把Form的边框属性设置为None,ShowInTaskbar设置为False.
如果可以的话,在PictureBox上叠加一个Label,用来显示初始化信息。
然后,在你对你的主程序窗口编码并调试完成后,手工修改你的主窗口的InitializeComponent方法,把它从主窗口的构造函数中拿出来,变成一个Internal 类型的方法,由外部调用。
然后设置一个类
public sealed class AppContext : ApplicationContext
{
private BackgroundWorker backWork; // 后台线程
private WelcomeForm welcome; // 欢迎窗口
private MainForm mainForm; // 主程序窗口
// 初始化
public AppContext() : base()
{
Control.CheckForIllegalCrossThreadCalls = false;
// 定义后台线程的基本数据
backWork = new BackgroundWorker();
backWork.WorkerReportsProgress = true;
backWork.DoWork = new DoWorkEventHandler(DoWork);
backWork.ProgressChanged +=
new ProgressChangedEventHandler(ReportProcess);
backWork.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(ThreadCompleted);
// 初始化主窗口
welcomeForm = new WelcomeForm();
// 初始化MainForm, 注意,初始化代码应该从
// MainForm的构造函数中取出,否则将非常耗时
mainFrom = new MainForm();
// 将当前主窗口设定为WelcomeForm
// 当程序执行到这行,将显示欢迎窗口。
base.MainForm = welcomeForm;
// 开始执行后台线程
backWork.RunWorkerAsync(mainFrom); // 用后台线程初始化
}
// ApplicationContext,用于WelcomeForm与MainForm切换
// 当WelcomeForm和MainForm调用Close()方法时,
// 将自动调用此方法
protected override void OnMainFormClosed(object sender, EventArgs e)
{
// 判断当时程序主窗口是哪个
if(sender is WelcomeForm) // 主窗口是Welcome
{
// 此时,后台线程调用完毕
// WelcomeForm 已经调用了Close方法
// 将主窗口设定为MainForm
base.MainForm = this.mainForm;
// 显示主窗口
base.MainForm.Show();
// 将后台线程控制对象取消掉
backWork.Dispose();
}
else if(sender is MainForm)
{
// 此时是主窗口关闭时引发的,
// 现在要执行的就是关闭程序
base.MainForm.Dispose();
base.OnMainFormClosed(sender, e);
}
}
// 后台线程执行的方法,用于初始化
public void DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker back = (BackgroundWorker) sender;
MainForm main = (MainForm) e.Argument;
back.ReportProgress(0, "正在初始化,请稍候... ");
main.InitializeComponent(); // 这时才初始化
e.Result = main;
}
// BackgroundWorker 负责调用的方法
// 只有这个方法,后台线程才能访问界面UI
public void ReportProcess(object sender, ProgressChangedEventArgs e)
{
// 后台线程报告进度,
// 此时,主窗体实际上是Welcome,它有个名为statusLable的Label控件
// 用于向客户提供反馈信息
base.MainForm.statusLabel.Text = (string) e.UserState;
}
// 后台线程工作完毕后调用的方法
// 这个方法负责关闭WelcomeForm,打开MainForm
public void ThreadCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if(e.Error != null) // 初始化失败,出现错误
{
// 调用错误处理代码,显示错误消息,退出...
}
mainForm = (MainForm) e.Result;
welcomeForm.Close(); // 关闭欢迎窗口
}
}
上面的思路就是在应用程序启动时需要进行长耗时的初始化工作,则使用欢迎窗口,
如PhotoShop和AutoCAD都是使用这个办法
最重要的就是,在构造函数的第一行必须加入
Control.CheckForIllegalCrossThreadCalls = false;
因为在.Net 2.0 里,由后台线程建立的UI对象,如控件,是不能把引用传递给UI线程使用的,
而这个类的作用就是用后台线程建立并初始化控件集合,所以必须加入这个代码把上面说的这个功能给关闭掉。
这个办法很好用。我写的程序比你的还要变态,我初始化的不是一个窗体,而是20多个窗体。
还有一堆Report组件,这个办法都可以。
[解决办法]
对了,忘了说,应用程序的Main入口方法应该修改
public static void Main(string[] args)
{
// 系统生成的代码
AppContext context = new AppContext();
Application.Run(context);
}
[解决办法]
路过留水!
早在gdi时代,都建议不要在线程中使用gui,不是不可以是程序容易出错.
window在控制程序重绘,如窗体大小改变,遮盖窗体移动.这时主线程需要刷新他,线程部分也要刷新.操作系统也参与进来了,恩,情况变复杂了,谁都想得到DC,但只有一个能够使用,假如主线程拥有了A资源,分线程拥有资源B.主线程要获取资源B,那么他就要等待,分线程要获取线程A,那分线程也要等待,结果...就不停等下去了.这就是.net中.net中提供的gui库不允许跨线程操作的原因了.如果你实在要做,请处理好资源间的关系.
[解决办法]
线程需要修改主线程(如:界面控件)控件,从VS2003的程序转换到VS2005下时,最简单的方法就是,程序所有初始化前添加Control.CheckForIllegalCrossThreadCalls = false;
但如果直接到VS2005下设计,微软还是建议用委托的方法比较好