65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 窗体事件的行为研究

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (13投票s)

2008年5月31日

CPOL

4分钟阅读

viewsIcon

47490

downloadIcon

411

加载时,我们可以在多大程度上依赖 WinForm 事件以特定顺序触发。

At96.png

引言

在我之前的文章(关于生命游戏)中,我犯了一个常见的错误,即期望在代码到达窗体调整大小事件时对象已被实例化。 这在我的测试中运行良好,但正如您将在文章下面的评论中读到的那样,chengkc 发现了空引用异常。 当 chengkc 将 DPI 设置更改回默认的 96 DPI(原为 120 DPI)时,问题就消失了。 本文试图阐明这种现象。

关于 DPI

您可以通过以下方式访问此设置

  1. 显示属性,
  2. 设置选项卡,
  3. 高级按钮,
  4. 常规选项卡。

AdvancedDisplayProperties.png

此属性页允许用户通过将 DPI 从 96 更改为 120 来增加桌面和任务栏图标的大小。 没有明显的理由说明此设置会影响 WinForms 的加载方式。

测试计划

我们想检测窗体启动期间引发的事件序列。 通过查看生成的事件列表并比较每种情况下的列表,我们应该能够收集重要的信息。 然后,这些信息可以用于理解为什么我们需要对有关窗体事件的假设保持谨慎。

代码

每个事件处理程序都必须将一个字符串添加到列表中。

//..
        private List<string> _Events = new List<string>();

        protected void Form1_Activated(object sender, EventArgs e)
        {
            _Events.Add("Form1_Activated");
        }

        protected void Form1_Deactivate(object sender, EventArgs e)
        {
            _Events.Add("Form1_Deactivate");
        }

        protected void Form1_Enter(object sender, EventArgs e)
        {
            _Events.Add("Form1_Enter");
        }
//..

连接这些事件最简单的方法是使用窗体设计器。

        #region Windows Form Designer generated code
//..
        private void InitializeComponent()
        {
//..
            this.SuspendLayout();
//..
            this.Deactivate += new System.EventHandler(this.Form1_Deactivate);
            this.Activated += new System.EventHandler(this.Form1_Activated);
            this.Enter += new System.EventHandler(this.Form1_Enter);
//..
            this.ResumeLayout(false);
            this.PerformLayout();
                }

但是,在使用此应用程序的第一个版本进行测试时,注意到某些事件没有按预期触发。 看来设计器并没有触发所有相关的事件;可能某些事件被“SuspendLayout”方法抑制了。

为了确保我们正确分析这一点,使用另外两种场景测试了事件连接代码。 这三种场景如下

  • 允许设计器连接这些事件。
  • 在调用“InitializeComponent”之前连接所有相关的事件。
  • 在调用“InitializeComponent”之后连接所有相关的事件。

这会将我们的测试计划扩展到至少六组测试结果(2 个 DPI 状态 x 3 个场景)。 为了让读者能够在其他配置上重复测试用例,代码已使用继承进行了重构。 CaptureForm 是我们的基类。

    public class CaptureForm : Form
    {
        private List<string> _Events = new List<string>();

        protected void Form1_Activated(object sender, EventArgs e)
        {
            _Events.Add("Form1_Activated");
        }

        protected void Form1_Deactivate(object sender, EventArgs e)
        {
            _Events.Add("Form1_Deactivate");
        }

        protected void Form1_Enter(object sender, EventArgs e)
        {
            _Events.Add("Form1_Enter");
        }

//..

        public string GetRecordedEvents()
        {
            StringBuilder sb = new StringBuilder();
            foreach (string eventText in _Events.ToArray())
            {
                sb.Append(eventText);
                sb.Append("\r\n");
            }
            return sb.ToString();
        }
    }

处理我们三种场景的窗体首先是 FormBefore

    public partial class FormBefore : CaptureForm
    {
        public FormBefore()
        {
            InitEventHandlers();
            InitializeComponent();
        }

        private void InitEventHandlers()
        {
            this.SuspendLayout();
            this.StyleChanged += new System.EventHandler(this.Form1_StyleChanged);
            this.Deactivate += new System.EventHandler(this.Form1_Deactivate);
            this.Load += new System.EventHandler(this.Form1_Load);
//..
            this.ResumeLayout(false);
        }

    }

然后是 FormAfter

    public partial class FormAfter : CaptureForm
    {
        public FormAfter()
        {
            InitializeComponent();
            InitEventHandlers();
        }

        private void InitEventHandlers()
        {
            this.SuspendLayout();
            this.StyleChanged += new System.EventHandler(this.Form1_StyleChanged);
            this.Deactivate += new System.EventHandler(this.Form1_Deactivate);
            this.Load += new System.EventHandler(this.Form1_Load);
//..
            this.ResumeLayout(false);
        }

    }

最后是 FormDesigner

    public partial class FormDesigner : CaptureForm
    {
        public FormDesigner()
        {
            InitializeComponent();
        }
    }

其中 FormDesigner 在窗体设计器代码中具有事件连接。

为了驱动测试,我们创建一个名为 CaptureForm 的主窗体,其中包含按钮、计时器和文本框。 应请求,此主窗体将循环遍历每个场景并记录事件。 事件列表将并排显示,以方便轻松比较。

在默认的 96 DPI 下,我们得到

At96.png

在 120 DPI 下,我们得到

At120.png

另一件需要提及的事情是,如果特定测试在后续过程中产生不同的结果,我们也会记录这些事件。 但是,上面看到的结果并没有显示超过我们预期的六组。

分析

为了分析我们的结果,我使用了一个众所周知的文件比较工具。

比较使用设计器连接的 96 与 120 DPI

InDesigner.png

比较设计器代码之前使用自定义连接的 96 与 120 DPI

BeforeDesigner.png

比较设计器代码之后使用自定义连接的 96 与 120 DPI

AfterDesigner.png

从这些比较中,我们看到对于 120 DPI,引发了额外的事件。 更具体地说,调整大小事件发生在任何其他事件之前。 如果程序员在调整大小事件处理程序中编写了依赖于在其他事件之一中设置的状态的代码,则会发生异常或逻辑错误。 在使用 96 DPI 进行开发和单元测试时,没有出现明显的问题。 部署后,一些用户会因不同的配置而遇到问题。

结论

首先要指出的是,我们对窗体事件序列所做的假设。 本文可能有助于使读者更加注意事件。 使用条件语句或后期引用属性可以缓解问题,但本文的重点不是更好的设计模式。

第二个也是最后一个要点是,测试用例仅限于计算机配置的一个可变方面(DPI)。 使用该应用程序,我为您留下了一个测试平台来仔细检查可能影响窗体事件的其他配置设置。 请在下面发布您的发现。

历史

  • 初始版本日期为 2008 年 5 月 31 日。
© . All rights reserved.