立即显示WinForms使用如图所示()事件

下载demo - 12.6 KB

介绍

我最讨厌的事情之一就是表单不能立即出现。您一直可以看到:用户单击一个按钮,然后等待几秒钟,直到预期的UI出现。这通常是由于新创建的表单在其Load()事件处理程序中执行一些耗时的(阻塞的)任务(或者在非-中执行)。NET世界,WM_INITDIALOG消息处理程序)。除了糟糕的UI设计(永远不要让用户疑惑发生了什么!)之外,它还会带来一些真正不希望看到的后果:单击按钮不会立即产生效果,用户会倾向于再次单击按钮,这可能会导致操作被调用两次。

从我最早的Windows编程开始,我总是在我的表单(对话框)中实现“即时反馈”。我的技术随着Windows API的发展而发展;我将讨论我的旧技术(仍可在. net中使用),并以使用. net WinForms api的当前实现结束。

我们的目标

创建一个简单、可靠的机制来进行后加载处理,以确保表单在后加载处理开始之前被“完全呈现”。这意味着表单本身中的所有控件以及所有子控件(在对话框模板中)都按照用户预期的方式绘制。

背景

这是一个非常基本的技巧;WinForms编程新手应该能够实现它。

了解表单(又称窗口、对话框)的启动顺序非常有用:http://msdn.microsoft.com/en-us/library/86faxx0d%28VS.80%29.aspx。

大多数这些事件都有直接的Windows message (WM_*)类似物。但是,. net添加了System.Windows.Forms.Form。所显示的事件——没有相应的Windows消息——并且该事件是执行post-Load()处理的一种相对干净的方式的基础。(MSDN文档关于show()事件:在这里阅读)。

使问题复杂化的是消息的异步性、不可完全预测的特性。在创建一个窗口(以及它的子窗口)时,各个窗口消息的确切顺序在实例之间是不同的。当然,每个窗口的消息每次都以相同的顺序出现,但是父/子消息的顺序是不可预测的,从而创建了竞争条件。最不幸的结果是,有时对话框会正确呈现,有时则不能。消除这种不确定性——确保可预测性——是这一解决方案的重要组成部分。

最后,在表单和所有子元素完全呈现之后执行阻塞处理非常重要。在渲染过程中阻塞UI线程(和消息队列),会导致一些相当难看的UI,看起来就像你的应用崩溃了!

请注意表单的框架和未剪切的客户区域是如何呈现的,但是某些子控件的客户区域仍然在表单“下面”显示UI。还要注意,listbox是如何呈现的,而其他控件(groupbox、combobox)没有呈现(我还没有研究为什么会这样)。

下面是它完全渲染后的样子:

解决方案:在。net之前

(如果你在。net之前写Windows代码——不管有没有MFC——这看起来应该很熟悉。)

以下是创建窗口时生成的消息的简化顺序:

隐藏,复制CodeWM_CREATE (window) or WM_INITDIALOG (dialogbox)

WM_SHOWWINDOW

WM_ACTIVATE (wParam has WA_* state)

在那些日子里,我做了这样的事情:

隐藏,复制Code#define WMUSER_FIRSTACTIVE (WM_USER+1)
bool m_bSeenFirstActivation = false;
virtual void DoPostLoadProcessing();

LRESULT WndProc(msg, wparam, lparam)
{
switch(msg)
{
case WM_ACTIVATE:
if((wparam==WA_ACTIVE) && !m_bSeenFirstActivation)
{
m_bSeenFirstActivation = true;
PostMessage(m_hWnd, WMUSER_FIRSTACTIVE);
}
break;
case WMUSER_FIRSTACTIVE:
DoPostLoadProcessing(); // Derived classes override this
break;
}
}

其原理是,在收到初始激活信息后,通过发信息给自己,我可以确信:

对话框已完全呈现,后加载处理在对话框的UI线程上异步执行:

使用PostMessage()可以确保它是异步的。发布的消息被添加到队列的末尾,并在所有挂起的(与ui相关的)消息被处理之后被处理。在对话框的UI线程中做一些事情是很重要的——如果你试图从UI线程以外的线程操作许多控件,它们会变得非常不舒服。

有时我会这样做:

隐藏,复制Code#define IDT_FIRSTACTIVE 0x100
bool m_bSeenFirstActivation = false;
virtual void DoPostLoadProcessing();

LRESULT WndProc(msg, wparam, lparam)
{
switch(msg)
{
case WM_ACTIVATE:
if((wparam==WA_ACTIVE) && !m_bSeenFirstActivation)
{
m_bSeenFirstActivation = true;
SetTimer(m_hWnd, IDT_FIRSTACTIVE, 50, NULL);
PostMessage(m_hWnd, WMUSER_FIRSTACTIVE);
}
break;
case WM_TIMER:
if(wParam == IDT_FIRSTACTIVE)
{
KillTimer(m_hWnd, IDT_FIRSTACTIVE);
DoPostLoadProcessing(); // Derived classes override this
}
break;
}
}

这里的原理本质上是一样的:在初始激活时设置一个快速(50ms)计时器。WM_TIMER消息异步发生并且有一些额外的延迟。计时器会立即终止(我们只需要第一个计时器消息),然后调用派生类的DoPostLoadProcessing()。(是的,我知道计时器<55毫秒对个人电脑没用。)

这两种技术都工作得很好,并且仍然可以在。net中使用,尽管它们很混乱。

解决方案:在。net中完成

最基本的

. net添加的show()事件大大简化了事情。现在,从理论上讲,您所需要做的就是处理show()事件并在那里进行处理:

隐藏,复制Codevoid MyForm_Shown(object sender, EventArgs e)
{
// Do blocking stuff here
}

简单,是吧?嗯,差不多。结果显示,show()事件在表单的所有子控件完全呈现之前被触发,因此您仍然有竞争条件。我的解决方案是调用Application.DoEvents()在做任何额外的后处理之前清除消息队列:

隐藏,本编解码器cdccc4

更完整的解决方案

上面基本解决方案的问题是必须记住在每个表单的show()事件处理程序中调用Application.DoEvents()。虽然这确实是一个小麻烦,但是我选择了更进一步的解决方案,实现了一个基本对话框类,它处理show()事件,调用DoEvents(),然后调用它自己的事件:

隐藏,复制Codepublic class BaseForm : Form
{
public delegate void LoadCompletedEventHandler();
public event LoadCompletedEventHandler LoadCompleted;

public BaseForm()
{
    this.Shown += new EventHandler(BaseForm_Shown);
}

void BaseForm_Shown(object sender, EventArgs e)
{
    Application.DoEvents();
    if (LoadCompleted != null)
        LoadCompleted();
}

}

并且,派生窗体仅仅实现了一个LoadCompleted()处理程序:

隐藏,复制Codethis.LoadCompleted += new
FormLoadCompletedDemo.BaseForm.LoadCompletedEventHandler(
this.MyFormUsingBase_LoadCompleted);

...

private void MyFormUsingBase_LoadCompleted()
{
// Do blocking stuff here
}

和…奖金!: LoadCompleted()出现在Visual Studio的属性/事件窗格中:

(我将事件命名为LoadCompleted,以便它会立即出现在事件窗格中的Load事件之后,使其更容易查找。)

有了它,您就可以在LoadCompleted()中完成所有的长期操作,并且确信在用户等待时基本UI将被完全呈现。当然,你仍应遵循好的UI实践,如显示WaitCursor也许禁用控制直到他们可用的(一个有用的视觉提示用户),甚至显示progressbar真的长时间等待(例如:显示一个选框progressbar在SQL调用,5 +秒)。

演示项目

附加的VS2008/ c#项目演示了这种行为和解决方案。它实现了一个带有四个按钮的表单:

这四个按钮都做了同样的事情,这是弹出一个子对话框,其中包含:

一个填充了50K项的列表框。这个填充会阻塞UI线程几秒钟,这允许应用程序演示问题的解决方案。其他一些控件,主要是组合框。不管出于什么原因,组合框特别容易由于阻塞代码而呈现不完整。

每个子窗体在其Load()事件中将WaitCursor设置为正在发生的事情的可视化反馈。

但是,每个按钮都会导致不同的表单加载行为:

在加载事件中执行处理——listbox在表单的Load()事件处理程序中被加载,导致表单直到listbox完全加载(错误)才出现。打开没有DoEvents的表单——listbox在表单的show()事件处理程序中被加载,但没有调用Application.DoEvents(),导致表单出现部分呈现,直到listbox加载完成(非常糟糕)。打开Form with DoEvents——listbox在表单的show()事件处理程序中被加载,并调用Application.DoEvents(),导致表单显示为完全呈现,直到listbox加载完成(很好!!)Open Form派生自BaseForm -与Open Form *相同,但使用基类和自定义事件实现。

结论

就像Windows(以及生活中!)中的许多东西一样,这个问题的解决方案非常简单,但要想弄清楚它需要一些尝试和错误。我希望上面概述的解决方案可以节省您的时间,并帮助您创建响应性更好的ui。

历史

2010年2月12日:初始版本

本文转载于:http://www.diyabc.com/frontweb/news3687.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值