第4章 项目的第一个版本(2)

4.3. 报表中心

我们定义的报表窗体为FormReportMain,设计如下图。

图像说明文字

FormReportMain窗体同样分为两个区域,分别是panel1(左)和panel2(右),其中,panel1包含了操作按钮;而panel2中包含了一个DataGridView控件(dataGridView1),用于显示报表数据。

需求分析过程中,我们已经确定了报表格式,即最终会生成Excel 2003格式的文件;在我们的项目中,生成Excel格式文件的方法并不止一种,主要看你熟练使用哪一种了;本书中,我们将使用比较传统的方法,即使用Excel 11.0对象库。在使用时,我们首先导入这个库,通过Visual C# 2010 Express菜单“项目”>>“添加引用”打开引用窗口,选择相应的库文件,如下图。

图像说明文字

请注意,引用此组件时,应先确认计算机中安装了Excel 2003。在项目中引用相应的资源后,我们还需要在FormReportMain窗体中添加对它的引用,如下面的代码:

using excel = Microsoft.Office.Interop.Excel;

此外,我们还在FormReportMain定义了一个私有字段currentReportTitle,用于指定当前报表的名称。

接下来,我们将实现两个典型的报表类型,即“用户忠诚度”和“外观满意度”,大家可以参考这两个报表数据的计算实现更多的报表类型。

4.3.1. “用户忠诚度”报表

在需求分析时,我们讨论过,用户忠诚度的计算方法是:

图像说明文字

如果我们比较熟悉SQL查询,那么,这两个需要的数据并不难得到,下面就是此报表的生成代码。

// 代码位置:FormReportMain.cs
private void button1_Click(object sender, EventArgs e)
{
    // 用户忠诚度, (q2[1]/q1[1])*100%
    ClearReport();
    // 获取所需要的数据
    int q2_1 = 0;
    int q1_1 = 0;
    //
    using (OleDbConnection cnn = new OleDbConnection(CApp.DbCnnStr))
    {
        cnn.Open();
        OleDbCommand cmd = cnn.CreateCommand();
        cmd.CommandText = @"select count(q1) as q1_count from questionnaire_data where q1=1";
        q1_1 = (int)cmd.ExecuteScalar();
        cmd.CommandText = @"select count(q2) as q2_count from questionnaire_data where q1=1 and q2=1;";
        q2_1 = (int)cmd.ExecuteScalar();
    }
    //
    if (q1_1 == 0 )
    {
        MessageBox.Show("报表所需数据不完整,不能生成报表");
        return;
    }
    //
    currentReportTitle = "用户忠诚度";
    this.Text = string.Format("报表中心 [{0}]", currentReportTitle);
    DataTable tbl = new DataTable("table1");
    tbl.Columns.Add("车主数量");
    tbl.Columns.Add("再次选择");
    tbl.Columns.Add("忠诚度(%)");
    tbl.Rows.Add(tbl.NewRow());
    tbl.Rows[0][0] = q1_1;
    tbl.Rows[0][1] = q2_1;
    tbl.Rows[0][2] = ((float)q2_1 / (float)q1_1) * 100;
    //
    dataGridView1.DataSource = tbl;
}

报表生成之前,我们使用ClearReport()方法用于清理当前的报表信息,其定义如下:

// 代码位置:FormReportMain.cs
// 清除报表
private void ClearReport()
{
    currentReportTitle = "";
    this.Text = "报表中心";
    dataGridView1.DataSource = null;
    dataGridView1.RowCount = 0;
    dataGridView1.ColumnCount = 0;
}

再看报表数据的计算,我们使用了标准的SQL语句,其中应用了 count函数,其功能是返回满足条件的记录数。OleDbCommand对象的ExecuteScalar()方法用于返回第一条记录第一个字段的值,其返回值类型是object,如果没有返回数据,则返回null值。

在获取了报表所需要的数据后,我们通过DataTable对象生成报表的表格;最后,将此表格绑定到dataGridView1控件。这样,我们的报表就会显示在FormReportMain窗体中了。报表显示类似下图。

图像说明文字

现在,我们已经在软件中实现了报表功能,稍后我们会讨论如何导出到Excel文件。

4.3.2. “外观满意度”报表

外观满意度的答案有三种,我们需要每一种答案的数量,这个看着很复杂的计算,实际上使用SQL也可以很方便的完成,但与上一个报表的实现又有些不同,我们先看一下代码。

// 代码位置:FormReportMain.cs
private void button2_Click(object sender, EventArgs e)
{
    // 外观满意度, q3a答案分组汇总
    ClearReport();
    //
    int q3a = 0;
    //
    using (OleDbConnection cnn = new OleDbConnection(CApp.DbCnnStr))
    {
        cnn.Open();
        OleDbCommand cmd = cnn.CreateCommand();
        //
        cmd.CommandText = @"select count(q3a) from questionnaire_data;";
        q3a = (int)cmd.ExecuteScalar();
        if (q3a == 0)
        {
            MessageBox.Show("报表所需数据不完整,不能生成报表");
            return;
        }
        //
        cmd.CommandText = @"select q3a as 是否满意,count(q3a) as 数量 from questionnaire_data group by q3a;";
        using (OleDbDataAdapter ada = new OleDbDataAdapter(cmd))
        {
            DataSet ds = new DataSet();
            ada.Fill(ds, "table1");
            currentReportTitle = "外观满意度";
            this.Text = string.Format("报表中心 [{0}]", currentReportTitle);
            //
            ds.Tables[0].Columns.Add("比例(%)");
            for (int row = 0; row < ds.Tables[0].Rows.Count; row++)
            {
                ds.Tables[0].Rows[row][2] =
                (Convert.ToSingle(ds.Tables[0].Rows[row][1]) / (float)q3a) * 100;
            }
            //
            dataGridView1.DataSource = ds;
            dataGridView1.DataMember = "table1";
        }
    }
}

在获取报表数据时,我们同样使用了SQL语句,也使用count()函数,同时,我们使用group by子句,这样就可以统计出字段不同值的数量了。在整理报表格式时,我们使用了DataSet对象,而其内部本质上讲还是在对DataTable对象操作。最后,同样是将数据绑定到dataGridView1控件中。生成后的报表如下图。

图像说明文字

4.3.3. 生成Excel文件

前面,我们生成的报表都显示在dataGridView1控件中了,这可以用户先预览一下;然后,根据需要,用户可以选择是否导出为Excel文件,下面就是导出Excel功能的代码。

// 代码位置:FormReportMain.cs
// 导出报表
private void button3_Click(object sender, EventArgs e)
{
    if (currentReportTitle == "" || dataGridView1.Rows.Count < 1)
    {
        MessageBox.Show("没有可导出的报表,请先生成报表后重试");
        return;
    }
    // 生成的Excel文件
    string path = CApp.GetAppPath() +
           @"temp\" + Guid.NewGuid().ToString("N") + ".xls";
    //
    excel.Application xApp = new excel.Application();
    excel.Workbook xWk = xApp.Workbooks.Add();
    excel.Worksheet xWs = xWk.Worksheets[1];
    // 报表名称
    xWs.Cells[1, 1] = currentReportTitle;
    // 字段名
    for (int col = 0; col < dataGridView1.ColumnCount; col++)
    {
        xWs.Cells[3, col + 1] = dataGridView1.Columns[col].HeaderText;
    }
    // 数据
    for (int row = 0; row < dataGridView1.RowCount; row++)
    {
        for (int col = 0; col < dataGridView1.ColumnCount; col++)
        {
            xWs.Cells[4 + row, col + 1] = 
                 dataGridView1.Rows[row].Cells[col].Value;
        }
    }
    // 保存
    xWk.SaveAs(path);
    xWk.Close();
    xApp.Quit();
    //
    xWs = null;
    xWk = null;
    xApp = null;
    // 打开
    System.Diagnostics.Process.Start(path);
}

生成Excel报表的步骤如下:

  • 如果 currentReportTitle为空,或者dataGridView1数据行是0,则提示没有可导出的数据,并终止导出操作。
  • 导出的Excel文件位于主程序所在目录中的temp目录中,文件名使用生成的GUID字符串命名,这样可以有效防止同名文件带来的问题。
  • 将报表标题和dataGridView1中显示的数据导出到Excel文件中。
  • 最后,使用System.Diagnostics.Process类的Start()方法打开导出的Excel文件。

现在,我们看一下导出报表的样子,如下图。

图像说明文字

实际应用中,我们还可以修改Excel的样式,这样就可以创建更加美观的报表了。

接下来,你可以根据前面需求分析中的报表要求自己动手创建一两个新的报表试一试;而关于报表的Excel文件名,你也可以选择使用更加直观的名称。此外,关于报表的创建,我们还可以思考一些问题,比如,当报表很多时,是不是可以批量生成并导出,使用什么方法呢?你是不是可以将报表导出别的格式呢?

4.4. 讨论区

经过前面的工作,我们已经完成了一个比较完整的数据处理项目了,在这个项目中,数据处理的一些基本方法也都出现了,如添加、修改、查询、计算、导出等;在实现这些功能的过程中,我们使用了C#编程语言、Access数据库、ADO.NET中的OLEDB组件、SQL语句,以及Excel对象库等开发资源。此外,虽然界面的设计非常简单,我们也不能忽视用户界面的设计问题。

一个项目完成了,不知道大家有没有检查代码、总结经验的习惯,我们认为,这些工作都是非常有必要的,在本章的最后,我们就来讨论一下关于这个项目实现过程中的各个方面,主要包括:

  • 数据库的设计
  • 数据的传递与转换
  • ADO.NET的使用
  • 界面设计
  • 已经封装的代码
  • 窗体显示问题
  • 异常处理问题

4.4.1. 数据库的设计

项目的数据库是在上一章就准备好的,我们在项目完成后再回过头看一下数据库的设计,有一些问题还是需要再讨论一下的。

问卷编号字段(RNumber)的定义

在我们的数据表questionnaire_data中,问卷编号字段(RNumber)设计为32位的整数;然而,在很多项目中,编号类的字段可能会设计为文本(字符串)类,这两种方案并没有绝对的对与错,而是适用性的问题。

本项目中,我们已经约定了问卷编号为6位整数,其含义我们也知道,前三位是店面编号,后三位是问卷编号;此时,将这个字段设计为6位文本类型也是合理的,那么,我们为什么偏向整数呢?

原因很简单,一般来讲,在数据处理中整数类型肯定比文本类型快,而且,在本项目中,整数类型也完全可以适用。如果我们需要截取店面编号,可以通过简单的整数运算来完成,以下就是相应的C#代码。

int shopNumber = RNumber / 1000;

除了字段的类型,还有一些特性需要注意。我们知道,理论上讲,问卷编号也是可以识别唯一一张问卷的,在数据库技术中,这个字段可以设置为主键(PK,primary key)或唯一值(unique)。你也许发现了,我们并没有这么做,而是将RID字段设置为了主键;不过,如果有需要,我们可以在检查数据正确性时检查问卷编号是否已存在,下面就是相应的检查方法。

// 代码位置:FormQuestionnaireInput.cs
private bool RNumberExists(int rNumber, int rid)
{
    using (OleDbConnection cnn = new OleDbConnection(CApp.DbCnnStr))
    {
        cnn.Open();
        OleDbCommand cmd = cnn.CreateCommand();
        cmd.CommandText = @"select top 1 rid from questionnaire_data where RNumber=@RNumber and RID<>@RID;";
        cmd.Parameters.AddWithValue("@RNumber", rNumber);
        cmd.Parameters.AddWithValue("@RID", rid);
        using (OleDbDataReader dr = cmd.ExecuteReader())
        {
            return dr.HasRows;
        }
    }
}

请注意SQL语句,当我们判断问卷编号(RNumber)是否已存在时,考虑了是否有正在编辑的问卷记录(通过记录Identity值,即RID字段);也就是说,检查问卷编号时,不会包含正在编辑的问卷。

然后,我们可以CheckData()方法中添加问卷编号的检查,如:

private bool CheckData()
{
    bool result = Interval(RNumber, 100000, 999999);
    // 检查问卷编号是否已存在
    if (result)
    {
        if (RNumberExists(Convert.ToInt32(RNumber.Text), CurrentId))
        {
            MessageBox.Show("问卷编号已存在");
            return false;
        }
    }
    // 其它代码
}

此外,问卷编号与记录ID(RID)的区别还是再说明一下。RID是记录的自动Identity数据,是在添加新记录时自动生成的,一般来讲,我们不应该手工修改这个数据,它的作用就是在数据库中或我们的操作代码中识别唯一的一条数据记录;而问卷编号(RNumber)则是人们可以阅读的信息,这个数据是我们人工管理,并且,数字的含义是有实际意义的(前三位为店面编号,后三位为问卷编号)。

字段取值范围的限定

在设计数据表时,我们可以设置字段数据的取值范围,在Access数据库中,通过字段设置的“有效性规则”来管理,而在SQL Server中,我们可以通过T-SQL中的check关键字来设置。

同样,你也可以发现,我们的数据库中并没有设置字段的取值范围检查。为什么呢?原因是我们将数据的检查放在了界面中来完成;一般来讲,用户不会也不应该直接对数据库进行操作,而是通过界面来完成相应的数据操作;所以,在这种情况下,我们在界面中完成数据的检查工作也是可以接受的方案。

无论是前面讨论的唯一值规则,还是这里讨论的取值范围规则,都是数据库设计中值得注意的问题;但有一点很重要,数据库中的规则应用在处理过程中都会有性能上的代价;所以,当我们把软件系统中的数据处理作为一个整体工程来看待的话,则可以将这数据处理工作合理地分配在界面、数据库、业务代码或代理中来完成,从而减少某一方面的处理负担,这也是大型项目中值得深入研究的问题。

当然,在这里并不是说我们就不需要深入学习数据库技术,正相反,只有我们熟练掌握一种或更多数据库系统的应用以后,才能更合理地在开发工作中进行取舍。

4.4.2. 数据的传递与转换

我们的项目看上去并不复杂,但实际上,我们可以看到,问卷的数据经过了不少的传递与转换操作。

首先,我们先回顾一下数据的流向问题,整个项目中,数据的流向可以通过以下示意图来表示。

图像说明文字

接下来,我们分别讨论这些节点。

界面录入

本项目中,我们录入的控件只有TextBox,而控件类型的统一,也让我们的数据处理可以更加方便(如清空控件内容);不过,在实际应用中,数据的录入使用哪种控件还是要根据具体的需要来使用的。

项目中的大多数据项都是数值型,你可以考虑使用NumericUpDown控件;有一点请注意,如果数据项可能为空的话,NumericUpDown控件可能就不适用了,此时,TextBox就是一个比较合适的选择。不过,如果数据不能为空,而且必须有一个数值时,使用NumericUpDown控件就是一个不错的选择。

总的来讲,在界面设计中使用哪种控件,我们需要在清楚这些控件特点的基础上,在项目中合理地应用。

数据检查

我们知道,TextBox的录入的内容为string类型,而我们大多数据类型都是int类型,在检查数据时,我们需要将录入的文本内容首先转换为整数类型,然后,我们还要判断它的范围是否正确;请注意转换方法,我们使用了值类型中定义的TryParse()方法,这是字符串类型转换为数值类型的常用的、也是比较合适的转换方法。

保存到数据库

将数据保存到数据库的操作时,我们使用了标准的方法,即使用了ADO.NET中的OLEDB组件,包括OleDbConnection、OleDbCommand等,通过标准的SQL语句,如Insert语句和Update语句将数据插入或更新到数据库,通过参数来传递相应的字段数据,并进行了相应的类型转换操作。请注意,OleDbCommand类中的AddWithValue()方法的使用,虽然第二个参数定义为object类型的,但我们仍然需要对数据进行相应的转换,才能正确地将数据传递到数据库中,因为将一个数据转换为object类型时,会保存它的原始类型信息。

在这里的类型转换操作,我们使用Convert类,它定义在System命名空间。由于我们在保存前对数据都进行了检查,所以,在这里的类型转换操作一般是没有问题的。但是,当你的数据类型比较复杂时,数据的操作可能就需要更多的检查和转换了。

数据查询

在数据库查询数据,我们使用了SQL语句中的Select语句,并使用OleDbDataReader对象载入数据记录,请注意,OleDbDataReader中的字段值在传递过程中都是object类型的,这也保证了它可以传递任意类型的数据。

载入到界面

前面,我们说过,界面中的数据控件使用的都是TextBox,但是,我们使用OleDbDataReader对象载入的数据都是object类型的,所以,在显示查询的数据时,我们还要进行相应的转换。

大多数的数据都可以通过ToString()方法直接转换成字符串类型,但有两个问题需要注意:

  • 当数据为空对象(null)时,不能使用ToString()方法。
  • 当数据是较复杂的类型或者多样化的数据时,请注意直接使用ToString()方法转换的结果是不是我们真正需要的格式,比如日期和时间数据的显示格式,或者是数值需要保留几位小数等等。

请注意,在查询和载入问卷数据时,我们只使用了两个类型类型,即object和string。可我们的问卷数据大多都是int类型,这事儿提都没提,怎么回事?

答案就是,在数据的传递过程中使用了object类型,可以保证可以兼容所有类型,甚至是null值。

当我们需要显示数据时,会根据所使用的控件类型来确定最终需要的数据类型,而这个类型就是string;所以,虽然数据库保存的数据实际是int类型的,但是,我们会根据需要直接将数据转换为相应的类型,而没有使用数据的原始类型。

数据计算

在生成报表时,我们需要对数据库中的数据进行一系列的计算,此时,最重要的技术就是SQL语句了,无论是聚合函数,还是group by子句,都是我们计算和汇总数据时的有力工具;当然,当你真正深入学习数据库技术之后就会发现,这只不过是冰山一角而已。

数据计算过程另一个需要注意的方面就是C#中的数据计算问题,大家可以在代码中看一看,我们在对返回的整数进行计算,特别是百分比的计算时,都会先将整数转换为浮点数再进行计算。这是因为在C#中,整数与整数运算结果还是整数,但是我们知道,在计算百分比时,数据是非常可能有小数部分的,所以,我们应该使用浮点类型来进行相应的计算。

生成报表

在生成报表时,我们使用ADO.NET组件中的脱机组件(又称为非连接类),主要是DataSet和DataTable,它们都定义在System.Data命名空间。为什么要强调它们的使用呢?因为它们与具体的数据库类型无关,而且与项目类型和界面类型都无关,所以,把这些组件作为数据计算和传递的中间组件是再合适不过的了。

使用DataSet和DataTable生成报表后,我们可以很方便地将数据绑定到DataGridView控件中,主要是设置它的DataSource和DataMember属性。在后续的项目发展中,我们还会看到使用脱机类数据组件带来的便利性。

当使用DataSet对象作为数据源时(指定DataSource属性),需要使用DataMember属性设置显示的表名;直接使用DataTable对象作为数据源时,则不需要设置DataMember属性。

导出Excel

也许你会看到,我们在导出Excel文件时使用了比较“原始”的方法,甚至可以追溯到.NET的史前时代(如果你使用过VB6,就应该知道我在说什么了)。

不过,Excel文件的基本模型是没有太大变化的,所以,当我们知道了Excel中的Application、Workbook、Worksheet、Cell、Range等对象的应用后,无论使用什么方法都可以很方便地对Excel文件进行操作。

在操作Excel文件时应注意,工作薄(WorkBook)中的工作表(WorkSheet)的数值索引值是从1开始的;而在工作表中的单元格,其行和列的数值索引也是从1开始的。

4.4.3. ADO.NET的使用

使用ADO.NET组件访问数据库时,你一定发现了,我们使用了大量的重复代码,比如OleDbConnection、OleDbCommand等组件的使用;当你有一定的开发经验后,你一定会对重复的代码感到非常的不舒服。

如果项目的数据库换成SQL Server或者MySQL呢?你需要使用SqlConnection和SqlCommand,或者是MySqlConnection和MySqlCommand来重写这些代码,这一定是一件非常不爽的事情吧? 现在,你可以考虑一下怎么解决这些问题。

4.4.4. 界面设计

原始、简陋、初级、没创意、……

你一定还有不少词来形容当前项目中的界面设计,不过,我还有一个词来形容——“简洁”。虽然我们设计的界面不够炫、也不够酷,但有一点我们可以断定,那就是用户在工作中并不会出现什么操作障碍。

当然,在这里,我们主要是演示代码,所以,并没有去设计很帅的界面,在实际项目中,在界面中添加一点图像和色彩可能会给用户一种友好的感觉,但这只是可能,如果你的图像不能和操作相匹配,也就是传说中的“图不达意”,恐怕只能给用户带来不必要的麻烦了。

所以,对于界面的设计,我们还是那两个基本原则: - “别让我思考”,简单、直观的操作元素是最重要的。 - 如果不能让用户非常满意,就尽量减少用户的不满意。

除了看上如何,在窗体设计上还有操作效率问题需要讨论。在界面中,我们将问题一的最大长度(MaxLength属性)设置为6,因为它是一个6位整数,而问题二到问题五的的TextBox控件的最大长度都设置为1,因为它们的答案就只是一位整数,那么,当用户录入相应位数的数值后,如果焦点能直接跳到下一个控件,是不是在数据录入操作时更有效率呢?

为了实现这一功能,我们应用TextBox控件的TextChanged事件来实现,以下代码就是RNumber和q1控件的TextChanged事件代码。

// 代码位置:FormQuestionnaireInput.cs
private void RNumber_TextChanged(object sender, EventArgs e)
{
    if (RNumber.Text.Length == 6) q1.Focus();
}

private void q1_TextChanged(object sender, EventArgs e)
{
    if (q1.Text.Length == 1) q2.Focus();
}

代码很简单,当输入的内容达到数据的最大长度后,就将焦点自动跳转到下一个输入控件,你可以自己添加其它TextBox控件的TextChanged事件代码。

作为问卷的最后一个数据项q6,我们怎么处理呢?它的内容长度是不一定的,可能没有,也可能是1到200个字符,所以,并不适合使用TextChanged事件来处理。那么,当用户在此TextBox回车时,是不是可以执行保存操作呢,就像下面的代码。

// 代码位置:FormQuestionnaireInput.cs
private void q6_KeyPress(object sender, KeyPressEventArgs e)
{
    if (e.KeyChar == (char)13)
        button2_Click(null, null);
}

在这里,我们使用了TextBox控件的KeyPress事件,而数值13则回车符的代码,当用户在q6控件中输入了回车键,则会执行数据检查和保存操作。 本例是一个简单的界面,数据项也不多,所以,实现这样的功能并不会太难,但是,如果是大量的数据表单需要处理时,实现这些功能可能会是一件非常麻烦的事件了。另外,在ASP.NET项目中的WebForm中如果需要实时响应文本框控件的TextChanged事件,就必须开启的自动回传属性(AutoPostBack),相信这对整个系统的性能是有影响的,特别是很多用户同时操作的时候(不过,你可以考虑是不是可以使用其它方法来实现自动跳转功能)。

总的来说,我们怎么处理这些问题,还是要根据具体的项目特点来选择。

4.4.5. 代码封装

在这个项目中,不知道你可以找到多少经过封装的代码,我们一起来找找看,好像也不太多,或许是我们封装的还不够吧。

CApp类

创建CApp类时,我们就讨论过它的作用了,作为项目的主类,它主要包括了项目数据库资源、项目初始化,以及其它的一些项目通用资源,如获取项目主程序的目录等。

InputBox对话框

项目中,我们创建了一个用于输入问卷编号的对话框,也就是FormInputBox窗体。实际上,如果我们想一想,这个窗体完全可以修改为一个通用的输入对话框,你只需要添加一些属性,就可以设置相关的内容了,如:

  • Title属性,用于设置或获取窗体的标题内容。
  • Description属性,用于设置或获取对话框中的提示信息。
  • DefaultValue属性,用于设置显示对话框时的默认内容。

当然,在ShowInputBox()方法中也可以添加相应的参数。

Interval()方法

在FormQuestionnaireInput窗体中,我们封装了三个版本的Interval()方法,我们可以看到,Interval(int,int,int)和Interval(string,int,int)是真正的通用版本,它们可以在任何类型的项目中使用,而Interval(TextBox,int,int)只适用于Windows窗体项目。

那么,对于真正通用的代码,我们是不是可以封装成自己的代码库中的一部分呢?

还有哪些代码可以封装

封装代码的一个重要原则就是,它们应该是真正可以重复使用的功能,根据这一原则,我们可以看到,在项目中,还是有一些内容可以封装的。

数据的转换方法

我们可以看到,在数据的转换和传递过程中,原始的数据类型并不占主导地位,而使用数据的地方才是,比如,在TextBox控件中需要显示int类型的数据;所以,无论原始数据类型是什么,当我们需要一定的数据类型时,就要将原始数据转换成这个类型,如果不能正确转换,也要有个交待,比如,是给出默认值,还是返回null值,在第11章,我们就讨论了这个问题的解决方案。

消息对话框

你可能认为MessageBox的使用已经足够方便了,比如,显示一个提示信息的的方法,可以是如下的代码:

MessageBox.Show("提示信息", "标题",
    MessageBoxButtons.OK, MessageBoxIcon.Information);

如果再简单点怎么样,像这样:

CDlg.ShowInformation("提示信息");

这个方法如何定义的呢,如下面的代码:

public static class CDlg
{
public static void ShowInformation(string msg)
{
        MessageBox.Show(msg, "",
        MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}

如果在处理数据后需要在提示信息中包含更多的数据,你还可以使用类似下面的方法。

CDlg.ShowInformation("共操作了{0}条记录", counter);

是不是很方便,在CDlg类中,这个方法的定义如下。

public static void ShowInformation(string msg, params object[] args)
{
    ShowInformation(String.Format(msg, args));
}

你可以根据这两个方法的定义,创建提示警告、错误或问题对话框的显示方法;其中,警告和错误消息对话框与信息对话框基本一样,只是显示的图标不同而已;问题对话框则应该注意一点,它应该有返回值,即用户单击的按钮结果,其类型是DialogResult。

也许你还想封装更多的内容,只管去做吧,谁也不能说个不字,对吧!^^

4.4.6. 窗体显示问题

在场景中,我们提到过小李使用的计算机安装了Windows XP操作系统,那么,我们用于开发的计算机呢?

不知道大家使用的是不是Windows 7操作系统,而且使用了显示缩放功能,如果是使用了125%或150%的显示比例,那么,建议大家改回到100%的显示比例;然后,看一看我们精心设计的窗体布局有没有变化,也许结果会让人大吃一惊,如果是在Windows XP操作系统中显示又会是什么样的效果呢?一试便知。

4.4.7. 异常处理问题

在数据处理过程中,异常是一件不容忽视的问题,同时也是非常不易处理的事情;但是,在我们的代码中,除了在CApp类中有一个try-catch语句以外,在整个项目的其它地方没有使用一个try-catch语句,难道我们对可能的异常漠不关心吗?

作为一个负责任的开发者,我们绝对不会这样,也绝对不应该这样!

作为软件开发者,我们自己也是软件的用户,在我使用过的软件系统中,有时会从抛出的异常提示中发现软件的开发技术和数据库类型;我想,作为开发者,我们可以明白这些信息,那么,作为非计算机专业的用户呢,这些信息对他们来会有意义吗?答案当然是没有任何意义。

现在,我们可以对本项目进行毁灭性测试,也就是说,可以对软件进行任何随意性的操作,你可以随便录入各种数据,也可以进行一些不科学的操作;可以肯定的是,我们的软件崩溃的机会并不是很大,为什么呢?我们还是要从异常的类型谈起。

在软件运行的过程中,异常一般会有两种:

  • 不可控异常,这种类型的异常往往是由于意想不到的问题所造成的,而对于这一类的异常,才是真正需要try-catch语句来捕捉的。因为它们是不可预知,或者是在开发阶段无法控制的(如网络连接突然中断);所以,当它们出现时,我们就需要根据它们的性质来做出合理的处理,比如,在本项目的CApp.AppInit()方法中,如果数据库连接不正确,我们只好终止程序的运行,因为数据库无法使用,我们的程序就是启动了也不会有任何的意义。
  • 可控异常。一般是可预知的异常,比如用户录入的数据是否正确,正是这些数据的正确性是可以检查的,所以,我们就可以通过严格的检查来消除这些可能的异常。这也是我们对软件的运行正确性比较有信心的原因。

如果你愿意,可以在任何代码中添加try-catch语句,但应该注意,我们对数据转换和传递的过程考虑地越多,程序出问题的可能性也就会越小;而另一方面,如果使用了太多的try-catch语句结构,建议大家对软件的性能做一下对比测试。

请注意,你对于异常的态度是影响软件质量的重要因素之一,而处理异常的方法又会是一项复杂的系统工作,所以,在处理异常的工作中还是要三思而后行。

又检查了一遍代码,也许你可以发现一些可以修改和优化的地方,做完这些工作,我们就可以将软件交给用户测试和使用了。接下来,对于总结过的内容,我们可以有选择性地深入学习一些。不过,有两个主题在本书中已经包括了,也就是第11章和第12章中的内容,为了示例项目的连续性,我把它们放在了后面,如果你现在还没有阅读这两章,我还是建议大家先看一看,因为我们很快就使用到这两章中讨论和封装的内容了。

目录