VB.NET家庭理财软件开发实例

编程乐园

几乎所有财务管理软件均要使用数据库。面向数据库编程始终是程序设计的一个难点和重点,VB.NET自身不具备对数据库进行操作的功能,对数据库的处理是通过.NET Framework SDK中面向数据库编程的类库和微软的MDAC来实现的。

由于数据库编程中所包含的内容十分丰富,一篇文章难以包容。本文将以设计一个家庭理财软件为例,引领你掌握数据库编程的方法与技巧。

一、NET数据库对象模型详解

在进行实际数据库设计之前,我们必须了解VB.NET数据库操作的对象体系——也就是.NET数据库对象模型以及其工作原理。

1.对象模型简介

在System.Data名称空间及其下的子名称空间容纳了VB.NET数据库操作的全部所需类。常用的对象存在System.Data. OleDb(主要针对Access)以及 System.Data.SqlClient(主要针对SQL Server)两个子空间中。

(1)数据库连接分两

.NET对象模型根据是否与实际的数据库连接可以分成两组:

与实际数据库相连接的数据堤供程序对象(Connection对象、DataAdapter对象、Command对象、DataReader对象);

与实际数据库无连接的数据对象DataSet,以及其包含的DataView对象、DataRow对象、DataTable对象、DataRelation对象。

(2)数据处理流程

根据对象模型,数据处理可以有两种流程:

有连接的数据处理步骤(这种方法数据处理阶段的操作是和实际数据库相连的,操作会立即反映到实际的数据库中):

Connection打开实际数据库;

Command对象发出查询记录的SQL数据库操作命令;

利用DataReader对象对查询的数据进行实际操作;

数据处理阶段;

关闭连接。

无连接的数据处理(在数据处理阶段对数据操作不会影响实际数据库,直到你发出指令将处理过的数据推回到实际的数据库中):

Connection打开实际数据库;

DataAdapter对象发出查询记录的SQL数据库操作命令,并将获得的记录集放到DataSet对象中;

关闭连接;

数据处理阶段利用DataSet对象对查询的数据进行实际操作;

将数据返回到实际的数据库中。

由于无连接的数据库伸缩性很灵活,所以本文主要针对System.Data. OleDb空间下的对象进行讲解。

2.数据操作对象基础

(1)Connection对象

不管是有连接的或是无连接的数据处理,使用数据源时都必须使用OleDbConnection打开一个数据连接。

OleDbConnection常用的属性及方法列表如下:

031.jpg

在实际编程中,只要设置合适的连接字符串信息到ConnectionString属性,然后使用Open方法就可以建立数据库连接。

ConnectionString连接字符串主要由以下参数子串构成(完整参数列表请查询MSDN):

032.jpg

(2)DataAdapter对象

在数据处理模型中,DataAdapter扮演的角色是Connection对象和DataSet对象的粘合剂,它不存储实际数据,主要功能只是将需要操作的数据插取到DataSet中或将DataSet更新的记录集返回到实际数据库中。

DataAdapter常用属性和方法列表如下:

033.jpg

(3)DataSet对象

作为VB.NET无连接数据库处理的核心组件,所有无连接的数据处理代码实际上都是围绕DataSet对象进行。以下是MSDN中有关DataSet的模型图。

vb1.gif.jpg
DataSet的模型图

上面的模型图可以这样理解:DataTable对应数据库中的一个表(DataTable下的DataColumn对应于表的字段定义,DataRow对应于表中的记录)、DataRelation对应于数据库中表的关系(基于主/外键建立父(主)表和子(详细资料)表之间的关系)。

二、家庭理财软件需求分析

1.实例目标分析

以本文介绍的个人财务软件为例,你在需求设计阶段就了解到该软件的目标:个人使用;财产分析。

根据这两个目标,我们考虑日常生活中普通人记账的习惯:记账的关键目的是对自己资产数量进行记录。资产只有收入、支出两种流向,另外,需要汇总某一时间段内收入/支出的金额和自己剩余的资产。程序基本功能描述如下:

记账功能(含修改);

查账功能(记录的筛选,汇总);

生成报表;

根据查账的结果打印。

2.数据库设计

按照需求,简单归纳出实现这种功能的数据库所需的实体集:金额 、资金动作(支出/收入)、发生日期、动作类别、动作描述。由于其他金额汇总功能都可以由这几个实体推导出来,所以可以简单确定数据库只需定义一个关系表即可。

我们将这个表命名为myproperty,表内包含编号(给每个资金事件一个唯一标识,即主键)、类别、描述、日期、金额、动作等字段。

034.jpg

因考虑是个人使用,数据量不会太大,所以采用Access数据库。启动Access,新建一个空白数据库,文件名为Wealth.mdb。选择“使用设计器创建表”选项,并定义好合适的字段类型保存即可。

3.程序界面设计

启动VB.NET,新建“Windows应用程序”,项目名称为:猫族四十大盗。根据上文定义的设计目标,我们选择TabControl控件作为其他控件的容器,以便有层次地与目标功能一一对应。

三、连接数据库

上文已给出了主要数据库对象的基本属性及方法的列表,下面我们将结合实例,按部就班地对代码编写过程进行剖析。

1.用Connection对象连接实际数据库

不管是有连接或是无连接的数据处理,使用数据源时第一步都必须使用OleDbConnection打开一个数据连接。

在我们的程序实例中,OleDbConnection对象实际操作代码如下:

'完整代码可参阅源程序frmMain.vb文件中的Public Sub sbLoadDatabase()过程

Dim tobjConnect As New OleDb.OleDbConnection

Dim tstrConnectString As String

'建立连接字符串:

'Provider指的是数据提供者,Microsoft.Jet.OLEDB.4字符串说明它是一个Access兼容的数据库

'Data Source数据源说明数据库的实际位置,这里给出数据库文件的存放位置

tstrConnectString = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source= " & _ IO.Path.Combine(Application.StartupPath, "Wealth.mdb")

tobjConnect.ConnectionString = tstrConnectString

tobjConnect.Open() '打开数据库连接

'执行其他代码…

'数据库连接资源在使用完毕后释放

tobjConnect.Close()

提示:由于我们的程序将数据库文的默认位置放在和应用程序所在的目录下,所以在调试时必须将“Wealth.mdb”放到工程文档的bin目录中。

2.利用DataAdapter实现记录读取及更新

在我们的程序中,Adapter的作用之一是发出查询命令将数据填充到一个DataSet类型的全局中以便进行数据处理,代码如下:

'完整代码可参阅源程序frmMain.vb文件中的Public Sub sbLoadDatabase()过程

'构造函数的第一个参数是一个SQL语名,说明需要在实际数据库中读取的记录集

'第二个参数是上文介绍的OleDbConnection对象

Dim tobjDataAdapter As OleDbDataAdapter

tobjDataAdapter = New OleDbDataAdapter("select * from myproperty", tobjConnect)

'Fill方法第一个参数是Dataset类型的对象,第二个参数是将筛选得到的记录映射到Dataset中的表名

tobjDataAdapter.Fill(gobjMyProperty, "myproperty")

程序中Adapter的另外一个使命就是将Dataset中处理修改过的数据更新到实际数据库中。要更新实际数据库,Adapter的DeleteCommand属性、InsertCommand属性、UpdateCommand属性必须赋值为ADO.NET的Command对象(需要自己编写SQL更新命令)。VB.NET提供了CommandBuilder对象自动生成所需的对象。在实例中插入的代码如下:

'完整代码可参阅源程序frmMain.vb文件中的Public Sub sbUpdateDatabase()过程

Dim tobjDataAdapter As OleDbDataAdapter

Dim tobjDataset As DataSet

Dim tobjCommandBuilder As OleDbCommandBuilde

'省略打开数据连接的代码…

tobjDataAdapter = New OleDbDataAdapter("select * from myproperty", tobjConnect)

'以下代码演示了Commandbuilder对象的用法

'构造函数只要将要需更新的DataAdapter对象作为参数传递进去

tobjCommandBuilder = New OleDbCommandBuilder(tobjDataAdapter)

'将CommandBuilder自动产生的更新指令放到DataAdapter对象中

With tobjDataAdapter

.InsertCommand = tobjCommandBuilder.GetInsertCommand

.DeleteCommand = tobjCommandBuilder.GetDeleteCommand

.UpdateCommand = tobjCommandBuilder.GetUpdateCommand

End With

'将Dataset对象修改过的数据项插入临时Dataset对象中等候更新

If gobjMyProperty.HasChanges = True Then

tobjDataset = gobjMyProperty.GetChanges

End If

Try '在并发更新有可以发生冲突

'使用DataAdapter提供的Update方法将待更新的数据推回到实际数据库中

tobjDataAdapter.Update(tobjDataset, "myproperty")

gobjMyProperty.AcceptChanges()

MessageBox.Show("成功更新")

Catch ex As Exception

End Try

'省略关闭数据连接的代码…

3.用DataSet对象进行实际的数据处理

以下代码针对上文获取的DataSet对象演示如何取得它的子对象:

'gobjMyproperty是上文例中用DataAdapter填充的Dataset对象

Dim tobjTable as DataTable=gobjMyProperty.Tables("myproperty") '得到数据库中myproperty表的引用。

'遍历表中的所有记录

Dim tobjDataRow As DataRow

For Each tobjDataRow In tobjTable.Rows

Console.WriteLine(tobjDataRow.Item("金额"))

'你还可以利用集合的Add,Remove等方法添加/删除或做其他的记录修改工作

Next

四、绑定数据源

我们已经初步了解.NET数据模型,装截着myproperty表的gobjMyProperty变量亦已准备完毕。现在让我们进入Windows数据库软件实施的另一重要步骤——数据源与Windows窗体的绑定。

所谓绑定,就是将数据库中的某一个数据对象,例如,表、某一条记录的字段值与控件的某一属性相关联。这种关联是双向的,当控件属性值发生改变时,数据源相应会发生更新;当数据源发生更新时,控件属性值也相应做出显示上的响应。

以下给出记账/查账结果界面部分涉及到本文数据绑定讲解的控件列表:

035.jpg

每个继承自Control的控件都有一个ControlBindingsCollection 类型的DataBindings属性,这个集合型的属性可以决定控件的数据绑定情况。

常规使用格式是:

控件名称.DataBindings.Add(控件属性, 数据源, 数据成员)

控件属性:说明将数据源的特定数据成员绑定到控件的那个属性上(通常取Text属性值)。

数据源:是DataSet模型中可以包含数据的对象(DataSet、DataView、DataTable等)。

数据成员:数据源中的元素。如DataSet中的DataTable名称,DataTable中的DataColumn名称等。

现在我们试着将金额和动作两个字段绑定到txtMoney、cboAction的text属性上:

txtMoney.DataBindings.Add("Text", gobjMyProperty, "myproperty.金额")

cboAction.DataBindings.Add("Text", gobjMyProperty, "myproperty.动作")

上述代码只完成了单纯数据绑定,此时,控件只会显示绑定的第一个数据项的值,在上例中txtMoney只显示第一个记录的金额,cboAction只显示第一个记录的动作值。解决方案是由我们实现数据表记录的导航功能。

VB.NET提供了BindingManagerBase抽象类专门实现对绑定到同一数据源的控件的导航功能,这个抽象类有一个Position属性,当这个表示记录项位置的属性发生改变时,BindingManagerBase会通知绑定到这个数据源的所有控件先将控件绑定属性的当前值传回给数据源,然后根据新Position指定位置的数据源记录值更新控件显示。我们通常是由绑定控件的容器控件(父控件)的BindingContext属性取回继承自BindingManagerBase类型的BindingContext实例对象。

常规格式:

BindingManagerBase对象=父控件名称. BindingContext(绑定到的数据源,数据成员)

'mobjBMBmyproperty是在类级别定义的BindingManagerBase对象

'Private WithEvents mobjBMBmyproperty As BindingManagerBase

'由于所有记账界面上的绑定控件都是直接绑定到Dataset对象gobjMyProperty 的myproperty表上

'注意它们的直接父控件都是tabcontrol控件tabmain的第一个tabpage页,而不是frmMain窗体

mobjBMBmyproperty = tabMain.TabPages(0).BindingContext(gobjMyProperty, "myproperty")

得到了indingManagerBase类型的对象后,导航就变得很简单了,以下是btnperview按钮单击事件中导航到前一个记录的代码:

'导航到前一个记录

Private Sub btnPrevious_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPrevious.Click

mobjBMBmyproperty.Position -= 1

End Sub

知道了向前导航一个元素,那么向后导航一个元素、导航到最前/最后的元素就不用详细解释了。下面是所有导航按钮内的实现代码:

mobjBMBmyproperty.Position = 0 '最前的记录

mobjBMBmyproperty.Position += 1 '下一条记录

mobjBMBmyproperty.Position = mobjBMBmyproperty.Count – 1 '最后一条记录

BindingManagerBase对象还具有向绑定的数据源添加新/删除记录的方法,这是在btnNewRow按钮实现的添加新记录代码:

'新记录

Private Sub btnNewRow_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnNewRow.Click

mobjBMBmyproperty.EndCurrentEdit() '将当前控件的状态先返回到数据源中

mobjBMBmyproperty.AddNew() '加新记录

'删除记录mobjBMBmyproperty.RemoveAt(mobjBMBmyproperty.Position)

'赋初值

txtMoney.Text = "0"

'其他赋初值代码省略

End Sub

在VB.NET中提供了一个专业级的数据处理控件 DataGrid,只要将DataGrid控件的DataSource属性设为合适的数据源,将DaxtaMember设为数据源中的表(如果数据源是DataView的话,这个属性也可以省略),它就可以自动显示数据。

将DataView类型的mobjDataView变量作为数据源绑定到dgdDetail控件上的示例代码如下:

mobjDataView = New DataView(gobjMyProperty.Tables("myproperty"))

dgdDetail.DataSource = mobjDataView

完成绑定后,运行程序新建几个记录后,你可以发现绑定的控件已自动列出myproperty的相应数据,DataGrid的功能更是强大,只用了两行代码,你就可以修改/删除(在行指示上按Del键)/新建记录(在最后空白行上输数据)。

提示:数据绑定的完整代码可参阅源程序的Public Sub sbBindDataControl()过程。

五、数据分析处理

通过上面的代码编写,程序的记账功能已绑定完成,接着要做的就是根据用户输入的数据实现查账功能(记录的筛选,汇总)。

前文之所以将DataGrid类型的dgdDetail控件绑定到DataView类型的mobjDataView变量上,其实是有计划的,顾名思义Dataview对象实现DataTable对象的视图,并且可以很方便地实现表内记录的排序和筛选。

1.查账中的数据滤选

查账中的筛选往往是需要根据某一特性查找记录,例如,根据动作了解到底有多少单收入的资金动作。

DataView对象的RowFilter属性主要根据一个表达式来决定DataView经筛选后仅保留那些基础表的记录。

RowFilter属性字符串基础格式是:

DataView.RowFilter="字段1 运算符1 值1 and/or 字段2 运算符2 值2…"

当值是字符串时可用两个单引号内,当值为日期时要包含在#号内。

例如,动作=“支出”、日期=#2005/10/11#

实现数据筛选的原理其实很简单,就是让用户在右侧的单选框中选择筛选条件,然后根据相应控件的值构建合适的过滤表达式到DataView的RowFilter属性中。以下是在源程序中插入金额筛选的代码:

Dim tstrFilterString As String = ""

'指定金额数量的记录查找

If chkSearchByMoney.Checked = True Then

If tstrFilterString <> "" Then '如果还有其他筛选条件,使用and连接

tstrFilterString &= "and"

End If

'cboSearchMoneyRange.Text是>、<等几个比较运算符其中之一

'txtSearchByMoney.Text是金额的值

'实际运行中产生的表达式可能是这样的"金额>=100"

tstrFilterString &= "金额" & cboSearchMoneyRange.Text & "'" & txtSearchByMoney.Text & "'"

End If

'其他的类似的筛选字符串构建…

mobjDataView.RowFilter = tstrFilterString '正式进行筛选数据

提示:完整代码可参阅frmMian.vb内的btnSerchStart_Click过程。

在筛选结束后,由于mobjDataView变量已与DataGrid控件dgdDetail绑定,所以筛选后的结果会立刻反映在查账结果的dgdDetail控件中。

2.查账中的数据汇总

数据库的汇总主要是根据表内记录中数据值计算平均值、总计。DataTable的Compute方法足以轻松完成任务。

基本格式:

DataTable对象. Compute(Expression ,Filter)

功能:聚合函数计算表内符合filter条件的所有记录的值。

Expression:要计算的聚合函数表达式(例如,AVG列平均值、Sum列的总计、Max列数据的最大值、Min列数据的最小值)。

Filter:表内记录的过滤字符串,与DataView的RowFilter设置一样。

在我们软件中需要汇总的有三个值:支出/收入的金额以及目前自己所有的资产。汇总的记录根据用户过滤的记录为依据,所以可以简单将DataView的过筛器直接应用到Computer方法上计算出收入和支出的数据。“目前资产”只要使用“收入值-支出值”就算出。实现代码如下:

Dim tcurIncome As Double = 0, tcurPayout As Double = 0

Dim tcurMyMoney As Double = 0

Dim tstrFilter As String

'先根据当前选择进行查询过滤

Call btnSerchStart_Click(Nothing, Nothing) '这是调用“开始筛选数据”按钮的Click事件

If mobjDataView.RowFilter <> "" Then

tstrFilter &= mobjDataView.RowFilter & " and "

Else

tstrFilter &= mobjDataView.RowFilter

End If

Try

'收入的过滤器是在当前筛选出的记录再用动作='收入'再筛选一次

'mobjDataView.Table可以引用mobjDataView的原始表,也就是”myproperty”表

'由于如果筛选后没有任何记录入选,Compute方法会出错,所以包含在try字句中

tcurIncome = mobjDataView.Table.Compute("Sum(金额)", tstrFilter & " 动作='收入'")

Catch ex As Exception

End Try

Try '计算支出的金额

tcurPayout = mobjDataView.Table.Compute("Sum(金额)", tstrFilter & " 动作='支出'")

Catch ex As Exception

End Try

tcurMyMoney = tcurIncome – tcurPayout '目前资产

提示:完整源程序可以参阅Private Sub btnShowCollectInfo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnShowCollectInfo.Click过程。

六、打印报表

在VB.NET中提供了水晶报报表控件,以实现数据库的报表打印。

单击“项目→添加新项”菜单命令,在对话框模板中选择“CrystalReport”,文件名保存为“CrystalReport2”。在打开的“CrystalReport对话框”中选择“使用报表专家”选项,单击“确定”按钮,然后根据报表向导提示操作。

在“数据”选项卡中选择数据源,展开“数据库文件”,选择Wealth.mdb数据库。单击“下一步”按钮;在“字段”选项卡中,点击“全部添加”按钮。单击“下一步”按钮;在“组”选项卡中选择报表字段“myproperty.动作”作为分组依据(数据在打印时根据这个选择会将支出和收入分开打印汇总)。单击“下一步”按钮;在“总计”选项卡的“汇总字段”中只保留“myperperty.金额”,汇总类型选择“求和”。单击“下一步”按钮;在“最前N项”选项卡中采用默认值即可。单击“下一步”按钮;在“图表”选项卡中选择合适的图表类型, 在“类型”中选择“饼图”,“数据”中的“放置图表”里选择“每个报表一次,页脚”,“更改为”选择“myproperty.动作”,“显示”选择“myproperty.金额的和”,“文本”中的标题更改为“资产状况”。单击“下一步”按钮;在“选择”选项卡中采用默认值即可。单击“下一步”按钮;单击“下一步”按钮;在“样式”选项卡中的标题设为“猫族四十大盗财务报表”,样式选择“下落式表”。单击“完成”按钮。

vb2.jpg
报表向导

此处还存在两个问题,在报表页脚中显示金额的和的FieldObject对象和实际我们的需要不一致,因为它会累计所有收入/支出的总额,而不是收入减支出的值,这与我们的需求显然不一致。删除它,单击鼠标右键,选择“插入→文本对象”命令,并命名为txtMoneySum(我们在程序中将使正确的值显示在这个对象上)。

另一个问题是详细资料编号如果采用数据库中的编号导出,输出报表时就不会从1开始排序了。解决方法也很简单:删除它,在右侧工具栏的特殊字段中拖个记录号的对象放到编号的装置即可。

程序界面中的打印部分包含有个名为CrystalReportViewer1的CrystalReportViewer控件,我们只要提供报表设计时指定字段的数据源,推到报表的DataSource中,其余的事就不用自己再费心。以下是从sbPrint过程中插取的打印代码:

Dim tobjCrystalReport2 As New CrystalReport2

Dim tobjReportTextObject As CrystalDecisions.CrystalReports.Engine.TextObject

'构建包含打印实际数据的DataSet对象的代码

'并利用前面介绍的技术统计出收入/支出的值以便下面代码中计算目前资产

'完整代码省略

'利用对象取得上文自行插入的txtMoneySum对象,并利用它的Text属性显示当前实际资产值

tobjReportTextObject = tobjCrystalReport2.ReportDefinition.ReportObjects.Item("txtMoneySum")

tobjReportTextObject.Text = "目前的资产:" & (tcurIncome - tcurPayOut) & "元"

tobjCrystalReport2.SetDataSource(tobjDataSet) '将数据源推到报表中

'将报表放到报表显示控件中

Me.CrystalReportViewer1.ReportSource = tobjCrystalReport2

完成了这些代码后,VB.NET在运行时会自动完成“打印预览/打印”的实际工作。