PDA

查看完整版本 : 基于VB的COM编程入门教程(二)


华亮
2004-11-16, 09:01 PM
深入COM
  在第二部分中,我们将利用上一面学到的知识来创建一个基于数据库的一个真实的COM组件,然后用自建的客户程序来处理它,这样我们就可以清楚的领略COM在编程中的简易性,最后将讨论如何测试这个COM组件!


第一节 可重用性           
     
还记得我们是怎么将一个类添加到标准的Visual Basic工程中的?

  是的,一流的COM体系的最大特点是其代码的可重用性。

  换句话说,如果用Visual Basic创建一个用于财务管理的类,那么该类不仅可以通过其他程序来访问,而且还可用于其他合作者的应用程序。比如,一张Excel电子数据表可能就需要这样的数据,或者一个C++程序员可能需要获取他人最新工程的一些信息,等等。

  但是,当我们将所需要的类添加到标准的Visual Basic应用程序中,或是再编译成最终的EXE程序时,其他人仍然不能在程序内部处理类或属性。

  解决这个问题的方法是将所有的类投放到其他人的各自程序中,这称为"COM-enabled"。

  换句话说,假如类有AnnualProfitsToDate属性,一旦该属性得到后,类就会遍历公司的数据库,然后进行相应的一些计算,最后返回一个数值,该值单位可能是英磅、美元、日元或是其它。

  现在,若Excel用户需要对数据库进行上述处理,则只需简单地提供"类程序",而不是真正的代码。该程序释放类和AnnualProfitsToDate属性,允许其他用户将其插入到程序中去。同样,我们也不必告诉C++程序员如何进行人工统计(尽管他能够处理),只需要提供给这个程序就可以了。这样,就减少了大家许多工作。

  这种在Visual Basic工程中添加类后,还将类放进各自程序中的方法,称为ActiveX组件方法。

  在这种方法中,ActiveX程序中的所有功能都可以被其他支持ActiveX的程序语言来处理。换句话说,AnnualProfitsToDate属性既可被Excel、C++得到,也可被Access等其他程序处理。

  那么,如何创建一个用来处理我们的类的ActiveX程序呢?


第二节 创建第一个COM对象

让我们直接来开始创建第一个真正的COM对象,它将被用于那些ActiveX程序中。

  具体步骤如下:

  启动Visual Basic;

  我们将看到一个"New Project"对话框,其中有几个与ActiveX相关的选项图标。下面来解释一下:

  ActiveX DLL——创建一个包含类的.DLL程序,这是我们将要采用的选项;

  ActiveX EXE——创建一个包含类的.EXE程序,以后将讨论这个类型;

  ActiveX Control——添加一个工程,允许自己创建用于toolbox中的控件。这里不去讨论它。

  ActiveX Document EXE——创建一个基于Web页的.EXE程序,这里也不去讨论。

  ActiveX Document DLL——创建一个基于Web页的.DLL程序,由于与我们的主题甚远,所以自然也不去讨论它。

  顺便说明一下,如果在工程列表中没有上述选项,那么你可能使用了Visual Basic的学习版。

  当然,在上述那么多选项中,我们真正感兴趣的是ActiveX DLLs和Active EXEs。等会再来讨论后一种,这里先看看第一种!

  选中"ActiveX DLL"项;

  单击[OK]按钮;

  这样,一个ActiveX DLL工程就建立好了。我们之所以创建ActiveX DLL是准备将它作为一个前端服务器,它基于Northwind数据库中的Customers表的(Northwind数据库是随Visual Basic一起发行的,位于VB98文件夹中)。

  这也就是说,我们后面一定会处理Customers信息,但那时我们仅仅需要对类的调用,而不想过多地停留在数据处理代码的纠缠中。

  当然,我们得首先创建这个类。虽然COM能使编程更容易一点,但这个类的构造还是比较困难的。尽管如此,在深入数据库之前,先来对ActiveX程序中的名称作一些修改。

  将类的Name属性改为"Customers";

  选择"Project"->"Project Properties"菜单,在弹出的对话框中,将工程名改为"Northwind";

  现在再来使类与数据库相连:

  选择"Project"->"References"菜单;

  在弹出的对话框中,选择"Microsoft ActiveX Data Objects 2.1 Library",单击[OK]按钮;该"引用"允许用户处理一个数据库,当然现在都使用COM对象来处理了。下面将围绕相应的记录集而展开:

  在我们的类中添加下列代码:

Dim rs As Recordset

  这是一个用于访问数据库的记录集对象。

  当然,当其他开始使用该类时,我们希望记录集对象能和数据库建立连接,而当类使用结束后,与数据库的连接能断开。基于这种思想,其代码如下:

  在代码窗口中,将Object组合框中当前的"(General)"项改为"Class";

  在右边的组合框中,确保当前项为"Initialize";

  代码窗口中将出现:

  Private Sub Class_Initialize()

  End Sub

  当类刚开始时,所有这里面的代码都会被执行,类似于表单中的Form_Load事件。

  在"Initialize"事件中键入下列代码:

  Set rs = New Recordset

  rs.ActiveConnection = "Provider=Microsoft." & _

             "Jet.OLEDB.4.0;Data Source=C:\Program Files\" & _

             "Microsoft Visual Studio\VB98\Nwind.mdb;" & _

             "Persist Security Info=False"

  rs.Open "select * from customers", , adOpenKeyset, adLockOptimistic

  这里不需要任何与该类相关的代码,它只是使用Visual Basic通用的ADO数据库处理代码,该类中的代码是用来如何与数据库建立连接的。

  需要说明的是,如果Northwind数据库Nwind.mdb不在C:\Program Files\Microsoft Visual Studio\VB98文件夹,那么必须将ActiveConnection字符串内容作适当修改!

  当类开始时,rs对象负责与数据库建立连接,但当类对象结束后或程序关闭它时,我们应该使该连接断开。

  编程时,我们使用Terminate事件,它与Form_Unload非常相似的。从名称来看,它们都有一个"n",但更相似的地方是当相应的对象关闭后,它们都会被激发。

  下面来加入数据库关闭的代码:

  从Object组合框中选择"Class",从Procedure组合框中选择"Terminate";

  在"Terminate"事件中,添加下列代码:

  rs.Close

  Set rs = Nothing

  这就是我们添加的又一段简单代码,它只是简单地关闭数据库,然后将rs设置为Nothing。这样,rs就会被有效删除。

  好了,本节就到这里。下一节中,我们将继续添加代码用来处理数据中的记录集。


第三节 添加属性和方法         



  下面,我们添加一个属性来让用户获取CustomerID字段的值,其相应的示例代码如下:

  Public Property Get CustomerID() As String
   CustomerID = rs("CustomerID")
  End Property

  Public Property Let CustomerID(NewValue As String)
   rs("CustomerID") = NewValue
  End Property



  显然,该属性的Get操作只是简单地返回"CustomerID"字段的值,相应地,Let操作是将"CustomerID"字段设置一个新值。

  换句话说,属性中有两个部分:"getting"和"letting",事实上可能还有另外一个"setting"操作。但对于不同场合来说,我们总需要Get和Let来进行读和写的操作。

  这里所引起注意的是,在上述属性过程中,应该对某些值进行必要的检测。例如,在调用Let属性时,用户可能有如下操作:

  ObjectName.CustomerID = "HALFI"

  该Let属性操作后,"CustomerID"等于新的字符串"HALFI"。但当查看Northwind数据库内容时,我们会发现"CustomerID"字段的字符长度不能超过5。如果用户有这样的操作:

  ObjectName.CustomerID = "HALFISTORE"

  则出现数据库操作错误。虽然,可以通过错误句柄来处理这个问题,但是如果能在代码中检测NewValue的长度岂不更好?如果该值超过5个字符,我们既可以通过裁剪取共前5个字符,也可以忽略这个新的字符串而弹出一个错误提示。但这里,我们采用后一种措施。

  在我们的类中添加下列代码:

  Public Property Get CustomerID() As String
   CustomerID = rs("CustomerID")
  End Property
  Public Property Let CustomerID(NewValue As String)
   'If the length of NewValue is greater than five
   If Len(NewValue) > 5 Then
    '... then raise an error to the program
    'using this class
    Err.Raise vbObjectError + 1, "CustomerID", _"Customer ID can only be up to five " & _"characters long!"
   Else
    '... otherwise, change the field value
    rs("CustomerID") = NewValue
   End If
  End Property

  好了,在完成下列步骤之前,我们已经为添加方法花费了不少时间。

  在我们的类中添加下列代码:

  Public Sub Update()
   rs.Update
  End Sub

  该Update方法只是简单地调用记录集对象的Update方法来更新记录。

  下一步,我们将用一个很小的样例程序来测试这个属性和方法,在测试时还将使用特定的技巧来追踪类和程序的运行。

第四节 类的测试

现在就来测试前面创建的类。

  按F5运行程序;在弹出的属性对话框中,选中"Wait for Components to Start"(启动工程时等待创建部件),然后按[OK]按钮;

  这时,类就会被激活,其他程序就可使用它的功能。

  再次运行Visual Basic另一个实例;

  创建一个新的"Standard EXE"工程;

  选择"'Project"->"References"菜单;

  浏览对话框中可引用的列表项,可以发现一些额外的组件。

  选中"Northwind"列表项;

  Northwind就是前面创建的ActiveX工程。

  单击[OK]按钮;

  现在添加一些代码来使用上述工程:

  在Form1表单中添加一个命令按钮;为命令按钮添加下列代码:

  Dim Test As Customers
  Set Test = New Customers
  MsgBox Test.CustomerID
  Set Test = Nothing

  该代码首先创建一个新的Customers对象,然后显示CustomerID信息,最后将Test对象置为Nothing,并关闭它。

  按F5键运行测试程序;

  需要说明的是,当运行时出现"invalid reference"错误提示时,肯定哪些地方有问题。这时可按下面步骤重新来一次:

  (1) 在测试工程中去掉Northwind引用;

  (2) 重新启动Northwind工程;

  (3) 在测试工程中添加Northwind引用,再运行!

  单击表单中的命令按钮;

  这时运行时可能需要几秒钟,毕竟还要做一些如数据库连接等工作。但是,除了一开始的停留外,后面的调用就快得多了。程序将显示包含"ALFKI"的消息对话框。

  关闭测试程序。

  现在,我们来看看程序背后究竟发生什么。

  将插入符移动到MsgBox Test.CustomerID这条语句上;按F9;

  该语句显示为红色,用来标记一个断点。当代码运行时,它会停留在这里。按F8将单步运行此语句,并移动到下一句代码上。

  按F5再次运行测试程序;

  单击命令按钮;

  流程将停留在MsgBox这条命令上。

  按F8,慢慢单步执行各条语句;

  将会看到系统在两个Visual Basic中来回切换,显示出不同属性的处理过程。

  结束后,关闭测试程序。

  下面再对前面的工程进行测试。这一次,我们不仅获取CustomerID的值,而且还设置这个值。

  将命令按钮的代码改为:

  Dim Test As Customers
  Set Test = New Customers
  Test.CustomerID = "KARLY"
  Test.Update
  MsgBox Test.CustomerID
  Set Test = Nothing

  该代码首先设置"CustomerID"字段,然后更新记录集,最后显示出CustomerID属性,其结果应该是设置的"KARLY"。

  假如愿意,仍然可以按F9高亮显示"Test.CustomerID =" 这条语句,然后按F8单步运行来查看其工作情况。

  至此,我们已经成功地创建并测试一个简单的基于数据库的类。但是,还没有对customerID的字符串长度作测试,如果其长度超过5个字符,看看会发生什么?

下一步,我们将扩充并改进这个数据库类。

  首先添加类的几个特征:其他的属性、一些方法甚至一两个事件。 其相应的代码如下:

  Dim WithEvents rs As Recordset
  Public Event RecordsetMove()
  Private Sub Class_Initialize()
   Set rs = New Recordset
   rs.ActiveConnection = "Provider=Microsoft." & _"Jet.OLEDB.4.0;Data Source=C:\Program Files\" & _"Microsoft Visual Studio\VB98\Nwind.mdb;" & _"Persist Security Info=False"
   rs.Open "select * from customers", , adOpenKeyset, adLockOptimistic
  End Sub

  Private Sub Class_Terminate()
   rs.Close
   Set rs = Nothing
  End Sub

  Public Property Get CustomerID() As String
   CustomerID = rs("CustomerID")
  End Property

  Public Property Let CustomerID(NewValue As String)
   'If the length of NewValue is greater than five
   If Len(NewValue) > 5 Then
    '... then raise an error to the program
    'using this class, by running
    'Err.Raise vbObjectError + OurErrorNumber
    Err.Raise vbObjectError + 1, "CustomerID", _"Customer ID can only be up to five " & _ "characters long!"

   Else
    '... otherwise, change the field value
    rs("CustomerID") = NewValue
   End If
  End Property
  Public Property Get CompanyName() As Variant
   CompanyName = rs("CompanyName")
  End Property

  Public Property Let CompanyName(ByVal NewValue As Variant)
   rs("CompanyName") = NewValue
  End Property

  Public Property Get ContactName() As Variant
   ContactName = rs("ContactName")
  End Property

  Public Property Let ContactName(ByVal NewValue As Variant)
    rs("ContactName") = NewValue
  End Property

  Public Property Get ContactTitle() As Variant
   ContactTitle = rs("ContactTitle")
  End Property

  Public Property Let ContactTitle(ByVal NewValue As Variant)
   rs("ContactTitle") = NewValue
  End Property

  Public Property Get Address() As Variant
   Address = rs("Address")
  End Property

  Public Property Let Address(ByVal NewValue As Variant)
   rs("Address") = NewValue
  End Property

  Public Property Get City() As Variant
   City = rs("City")
  End Property

  Public Property Let City(ByVal NewValue As Variant)
   rs("City") = NewValue
  End Property

  Public Property Get Region() As Variant
    Region = rs("Region")
  End Property

  Public Property Let Region(ByVal NewValue As Variant)
   rs("Region") = NewValue
  End Property

  Public Property Get PostalCode() As Variant
   PostalCode = rs("PostalCode")
  End Property

  Public Property Let PostalCode(ByVal NewValue As Variant)
   rs("PostalCode") = NewValue
  End Property

  Public Property Get Country() As Variant
   Country = rs("Country")
  End Property

  Public Property Let Country(ByVal NewValue As Variant)
   rs("Country") = NewValue
  End Property

  Public Property Get Phone() As Variant
   Phone = rs("Phone")
  End Property

  Public Property Let Phone(ByVal NewValue As Variant)
   rs("Phone") = NewValue
  End Property

  Public Property Get Fax() As Variant
   Fax = rs("Fax")
  End Property

  Public Property Let Fax(ByVal NewValue As Variant)
   rs("Fax") = NewValue
  End Property

  Public Sub AddNew()
   rs.AddNew
  End Sub

  Public Sub Update()
   rs.Update
  End Sub

  Public Sub CancelUpdate()
   If rs.EditMode = adEditInProgress Or _rs.EditMode = adEditAdd Then
    rs.CancelUpdate
   End If
  End Sub
  Public Sub MoveNext()
   rs.MoveNext
  End Sub

  Public Sub MovePrevious()
   rs.MovePrevious
  End Sub

  Public Sub MoveFirst()
   rs.MoveFirst
  End Sub

  Public Sub MoveLast()
   rs.MoveLast
  End Sub

  Public Function FindByCustomerID(CustomerID As String) As Boolean
   'Uses the Find method to locate customers
   'with a matching CustomerID.
   'Returns True value is customer(s) found
   Dim varBookmark As Variant
   rs.MoveFirst
   rs.Find ("CustomerID='" & CustomerID & "'")
   If rs.EOF = True Then
     FindByCustomerID = False
     rs.Bookmark = varBookmark
   Else
     FindByCustomerID = True
   End If
  End Function

  Public Property Get EOF() As Boolean
  'Example of a read-only property
  No Property Lets here
  EOF = rs.EOF
  End Property
  Public Property Get BOF() As Boolean
   'Another example of a read-only property
   BOF = rs.BOF
  End Property
  Private Sub rs_MoveComplete(ByVal adReason As ADODB.EventReasonEnum, _
       ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
       ByVal pRecordset As ADODB.Recordset)

   'Reacts to the recordset MoveComplete
   'method - raises event with each move
   RaiseEvent RecordsetMove
  End Sub

  需要说明的是:迄今为止,我们仅仅是在一个类中添加代码。当然,也可以选择"Project"->"Add Class"菜单来向工程添加多个类,而且还可利用"collections"使这些类工作在一起。但是在这里,我们仍然想用一个类来处理一个数据表。

  将上述类的代码复制并粘贴到自己的类中,下一节将讨论该程序的编译。


第五节 建立一个测试程序    

 这里我们直接创建一个应用程序来测试一下前面生成的ActiveX EXE文件监视组件:

  新建一个"Standard EXE"工程;

  下面需要添加一个引用到我们的新的文件组件中去,然后添加少量的代码作尝试:

  选择"Project"->"References"菜单;

  选中"File"组件选项,然后单击[OK]按钮;

  在表单的通用声明部分添加下列代码:

  Dim WithEvents MyFileObject As FileCheck

  从"Object"下拉列表中选择"MyFileObject";

  确保插入符在FileFound事件程序中;

  键入下列代码:

  MsgBox "Found: " & Filename

  在表单Form1中添加一个命令按钮;

  在该按钮中添加下列代码:

  Set MyFileObject = New FileCheck

  MyFileObject.MonitorFile ("c:\test.txt")

  这里,我们是将MyFileObject定义成FileCheck的一个新实例,然后用c:\test.txt参数运行MonitorFile方法。这时,程序在后台中启用计时器,且每隔60秒都来检测一次这个文件。

  由于,我们的计算机中还没有这个文件,所以什么也没有发生。现在,我们用Notepad(记事本)在C盘中创建一个名为test.txt的文件。

  则在60秒内,弹出一个消息对话框用来表示我们的文件被找到。这就是我们刚刚创建的ActiveX EXE!

  对于上述这样的组件,使用异步处理是没有太大的必要的。正如我们以前讨论的那样,ActiveX EXE是有自己的进程空间的。因此,当计时器启用并检测文件的存在性时,它不会使其它程序暂停。而如果使用的是DLL,那么就可以使用异步处理了。

  即使这样,你能在计时器中放入任何代码吗?是能放入创建大型报表的程序、复杂的计算代码,还是其他?

  至此,我们构造了一些实际的常规组件,但我们还没有来得及讨论它们的发布问题,这将在下一节中进行。

第六节 全面测试

   这一次,我们不用Visual Basic来测试前面的程序,而是先来编译:

  选择"File"->"Make Project1.exe"菜单;

  择要保存的文件夹,单击[OK]按钮;

  退出Visual Basic;

  运行刚才编译过的.EXE文件;

  当表单出现时,试试打开一个customer,然后对其修改并单击[OK]按钮。再打开刚才的customer,看看其资料是否被修改?结果应该是这样,因为ActiveX DLL会自动将结果保存在数据库中。

  现在,让我们静下来想一想刚才做过的事件。我们只用了几条简单的语句就建立了一个自己的数据库应用程序,虽然这里只使用了COM一点功能,但却使得应用程序变得如此容易。

  关闭已测试完的应用程序;

  如果上述过程比较顺利的话,那么我们再做这样的测试:

  打开Northwind工程;

  假如对Northwind工程作了某些修改,那么还需要重新编译:

  选择"File"->"Compile Northwind.dll"菜单;

  选择和上次保存Northwind.dll相同的文件夹,以便覆盖原来的文件,单击[OK]按钮;

  需要说明的是,如果编译时出现错误,那肯定在程序中有一些不对的地方。关闭所有正在运行的程序,然后再重新试一试。

  编译后,退出Visual Basic;

  再次运行Project1.exe;

  天啦,居然会有错误信息,类已不再支持原来接口。

  这就是我们做的一种测试,当重新编译ActiveX工程时,使用它的程序就会被支解。

  解决上述问题的一种办法是将Project1文件打开并重新编译。但是假如工作组中有两百个员工,这就是说,我们得把重新编译好的工程和新的DLL分发到这两百个员工手上。

  你能受得了吗?

  不,不能这样做。我们必须搞清楚错误产生的原因,以及弄明白为什么我们的工程不能和最新的DLL一起工作,难道是兼容性的问题吗?本教程的最后一部分将详细探讨这些内容。