原标题:Excel 排课表软件的研发 (第6部分 / 共6部分)李 岗 2006年
第7章 排课表软件VBA应用实例
本章导读
前面介绍的一款排课表软件使用了Excel函数,可以完成自动分配教师个人课表的功能。在本章中,将利用VBA编程,开发使用排课表的一些自动功能;如两个单元格的快速互换、在工作表里面快速准确地寻找教师个人课表、教师姓名的自动更新等。这些操作的自动化,使得排课表软件的使用更加安全、更加方便。
7.1 在排课表软件里开发互换单元格程序
在上一章本书介绍了一个可以很方便快捷地互换两个单元格的宏,使用它可以基本上解决排课表时频繁调课的问题。实际上,只要把带有那个宏的工作簿打开,就可以对任何工作簿实施互换单元格操作,并不只局限于排课表软件。
但是,对于使用排课表软件来说,这样的解决方案还有一点不方便之处。那就是每次使用该功能,都要打开两个工作簿。能否只打开排课表软件(工作簿),就能够使用单元格互换功能呢?这就是本节讨论的主题。
7.1.1 在排课表软件工作簿中实现单元格互换功能
7.1.1.1 通过事件触发程序
摆在面前的任务是,当用户在排课表软件里,用鼠标选择了两个单元格后,怎样才能让电脑知道这时候应该执行互换单元格程序,完成互换单元格任务呢?
一般来说,打开工作簿后,执行VBA程序可以有两个途径。一个就是前面介绍的在模块里面开发过程,然后通过执行宏来运行程序;具体开发可以通过录制宏,也可以通过编写宏代码来实现。另一种方法就是在工作簿和工作表里面,开发通过各种“事件”触发的程序。
什么是工作簿的“事件”?怎样使用工作簿的事件呢?所谓工作簿事件,是指用户对于工作簿的一些操作,例如打开一个工作簿,就产生了一个工作簿的Open事件;选中一个工作表上的一个单元格,就产生了一个工作簿的SheetSelectionChange事件。
当然,对于工作表来说,同样有选择一个单元格后产生的工作表的SheetSelectionChange事件。
为了在工作簿的工作表里面执行互换单元格程序,可用选择单元格产生的SheetSelectionChange事件来触发程序;当用户编写好一个这样的程序时,只要用鼠标选择了两个单元格或者两个单元格区域,就会执行互换单元格程序,完成单元格互换的任务。
具体到在一个工作表里面互换两个单元格,可以针对整个工作簿来开发程序;这样,只要编写一个过程,无论打开那个工作表,都可以运行程序,从而完成任务。
7.1.1.2 编写事件过程代码
打开一个排课表软件的工作簿,按动快捷键Alt+F11进入VBE窗口。在左边的“工程资源管理器”窗口双击“ThisWorkbook”,右边的代码编辑窗口上面的出现两个下拉选择列表框;在左边的对象列表框中,选择“Workbook”,在右边的事件列表框里选择“SheetSelectionChange”;如图7.1-1所示。

图7.1-1
点选“SheetSelectionChange”事件后,代码窗口出现了如图7.1-2所示的两行代码:
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
End Sub
这两行代码创建了工作簿“SheetSelectionChange”事件的空过程;它只有过程的开头和结尾两行代码。它和以前的宏代码的开头有所不同;区别就在于开头不是“Sub”而是“Private Sub”;如图图7.1-2所示。

图7.1-2
Private Sub的含义是私用的过程,也就是指只有所在工作簿才能使用的过程;一般来说,其他过程不能直接调用它;其他工作簿不能够使用它。而Sub是指共用的过程,其他模块可以调用它;它也可以在其他工作簿中使用。
怎样编写这个事件过程的代码呢?把原来的互换单元格的宏代码原封不动地复制到这个空过程里面,看看能否执行。
● 什么是事件
事件是指由系统事先设定的、能被对象识别和响应的动作。当对象发生了某个事件,就会执行与此对象的这个事件相应的代码,这段代码被称为“事件过程”。事件过程的命名格式为:
对象名_事件名
对象名称和相应事件的名称共同构成了事件过程的名称,它们之间要用一个英文字符的下划线“_”连接。例如选中一个工作表上的一个单元格,就产生了一个工作簿的SheetSelectionChange事件;并执行与其对应的事件过程。这个的事件过程名称为:
Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
可以为事件过程编写代码,使得当某个对象发生了某个事件时可以执行用户需要的一些操作。
7.1.1.3 代码的检测
进入一个年级课表,首先用鼠标选择一个单元格,再按下Ctrl键,然后用鼠标选择另一个单元格;奇迹出现了,几乎是在用户选定第二个单元格的同时,这两个单元格已经互换了数值。程序取得了圆满的成功。
继续试验;选择两个任意的包含有多个单元格的区域,但是保证它们的形状相同;两个区域的数值也立刻互换。
最后是选择两个形状不相同的单元格区域,看看程序能否终止互换,并且出现提示信息框。
通过以上的检测,说明这段程序获得了成功。这个互换单元格操作,只须分别选中两个单元格,就可完成,比起使用快捷键的宏,速度更快。
继续检测,在两个单元格互换完成后,如果反悔,想恢复原来的状态,该怎样操作呢?通过试验,只能重新选择这两个单元格,然后恢复原状。而原来使用快捷键的宏,只需再按动一次快捷键,就可以恢复原状。这样看起来,这个由事件触发的互换单元格程序与用快捷键的宏在使用上各有长处。
最后修改一下代码的注释。就得到全部的代码:
Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
‘可以不用快捷键,只要用鼠标分别选定两个单元格,它们就自动互换内容
If Selection.Areas.Count = 2 Then
Areas1 = Selection.Areas(2).Value
Areas2 = Selection.Areas(1).Value
‘两次选择区域如果不全等,则不能进行互换。而且给出提示框
If Selection.Areas(2).Rows.Count = Selection.Areas(1).Rows.Count _
And Selection.Areas(2).Columns.Count = Selection.Areas(1).Columns.Count Then
Selection.Areas(1).Value = Areas1
Selection.Areas(2).Value = Areas2
‘不能互换,并给出一个提示对话框,确定后返回
Else
MsgBox “两次选择的区域不同,请重新选择”, 32, “请慎重选择区域”
End If
End If
End Sub
在VBE窗口的代码框中,这段代码如图7.1-3所示。

图7.1-3
● 代码的缩进
也许用户已经看到,在上面的代码窗口里面的代码,各行的开头并不是对齐的,从上到下,开始行首不断向右面缩进;然后又逐渐恢复。为什么要在代码里使用缩进?缩进可以使代码更容易阅读和理解;特别是输入一些做决定或重复性工作的代码行时,更应该使用缩进。
从上面的例子可以看出,一般地对于Sub和End Sub、If和End If和For和Next等成对出现的代码,而且它们都在行首,用户习惯把它们对齐;写在它们中间的代码,都要缩进。
在VBE窗口菜单上的“工具”——“选项”,进入编辑器对话框,勾选“自动缩进”选项,就可以自动缩进所选的代码行;缩进的量为“Tab宽度”文本框里的数字;默认的自动缩进量是4个字母,也可以在文本框里输入一个新的数字来改变Tab宽度。
在代码窗口中,将光标放在需要编辑的代码行的任意位置上,最好把光标放在行首;然后点击编辑工具条上的缩进按钮,或者按键盘上的Tab键;选中的指令会向右移动4个字母的位置。点击编辑工具条上的“凸出”按钮,或者按Shift+Tab将选中的指令行返回原来的位置。
可以用鼠标选中多行代码,然后执行上面的操作,就可以把它们同时缩进。
7.1.2 在排课表工作簿里增加单元格互换模块
7.1.2.1 解决互换单元格反悔操作的方案
上面把排课表软件工作簿里面开发了一个互换单元格程序,这个程序可以在本工作簿打开的任何一个工作表里面执行。从此,使用排课表软件就更加方便快捷了。但是它不能像使用快捷键的宏那样,反复地操作,实现反悔的功能。怎样解决这一问题呢?
使用快捷键的宏命令放在一个工作簿的模块中;所以自然而然地会想到,如果把排课表软件工作簿也安装上具有互换单元格功能的模块,那么这个排课表软件就既能快速方便地互换单元格,而且在互换之后,又能马上实施反悔操作。
怎样把互换单元格的宏模块,移植到排课表工作簿里面呢?这样的组合,能否成功呢?
把一个工作簿的模块移植到另一个工作簿有多种方法。比如可以在新工作簿里面插入一个模块,然后把原来模块里面的过程代码复制到这个新模块里面,问题就基本解决了。这里之所以说是基本解决,是因为原来模块的一些设置,并不能自动复制到新模块中,还需要进行重新设置。所以下面介绍一种更好的办法:使用模块的导出和导入功能。
7.1.2.2 模块的导出
一个工作簿里面的模块,可以当作一个模块文件,移动到电脑的任何一个文件夹里。这个操作叫做模块的导出。下面把前面开发的互换单元格宏模块导出。
首先打开含有互换单元格宏模块的工作簿,进入VBE窗口;打开如图7.1-1所示的工程资源管理器;用鼠标点击模块文件夹,让模块文件夹显示出来。如果模块文件夹里面有多个模块,它们通常以“模块1”、“模块2”……命名。双击模块1,则可在代码窗口里面打开模块1的全部代码。
右击需要导出的模块名,不妨假设是模块1,在出现的菜单中选择“导出文件”;如图7.1-4所示。

图7.1-4
在出现的如图7.1-5所示的导出文件对话框中,可以选择保存文件的路径,更改模块的文件名等操作,“保存”后即可退出。

图7.1-5
原有的模块文件名为“模块1”,导出实际上是另存为操作,可以修改导出的文件名,例如导出为“单元格互换”模块。
模块文件导出后,可以在资源管理器中文件所在的文件夹里面找到这个文件,它的后缀是“.bas”,文件名是“单元格互换.bas”。
7.1.2.3 模块的导入
现在可以把上面导出的模块文件导入的排课表工作簿里面了。打开目标工作簿,进入VBE窗口。进入工程资源管理器窗口,这时会看到这里面有两个工作簿的工程文件夹;如图7.1-6所示。

图7.1-6
点选导入文件,进入导入文件对话框;如图7.1-7所示。

图7.1-7
在查找范围框里面找到目标文件夹,选择目标文件“单元格互换.bas”模块,单击“打开”后,排课表工作簿就添加了一个模块“单元格互换”。用鼠标双击文件图标,就可以进入代码窗口,看到宏代码。
这时,排课表软件工作簿就具有了更强大的互换单元格功能:在工作表里,选择两个单元格后,无需任何操作,两个单元格就完成了互换;再按动Ctrl+q快捷键,两个单元格就恢复了原状。
这两个过程,一个在工作簿的“SheetSelectionChange”事件中,一个在工作簿的“单元格互换”模块中;实际操作表明,它们可以很好地配合工作。
● 怎样给模块改名
在上面把模块导出的操作中,把“模块1”改成了“单元格互换”。一般来说,原始的模块名称为:模块1、模块2……;有时候模块多了,就有必要给模块重新命名。在工资资源管理器中,打开模块文件夹,用鼠标选中一个模块,不妨是“模块1”。在下面的属性窗口中就会出现一个属性列表;如图7.1-8所示。

图7.1-8
列表“名称”的右边表格里显示这个模块当前的文件名“模块1”;进入这个表格,就可以直接输入新的模块文件名了;例如可以删除原有的文件名,然后输入新模块名“单元格互换”。
7.1.3 开发互换单元格程序的总结与反思
7.1.3.1 互换单元格程序功能的评价
使用互换单元格程序,在工作表上,只要选择两个形状相同的单元格区域,就可以将它们的数值互换。可以说,这样的功能是非常圆满的。之所以这样说,是因为事先用户也许没有这样的奢望;如果能做到用户选择两两个单元格区域,再按动一个专用的按钮,单元格的数值完成互换;有这样的效果就很满足了。
为了更深刻地认识这个软件的功能,应该先对原来的技术做一个评价。原来手工操作大致有两种方法,一种是直接在单元格输入另一个单元格的内容;这无疑是一种笨拙的方法,它的更严重的缺点是有可能输入错误。另一种方法使用复制粘贴的方法,操作过程大致是先将一个单元格的数值复制到第三方单元格,然后将另一个单元格复制到第一个单元格,最后将第三方单元格复制到第二个单元格。这样做严格地说不能使用简单地复制粘贴命令,而要使用选择性粘贴——数值,以避免将两个不同单元格的格式弄乱。这个方法无疑是比较好的手工操作方法;在没有互换单元格程序之前,相信大多数用户都是采用的这种方法。
下面通过列表比较各种技术互换单元格的步骤。

从上表可得出结论,互换单元格程序比起手工操作可提高工作效率6倍。
7.1.3.2 互换单元格程序开发过程点评
首先反思一下单元格互换软件的开发过程;下表列出了开发的几个阶段:

通过以上反思,可以得到一个重要的启示,那就是要以用户为中心开发软件;对技术要精益求精,不断提出新任务,不断解决新问题,实现更好的效果。
反思的另一个收获是,大胆地试验、实践、体验是成功的保证。从毫无把握到圆满解决,没有人事先给出现成的解决方案。例如当把互换单元格的宏代码移植到工作簿事件中,当把宏过程和事件过程组合到一个工作簿里面,事先并不知道这样做的结果;但是通过实践,它们成功了。
当用户在开发软件的过程中遭遇失败时,应该怎样办呢?下面的建议也许有用:
1.向专家当面请教——这是最有效、最快捷的途径;
2.阅读相关资料获取帮助——例如电脑的帮助文件;
3.在网络上查找答案;
4.在网络上向专家求教。
但无论哪种方法,都取代不了自己的探索、自己的实践和体验。
● 了解对象,属性和方法
对象
“对象”是什么呢?“对象”是通过VBA控制的东西。工作簿,工作表,工作表里的单元格区域,图表或者工具条等,这些都是用Excel可以控制的东西;这些东西就是对象。Excel含有一百多种可以通过不同方式操作的对象。所有的VB对象都被分层归类。一些对象本身又可能含有其它的对象;例如,Excel是一个对象,这个对象包含其它对象,例如工作簿或者命令条。工作簿对象也可以包含其它对象,如工作表或者图表。
一组相似的对象被称为“集合”。例如,工作表的集合包含所有具体工作簿中的工作表;命令条的集合包含所有的工具条和菜单。对象集合同样是对象。Excel中使用得最频繁的集合是表(Sheets)集合,它代表所有的工作表和图表;还有工作簿集合以及窗口集合。当用户使用集合时,相同的动作可以在这个集合中所有的对象上执行。
属性
每一种对象都有一些特征供用户描述。在VB里,这些对象的特征被称为“属性”。
例如,工作簿对象有名称属性;区域对象有列、字体、公式、名称、行、样式和值等属性。这些对象属性是可以设置的。用户通过设置对象的属性控制对象的外观和位置。对象属性一次只能设置为一个特定的值。例如,当前工作簿不可能同时有两个不同的名称。
在前面一章的实例中,选定了全体单元格,然后设置单元格的行高和列宽,使用了如下的代码:
Selection.RowHeight = 27.75
Selection.ColumnWidth = 4.25
这里,Selection就是选定的单元格对象,RowHeight是对象的行高属性,ColumnWidth是对象的列宽属性;用户通过赋值语句,重新设置了单元格的行高和列宽。
有些属性同时又可以是对象。想想区域(Range)对象,用户可以通过设置字体颜色来改变选定单元格的外观。但是,字体(Font)可以有不同的名称(Times New Roman, Arial, …),不同的字号(10,12,14,…)和不同的样式(粗体,斜体,下划线,…)。这些是字体的属性;在这里字体变成了对象。
方法
如何控制对象的操作呢?对象有方法;每一种用户想要对象做的操作都被称为“方法”。例如Add方法;用户可以使用这个方法添加一个新工作簿或者工作表。对象可以使用不同的方法。例如,区域(Range)对象有专门的方法让用户清除单元格内容(ClearContents方法),清除格式(ClearFormats方法)以及同时清除内容和格式(Clear方法)。还有让用户选择,复制或移动对象的方法。
例如在前面第10章程序中出现的代码:
Cells.Select
就是把工作表全体单元格都选中的操作;其中Cells是工作表上全体单元格组成的对象,Select是把对象选中的方法。
7.2 在班级课表中自动寻找教师个人课表
在使用排课表软件时,进入年级课表后,无论是查找课表,还是维护和调整课表,都需要频繁地查找教师个人课表。由于课表占用了多达数百行单元格,而电脑屏幕只能显示几十行表格,所以查找教师个人课表时,需要用鼠标或者键盘,上下调整显示画面,才能找到目标。当然,用户可以利用Excel窗口的拆分功能,把窗口上下一分为二;一般可以选择上面的窗口显示班级课表,用下面的窗口显示教师个人课表;如图7.2-1所示。这样虽然可以给用户在查找教师个人课表时带来极大方便,但是,频繁地手工操作还是无法避免。于是,一个迫切的任务摆到了面前,那就是如何从上面的班级课表,快速地查找出所需要查找的教师个人课表。

图7.2-1
7.2.1 初步解决方案——自动寻找指定学科课表
7.2.1.1 任务分析
一般来说在班级课表中,要找一个教师个人课表,通常是想要查找出某一学科的任课教师的个人课表。举例来说,看到了高一1班的课表,想要找出英语教师的个人课表。怎样让电脑代替手工操作去寻找呢?
一种解决方案是这样的,用鼠标点击班级课表中目标学科单元格,如单元格C35“英”。然后通过一种查找程序,让电脑找到该学科的任课教师“王鸿雁”;具体到这个工作表,就是希望在下面的教师个人课表显示窗口里面,自动显示出如图7.2-1所示的王鸿雁老师的个人课表。
模仿人工查找过程,查找途径应该是:
1.从班级课表已知条件: 1班——英语学科;
2.转到下面的教师个人课表区域:
3.找到英语学科——1班——教师姓名。
显然,通过编程来解决这一问题是很有意义的,但是也是具有难度的问题。困难就在于一开始电脑不知道这个学科的英语教师的姓名是王鸿雁;何况如果连电脑窗口是怎样上下翻屏都不知道,又怎么能指望电脑能够代替用户的眼睛,代替用户的手指找出该班的英语教师王鸿雁呢?
既然根据班级课表的指定学科,准确地查找出该学科的教师个人课表的任务过于艰难,能否找出一个可以实现的替代解决方案呢?
如果降低任务目标的难度,不指望查找出高一1班的英语教师王鸿雁,只要能够查找出任何一个英语教师的个人课表就可以了,问题也许会得到解决。
7.2.1.2 自动寻找指定学科教师个人课表的解决方案
既然用户在高一1班课表中的任务是要寻找英语教师,用户可用鼠标选中该学科的单元格,告知电脑查找的目标;程序就应该从这个鼠标事件开始。电脑获取了目标科目的信息后,就应该按照目标,在下面的教师个人课表区域里面寻找英语教师的课表。这个任务比较容易完成。可以用逐个单元格搜索的方法,查找目标“英”。如果找到了,就把这个单元格及其所在的教师个人课表显示在下面的窗口里面,任务就完成了。
根据这个解决方案,首先采用鼠标的双击事件。这是因为用鼠标选中一个“英”学科单元格目标有3种方法:单击、双击和右击;单击容易和键盘移动单元格操作相混淆,所以不宜采用。双击操作还是比较适宜的。
确定了在上面的班级窗口里面选择目标学科单元格的方式为双击之后,下面需要解决的首要问题就是如何在下面的窗口里面寻找目标单元格。首先必须知道如何进入上下两个不同的拆分窗口;为了解决这一问题,不妨采用录制宏的方法,获取拆分窗口的代码。
将工作表窗口拆分成上下两个窗口;打开宏记录器,然后开始操作。进入下面的窗口,在一个单元格里面输入“下面的窗口”;然后进入上面的窗口,选择一个单元格输入“上面的窗口”。然后进入宏编辑器,就可以看到下面的代码:
Sub Macro1()
‘ Macro6 Macro
ActiveWindow.Panes(2).Activate
Range(“D14”).Select
ActiveCell.FormulaR1C1 = “下面的窗口”
ActiveWindow.Panes(1).Activate
Range(“D4”).Select
ActiveCell.FormulaR1C1 = “上面的窗口”
End Sub
通过分析代码,可以得到上下窗口的名称和选取它们的方法:
ActiveWindow.Panes(1) 表示上面的窗口;
ActiveWindow.Panes(2)表示下面的窗口;
ActiveWindow.Panes(1).Activate表示激活上面的窗口。
当用户把鼠标移动到该窗口里面的一个单元格里面,实际上就激活了该窗口。
至于寻找指定学科的教师个人课表,打算采用这样的技术路线:以“英”(英语)学科为例,可在教师个人课表区域内,寻找每一个教师个人课表左上角的学科标记单元格;寻找的方式是从上到下,从左到右,逐个寻找;一旦找到了,就停止寻找,进行该教师个人课表的定位显示操作,将结果显示在工作表上。
进入VBE窗口,选择工作簿对象,在事件列表中选中双击事件,编写如下代码:
Private Sub Workbook_SheetBeforeDoubleClick(ByVal Sh As Object, ByVal Target As Range, Cancel As Boolean)
‘用双击鼠标,找出相应学科教师的个人课表。
‘判断选择的单元格的值是否满足要求
For i = 111 To 401 Step 10
‘找到后退出循环
If Cells(i, 1) = Target Then
GoTo AA
End If
Next i
i = 111
AA:
‘将工作表屏幕移动到以该行为顶的位置上
ActiveWindow.Panes(2).Activate
Cells(i + 4, 1).Select
End Sub
这段代码运行后,可以基本上实现预定的目标。
7.2.1.3 代码分析
上面的代码可分成两部分。第一部分的主要功能是查找目标;第二部分是把找到的目标正确地显示在工作表上,也就是单元格区域的定位。
代码:
For i = 111 To 401 Step 10
Next i
是一个单循环语句,循环变量i代表的是单元格的行数,循环范围从111到401,步长10。注意到循环体中的代码:
If Cells(i, 1) = Target Then
GoTo AA
End If
里面的Cells(i, 1)将从Cells(111, 1) 、Cells(121, 1)、 Cells(131, 1)……一直到最后一个Cells(401, 1);也就是第一列的单元格A111行开始,向下搜索,每个10行搜索一次,这些单元格都是教师个人课表左上角的学科标记。为什么要搜索到第401行呢?这是根据课表的长度决定的;如果课表比较长,可以增大这个数值。每搜索一次,都要看看这个单元格是否等于单元格“Target”。这个Target英文的意思是“目标”,在这里是一个参数,表示双击事件中的目标单元格。为什么呢?原来工作簿双击事件是带有参数的:
Workbook_SheetBeforeDoubleClick(ByVal Sh As Object, ByVal Target As Range, Cancel As Boolean)
其中的“ByVal Target As Range”的作用是指出参数Target是双击操作的目标区域。所以这里可以用Target来代替比较复杂的对选中单元格对象的描述方法。
具体到以双击班级课表中“英”单元格为例,当程序没有找到“英”单元格时,不执行语句组:转向语句“GoTo AA”;程序继续在循环体中循环,不断地查找;当程序搜索到A151单元格时,有:
Cells(151, 1) = Target(=“英”)
这时,判断语句的条件为真,程序执行语句“GoTo AA”,也就是跳出循环,转到后面的显示教师个人课表的代码:
ActiveWindow.Panes(2).Activate
Cells(i + 4, 1).Select
这段代码的第一行执行激活下面教师个人课表窗口的操作;第二行是选中找到的“英”单元格的下面第4个单元格。这样,这个“英”所在的教师个人课表就可以显示在下面窗口里。如果不是选中它下面的第4个单元格,则这个教师个人课表的下面几行有可能被遮挡。
细心的读者也许会发现,这段代码肯定是经过多次调试才得到的。的确,一开始缩写的代码为:
Cells(i, 1).Select
执行结果是显示的教师个人课表位置偏下,不能全部显示;经过几次调试才取得上面的成功。
有了这样的结果,寻找教师个人课表的任务可以说基本完成了。这是因为一个学科的教师一般不超过6名;在工作表中,教师个人课表从左到右每页有3个。用户是按照学科集中排列教师个人课表;从上到下,是语文、数学、外语、物理、化学……;例如英语教师有4个课表,占用两行。程序自动寻找的是第一行英语教师的个人课表,共3人。也就是说,虽然这个程序不能准确地寻找出一个英语教师的个人课表,但是第一次寻找也能找到3个,只有一个不能显示。也就是说这种寻找方式,在大多数情况下,都能够达到目的;对于运气不好的情况,虽然没有找的目标,但是目标肯定在所寻找结果的下面一行教师个人课表中。
有了这样的结果可以基本满足用户的要求。
但是,这个比较成功的代码是否可以让人放心的投入使用呢?通过多次使用可以发现,它存在着一些问题与不足,需要继续改进。
7.2.1.4 当窗口没有拆分时程序出现中断的解决方案
经过使用会发现一个问题,就是这个程序只能在拆分成上下两个窗口里面使用。如果对一个没有拆分窗口的工作表执行双击操作,程序会崩溃;出现如图7.2-2所示的结果。

图7.2-2
这时,点击提示窗口上的“调试”就可以进入代码窗口;如图7.2-3所示。

图7.2-3
代码窗口中出现一行黄色的代码,提示用户这里出现了问题;程序执行到了这句代码,出现了错误,需要修改这句代码。
什么错误呢?信息框给出的提示是“类Pane的Activate方法无效”。也就是以下代码无效:
ActiveWindow.Panes(2).Activate
原来,ActiveWindow是一个对象,表示当前的窗口;Panes(2)是窗口对象的一个成员,多个Pane组成了窗口的类;Panes(2)也是一个对象,Activate是对象Pane的一个方法,表示激活这个对象。仔细分析一下,问题找到了。现在Excel窗口没有拆分,只有一个窗口;窗口Panes(2)并不存在;而代码要激活ActiveWindow.Panes(2),也就是下面的窗口,这时程序无法完成的事情,所以自然就会出现程序中断的情况了。
怎样解决这一问题呢?
有人说,既然如此,在使用程序时加以注意就可以了。是的,只要随时注意,保证工作表窗口拆分成两个就可以避免错误了。这是一种逃避的处理方法,也可以说是一种不处理的方法;这不符合以用户为中心的原则,这种方法不值得提倡。
下面采取对症下药的方法来解决。程序应当具有识别和判断窗口数量的功能。当窗口是一个的时候,执行另一个查找程序,以避免出现程序的中断现象。于是添加如下代码:
If ActiveWindow.Panes.Count = 1 Then
ActiveWindow.Panes(1).Activate ‘只有一个窗口,则在这个窗口内运行
Cells(i + 4, 1).Select
Else ‘若有两个窗口,则在下面的窗口运行
ActiveWindow.Panes(2).Activate
Cells(i + 4, 1).Select
End If
这段代码采用了分支条件语句,对一个窗口还是两个窗口的情况进行判断,并分别执行相应的查找操作。
代码中的:
ActiveWindow.Panes.Count
是一个对象的属性,它给出了活动窗口的拆分个数统计结果。用条件语句对这个结果的数值进行判断;如果当前的窗口是1个,则执行下面的语句:
ActiveWindow.Panes(1).Activate
这句代码是一个对象的方法,表示激活当前的窗口,也就是这个唯一打开的窗口。这样,当窗口没有拆分,程序不但不会中断执行,产生错误,而且还能继续执行用户的查找操作;与前面不同的是它是在当前窗口里面查找教师个人课表。
如果窗口已经拆分成两个窗口,则在下面的窗口里面进行查找教师个人课表的操作。
至此,不论工作表是否拆分,查找教师个人课表的程序都能够正确运行。
7.2.1.5 当双击目标是一个合并单元格时程序出现中断的解决方案
问题不止出现在上面的情况,当用户双击一个单元格目标,而这个目标恰恰是一个合并单元格;这时,程序又出现了中断。例如当用户双击班级课表中的班主任姓名,这是一个由3个单元格合并的单元格;电脑出现了中断,并给出了信息框;如图7.2-4所示。

图7.2-4
单击图7.2-4中的信息框上的“调试”按钮,可以看到出错的代码;如图7.2-5所示。

图7.2-5
报错信息为“类型不匹配”;产生错误的代码为:
If Cells(i, 1) = Target Then
这句代码为什么会出现类型不匹配的错误呢?判断条件是一个等式,等号的左边“Cells(i, 1)”表示一个单元格的数值,等号的右边“Target”表示双击的目标的数值。为什么当用户双击一个单元格,程序可以运行;双击了合并单元格,程序就不能运行呢?原因就在于双击了合并单元格,等号右边的数值是一个合并单元格的数值,它是由3个单元格合并而成的区域,这导致了目标Target选中的值为多个值;左边是一个数值,右边是多个数值,两者的类型的确是不匹配的;所以程序无法执行下去,只好中断并发出提示信息。
怎么解决这一问题呢?
一种解决方案是限制双击单元格的有效范围;因为上面错误的出现是因为操作者误操作造成的。正确的操作方法是不应该选择班级课表上面的学科单元格之外的单元格的。所以可以在程序上对双击目标的区域加以限制,如果双击了无效的单元格,则程序不进行查找教师个人课表的操作;这样可以有效地避免上述问题的发生。本书将在后面的章节里,介绍这样的解决方案。
还有一种解决方案是对双击目标单元格的数量进行判断;如果发现双击了合并单元格,则不执行查找教师个人课表的操作,同时也不做任何反应;当然也就不会出现程序中断的现象了。
怎样编写这种解决方案的代码呢?既然合并单元格的数量大于1,所以可以通过对目标单元格的数量进行判断;代码如下:
‘对选定的区域中单元格的数量判断是否大于1,如果是合并单元格,肯定大于1
If Selection.Cells.Count > 1 Then
i = 111 ‘在合并单元格被双击的情况下,也要执行程序,让教师的课表走到最上面语文的课表
GoTo AA ‘然后跳出搜索教师课表的程序
End If
其中代码“Selection.Cells.Count”给出了选定目标的单元格数量属性;通过条件语句对合并单元格进行判断,当发现双击了合并单元格,则执行代码:
i = 111
GoTo AA
首先给循环变量i赋值111;然后执行程序转向语句,直接转到执行显示教师个人课表的语句。这样,下面的窗口就显示了语文教师的个人课表。原来,程序在操作者出现误操作的情况下,不再执行任何查找教师个人课表的操作;而将第一行的教师个人课表当作默认的查找结果。实践表明,这样的处理结果不但避免了程序中断,而且还给出了一个教师个人课表,是符合用户使用习惯的。
7.2.2 从班级课表准确寻找教师个人课表
上面介绍的查找教师个人课表的解决方案,虽然可以基本上满足用户的需求,但是它只是找出了所需要查找的教师个人课表的相同学科的3个教师的个人课表,只能算是近似地解决了问题。在没有找到更好地解决方案的情况下,这种简单地解决方案可以暂时满足用户的需要。从班级课表里面选择一个学科,准确地找出该学科的任课教师个人课表,仍然是排课表软件追求的目标。
7.2.2.1 从班级课表准确寻找教师个人课表的解决方案
既然要找到教师个人课表,就要知道教师的姓名。怎样让电脑知道某班级、某学科的任课教师姓名呢?
仿照人工查找某班级、某学科教师姓名的方法,一个解决方案浮现出来。
原来,人工查找的方法是用教师任课岗位表,也就是本书第一章第1节制作的教师任课岗位表。这个岗位表不仅仅为了分配工作岗位之用,在排课表工作中始终起着至关重要的作用。例如,在使用排课表软件编制新课表时、在使用排课表软件调整课表时,都要以这个岗位表为依据。这个岗位表已经制作完成,放到了一个工作簿里面。用户在制作课表时,通常是打开这个岗位表,在课表里录入班级课表的班主任、教师个人课表的教师姓名和任课班级等的重要信息。既然如此,为何不将这个教师任课岗位表放到排课表工作簿的一个工作表上呢?
在排课表软件工作簿里面添加一个工作表,把教师任课岗位表复制到里面,并命名为“teacher”;如图7.2-6所示。本书在前面提倡所有的文件夹、文件名、工作表名都要用长中文命名;为什么这次使用了英文名称呢?原来这个工作表名“teacher”在程序中经常要用到,为了程序运行速度更快,程序更稳定,所以使用了英文名字。

图7.2-6
有了这个岗位表,用户在双击一个班级课表的某个学科时,就可以仿照人工的查找过程:
班级——学科——教师姓名——教师个人课表。
下表给出了在高一1班,双击“英”后,使用教师任课岗位表“teacher”查找该学科的任课教师的解决方案。

7.2.2.2 从班级课表准确寻找教师个人课表的程序代码
根据上面的解决方案,按照4个步骤,编写出下面的代码:
Private Sub Workbook_SheetBeforeDoubleClick(ByVal Sh As Object, ByVal Target As Range, Cancel As Boolean)
‘可以自动寻找准确的教师个人课表,双击班级课表中任何一个学科,就可以让下面的窗口自动显示该学科的任课教师个人课表
Dim teaname As String ‘教师姓名变量
Dim banji As Integer ‘班级号
Dim tealie As Integer ‘教师姓名所在列变量
Dim tarrow, tarcol As Integer ‘双击单元格的行号和列表号变量
tarrow = Target.Row
tarcol = Target.Column
teaname = “”
‘双击有效区域仅限于12个班级课表所在区域
If Target.Row > 34 And Target.Column < 34 And Target.Row < 70 Then
‘鼠标双击一个合并单元格,程序崩溃。
If Selection.Cells.Count > 1 Then GoTo bb ‘它是计算选定的区域中单元格的数量,判断是否大于1,如果是合并单元格,肯定大于1
‘从班级课表的位置,算出是哪一个班级:1-12
banji = 3 * Int(Target.Row / 10) – 8 + Int((Target.Column * 0.9) / 10)
‘在教师任课表里面找教师姓名
For k = 4 To 20
‘先找科目的列
If Sheets(“teacher”).Cells(2, k).Value = Target.Value Then
tealie = k
Exit For
End If
Next k
‘大于20表示没有找到学科,否则程序死
If k > 20 Then GoTo bb
‘下面根据班级号在教师任课表里面找教师姓名
‘高一
If ActiveSheet.Name = Sheet1.Name Then
banji = banji + 3
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
‘高二
ElseIf ActiveSheet.Name = Sheet2.Name Then
banji = banji + 16
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
‘高三
ElseIf ActiveSheet.Name = Sheet3.Name Then
banji = banji + 29
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
Else: GoTo bb
End If
‘如果姓名是空白,必须退出,否则找照片程序崩溃。
If teaname = “” Then GoTo bb
‘下面是根据教师姓名条件找教师个人课表语句
For i = 111 To 471 Step 10 ‘按学科寻找
For j = 27 To 5 Step -11 ‘按姓名寻找
If Cells(i, j) = teaname Then GoTo aa ‘找到了教师姓名
End If
Next j
Next i
If j < 5 Then j = 5 ‘避免单元格列小于1
aa: ‘用活动单元格选中来实现教师个人课表的自动定位
If ActiveWindow.Panes.Count = 1 Then
ActiveWindow.Panes(1).Activate ‘只有一个窗口,则在这个窗口内运行
Cells(i + 4,1).Select ‘这句使得个人课表正好在窗口上下的中间
Cells(i, j).Select ‘这句让光标回到教师个人课表的教师姓名上,便于观看
Else ‘若有两个窗口,则在下面的窗口运行
ActiveWindow.Panes(2).Activate ‘ 1表示上面窗口,2表示下面的
Cells(i + 4, 1).Select ‘这句使得个人课表正好在窗口上下的中间
End If
bb:
End If
End Sub
7.2.2.3 代码分析
1.命名变量
一开始,程序给下面5个变量命名:
Dim teaname As String ‘教师姓名变量
Dim banji As Integer ‘班级号
Dim tealie As Integer ‘教师姓名所在列变量
Dim tarrow, tarcol As Integer ‘双击单元格的行号和列表号变量
2.变量的赋值
紧接着变量的命名,就是给它们赋值:
tarrow = Target.Row
tarcol = Target.Column
teaname = “”
赋值语句“tarrow = Target.Row”的作用是把双击目标单元格“Target”的行号“Row”存放到变量“tarrow”里面。同样的道理,变量“tarcol”存放了双击目标单元格的列标。
至于变量“teaname”存放的是查找到的教师姓名的变量,每次程序运行到这里,都要清空;因为用户定义它是一个字数串变量,所以它的空值是空白。
3.主程序代码分析
所谓主程序是指双击目标单元格后,电脑在教师任课岗位表“teacher”里面查找任课教师姓名的程序代码。一旦这个任务完成后,后面的工作就是根据教师的姓名查找教师个人课表;而这个工作可以用前面介绍的查找指定学科教师课表的方法完成。
这个解决方案采用了在工作簿的一个双击事件。只要双击了一个班级课表,程序就开始运行。这时首先要计算出双击目标单元格后,双击的是哪一个班级的班级课表。计算的方法是采用公式:
banji = 3 * Int(Target.Row / 10) – 8 + Int((Target.Column * 0.9) / 10)
这句代码是从经验得到的;它可称为经验公式。
12个班级课表按平面分布在工作表上,从左到右的最上面一行分别为高一1、2、3班;第二行是4、5、6班;……。每个班级课表占用8行10列单元格区域。例如高一1班课表占用了区域“A32:J39”;其中的学科标记占用了区域“B35:J39”,共有45个单元格。如果双击了里面的“英”单元格“C35”,得到的行坐标“Target.Row”为35,列坐标“Target.Column”为3;代入上面的计算公式:
banji = 3 * Int(35 / 10) – 8 + Int((3 * 0.9) / 10)=1
计算出结果为1,也就是算出高一1班。这个计算实质上是将一个2维平面的数据(行、列坐标)转换成1维的数据结果。从总的规律来看,行数较大,班级序号较大;列标较大,班级序号较大。根据这个思路,采用不断试验的方法,得到了这个经验公式。
然后是进入到在教师任课岗位表“teacher”里面找教师姓名。先按列寻找是哪一个学科的教师;用户的目标是“英”。代码如下:
‘先找科目的列
For k = 4 To 20
If Sheets(“teacher”).Cells(2, k).Value = Target.Value Then
tealie = k
Exit For
End If
Next k
‘大于20表示没有找到学科,否则程序死
If k > 20 Then GoTo bb
在教师岗位表的第2行、第4列开始,为学科标记;如图7.1-6所示。程序采用了循环语句,逐个寻找“英”学科。到了第6列找到了结果,循环变量k当时取值为6,则用赋值语句把这个结果存放在变量tealie里面,然后执行语句:
Exit For
退出循环。
下面的任务是根据班级序号1和英语学科的列坐标6查找教师姓名。
工作簿里面有“高一”、“高二”、“高三”3个年级的课表,分别放在名为 “高一”、“高二”、“高三”的工作表中。无论用户双击了哪一个年级的课表,这个程序都可以运行。教师任课岗位表是全校范围的,里面有高一、高二、高三这3个年级的子表。程序必须首先判断双击了哪一个年级的课表。程序采用了多分枝判断语句来解决这一问题;判断的条件是双击的工作表为激活的工作表“ActiveSheet.Name”等于哪一个年级的工作表。在工作簿中,这3个年级的工作表有一个顺序,它们分别是Sheet1.Name、Sheet2.Name、Sheet3.Name。所以采用如下的判断语句:
‘高一
If ActiveSheet.Name = Sheet1.Name Then
banji = banji + 3
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
‘高二
ElseIf ActiveSheet.Name = Sheet2.Name Then
banji = banji + 16
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
‘高三
ElseIf ActiveSheet.Name = Sheet3.Name Then
banji = banji + 29
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
Else: GoTo bb
End If
If teaname = “” Then GoTo bb
程序首先找到了高一1班的英语任课教师在第4行,然后找到教师姓名;再把结果高一1班的英语教师姓名“王鸿雁”存放到变量teaname里面。
然后就时根据教师姓名条件找教师个人课表的代码。这些代码的原理前面已经介绍过,这里就不再赘述了。
7.2.3 准确寻找教师个人课表程序的实用化
和前面开发的程序一样,将初步编写的代码进行测试、检验,找出错误,修改代码;还要在使用中不断完善功能,补充新的代码;最后得到可供用户使用的程序。
7.2.3.1 避免在冻结窗口运行的中断
Excel工作表窗口除了可以拆分成几个子窗口,还可以执行“冻结窗口”的操作。在冻结窗口下,如果执行上面查找教师个人课表的程序,程序就会出现错误;如图7.2-7所示。

图7.2-7
电脑同时给出一个运行发生错误的提示信息框,提示“类Pane的Activate方法无效”。这个错误前面已经遇到过。发生的原因就是因为冻结窗口虽然也把窗口分成了上下两部分,但是与拆分窗口不同;拆分窗口两个各部分都可以激活,都可以进行各种操作;冻结窗口的上面窗口被冻结,不能进行任何操作。而程序需要在不同的窗口里面进行转换,当程序要转换到上面窗口时,自然就会发生错误。
解决的方法是增加一段代码,判断当前是否有冻结窗口;如果有,则给出一个信息提示框,告诉用户“这项操作不能在冻结窗口下运行”。用户确定后,程序不进行任何操作。这样的代码如下:
If ActiveWindow.FreezePanes = True Then
MsgBox “这项操作不能在冻结窗口下运行”
GoTo bb
End If
其中的条件:
ActiveWindow.FreezePanes = True
是对激活窗口的冻结窗口属性状况的一个判断;当用户使用了冻结窗口,属性ActiveWindow.FreezePanes的取值为True(否则为False)。如果这种情况发生,则执行语句:
MsgBox “这项操作不能在冻结窗口下运行”
然后执行转向语句,退出下面的查找程序代码,执行结束语句。
用户单击信息框上的确定按钮后,即可重新开始操作;如图7.2-8所示。当然,用户如果还要继续查找教师个人课表,就必须结束当前的冻结窗口状态,进入拆分窗口状态。

图7.2-8
● 举一反三
通过测试,就会发现程序的另外一个故障;当双击一个课表上的空白单元格时,程序会出现中断。当查找的教师姓名为空格,也就是程序没有找到任何教师,程序也会出现中断。这些问题都可以用同样的方法来解决。例如下面是解决双击空白单元格问题的代码:
‘空白单元格要处理,否则程序死
If Target = “” Then GoTo bb
以下是解决教师姓名为空格问题的代码:
‘如果姓名是空白,必须退出
If teaname = “” Then GoTo bb
7.2.3.2 解决查找教师个人课表时遇到故障单元格程序中断的问题
通过测试,就会发现程序的另外一个故障;课表上教师姓名所在的单元格由于某种原因是一个故障单元格,程序会出现中断。例如某个单元格由于链接的错误,是一个显示“#REF!”的单元格,也称之为故障单元格。这时可用以下代码来解决:
If Application.WorksheetFunction.IsError(Cells(i, j)) = False Then
把这句代码逐个插入教师姓名的双重循环体中:
For i = 111 To 471 Step 10 ‘按学科寻找
For j = 27 To 5 Step -11 ‘按姓名寻找
If Application.WorksheetFunction.IsError(Cells(i, j)) = False Then
If Cells(i, j) = teaname Then GoTo aa ‘找到了教师姓名
End If
Next j
Next i
这句代码的判断条件是对循环体重的目标单元格Cells(i, j)故障情况进行判断,如果没有故障,也就是计算结果是“假”,则进行查找。如果发生了故障现象,这个教师姓名单元格没有姓名,而是“#REF!”,则不执行查找;转而执行下一个循环,继续查找其他单元格。
7.2.3.3 教师个人课表显示功能程序的改进与完善
既然是准确地找教师个人课表,显示结果就应该给出该教师的个人课表。但现有的结果是显示出包含有该教师个人课表的3名教师的个人课表。怎样在结果中显示出目标呢?
还有,按照以前的解决方案,显示结果后,活动单元格在教师个人课表所在的下面的拆分窗口;这种情况不利于用户继续在上面的拆分窗口中继续寻找其他班级的课表。而且,此时鼠标定位在教师个人课表上,也没有必要。如果在显示了教师个人课表后,鼠标仍然回到原来双击的目标单元格上,对操作很有利;除了上面说到的可以继续在班级课表里查找之外,还可以把这次双击当作一次互换单元格的第一次选择。如果要调换该老师的课,只要再按下Ctrl键,用鼠标选中另一个目标单元格,两个单元格就完成了互换;两位老师的课程也完成了一次调换。
准确地显示教师个人课表可以用给教师姓名单元格背景染上颜色的方法来解决;如图7.2-9所示。

图7.2-9
采用这样的解决方案,并不能只是给找到的教师个人课表的姓名上增加颜色;否则随着连续查找,很多教师姓名单元格都被染成了红色,这样的功能也就没有作用了。每次找到一个教师个人课表之后,都要把上一次的结果中教师姓名单元格的颜色清除。为此在给找到的教师个人课表定位显示的代码后面增加这样的代码:
‘找到的课表,教师姓名单元格变成粉红色,醒目
Cells(i, j).Interior.ColorIndex = 38
为了清除上一次的颜色,在双击之后,进入查找程序之前,添加如下代码:
‘之前,先把以前的教师姓名的颜色清除
For i = 5 To 27 Step 11
For j = 111 To 471 Step 10
Cells(j, i).Interior.ColorIndex = 0
Next
Next
If j > 471 Then j = 111
这段代码采用循环语句,逐个将所有教师的姓名单元格的颜色重新设置成无色。最后还要将循环变量从471返回到111。
在下面的窗口中查找并显示教师个人课表,需要将下面的窗口激活;激活后再将活动单元格放到这个教师个人课表上;这样,目标课表就可以全部显示在窗口上。但是这样活动单元格就不再是上面的窗口中用户开始用鼠标双击的学科目标单元格了;例如用户双击高一1班的“英”后,马上就准确地在下面的窗口里面找到并显示出了结果:王鸿雁老师的课表出现了,王鸿雁所在的单元格也被染成了红色;但是活动单元格也在下面的窗口的第一列单元格上,而不是在用户开始双击的学科“英”单元格上。这并不是用户所需要的。怎样才能把教师个人课表查找并显示出来后,活动单元格仍然保留在原来的双击单元格上面呢?
在查找代码的最后,添加这样一句代码:
‘这句让光标回到教师个人课表的学科
Target.Select
就可以解决这一问题。
综上所述,准确寻找教师个人课表程序的实用化完成后,全部的代码如下:
Public teaname As String ‘教师姓名变量
Private Sub Workbook_SheetBeforeDoubleClick(ByVal Sh As Object, ByVal Target As Range, Cancel As Boolean)
‘1.可以自动寻找准确的教师个人课表,只要双击班级课表中任何一个学科,就可以让下面的窗口自动变成该学科的任课教师的个人课表。
‘2.双击后,教师个人课表自动在下面的窗口中定位,然后活动单元格返回原来选择的学科,可以连续用鼠标单击另一学科互相调换。
‘3.找到的个人课表,教师姓名变成粉颜色。
‘4.具有找到#REF!等错误单元格后避免程序崩溃的功能。
‘5.有一定的区域范围限制;空白单元格不执行程序。
teaname = “”
‘因为程序在冻结窗口下会中断,所以增加了下面的一段程序:
If ActiveWindow.FreezePanes = True Then
MsgBox “这项操作不能在冻结窗口下运行”
GoTo bb
End If
Dim banji As Integer ‘班级号
Dim tealie As Integer ‘教师姓名所在列变量
Dim tarrow, tarcol As Integer ‘双击单元格的行号和列表号变量
tarrow = Target.Row
tarcol = Target.Column
‘双击有效区域仅限于12个班级课表所在区域
If Target.Row > 34 And Target.Column < 34 And Target.Row < 70 Then
‘鼠标双击一个合并单元格,程序崩溃
If Selection.Cells.Count > 1 Then GoTo bb ‘选定单元格的数量是否大于1
‘空白单元格要处理,否则程序死
If Target = “” Then GoTo bb
Cancel = True ‘返回后不是编辑状态
‘之前,先把以前的教师姓名的颜色清除
For i = 5 To 27 Step 11
For j = 111 To 471 Step 10
Cells(j, i).Interior.ColorIndex = 0
Next
Next
If j > 471 Then j = 111
‘从班级课表的位置,算出是哪一个班级:1-12
banji = 3 * Int(Target.Row / 10) – 8 + Int((Target.Column * 0.9) / 10)
‘在教师任课表里面找教师姓名
For k = 4 To 20
‘先找科目的列
If Sheets(“teacher”).Cells(2, k).Value = Target.Value Then
tealie = k
Exit For
End If
Next k
‘大于20表示没有找到学科,否则程序死
If k > 20 Then GoTo bb
‘下面根据班级号在教师任课表里面找教师姓名
‘高一
If ActiveSheet.Name = Sheet1.Name Then
banji = banji + 3
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
‘高二
ElseIf ActiveSheet.Name = Sheet2.Name Then
banji = banji + 16
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
‘高三
ElseIf ActiveSheet.Name = Sheet3.Name Then
banji = banji + 29
teaname = Sheets(“teacher”).Cells(banji, tealie).Value
Else: GoTo bb
End If
‘如果姓名是空白,必须退出,否则找照片程序崩溃。
If teaname = “” Then GoTo bb
‘下面是根据教师姓名条件找教师个人课表语句
For i = 111 To 471 Step 10 ‘按学科寻找
For j = 27 To 5 Step -11 ‘按姓名寻找
If Application.WorksheetFunction.IsError(Cells(i, j)) = False Then
If Cells(i, j) = teaname Then GoTo aa ‘找到了教师姓名
End If
Next j
Next i
If j < 5 Then j = 5 ‘避免单元格列小于1
aa: ‘用活动单元格选中来实现教师个人课表的自动定位
If ActiveWindow.Panes.Count = 1 Then
ActiveWindow.Panes(1).Activate ‘只有一个窗口,则在这个窗口内运行
Cells(i + 4, 1).Select ‘这句使得个人课表正好在窗口上下的中间
Cells(i, j).Select ‘这句让光标回到教师个人课表的教师姓名上
‘找到的课表,教师姓名单元格变成粉色,醒目
Cells(i, j).Interior.ColorIndex = 38
Else ‘若有两个窗口,则在下面的窗口运行
ActiveWindow.Panes(2).Activate ‘2表示下面的窗口
Cells(i + 4, 1).Select
ActiveWindow.Panes(1).Activate ‘回到上面的窗口
‘找到的课表,教师姓名单元格变成粉色,醒目
Cells(i, j).Interior.ColorIndex = 38
Target.Select ‘这句让光标回到教师个人课表的学科,便于下一步调课
End If
bb:
End If
End Sub
● 关于转向语句
本实例的代码中使用了两次转向语句。转向语句的语法为:
GoTo 语句名称(bb)
跳过的语句组
语句名称(bb):
关于转向语句,有一些有趣的评价。有人说,程序员分为两种。一种是从来不使用转向语句的;一种是使用转向语句的。更有人对使用这种语句给出了很低的评价,认为这是一种不好的编程习惯。一种观点特别推崇结构化编程,而在结构化编程里,是不允许使用转向语句的。或许读者也听说过这样的说法,GoTo语句是一种令人迷惑的程序代码,被称为意大利式面条代码。
笔者认为是否使用转向语句完全应该从实际需要出发。如果使用转向语句,可以达到目标,就完全可以放手使用。毕竟程序需要经过运行才能证明是否正确;只要通过了各种测试,表明程序可用,就说明使用转向语句是成功的。
7.3 自动在班级课表中添加班主任姓名
从前面的各种实例可以看出,VBA的确具有强大的“遥控”Excel功能。既然它可以仿照人工查找教师个人课表的操作,实现自动查找教师个人课表,其他一些操作是否也可以实现自动化呢?比如在班级课表中,原来需要手工输入每个班主任教师的姓名;现在能否让电脑根据教师任课岗位表,自动地在各个年级课表中的每个班级课表上添加班主任姓名呢?
7.3.1 自动给班级课表添加班主任姓名的解决方案
这里遇到的第一个问题是,何时添加班主任姓名?
有多种选择。比如说使用人工激活法,触发了某一事件,就执行一次添加班主任姓名的操作。也可以采用打开工作簿事件,这样每次打开派课表软件,都向各个年级的课表里面添加一遍班主任姓名,实际上就是更新一次班主任姓名。还可以采用打开一个工作表事件;这样每次打开一个新工作表,就会更新一遍班主任姓名。
以上各种更新的时机各有利弊。综合各种因素,决定选用打开工作表事件激活程序。
怎样从“teacher”工作表向各个年级的班级课表添加或者更新班主任姓名呢?一般来说,解决步骤是这样的:
1.判断打开的是哪一个年级的课表;
2.在“teacher”工作表找到班主任姓名的数据区域;
3.在年级课表里面向各个班级课表的班主任姓名单元格添加数据。
如果使用了激活年级课表的解决方案,上述步骤的第一步就可以省略了。
可以在高一年级课表的工作表编写一个打开工作表事件的程序,然后将这个程序分别复制高二、高三年级就可以了。
7.3.2 自动给班级课表添加班主任姓名的程序
7.3.2.1自动给班级课表添加班主任姓名的程序代码
打开排课表软件工作簿,进入VBE窗口。在左边的工程资源管理器窗口中,点击工作簿文件名,打开文件夹列表;双击文件夹“高一”,然后在右边代码窗口上面的对象列表中选择“Worksheet”;如图7.3-1所示。

图7.3-1
然后再右边的事件列表框里面点选激活(打开)事件“Activate”;这时在代码窗口中就会出现两行代码:
Private Sub Worksheet_Activate()
End Sub
有时候在点选Worksheet对象的时候,代码窗口会自动出现两行代码:
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
End Sub
这是电脑给出的工作表对象默认的事件代码。用户不需要它;也不必管它,只要按照上面的操作,得到事件“Activate”代码后,将上面不需要的两行代码删除就可以了;如图7.3-1所示。
这时就可以编写如下的代码:
Private Sub Worksheet_Activate()
‘班级课表之班主任姓名自动链接程序,二维数据变一维数据的尝试
bj = 0 ‘计数器归零
For i = 32 To 62 Step 10 ‘在班级课表中教师姓名单元格扫描
For k = 5 To 27 Step 11
bj = bj + 1 ‘生成班级序号
‘为教师姓名单元格写入函数公式,求出teacher中的班主任姓名:
Cells(i, k) = Sheet4.Cells(bj + 3, 2)
Next
Next
End Sub
高一年级的班主任姓名自动添加程序代码编写完毕,可以通过运行检查该程序是否可以正常工作。
7.3.2.2 自动给班级课表添加班主任姓名程序检测
代码输入完毕,可以回到工作表窗口运行程序了。如果当前窗口不是高一年级课表,则可以用鼠标点击进入“高一”课表。这时,如果程序工作正常,则在各个班级课表上添加或者更新一次班主任的姓名。
可是如果原来班级课表上已经有班主任的姓名,而且这些班主任姓名并没有变化,则即使程序工作正常,用户也观察不到任何变化。为了检测程序的运行,可以从两方面来进行。首先可以将部分或者全部的班级课表上的班主任的姓名删除,然后进入其他年级课表,再进入高一课表;在这种情况下,如果程序正常,就会看到班级课表上原来没有姓名的单元格又重新出现了班主任的姓名。
还可以进入到教师任课岗位表“teacher”工作表,将高一年级的班主任姓名全部删除,或者更改全部班主任的姓名;再回到高一课表,如果程序正常,这时就会看到变更后的结果。
如果程序不能正常运行,则要检查代码,找出错误,直至程序可以正常运行为止。
7.3.2.3 自动给班级课表添加班主任姓名的程序代码分析
代码由3部分组成。开头和结尾组成了程序运行的条件:
Private Sub Worksheet_Activate()
End Sub
“Worksheet_Activate”表示工作表被打开的事件,这个事件一旦发生,就开始运行程序的代码。
第二部分是计数器“bj”的使用。它实际上是一个变量,用来记录下面两重循环语句中循环体循环的次数。在循环体外,也就是每次循环之前,要给计数器清零:
bj = 0 ‘计数器归零
在循环体内,每循环一次,则将计数器加1:
bj = bj + 1 ‘生成班级序号
这样,变量bj就存放了班级序号。这是为什么呢?这还要从两重循环说起。
第三部分是循环语句,用来逐个给1-12个班级添加或者更新班主任的姓名:
For i = 32 To 62 Step 10 ‘在班级课表中教师姓名单元格扫描
For k = 5 To 27 Step 11
循环体
Next
Next
原来在班级课表上,虽然班主任姓名所在的单元格是合并单元格,但是每个合并单元格的坐标是由左上角的单元格坐标决定的。这12个左上角的单元格表示成Cells(i, k)的形式为:
Cells(32, 5)
Cells(32, 16)
Cells(32, 27)
Cells(42, 5)
Cells(42, 16)
Cells(42, 27)
Cells(52, 5)
Cells(52, 16)
Cells(52, 27)
Cells(62, 5)
Cells(62, 16)
Cells(62, 27)
采用上面的两重循环语句就可以从左到右,从上到下逐个为高一1、2、3、……写出班主任的姓名。
这是因为虽然班级课表分布在一个2维平面上,但是在循环体中的计数器可以把循环的次数记录下来,这个循环次数正好和班级序号相等;所以这样的代码就完成了将2维数据转换成1维数据的任务。
本书在前面介绍过将1维数据转换成2维数据的程序。1维数据与2维数据的相互转换在工作表编程时需要经常使用;用户要牢牢掌握这种转换的方法。
循环体的核心代码用来添加和更新班主任的姓名:
Cells(i, k) = Sheet4.Cells(bj + 3, 2)
这是一句赋值语句。等号的左边是班主任姓名所在的单元格;等号的右边是“Sheet4”的单元格“Cells(bj + 3, 2)”。原来Sheet4就是teacher所在的工作表的名称,单元格Cells(bj + 3, 2)的取值范围是从Cells(4, 2)到Cells(15, 2),正好是教师任课岗位表的高一年级班主任列的12个班的班主任姓名。通过这个赋值语句,就可以把teacher表里面的高一年级班主任姓名添加到高一课表里面。
● 工作表的对象名称(Sheet1)和标签名称(Name)
在VBA编程时,工作表的对象名称和标签名称是不同的两个概念。
一般打开一个新工作簿,里面出现的默认工作表有3个,这时显示在工作表标签的就是按照出现的顺序的内部名称:Sheet1、Sheet2、Sheet3。此时,工作表的对象名称和标签名称相同。
如果用户在工作表标签上面给工作表重新命名;命名了“高一”、“高二”等新名称;这时,这些工作表的标签名称就变成了“高一”、“高二”。但是该工作表的对象名称是不变的。这一点在编程时必须注意到。下面进入VBE窗口来观察工作表的对象名称和标签名称(Name);如图7.3-2所示。

图7.3-2
在工程资源管理器中打开文件夹,双击“高一”文件,在下面的属性窗口中就会看到属性列表。上面的文件夹列表为:
Sheet1(高一)
它的格式为:工作表对象名称(工作表标签名称)
下面的属性列表更加清楚地把工作表的名称和名字的区别表述出来:
(名称)
Sheet1
……
Name(标签名称)
高一
用户可以在属性列表里面修改工作表的标签名称。对象
在上面的实例中,用户在程序中引用了另一个工作表里面的单元格数据,使用的就是目标工作表的对象名称;例如代码:
Sheet4.Cells(bj + 3, 2)
责任编辑: