回 帖 发 新 帖 刷新版面

主题:[原创]VB6文本框的一个重大BUG及解决办法

       VB6文本框的一个重大BUG及解决办法


一、问题的提出
  文本框有三个编辑属性:

  SelStart:用于设置或获取被选中的字符串的起点,它的最小值为 0,表示文本框第 1 行第 1 列的位置。这个位置值是按字符来计算的,1 个 Ascii 字符和 1 个汉字都算一个字符。
  SelLength:用于设置或获取被选中字符串的长度, 也就是字符个数。
  SelText:用于设置或返回被选中字符串的内容。 
  如上所述, 我们可利用文本框的这 3 个属性来设置文本的编辑功能。

  新建一个窗体 Form1,Form1 上添加一个文本框控件 Text1,在菜单编辑器中添加一级菜单“编辑”,在“编辑”下添加10个二级菜单项(各个菜单项的快捷键,朋友们自己设置吧):

  撤消
  剪切
  复制
  删除
  粘贴
  全选
  删除光标至行末
  删除光标至文末
  光标至文末
  光标至文首

  将前 5个菜单项去掉“有效”复选框中的勾。随后编写代码,我们以“剪切”为例:

Private Sub 剪切_Click()
ST = Text1.SelText '将选中的文本赋给字符串变量 ST,以便粘贴
Text1.SelText = "" '删除选中的文本
撤消.Enabled = True
粘贴.Enabled = True
End Sub

  在文本不是很大的情况下,这样的代码是可行的。这段代码中,看起来没有用到 SelStart 属性,其实它不但是个“无名英雄”,还是问题的关键。
  笔者经多次试验发现, SelStart 属性有一个无法克服的缺陷:它的最大值是 34373,也就是说,如果我们要操作的字串的起始位置大于 34373 个字( VB6 的文本框可容纳的最大字数,不论英文还是汉字都是 65535),那么它就无能为力了! 一旦我们在大于 34373 字数的地方选中了文本,那么返回的这三个属性值都将是错误的。这个缺陷大概是VB6 的一个重大 BUG。也许有人会反驳说,这根本就不是BUG,文本框就是在字数不多的场合才用的嘛,字数多了可用富文本框啊。我不能同意此观点。第一,如果文本框只能用于字数少的场合,那么微软就不会将它从32K的容量扩大为64K了,甚至连32KB也不需要,16K就绰绰有余了;第二,既然已经扩容,那么它所有的属性、方法等等理应都相应改变,以适应这个变化,何况SelStart还是文本框非常重要的属性,显然是微软忘记修改这几个属性了,这不是BUG又是什么?如果这都不算 BUG,那所有的东东都可以说没有BUG了,谁都可找出辩解的理由……呵呵,题外话,言归正传。
  所以上面那段代码实际上没什么用处。
  为了解决这个问题,笔者尝试使用 API 的 SendMessage 消息函数,来替代文本框的那三个属性,编写了“编辑”的功能代码。


二、SendMessage 消息函数简介
  SendMessage 消息函数共有 4 个参数:

  第一个参数:接收消息的对象的句柄。句柄是一个标识符(或许说句柄是一个编号更容易理解),是拿来标识对象或者项目的,它就象人的姓名一样,每个人都会有一个。从数据类型上来看,它是一个长整形的无符号整数。
  第二个参数:消息标识。例如复制,它的消息标识是 WM_COPY,懂英文的人很容易就能看懂。消息标识也称常数。在实际运用时,这些常数还必须预先赋以相应的常数值(其实,我认为,称“消息变量”要准确些,称常数是不准确的,因为既然是常数,它就应该代表一个唯一的值,而不必再赋什么值了,象 VB 中的常数都有唯一值,不需要再赋值了),事实上,SendMessage 函数是根据常数值来进行操作的。为了简单起见,笔者省去了给消息标识赋值的步骤,干脆直接使用 10 进制的常数。在后面的分析中,笔者将用“第×号消息”来表述,例如将 WM_COPY 称作“第 769 号消息”。
  第三、四个参数:是对第二个参数的补充或提供具体的数据,对于不同的消息来说,第三、四个参数是不相同的,有的消息不需要这两个参数,有的消息只需要其中的一个,有的消息则两个都需要。不需要的参数我们可以用 0 代替。

  API 消息函数在使用前必须先加以声明,可以在标准模块中声明,也可以在某个窗体中声明,这要根据使用范围来决定。如果是在标准模块中声明的,就能在工程的所有窗体中都使用它。
  下表是将要用到的消息常数及其所代表的操作:

  SendMessage 函数的常数(10进制)及其作用
 ---------------------------------------------------------
 常数  作用
 ---------------------------------------------------------
 176  获取光标在文本中的位置,以字节数表示
 177  根据第三和第四个参数的不同,有两个作用:
     ①移动光标至指定位置
     ②将指定范围的字符反相显示,相当于“选定”的作用
 183  把可见范围移至光标处
 187  获取光标行首字符在文本中的位置,以字节数表示
 193  获取光标行的字节数
 768  剪切选定的文本到剪贴板
 769  复制选定的文本到剪贴板
 770  粘贴剪贴板中的文本
 771  删除选定的文本
 772  撤消刚才的操作
 ---------------------------------------------------------



三、用 API 函数编写的“编辑”功能代码

Option Explicit

Private Declare Function SendMessage Lib "user32" Alias _
"SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, _
ByVal wParam As Long, lParam As Any) As Long

Dim ZT As Boolean '粘贴标志
Dim XI As Boolean '撤消标志
Dim HNo As Long '鼠标按下时的光标位置

Private Sub Text1_MouseDown(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
SendMessage Text1.hwnd, 176, 0, HNo '获取光标位置
If Button = 2 Then
  Text1.Enabled = False: Text1.Enabled = True
  PopupMenu 编辑 '弹出自定义的右键“编辑”菜单
End If
End Sub

Private Sub Text1_MouseUp(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
ZT = False: XI = False
End Sub

Private Sub 删除光标至行末_Click()
Dim J As Long, L As Long
L = SendMessage(Text1.hwnd, 187, -1, 0) '光标行首字符的位置
J = SendMessage(Text1.hwnd, 193, -1, 0) '光标行的字节数
SendMessage Text1.hwnd, 177, HNo, ByVal L + J '光标至行末的文本反相显示
删除_Click
End Sub

Private Sub 删除光标至文末_Click()
SendMessage Text1.hwnd, 177, HNo, ByVal -1 '光标至文末的文本反相显示
删除_Click
End Sub

Private Sub 光标至文末_Click()
SendMessage Text1.hwnd, 177, -2, ByVal -1
SendMessage Text1.hwnd, 183, 0, 0
End Sub

Private Sub 光标至文首_Click()
SendMessage Text1.hwnd, 177, 0, ByVal 0
SendMessage Text1.hwnd, 183, 0, 0
End Sub

Private Sub 编辑_Click()
撤消.Enabled = False
剪切.Enabled = False
复制.Enabled = False
粘贴.Enabled = False
删除.Enabled = False
If Text1.SelLength > 0 And XI = False Then
  剪切.Enabled = True
  复制.Enabled = True
  删除.Enabled = True
  Clipboard.Clear
End If
If Clipboard.GetText <> "" And XI = False Then ZT = True
If XI Then 撤消.Enabled = True
If ZT Then 粘贴.Enabled = True
End Sub

Private Sub 全选_Click()
SendMessage Text1.hwnd, 177, 0, -1 '所有文本反相显示
ZT = False: XI = False
End Sub

Private Sub 删除_Click()
SendMessage Text1.hwnd, 771, 0, 0
XI = True
End Sub

Private Sub 撤消_Click()
SendMessage Text1.hwnd, 772, 0, 0
Clipboard.Clear
End Sub

Private Sub 复制_Click()
SendMessage Text1.hwnd, 769, 0, 0
SendMessage Text1.hwnd, 177, HNo, ByVal HNo '这一句是可选的
End Sub

Private Sub 剪切_Click()
SendMessage Text1.hwnd, 768, 0, 0
XI = True
End Sub

Private Sub 粘贴_Click()
SendMessage Text1.hwnd, 770, 0, 0
End Sub



四、代码简析
  这几段代码中有两个布尔变量 XI、ZT 分别是撤消和粘贴操作的标志,其作用是根据它们的值来决定有关菜单项是否有效。要求是:
  当用户用鼠标选中了一段文本时,剪切、复制、删除三项有效;
  当用户剪切或复制了被选中的文本时,粘贴有效;
  当用户从别的窗口复制了一段文本欲粘贴到文本框时,粘贴有效
  当用户剪切或删除了被选中的文本时,撤消有效,并且可以反复撤消。
  
  “Text1_MouseDown”过程有两个作用:一是计算鼠标点击时的光标位置,二是使用我们自已的弹出式菜单,下面分析一下第二个作用:
  程序运行时,用鼠标右键单击文本框,会弹出系统默认的菜单,如果要使用自定义的菜单,可使用 PopupMenu 方法,其用法是:

  对象.PopupMenu 要显示的弹出式菜单名

  其中,“对象”是可选的,如果省略,则默认为当前具有焦点的窗体或控件;“要显示的弹出式菜单名”是必须的,且菜单中至少要有一个菜单项。
  但是,还有一个问题:当我们右键点击文本框时,第一次弹出来的仍然是系统默认的菜单,要第二次右键点击时才是自定义的菜单,这就必须屏蔽系统菜单了。屏蔽系统菜单有多种方法,笔者采用了资料上介绍的最简单的一种:在用 PopupMenu 方法之前, 先使文本框失效,这相当于第一次点击,但由于文本框不响应,所以系统菜单不显示出来;再使文本框有效,这相当于第二次点击,自定义的菜单显示出来了。但这个方法有一个毛病,在用右键点击文本框时,画面要闪动一下,算是美中不足吧。
  另外,在使用这个方法之前,还要判断用户点击的是鼠标右键,这可由 Button = 2 来加以确定(如果 Button = 1 ,则说明用户点击的是鼠标左键)。

  “删除光标至行末_Click”删除当前行从光标处至行末的文本字符。 这里调用了三次 SendMessage 函数(如果算上“删除_Click”中的一次,共调用了四次)。分析这段代码我们要反过来从后面分析起:
  用 SendMessage 函数的 771 号消息实现删除功能,必须先选定欲删除的文本,那么,怎样才是“选定”呢?简单地说,一段文本被反相显示时,就可以说这段文本被“选定”了。771 号消息删除的就是被反相显示的文本。 
  用 SendMessage 函数的 177 号消息能够反相显示指定位置的文本,它用到了该函数的第三第四两个参数。其中,第三个参数是欲删文本的起始字节,在本功能中就是当前光标位置,这是我们已知的(窗体级变量 HNo 之值),第四个参数是欲删文本的终止字节, 这里要注意的是,第四个参数一定要用传值关键字 ByVal。
  为了计算欲删文本的终止位置,要先计算出光标行的第一个字符在文本中的位置,再计算出光标行的字节数,这两个数值相加,就是光标行的终止位置。调用 SendMessage 函数的 187、193 号消息可获得这两个数值。请注意 187、193 号消息第三个参数,“-1”表示光标所在行,但也可使用光标的实际位置值 HNo。

  “删除光标至文末_Click”删除从当前光标处至文本末的所有字符,这个语句很简单,要注意 177号消息的第四个参数, “-1”表示文本末尾。

  “编辑_Click”是当用户点击一级菜单“编辑”时产生的事件。这段代码的作用是决定哪个二级菜单项有效,可以响应用户的点击事件。这里首先将二级菜单中的前五个菜单项都设为无效,以后根据情况再使之有效。
  第一个判断语句: 文本框.SelLength > 0,说明用户用鼠标选中了一段文本,可能的操作是剪切、复制、删除, 因此这 3 个菜单项必须有效。这里为什么还要使用 SelLength 属性呢? 因为当选中的文本位置 > 34373 个字时,SelLength 属性值尽管是错误的,但毕竟还是 > 0 的, 所以可以加以利用。后面的语句 Clipboard.C-lear 是 Clipboard 对象的 Clear 方法。 Clipboard 对象用于操作剪贴板上的文本和图形,Clear 方法清空剪贴板中的文本。
  第二个判断语句:Clipboard.GetText <> "" ,这说明系统剪贴板中有待粘贴的文本, 这时要将粘贴标志 ZT 设为 True,以便在第四个判断句中开放“粘贴”菜单项。Clipboard.GetText 是 Clipboard对象的 GetText 方法,该方法用于检索文本。
  第三个判断语句:如果 XI=True, 说明刚才已经进行了剪切或删除或撤消操作,现在应开放“撤消”菜单项,让用户可以“吃后悔药”。这里要指出的是,撤消操作是可以反复进行的。
  第四个判断语句:如果 ZT=True, 说明刚才进行了剪切或复制操作,可以开放“粘贴”菜单项。

  “光标至文末_Click”将当前光标移至文本末尾,这里也使用了SendMessage 函数的 177 号消息,但要注意第三第四个参数的运用。朋友们可能会问, 既然 177 号消息已经将光标移到了文本末尾,为什么还要使用 183 号消息呢?原来, 177 号消息虽然将光标移到了文本末尾,但文本框内的文本并没有向下滚动,所以还是看不到光标,183 号消息的作用就是将可见范围移至光标处。
   
  其它过程的代码就请朋友们自行分析吧。
  本代码已在 VB6中文企业版/Windows XP/2003 上通过。

回复列表 (共5个回复)

沙发

收藏了

板凳

学会了很多,谢谢楼主

3 楼

有些限制是因为用了比较小的变量吧.象LZ说的SelStart限制于34373,不象是变量的问题啊?

4 楼


我这里selstart最大65535
text好像没有最大值

5 楼


Private Sub Form_Load()
Dim fo As Long, s As String
s = Space("140000")
For fo = 1 To 140000
Mid$(s, fo, 1) = Chr(Asc("a") + Int(Rnd * 24))
Next
Text1 = s
End Sub

Private Sub Text1_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
Caption = Text1.SelStart & "&" & Text1.SelText & "&" & Text1.SelLength & "&" & Len(Text1)
End Sub





[img]http://blog.pfan.cn/upfile/200906/200906302306027.jpg[/img]

我来回复

您尚未登录,请登录后再回复。点此登录或注册