我的VB感受 作者:梁利锋

有关于 VB



    Visual Basic 已经变得像 C 一样结构化,像 Pascal 一样灵活,像 FORTRAN 一样适于科学工作,比 COBOL 更适用于商业工作,比 Xbase 更适于操作数据,甚至可能像 Smalltalk 一样面向对象,像 LISP 一样长于列表处理。

——援引自《Visual Basic 5.0 核心技术》



一 几件小事

Visual Basic 是强大的,但是我想在开始时先说一些小事。虽是小事,但有时却会令人头疼不已,所以先说。

  1. 如果你也经常将工程命名为中文长文件名,可能你也在启动 VB5 时遇到过“未知的错误”提示,这时运行“RegEdit”,打开“HKEY_CURRENT_USER/Software/Microsoft/Visual Basic/5.0/RecentFiles”键,将名为“1”的字符串值删掉,VB5 又可正常运行了,但“打开”中的“最新”就被清空了。
  2. 打开“要求变量声明”选项。不要求变量声明在很多情况下也可正常,但是如果程序里有拼写错误,你将对程序的莫名其妙的错误白白浪费许多时间。另外,我一般将“自动语法检测”关掉,因为它非常讨厌,而且关掉后,语法错误的程序一样以“红色显示”(缺省),足以提示了。
  3. 系统颜色的使用。Windows 可以自定义系统颜色,也就是说,按钮表面不一定是灰色,按钮文本也不一定是黑色,所以一幅灰色背景的图并不一定会和窗体背景融合。也有些人也把窗体背景色也硬性定义为灰色,但是边框却还是用户定义的颜色!所以还是要注意。
  4. 尽量多用常数。VB 定义了很多常数,比如,vbCr 是回车,vbLf 是换行,vbCrLf 是回车换行。
  5. 删除图标按 Del 键。我看过一篇文章说,要除去 Icon ,需在 Form_Load 里加入 Set Me.Icon=LoadPicture("") ,首先,LoadPicture("") 返回 Nothing ,故可写作 Set Me.Icon=Nothing ;其次,如前所述,这条语句也属多余。
  6. 多用热键。比如,Ctrl+J 显示 属性/方法 列表, Ctrl+Shift+J 显示常数列表,Ctrl+I 显示快速信息,Ctrl+Shift+I 显示参数信息。(VB 不能自定义热键颇为可恶。)
  7. 全编译执行程序。大多数情况下,执行程序即可,但全编译执行可以帮你迅速排除一些低级错误,特别应该用于 API 回调函数等不能中断的程序调试时。
  8. Dim 时,每个变量皆应声明。用过其它语言如 C、Pascal 等,在 VB 中一般会这样定义变量: Dim i,j As Integer ,自以为定义了 i、j 两个整数,其实是定义了一个变体变量 i 和一个整数 j 。正确的方法应是 Dim i As Integer,j As Integer 。
  9. 预定义对象和集合的使用。如 App、Err、Screen 等对象,Forms、Controls 等集合。也许我们千方百计不能达到的目的就在这些对象和集合中有异常简单的解决方法。
  10. 各种属性的搜刮。比如早期我编俄罗斯方块对 PictureBox 的键盘事件编程,需要时时防止焦点离开,后发现窗体有 KeyPreview 属性,所以现在不需时刻提防,程序却更稳定了。
  11. 注意参数的传递方式。比如,开始我并不知道如何控制文本框的输入,后来才知道可以通过给 KeyAscii 赋值来控制,这才发现 KeyAscii 是按指针传递的。其实,VB 提供的事件的参数只要是按指针传递的,基本可以肯定通过改变此参数的值可以控制此事件过后 VB 的行为。
  12. Sub 的调用方法。习惯了 C 或 Pascal 中调用函数的方法,在 VB 中也会这样调用过程:Test (x) ,函数名和括号之间的那个空格使它和正常调用过程不同;如果是函数而不关心其返回值的话,也有人这样写:temp=Test(x) 。其实正确的调用方法是 Test x 或 Call Test(x) ,当然,像前面的写法并不一定会出错,但要指出,我以前在使用 Test (x) 这样的语法时曾出过错。
  13. 数组从 0 开始。Dim x(10) 其实和 Dim x(0 To 10) 的结果一样,你得到的是十一个元素,而不是十个。特别的,ReDim x(0) 等同于 ReDim x(0 To 0) ,并非删除了所有的数组元素,而是有一个元素,一定要注意(要删除动态数组的所有元素,用“Erase”)。
  14. True False 的值。VB 中 True 的值是 -1 ,False 的值是 0 ,用 If 语句直接判断时和 C 并无差别,但是如果用了等号或 Not 等操作符,也许结果就事与愿违了。


二 中文函数名、变量名、控件名以及窗体名

使用 VB5 及其以上版本,不论中英文版,都可以使用中文函数名、变量名、控件名以及窗体名。每次引用时都输入中文当然有些力不从心,所以我现在最熟练的操作是“Ctrl + C”、“Ctrl + X”、“Ctrl + V”。 :)

我曾在多种语言上试验,只有 VB 允许这样做。Visual FoxPro 应该也可以,但我把它看作 VB 的同类,其它如 VC、Dephi、C++ Builder、Perl,甚至于 JavaScript、VbScript,都不能这样做。

最近听到一种说法,以为用中文函数名、变量名、控件名以及窗体名可能兼容性不好。好吧,让我们看看结果如何:将有中文函数名、变量名、控件名以及窗体名的工程编译,再在编译好的程序里查找中文函数名、变量名——找不到,再查找控件名、窗体名——找到了。如此,我们能确定函数名和变量名被编译成了地址的形式,所以找不到;那么找到了控件名和窗体名是否说明控件和窗体是用名字调用的呢?非也,非也,之所以找到控件名和窗体名是因为你可能在程序里访问控件和窗体的“Name”属性——“岂予所欲哉!予不得已也”。

当然,是否使用中文编程是由各位自己决定的。我曾数次 Mail 给“网络蚂蚁”的作者,希望他将界面改为中文而终不可得,更何逞用中文编程乎?



三 函数式调用对话框

VB 中有 MsgBox 和 InputBox 两个函数用来进行最常用的输入输出操作,但有时却不够我们的需求,而且 MsgBox 和 InputBox 在执行时就暂停了程序,大部分情况下正是我们所要的,但是有些情况下,我们却希望在执行这一类输入输出时不不暂停程序,而是能在后台仍执行程序,用模式对话框可以实现,但又失去了 MsgBox 和 InputBox 的快捷和方便,这时函数式调用对话框便成为首选方案。

当然,这只需在标准模块里定义函数原型,再在此函数里调用“自定义的窗体.Show vbModal”即可。在这里最重要的问题是:返回值如何返回?

首先,可以定义全局变量,在“自定义的窗体”卸载以后检测这些变量,从而得知作了哪些改变。这种方法并不差,我也常用,但是它有问题:使用了全局变量。全局变量的使用过多,会造成程序混乱,一有可能自己也记不清此变量的用处,二有可能在其它地方调用变量时不经意的使用了同一个名字,从而造成错误,另外,也许你从以前编的几个程序里各取一部分加入新的工程,却发现许多变量同名,又不得不重改变量名。

其次,可以使用我介绍的下一种方法。

在“自定义的窗体”里定义一个 Public 类型的“自定义函数”,在其中加入初始化代码,然后调用“Me.Show vbModal”,然后“自定义函数=返回值”、“Unload Me”,在卸载窗体时令其不卸载,只隐藏,然后在标准模块定义的函数里调用“自定义的窗体.自定义函数”,如下:

Option Explicit
Private 返回值 As String

Public Function 自定义函数() As String
    初始化
    Me.Show vbModal
    自定义函数 = 返回值
    Unload Me
End Function

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    If UnloadMode = vbFormControlMenu Then
        Cancel = True
        取消_Click
    End If
End Sub

Private Sub 取消_Click()
    返回值 = ""
    Me.Hide
End Sub

Private Sub 确定_Click()
    返回值 = 输入值
    Me.Hide
End Sub

最后,我想说一下,因为模式窗体并不阻止程序运行,就有可能此函数被调用多次,从而在“Me.Show vbModal”时产生“运行时错误”,如果不希望出现多个窗体,应该避免出现这种情况;如果希望出现多个窗体,在标准模块的函数里应该用“New”产生新的“自定义的窗体”,然后调用,如下:

Public Function 自定义函数() As String
    Dim 新窗体 As New 自定义的窗体
    自定义函数=新窗体.自定义函数
End Function

也许有人对窗体是否真的被卸载表示怀疑,很好,你可以用“Forms”集合自己检测。



四 数组的应用

数组其实能完成一些本来看似不应由数组完成的任务,我当然不是吃饱了没事干,它使有些程序看来更舒服——至少我这么认为。

比如,VB 的 WeekDay 返回一个 Integer 值,我们显示时却希望显示字符串,Format 函数返回的是英文字符串,于是我们需要自己完成转换:

Private Function 询问星期(ByVal 星期几 As Integer) As String
    Select Case 星期几
    Case vbSunday
        询问星期 = "星期日"
    Case vbMonday
        询问星期 = "星期一"
    Case vbTuesday
        询问星期 = "星期二"
    Case vbWednesday
        询问星期 = "星期三"
    Case vbThursday
        询问星期 = "星期四"
    Case vbFriday
        询问星期 = "星期五"
    Case vbSaturday
        询问星期 = "星期六"
    End Select
End Function

如果使用数组的话,可以写成:

Private Function 询问星期(ByVal 星期几 As Integer) As String
    Dim 星期
    星期 = Array("日", "一", "二", "三", "四", "五", "六")
    询问星期 = "星期" & 星期(星期几 - vbSunday)
End Function

具体选用那种方法当然由你决定。我要说的是这种方法也可用于其它语言,不过要注意,数组毕竟不是 Select Case ,后者所能完成的大多数任务不能由数组替代,事实上,VB 的 Select Case 几乎是所有语言中最强大的,善用 Select Case 将精简大量代码。

不过 VB 中的数组还有一些其它的特性,使得它的用途也是十分广泛,关于这方面的一些情况,请参见我的下一篇文章“VB Vs Perl”。



五 事件的应用

在 VB5 的面向对象编程时,我们不只可以自定义属性,方法,还有非常强大的“自定义事件”。

在类模块中定义事件,然后在适当的时候,用 RaiseEvent 产生事件即可,本没有更多可说的,但我在编一个 Socket 通信的程序时,却发现事件的功能比我想象的大。

就以 Socket 通信为例,我所要做到的是:不同的 Socket 字符串送给不同的窗体。我做了一个类,用来分析 Socket 字符串,并分别产生不同的事件,问题是只能有一个 Socket 控件接收 Socket 信息,所以由此类产生的对象只能和 Socket 控件同在“主窗体”中,那么,此类所产生的事件是否也只是发向这个“主窗体”呢?

开始我以为是的。于是我在“主窗体”里对每一个事件编程,在事件里调用不同窗体的 Public 类型的函数。但有一个问题:我所调的窗体可能还没有加载,而且我也不希望它现在加载。我用“窗体.Visible”检测窗体加载与否,极不理想。

后来我发现可以这么作:将“主窗体”里产生的对象定义为 Public ,比如类名为“协议”,如下:

Option Explicit
Public WithEvents 分析员 As 协议

Private Sub Form_Load()
    Set 分析员 = New 协议
    Socket控件.Connect
end Sub

Private Sub Socket控件_DataArrival(ByVal bytesTotal As Long)
    Dim Temp As String
    Socket控件.GetData Temp
    分析员.分析信息 Temp
End Sub

在需要事件的窗体里,这样定义:

Option Explicit
Public WithEvents 事件发生器 As 协议

Private Sub Form_Load()
    Set 事件发生器 = 主窗体.分析员
end Sub

Private Sub 事件发生器_狼来了()
    Me.Caption = "快逃啊!"
end Sub

这样,用这种方法完成了这一操作。你可以在所有窗体里对此事件编程,也可以只对几个窗体编程,从而可以用简单的方法完成复杂的任务。



六 Image 控件与 PictureBox 控件的比较

PictureBox 控件功能强大,但占资源较多;Image 控件功能不强,但占资源较少。但是还有另一面:感觉。

因为 PictureBox 控件是真正意义上的控件:有窗口类衍生而来;Image 控件却不是。这一点可用 Spy++ 检测得知。

由此,我们知道了 Image 控件事实上是在其父窗体上画出的,虽然它占用资源确实很少,但是在刷新控件时,即使不是刷新了整个父窗体,也是在刷新时在整个父窗体范围内关闭了鼠标。这样,在频繁刷新的情况下(如进度条),只要鼠标在父窗体范围内,就会不停的闪动,这时用 PictureBox 控件代替 Image 控件,闪动就只在控件内部了。( 2 色鼠标光标一般肉眼看不出闪动,256 色鼠标光标就非常明显了。)



七 控件数组动态增删

控件数组并非真正的数组,所以它的动态增删与普通数组也是不同的。

新增一个控件数组的元素,使用的是“Load 控件(n)”;删除一个控件数组的元素,使用的是“Unload 控件(n)”;n 自增;但是这样反复几次后会发现控件数组中出现了“空洞”,虽然可以用“For Each”来访问每一项,但却为以后的增删产生了麻烦。

本来,如果控件索引是“Long”型,“空洞”问题也不怎么严重。因为有 4294967295 个数可选,即使一秒钟产生一个“空洞”,也需要 136 年才会有重复的可能性。在这些年里,即使我们不被微软强迫升级 Windows , Windows 自身也会先于我们的软件崩溃的。只是可惜,控件索引是“Integer”,也就是只有 65535 个数可选,也就是说如果一秒钟产生一个“空洞”,18 个小时后就会产生重复的数,从而产生错误。

比如我在编 Socket 服务器时是这样解决的:

Private Sub 连接_ConnectionRequest(Index As Integer, ByVal requestID As Long)
    If Index = 0 Then
        Dim 连接数 As Long
        连接数 = 空余
        Load 连接(连接数)
        连接(连接数).LocalPort = 0
        连接(连接数).Accept requestID
        新增连接 连接数, "未知", 0, 连接(连接数).RemoteHost,  _
                连接(连接数).RemoteHostIP, "无操作"
    End If
End Sub

Private Function 空余() As Long
    Dim i As Long, Temp As Long
    On Error GoTo 完成
    For i = 连接.LBound To 连接.UBound
        Temp = 连接(i).LocalPort
    Next i
完成:
    空余 = i
End Function

Private Sub 新增连接(连接数 As Long, 用户名 As String, 级别 As Long,  _
	主机名 As String, IP As String, 操作 As String)
    Dim Item As ListItem
    Set Item = 用户列表.ListItems.Add(, "U" & 连接数, 用户名)
    Item.SubItems(1) = 级别
    Item.SubItems(2) = 主机名
    Item.SubItems(3) = IP
    Item.SubItems(4) = 操作
End Sub

因为“空洞”并不影响数组控件的索引值,所以删除较为简单:

Private Sub 连接_Close(Index As Integer)
    删除连接 Index
End Sub

Private Sub 连接_Error(Index As Integer, ByVal Number As Integer,  _
	Description As String, ByVal Scode As Long,  _
	ByVal Source As String, ByVal HelpFile As String,  _
	ByVal HelpContext As Long, CancelDisplay As Boolean)
    ShowError "连接" & Index & "发生错误:" & Description
    CancelDisplay = True
    删除连接 Index
End Sub

Private Sub 删除连接(ByVal Index As Long)
    If 索引 >= Index Then 索引 = 索引 - 1
    Unload 连接(Index)
    用户列表.ListItems.Remove "U" & Index
End Sub

Private Sub ShowError(详情 As String)
    Me.Caption = 详情
End Sub

最后我说一下,VB5 的 Socket 控件在调用“Close”方法后,连接却并不一定“Close”掉了,据说需要做一个“Do”循环不断检测“State”属性,直到真的“Close”掉了为止,我甚觉其可恶,所以现在一般用 VB6 的 Socket 控件。



八 TextBox 的输入控制

TextBox 的 KeyPress 事件的参数 KeyAscii 是以指针的方式传递(准确的说是按引用传递的)的,所以能通过对它的赋值起到控制其输入的目的:

Private Sub 输入框_KeyPress(KeyAscii As Integer)
    If KeyAscii > vbKey9 Or KeyAscii < vbKey0 Then
        KeyAscii = 0
        Beep
    End If
End Sub

如果有多个 TextBox 需要控制,可以这样在 KeyPress 事件中调用过程:

Private Sub 输入框_KeyPress(KeyAscii As Integer)
    字符过滤 KeyAscii
End Sub

如果将需要控制的 TextBox 定义为控件数组,会更方便,只是有一些限制。

“字符过滤”在标准模块中定义:

Option Explicit
Private Const vbKeyDot = 46
Public Enum 过滤方式
    只留整数
    只留十六进制
    只留数字
End Enum

Public Sub 字符过滤(字符 As Integer, Optional 方式 As 过滤方式 = 只留整数)
    If 字符 > 26 Then
        Select Case 方式
            Case 只留整数
                If 字符 > vbKey9 Or 字符 < vbKey0 Then
                    字符 = 0
                    Beep
                End If
            Case 只留数字 '可能输入多个小数点
                If (字符 > vbKey9 Or 字符 < vbKey0) And 字符 <> vbKeyDot Then
                    字符 = 0
                    Beep
                End If
            Case 只留十六进制
                Select Case 字符
                Case vbKey0 To vbKey9, vbKeyA To vbKeyF, 97 To 102
                Case Else
                    字符 = 0
                    Beep
                End Select
        End Select
    End If
End Sub

可能你已经注意到了我先检测“If 字符 > 26”,那么,“字符 < 26”是什么呢?原来是“Ctrl + 字母”。比如:“Ctrl + V”时,“字符”的值为 22 ,这时将“字符”赋值为 0 ,就使“粘贴”操作不被执行,这样,几乎所有的 TextBox 操作都能控制了。



九 调用 API 时的字符串返回值

说明 API 中的字符串时将字符串定义为“ByVal”就可以在 API 中用字符串了:

Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
    (ByVal hwnd As Long, ByVal lpClassName As String, _
    ByVal nMaxCount As Long) As Long

而要取得字符串返回值可以用这样的函数:

Public Function 窗体类名(ByVal 句柄 As Long) As String
    Dim 类名 As String * 256, 类名长度 As Integer
    类名长度 = GetClassName(句柄, 类名, 255)
    窗体类名 = Mid$(类名, 1, 类名长度)
End Function

还可以用这样的函数:

Public Function 窗体类名(ByVal 句柄 As Long) As String
    窗体类名 = Space$(256)
    GetClassName 句柄, 窗体类名, 255
    窗体类名 = Api字符串还原(窗体类名)
End Function

Public Function Api字符串还原(原字符串 As String) As String
    Dim 长度 As Long
    长度 = InStr(1, 原字符串, vbNullChar) - 1
    If 长度 < 0 Then
        Api字符串还原 = 原字符串
    Else
        Api字符串还原 = Left$(原字符串, 长度)
    End If
End Function

第二种方法比较通用,第一种方法比较简单,具体使用悉听尊便。不过我想说一说,我以前喜欢用定长字符串调用 API (第一种),现在却喜欢用变长字符串(第二种),定长字符串的问题在于首先定义“Dim 类名 As String * 256”,调完 API 后,执行“类名 = Mid$(类名, 1, 类名长度)”,这时“类名”并非真正的类名,而是“真正的类名 + Space$(256 - Len(真正的类名)”,非“我所欲也”。



十 产生错误

适时地产生“运行时错误”,不仅有利于调试程序,在有些情况下,也能起到简化编程的目的。

首先应该知道,“On Error”语句有很强的包容性,比如在一个函数里有“On Error Goto 出错”,而在这个函数中调用了另一个函数,这“另一个函数”产生了“运行时错误”,也会同样“Goto 出错”的。

正因为如此,我们可以在“另一个函数”产生“运行时错误”,而在原函数里统一用“On Error”处理错误,从而简化编程。

Private Function 执行MCI命令(命令 As String) As String
    Dim 返回 As String, 执行情况 As Long
    返回 = Space(256)
    执行情况 = mciSendString(命令, 返回, 255, 0)
    If 执行情况 = 0 Then
        执行MCI命令 = Api字符串还原(返回)
    Else
        Err.Raise vbObjectError + 1, , _
            "执行 MCI 命令出错:" & vbCrLf & _
            命令 & vbCrLf & "返回:" & 执行情况
    End If
End Function

“Goto”语句曾倍受奚落,因为它不是结构化的,但是在 VB 作了一些手脚后,“Goto”也很好用,且不容易出错了:VB 的行号是过程内有效的。这也就是说,在不同的函数里,可以定义同为“出错”的行号,而不会出错,这样“Goto”好像也有些“结构化”了。 :)



十一 随机数的问题

产生随机数也是一个简单的问题:首先用“Randomize”初始化随机数发生器,然后就可以用“Rnd”产生随机数了。(需要初始化的原因是随机数其实并非真的随机,初始化的值如果确定,则产生的随机数序列也是确定的,所以一般用当前时间初始化随机数发生器,不过我在这里就不详细讨论了。)

我在这里想说的是另一个问题。

比如 WinAmp 的随机播放,只是简单的调用了随机数发生器,所以有可能刚听完这一首歌,随机数发生器又产生这个数,就要再听一遍这一首歌,而有可能有些歌在全部循环几遍之后才能播放一次。

简单的调用随机数发生器,更严重的问题会出现在“挖雷”上:我们自定义 10×10 的方阵,99 颗雷,越往后越难产生正确的雷的位置了。

所以我在“俄罗斯方块”里这样产生“随机”的“底图”:

Dim i As Long, j As Long, n As Long
n = 文件过滤器.listcount - 1
If listcount(随机控制) <> (n + 1) Then
    If n >= 0 Then
        ReDim 随机控制(0 To n)
        If 图号 < 文件过滤器.listcount Then
            随机控制(图号) = True
        End If
    End If
End If
For j = 0 To n
    If 随机控制(j) = False Then i = i + 1
Next
If i = 0 Then
    ReDim 随机控制(0 To n)
    i = n
    If 图号 < 文件过滤器.listcount Then
        随机控制(图号) = True
    End If
End If

i = Int(Rnd * i)
For j = 0 To n
    If 随机控制(j) = False Then
        i = i - 1
        If i = -1 Then
            随机控制(j) = True
            Exit For
        End If
    End If
Next

图号 = j
Set 选择的图像 = LoadPicture(文件过滤器.List(图号))

程序很罗嗦,但效果良好,即使只有两幅“底图”,也不会连续两次得到同一幅“底图”了。



十二 回调函数

我并不是要说“回调函数”的全部,这个题目太大,不只版面不允许,我的能力也不允许。所以我要说的是“回调函数”的一些零碎的问题。

我很喜欢类模块的封装性,但是“回调函数”却必须有一个标准模块,因为 AdressOf 只能取得标准模块中的函数地址,但是问题是,我在标准模块里怎么令类模块产生“事件”呢?

从我收集的源代码来看,有一种是直接调用确定窗体的函数,不免有失通用性;另一种是用“CopyMemary”作了一些奇怪的操作来完成的,让人不敢放心,而且我见的例子只在编控件时有效,在类模块时不正常。这真是让人难以愉快啊!

后来,我发现“正统”的方法应该是这样的:(标准模块)

Option Explicit
Public PrevWndProc As Long
Private 系统栏对象 As New Collection

Public Function SubWndProc(ByVal hwnd As Long, _
        ByVal Msg As Long, ByVal wParam As Long, _
        ByVal lParam As Long) As Long
    On Error Resume Next
    Select Case Msg
    Case TRAY_CALLBACK
        Dim CurTray As 系统栏
        Set CurTray = 系统栏对象.Item("H" & hwnd)
        CurTray.SendEvent lParam, wParam
    End Select
    SubWndProc = CallWindowProc(PrevWndProc, hwnd, Msg, wParam, lParam)
End Function

Public Sub 注册新对象(新对象 As 系统栏, H窗口 As Long)
    系统栏对象.add Item:=新对象, Key:="H" & H窗口
End Sub

Public Sub 注销对象(H窗口 As Long)
    系统栏对象.Remove "H" & H窗口
End Sub

名为“系统栏”的类模块:(部分)

Public Sub 初始化(hwnd As Long, Icon As StdPicture, _
        Optional Tip As String = defTrayTip)
    gInTray = defInTray
    gAddedToTray = False
    gTrayId = 0
    gTrayHwnd = hwnd
    InTray = defInTray
    TrayTip = Tip
    Set TrayIcon = Icon
    注册新对象 Me, hwnd
End Sub

Private Sub Class_Terminate()
    If InTray Then InTray = False
    注销对象 gTrayHwnd
End Sub

Friend Sub SendEvent(MouseEvent As Long, Id As Long)
    Select Case MouseEvent
    Case WM_MOUSEMOVE
        RaiseEvent MouseMove(Id)
    Case WM_LBUTTONDOWN
        RaiseEvent MouseDown(vbLeftButton, Id)
    Case WM_LBUTTONUP
        RaiseEvent MouseUp(vbLeftButton, Id)
    Case WM_LBUTTONDBLCLK
        RaiseEvent MouseDblClick(vbLeftButton, Id)
    Case WM_RBUTTONDOWN
        RaiseEvent MouseDown(vbRightButton, Id)
    Case WM_RBUTTONUP
        RaiseEvent MouseUp(vbRightButton, Id)
    Case WM_RBUTTONDBLCLK
        RaiseEvent MouseDblClick(vbRightButton, Id)
    End Select
End Sub

这样,几近完美的完成了“事件”的产生,既保持了比较好的封装性,又有很好的通用性。

在“回调函数”中,还有这样一个问题:超时。比如精确定时函数“timeSetEvent”,你可以定义一毫秒产生一次回调,但是这样你就必须在一毫秒内完成所有操作,如果不然,你就需付出死机的代价。

面对这种情况,也有几种解决办法。比如,你可以在“回调函数”中使一个全局变量自增,而在主程序的一个死循环中检测这个变量用以决定操作,这种方法其实也很好,只是改变了事件驱动的编程方式,但换来了比较好的响应度和稳定性。

我更喜欢事件驱动的编程方式,并且偏执的认为事件驱动的效率更高,所以我用下面的方法:

Option Explicit
Private m_TID As Long
Public TimeEnabled As Boolean
Private CurdwUser As Long

Public Sub TimeProc(ByVal uID As Long, _
    ByVal uMsg As Long, ByVal dwUser As Long, _
    ByVal dw1 As Long, ByVal dw2 As Long)
    If TimeEnabled = True Then
        If m_TID = uID Then
            If CurdwUser = dwUser Then
                If 主窗体.Visible = True Then
                    PostMessage 主窗体.HWND, WM_KEYDOWN, 1, 0
                End If
            End If
        End If
    End If
End Sub

Public Sub AddTimer(ByVal dwInt As Long, _
        Optional ByVal dwUser As Long = 0)
    CurdwUser = dwUser
    m_TID = timeSetEvent(dwInt, 0, _
        AddressOf TimeProc, dwUser, TIME_PERIODIC)
    If m_TID = 0 Then 主窗体.Caption = "创建时间函数失败"
End Sub

Public Sub RemoveTimer()
    If m_TID > 0 Then timeKillEvent m_TID
End Sub

本来应该“PostMessage”一个自定义消息的,但是那样又需完成另一个“回调函数”,故不取,而令“PostMessage”产生一个 KeyDown 消息,送出“1”(在键盘上是无法键入 AscII 值为“1”的键值的),这样就可以利用“主窗体_KeyDown”事件完成这个操作了。

好了,我不妨也自高自大一回,说一句“程序本天成,妙手偶得之”(此句剽窃而来)。



十三 关于帮助

最后,我要说 VB 的帮助是非常全面的,经常查一下帮助(包括联机手册),平常所遇到的问题一般能迎刃而解。但是,迷信其帮助也是不行的,让我们来看一看这个“令人心碎、绝望和徒劳无功的小故事”:

    WritePrivateProfileString 过去是一个是流行的函数,因为它允许你在任务之间保存数据,这是 Visual Basic 很早就应该提供而直到版本 4 才提供的功能。每一个人都想使用这个函数。每一个人都从 API 声明文件中粘贴其声明部分。每一个人在使用它时都遇到了麻烦。然后每一个人都寻求 Microsoft 产品支持,或者如果他们在 Microsoft 工作,则发送电子邮件到内部 Visual Basic 编程部门。我过去至少每一个月看到一次关于 WritePrivateProfileString 的这种查询。

——援引自《Visual Basic 5.0 核心技术》

而且对于一些高级问题,可能 VB 的帮助有点力不从心,这时需要一本关于 VB 的好书,不过好书实在不多,我所见的只《Visual Basic 5.0 核心技术》值得推荐一下。

原文地址:https://www.cnblogs.com/ainima/p/6331631.html