类 | 描述 |
---|
NumberOfSettings | 包含步骤配置工作表UFormConfig中的列数 |
Worksheet | 告诉类到哪里查找向导的每步的信息 |
CurrentPage | 在向导中存储当前步骤的值 |
PreviousPage | 基于CurrentPage属性计算;返回向导中前一步骤的值 |
NextPage | 基于CurrentPage属性计算;返回向导中下一步骤的值 |
PreviousButton | 存储用户窗体中导航到向导的前一步的按钮的指针 |
NextButton | 存储用户窗体中导航到向导的后一步的按钮的指针 |
需要在类中添加更多的属性。下面的只读属性包含cStep对象的集合,包含向导的每一步的信息。
PageSettings属性存储该集合,使用HRWizard用户窗体后台的客户端代码返回一个Collection对象。
PageSettings属性的代码如下:
Public Property Get PageSettings() As Collection Dim colReturn As Collection Dim numrows As Integer Dim row As Integer Dim col As Integer Dim sKey As String Set colReturn = New Collection numrows = m_oWorksheet.Cells(Rows.Count, 1).End(xlUp).row For row = 2 To numrows Set m_oStep = New cStep For col = 1 To m_iNumSettings Select Case col Case 1 m_oStep.Order = m_oWorksheet.Cells(row, col).Value sKey = CStr(m_oStep.Order) Case 2 m_oStep.Page = m_oWorksheet.Cells(row, col).Value Case 3 m_oStep.Caption = m_oWorksheet.Cells(row, col).Value End Select Next col colReturn.Add m_oStep, sKey Next row m_iNumSteps = colReturn.Count Set PageSettings = colReturnEnd Property |
我们首先做的是获取工作表中已使用的区域的行数:
numrows = m_oWorksheet.Cells(Rows.Count, 1).End(xlUp).row |
注意,虽然Excel的Worksheet对象有Rows.Count方法,但是在这里不能使用(m_oWorksheet.Rows.Count)。这将返回工作表中的总行数,这样不仅提供不正确的值,而且也会使Integer变量溢出。
接下来,循环填充cStep对象集合,代码如下:
For row = 2 To numrows Set m_oStep = New cStep For col = 1 To m_iNumSettings Select Case col Case 1 m_oStep.Order = m_oWorksheet.Cells(row, col).Value sKey = CStr(m_oStep.Order) Case 2 m_oStep.Page = m_oWorksheet.Cells(row, col).Value Case 3 m_oStep.Caption = m_oWorksheet.Cells(row, col).Value End Select Next col colReturn.Add m_oStep, sKey Next row |
上述代码中,首先做的是实例化一个新的cStep对象,然后移到内部循环遍历配置工作表中的列,将它们赋给内部的cStep对象的相应属性。这段代码运行前,已经通过NumberOfSettings属性设置m_iNumSettings值。
最后,将cStep对象添加到内部的集合colReturn中,在该集合中传递Order值作为主键。
注意,在外部循环中的第一行代码,Set m_oStep=New cStep,是重要的。如果忽略该代码,那么集合中将以四个相同的cStep对象结束(全部都包含从工作表中读取的最后一个配置项中的数据)。这是因为m_oStep对象引用仍然是当前引用,所以每次调用时都会修改任何已存在的实例。通过使用New关键字,创建新的、单独的对象实例。
最后,设置内部的m_iNumSteps变量,用来追踪前一个和下一个可用的命令按钮,并且最终返回集合:
m_iNumSteps = colReturn.Count Set PageSettings = colReturn |
现在,将注意力转向PreviousButton属性和NextButton属性。记得这些属性的内置变量被声明为WithEvents。当声明一个对象时使用WithEvents时,可以通过VB代码窗口的对象框访问该对象的事件代码,如下图所示。

从对象框中选择m_oNextButton和m_oPreviousButton,在类模块中插入事件处理代码块,并在其中添加代码如下:
Private Sub m_oNextButton_Click() m_oNextButton.Enabled = Me.NextPage <> m_iNumSteps + 1 m_oPreviousButton.Enabled = Me.PreviousPage <> 0End Sub Private Sub m_oPreviousButton_Click() m_oPreviousButton.Enabled = Me.PreviousPage <> 0 m_oNextButton.Enabled = Me.NextPage <> m_iNumSteps + 1End Sub |
这段代码基于cStepManager类的NextPage或PreviousPage属性控制每个按钮是否启用。当该类首次在客户端代码中被创建时,再添加一个方法初始化按钮:
Public Sub HandleControls() m_oPreviousButton.Enabled = Me.PreviousPage <> 0 m_oNextButton.Enabled = Me.NextPage <> m_iNumSteps + 1End Sub |
到现在为止,我们已经创建了相当数量的代码,全都存储在跨越许多类模块的对象中。通过划分功能,使维护代码的工作非常容易。如果需要绑定列表到目前还没有处理的数据源,只需在cListManage类中添加一个新方法。如果需要在数据处理过程中添加一个屏幕界面,则在多页控件中设计一个新页面,创建一个新类去存储屏幕信息,并在配置表中添加一行。
在添加完所有的类模块并编写好代码后,工程资源管理器中的类模块文件夹应该如下图所示。

编写HRWizard用户窗体代码
现在,我们已经完成了最艰难的工作。是到将对象放进HRWizard用户窗体里并使这些对象工作的时候了。
打开HRWizard用户窗体代码窗口,添加下列模块级的变量声明:
Dim m_oEmployee As cPersonDim m_oLM As cListManagerDim m_oWizard As cStepManagerDim m_colSteps As Collection |
虽然我们创建了9个分开的类模块来运行我们的应用程序,但是许多类都是通过在声明部分列出来内部使用。使用cPeason类收集新员工的数据,使用cListManager类来填充HRWizard用户窗体中不同的组合框,使用cStepManager类决定何时且按什么顺序显示哪个屏幕,并控制导航命令按钮的可用性。最后,使用标准的VBA Collection对象,用于存储cStepManager对象的PageSettings集合。
初始化应用程序
在HRWizard用户窗体的Initialize事件中,将初始化自定义的对象并添加代码来设置向导、列表和显示用户窗体。
在UserForm_Initialize事件中添加下列代码:
Private Sub UserForm_Initialize() Set m_oEmployee = New cPerson Set m_oLM = New cListManager Set m_oWizard = New cStepManager InitWizard InitLists InitFormEnd Sub |
下面,创建三个Init函数,分别设置向导、列表管理器和用户窗体对象。
初始化向导
在用户窗体代码窗口添加新的子程序,将其命名为InitWizard,并添加下列代码:
Private Sub InitWizard() With m_oWizard Set .Worksheet = Sheets("UFormConfig") .NumberOfSettings = 3 Set m_colSteps = .PageSettings Set .PreviousButton = Me.cmdPrevious Set .NextButton = Me.cmdNext .CurrentPage = MultiPage1.Value + 1 End WithEnd Sub |
上述代码完成下列工作:
- 告诉cStepManager对象在哪里找到配置数据
Set .Worksheet = Sheets("UFormConfig") |
- 告诉cStepManager对象获取数据的列数
- 放置页设置到集合里
Set m_colSteps = .PageSettings |
- 设置导航按钮
Set .PreviousButton = Me.cmdPreviousSet .NextButton = Me.cmdNext |
- 设置当前页
.CurrentPage = MultiPage1.Value + 1 |
因为多页控件的Page集合基于0,所以使用多页控件的Value属性加1来设置CurrentPage属性。
在初始化用户窗体之前,必须设置cStepManager对象,因为该用户窗体使用PageSettings集合来设置它自已。
初始化组合框
下一步是将组合框绑定到它们各自的列表。该列表被存储在ListMgr工作表中。
插入一个新的子程序,并将其命名为InitLists,添加下列代码:
Private Sub InitLists() With m_oLM .BindListToRange "Departments", Me.cboDept .BindListToRange "Locations", Me.cboLocation .BindListToRange "NetworkLvl", Me.cboNetworkLvl .BindListToRange "ParkingSpot", Me.cboParkingSpot .BindListToRange "YN", Me.cboRemoteAccess End WithEnd Sub |
同样,上述代码也非常简单,它们为应用程序中的每个列表调用cListManager对象的BindListToRange方法。
初始化用户窗体
在设置应用程序中的最后一步是初始化用户窗体自身。创建一个名为InitForm的新子程序,并添加下列代码:
Private Sub InitForm() Dim iFirstPage As Integer Dim i As Integer Dim iPageCount As Integer iFirstPage = m_colSteps("1").Order - 1 Me.MultiPage1.Value = iFirstPage Me.MultiPage1.Pages((m_colSteps("1").Page) - 1).Caption = m_colSteps("1").Caption m_oWizard.HandleControls iPageCount = MultiPage1.Pages.Count For i = 1 To iPageCount - 1 MultiPage1.Pages(i).Visible = False NextEnd Sub |
这里,设置多页控件的Value属性为PageSetting集合(m_colSteps)的项目(其键值为1),并设置其标题:
iFirstPage = m_colSteps("1").Order - 1 Me.MultiPage1.Value = iFirstPage Me.MultiPage1.Pages((m_colSteps("1").Page) - 1).Caption = m_colSteps("1").Caption |
记住,我们传递Order属性的值作为键值,这使得它非常容易去判断要移动至哪页。当设置多页控件的Value属性时,正使用相对应的值激活该页。在这里,该值为1.
然后调用m_oWizard对象的HandleControls方法初始化导航按钮为正确的设置:
接下来,隐藏除第一页外的所有页:
iPageCount = MultiPage1.Pages.Count For i = 1 To iPageCount - 1 MultiPage1.Pages(i).Visible = False Next |
记住,多页控件的Page集合是基于0的,因此通过以1开始循环计数器,保持该页面可见。
此时,可以运行用户窗体。
1、在VBE中,双击工程资源管理器窗口的用户窗体。
2、单击标准工具栏中的“运行子过程/用户窗体”按钮或者按F5键,如下图所示。

注意,下图中在选项卡中出现的标题,并且前一步按钮被禁用。

再看看Department组合框,绑定Departments命名区域到该组合框。

3、通过单击右上方的X按钮,停止用户窗体的运行。
给用户窗体添加导航
导航按钮在向导应用程序中具有移动步骤的任务。但它们也需要放置每个屏幕中的数据到其在用户窗体的cPerson对象里的位置的能力。
在cmdNext_Click中添加下列代码:
Private Sub cmdNext_Click() Dim iNext As Integer StoreData iNext = m_oWizard.NextPage Me.MultiPage1.Value = m_colSteps(CStr(iNext)).Order - 1 Me.MultiPage1.Pages((m_colSteps(CStr(iNext)).Page) - 1).Caption = m_colSteps(CStr(iNext)).Caption ShowNextPage "up"End Sub |
在向导中移到下一步之前首先需要做的是,保留在当前用户窗体中输入的值。StoreData方法决定用户处于哪一步并基于该位置调用正确的存储方法,代码如下所示:
Private Sub StoreData() Select Case m_oWizard.CurrentPage Case 1 StorePerson Case 2 StoreAddress Case 3 StoreEquipment Case 4 StoreAccess End SelectEnd Sub |
上述代码中的存储方法的代码如下:
Private Sub StorePerson() With m_oEmployee .FName = Me.txtFname.Value .MidInit = Me.txtMidInit.Value .LName = Me.txtLname.Value If Len(Me.txtDOB.Value & "") > 0 Then .DOB = Me.txtDOB.Value End If .SSN = Me.txtSSN.Value .Department = Me.cboDept.Text .JobTitle = Me.txtJobTitle.Value .Email = Me.txtEmail.Value End WithEnd Sub Private Sub StoreAddress() With m_oEmployee.Address .StreetAddress = Me.txtStreetAddr.Value .StreetAddress2 = Me.txtStreetAddr2.Value .City = Me.txtCity.Value .State = Me.txtState.Value .ZipCode = Me.txtZip.Value .PhoneNumber = Me.txtPhone.Value .CellPhone = Me.txtCell.Value End WithEnd Sub Private Sub StoreEquipment() Dim opt As MSForms.OptionButton With m_oEmployee.Equipment For Each opt In Me.fraPCType.Controls If opt.Value = True Then .PCType = opt.Caption Exit For End If Next For Each opt In Me.fraPhoneType.Controls If opt.Value = True Then .PhoneType = opt.Caption Exit For End If Next .Location = Me.cboLocation.Text If Me.chkFaxYN = True Then .FaxYN = "Y" Else .FaxYN = "N" End If End WithEnd Sub Private Sub StoreAccess() Dim opt As MSForms.OptionButton With m_oEmployee.Access If Len(Me.cboNetworkLvl.Text & "") > 0 Then .NetworkLevel = CInt(Me.cboNetworkLvl.Text) End If .ParkingSpot = Me.cboParkingSpot.Text .RemoteYN = Me.cboRemoteAccess.Text For Each opt In Me.fraBuilding.Controls If opt.Value = True Then .Building = opt.Caption Exit For End If Next End WithEnd Sub |
这段代码简单地从屏幕中接收数据,并将其放置在cPerson里的相应的对象中。
接下来,确定下一页。(记住,多页集合是基于0的,因此从Order属性中减1以获得下一页的值)
iNext = m_oWizard.NextPage Me.MultiPage1.Value = m_colSteps(CStr(iNext)).Order - 1 Me.MultiPage1.Pages((m_colSteps(CStr(iNext)).Page) - 1).Caption = m_colSteps(CStr(iNext)).Caption |
然后,调用ShowNextPage方法,告诉它我们想移动的方式:
ShowNextPage方法的代码如下:
Private Sub ShowNextPage(Direction As String) Dim iCurrPage As Integer Dim iUpDown As Integer iCurrPage = MultiPage1.Value If LCase(Direction) = "up" Then iUpDown = 1 Else iUpDown = -1 End If MultiPage1.Pages(iCurrPage + iUpDown).Visible = True MultiPage1.Pages(iCurrPage).Visible = FalseEnd Sub |
这个方法查找CurrentPage属性的值,基于传递给该方法的Direction参数加或减1。
cmdPrevious按钮的Click事件看起来非常相似:
Private Sub cmdPrevious_Click() Dim iPrevious As Integer StoreData iPrevious = m_oWizard.PreviousPage Me.MultiPage1.Value = m_colSteps(CStr(iPrevious)).Order - 1 Me.MultiPage1.Pages((m_colSteps(CStr(iPrevious)).Page) - 1).Caption = m_colSteps(CStr(iPrevious)).Caption ShowNextPage "down"End Sub |
唯一的差别是传递关键字down到ShowNextPage方法以便向用户移动到合适的方向。
下面,添加最后一个事件处理来帮助我们使用导航。无论何时改变多页控件中的页面,控件的Change事件被触发。我们使用事件去捕捉当前页面的值,并将其存储在m_oWizard对象的CurrentPage属性中。
添加下面的代码到MultiPage1控件的Change事件:
Private Sub MultiPage1_Change() m_oWizard.CurrentPage = MultiPage1.Value + 1End Sub |
现在,让我们来试试导航的工作。
1、在设计视图下打开用户窗体,单击标准工具栏中的“运行子程序/用户窗体”按钮或按F5键。
2、打开用户窗体后,单击下一步按钮移动到向导中的第二步(已在配置工作表中定义),应该是Address屏幕。注意到两个导航按钮现在都能用了,如下图所示。

3、单击前一步按钮导航回到Personal屏幕,此时前一步按钮不再是活动的了。
4、单击下一步按钮直至最后一个屏幕(已在配置工作表中定义),应该是NetWork Access屏幕,此时下一步按钮不再能够使用,如下图所示。

5、通过单击右上方的X按钮,停止用户窗体的运行。
保存员工记录
至此,我们已经做了大量的工作,获得了一些完美干净的功能从自定义对象提供给用户窗体。唯一没有做的就是将数据保存到EmpData工作表。
一般来说,可以创建一个子程序,将其命名如SaveData(),将从cmdSave_Click事件中调用该程序,但是cHRData类已经具有了SaveEmployee方法。我们可以直接从cmdSave_Click中调用而不需要再创建保存函数。
在cmdSave_Click事件中插入下列代码:
Private Sub cmdSave_Click() Dim oHRData As cHRData Set oHRData = New cHRData Set oHRData.Worksheet = Sheets("EmpData") oHRData.SaveEmployee m_oEmployee Set oHRData = NothingEnd Sub |
在设置Worksheet属性之后,以便于cHRData对象知道在哪里保存数据,调用SaveEmployee方法,传递m_oEmployee对象,那里包含要保存的所有数据。
清理
我们几乎已经获得了一个完整的应用程序。下面让我们添加Cancel按钮的代码并在用户窗体的Terminate事件中放置清理代码。
在cmdCancel按钮的Click事件中添加下面的代码行:
Private Sub cmdCancel_Click() Unload MeEnd Sub |
这行代码简单地卸载用户窗体而不保存任何数据。
现在我们清除HRWizard用户窗体使用的对象。在UserForm_Terminate事件处理中添加下列代码:
Private Sub UserForm_Terminate() Set m_oEmployee = Nothing Set m_oLM = Nothing Set m_oWizard = NothingEnd Sub |
下面再添加一个简单的函数用来打开向导窗体。在VBE中,添加一个标准模块,在其中添加下列代码:
Sub StartWizard() HRWizard.ShowEnd Sub |
测试HRWizard应用程序
测试时间到了!我们在向导中的每一屏幕中输入数据,并将其保存到EmpData工作表中。
从Excel工作簿中,从宏对话框中运行StartWizard子程序,如下图所示。

下图中显示了一些简单的输入值以及在EmpData工作表中保存的数据。





学习小结
- 学习优秀的示例是一种好的学习方法。不仅能够开阔视野,而且能够学到好的编程习惯和好的技巧,并且在实践中借鉴他人的做法,能够增加经验,少走弯路。
- 输入时时常编译代码,及时找出一些错误,例如变量名拼写错误、过程名相同、缺少End With等。
- 如果调试总是有错,但总是觉得是对的,就是总是找不出错误在哪儿,累了,休息一会,做点别的事儿,再回来。不要觉得自已没错,既然程序运行有错,那一定是有错。休息一下,换换脑筋,就会发现并改正错误了。
- 示例中建立起了自已的对象层次模型。
- 通过使用类,可以更好地组织和管理代码。虽然编写类模块可能会花费一些时间,但这些努力绝对是值得的。并且,很多类都可以在其它地方重复使用。