回 帖 发 新 帖 刷新版面

主题:[原创]用户控件制作实例与讲解(上)

用户控件制作实例与讲解

  本次准备的实例共有17个,包括:三个时钟控件,两个标签控件,一个图片特技控件,一个混合四
则运算控件,四个进度条控件,一个消息框控件,一个按纽控件,一个选项卡控件,三个菜单控件。
  实例中有六个控件不是我的原创,但我都进行了重大更改(研究别人的代码也许比自己编写代码更
费时费力),所以我对它们至少应拥有30%的“股份”,呵呵。
  实例中的有些控件其实是不需要制作成控件的,在窗体代码中实现这些功能也许更简单,但笔者的
目的是:1.让初学者多做试验,尽快掌握制作用户控件的方法;2.以此说明,只要你愿意,窗体代码的
功能有很多是能够制作成用户控件的,不要把制作用户控件看成畏途,世上无难事,只要肯钻研。
  本讲解要是能解决你制作用户控件时的某些困惑,请给点鲜花和掌声,要是发现了讲解中的错误,
恳请指出而不要扔砖头和臭鸡蛋,一言为定呵!
  如果你从来没有制作过用户控件,请同时参阅我的旧贴《打造自己的多风格按纽--用户控件制作
详解》。
  好了,开场白道过,下面要言归正传了,还是老习惯,把编写代码的程序员称为中间用户,把使用
程序的用户称为最终用户。

        上篇

(附件中包括以下控件的工程文件:时钟控件、四则运算控件、标签控件、图片特技控件、按纽控件共
计8个控件)

  制作用户控件,主要就是进行以下三项代码编写工作:
1.定义控件的属性、事件和方法,其中属性是最常使用的。
2.保存和读取中间用户设置的属性值。
3.为达到你的预定目的而调用的各种技术手段。

  在用户控件中定义的属性、事件、方法,其性质都必须是公用的,也就是说,只有用 Public 来定
义,这样你才能在主程序代码中使用这些事件和方法,以及设置或获取这些属性值,也只有公用的属性
才会在窗体页面相关控件的属性窗口显示出来。

一、属性
  属性是用户控件最基本的东东,用户控件可以没有事件,可以没有方法,但不能没有属性(当然,
技术上来说是可以没有属性的,但这样的控件使中间用户无法进行任何设置,是没有什么意义的)。那
么,如何定义用户控件的属性呢?为用户控件添加属性有两种办法:

1.公用变量法:

public 变量名称 as 类型 

  这里的变量名称就是属性名称。这样定义的属性一般不会保存属性值,所以是只读属性,常用于对
主程序返回一个必要的值。例如“四则运算”控件中的“ComputeAnswer”属性:

Public ComputeAnswer As String

  它返回的是计算结果,而计算结果是不需要保存在控件中的,所以把它用公用变量法定义。再例如
菜单控件“MenuControl”中的“SelectedItem”属性:

Public SelectedItem As String

  它返回用户选中的菜单项的编号,这个编号也只需要在主程序中处理,而无需保存在控件中,所以
也用公用变量法定义成只读属性。


2.property 过程法:

public property Get 过程名称()  as 类型
……
end property 

public property Let 过程名称(new值 as 类型) 
……
end property 

  这里的过程名称就是属性名称。
  而 property 过程法又有两种:一种是如上所述的标准过程法,另一种就是枚举法。

  ㈠标准过程法
  这是用得最多的一种属性定义方法。在用户控件的代码页面选中“工具→添加过程”,会跳出一个
对话框,然后在单选按纽中选择“属性”,再在“名称”栏中输入属性名,点击确定,VB 就会自动生成
上述的几行代码,你将“类型”改为你所需要的,再输入相关代码即可。
  标准过程法中,Get 过程和 Let 过程一般是成对出现的。例如“四则运算”控件中定义文本框前景
颜色 ForeColor 属性的代码:

Public Property Get ForeColor() As OLE_COLOR
ForeColor = cForeColor
End Property

Public Property Let ForeColor(ByVal NewValue As OLE_COLOR)
cForeColor = NewValue
Text1.ForeColor = NewValue
PropertyChanged "ForeColor"
End Property

  这两段代码中的“OLE_COLOR”是颜色数据类型,实质上也是长整形的数据类型,但它会自动调出
颜色对话框。“ForeColor”是属性名称,在窗体界面相关控件的属性窗口中显示的就这个属性名称。而
“cForeColor ”是中间变量,中间变量是私用的,用 Dim 定义即可。在用过程法定义属性时通常都需
要中间变量。中间变量的身份是“代表”(代表属性名),作用有两个,一是上传下达,在 Get/Let 过
程与 ReadProperties/WriteProperties 过程中充当“邮递员”;二是参与,在许多别的过程中都要与
中间变量打交道。
  Get 过程的作用是获取相关的属性值,并将属性名称和属性值显示在属性窗口(如果你去掉这个过
程,在属性窗口就不会出现相关的属性名称和属性值了)。它在三种情况下被激活:①中间用户在窗体
页面刚刚把焦点移到窗体上的控件时(例如点击该控件),在属性窗口显示出原先设置的属性值;②中
间用户在属性窗口修改了属性值,在属性窗口显示出修改后的属性值;③程序运行中用代码获取属性值
时,假设代码有这么一句:RGB = Cipher.BackColor,那么也会激活该过程。
  Let 过程的作用是设置相关的属性值,它在两种情况下被激活:①中间用户在属性窗口修改了控件
的属性值(执行顺序是:Get 过程→Let 过程→Get 过程);②程序运行中用代码设置新的属性值时,
例如:Cipher.BackColor = RGB。变量 NewValue 是被赋的新值(这个变量名是可以改变的),你可以
把得到的 NewValue 的值按自己的需求作任何用途。
  要注意的是,如果这个属性是一个对象,那么就不能用 Let 过程而必须用 Set 过程了,这是因为
保存在控件内部的对象变量,保存的并不是对象的拷贝,而只是对象的引用(也就是一个内存地址)。
所以在它的 Get 和 Let 两个属性过程中,均须在等式的前面加上“Set”关键字。来看看“四则运算”
控件中的有关代码:

Public Property Get Font() As Font
Set Font = Text1.Font
End Property

Public Property Set Font(ByVal newFont As Font)
Set Text1.Font = newFont
PropertyChanged "Font"
End Property

  这是设置文本框的 Font 属性的,而 Font 本身也是一个对象,所以必须使用 Set 了。
  还有 Picture 属性也是如此,它也必须使用 Set 过程。然而,它这个对象却有一点特殊之处:如
果你想在程序运行当中使用 LoadPicture 语句动态加载图片的话,你就必须给它增加一个 Let 过程,
否则,你将只能在设计模式时在属性窗口加入图片。而新增的 Let 过程中不需要任何代码,只要一个注
释符就行了。以“酷时钟”控件中的 Picture 属性为例:

Public Property Get Picture() As Picture
Set Picture = UserControl.Picture
End Property

Public Property Set Picture(ByVal NewPic As Picture)
Set UserControl.Picture = NewPic
PropertyChanged "Picture"
End Property

Public Property Let Picture(ByVal NewPicture As Picture)
'
End Property


  ㈡枚举法
  枚举是一种很常见的的方式,它提供了一个下拉列表和若干选项让用户选择。这样既方便了用户的
操作,又不用考虑过多的兼容性和错误处理问题,简化了属性设置,而且更加安全。
  要实现枚举法,首先必须建立一个枚举结构,放在声明部分,然后在结构中给出一系列的常量和对
应的字符串。后面的常量值必须是比前面常量值大的整数。如果没有给出常量,那么 VB 会自动为其赋
值,第一个字符串赋值为零,其它的值则为前面一个数加一。例如在“特效标签”控件中,字体打印特
技的枚举结构声明:

Public Enum cTxtEffect
  雕刻  '自动赋值=0
  立体  '自动赋值=1
  浮雕  '自动赋值=2
End Enum

  如果你要为项目赋值为从 1 开始,也是可以的(当然有关代码要改一下):

Public Enum cTxtEffect
  雕刻 = 1
  立体 = 2
  浮雕 = 3
End Enum

  要实现枚举属性,还必须创建一个带有 Let 和 Get 属性过程的标准属性,但必须将属性的类型声
明为枚举类型。仍以“特效标签”控件为例,你还必须有这样两个过程:

Public Property Get TxtEffect() As cTxtEffect
TxtEffect = mTxtEffect
End Property

Public Property Let TxtEffect(ByVal NewValue As cTxtEffect)
mTxtEffect = NewValue
PropertyChanged "TxtEffect"
End Property

  注意这两个过程中的数据类型都改为了 cTxtEffect。
  枚举属性的读、写、保存和检索,都和标准属性是一样的。


3.保存或读取属性值
  上述的 Let 过程中的 PropertyChanged 方法是用户控件特有的方法,其作用是,通知系统某个属
性发生了改变,系统根据具体情况决定是否将改变后的属性值保存到属性包(或 .frm 文件)中。所谓
具体情况是指:在设计模式(正在被中间用户使用)就保存,在运行模式(正在被最终用户使用)就不
保存。比如你的程序代码中有这么一句:Cipher.BackColor = RGB,那么运行该程序到这一句时,就会
将 Cipher 控件的背景色改变为 RGB 所代表的颜色,但不会将这个 RGB 值 保存到属性包中,所以,下
次你运行程序时,只要没有运行到这一句,Cipher 控件的背景色依然是你设计时的颜色。
  保存属性值是由 WriteProperties 事件过程执行的,在销毁创建的控件之前,该事件会根据 Prop
ertyChanged 方法的提示,以及当时的运行模式,来决定是否通知 PropertyBag 对象把数据写入属性包
(或 .frm 文件)中。保存时,所有在该事件过程中的属性值都会同时保存,而不仅仅是保存某一个提示
改变的属性值。
  读取属性值是由 ReadProperties 事件过程执行的,在创建控件之前,该事件会通知 PropertyBag
对象从属性包(或 .frm 文件)中把保存的所有属性值都同时读取出来,读出的数据由 Get 过程使用。 
  PropertyBag 对象是具体实施保存或读取功能的,它有两个方法:WriteProperty 方法用来写属性
值,ReadProperty 方法用来读属性值。

  特别提醒:对于新手来说,一定要搞清楚 cTxtEffect、mTxtEffect、TxtEffect 和“TxtEffect”
这四个东东的意义:
  cTxtEffect:是结构名。
  mTxtEffect:是代表 cTxtEffect 结构中某个项目值的中间变量,如果没有枚举结构,则是代表属
性值的中间变量,它是模块级的变量。
  TxtEffect:是属性名,它会出现在窗体页面的属性窗口中。
  “TxtEffect”:是保存到属性包时所用的名称,我为了方便,把它与属性 TxtEffect 取了同一个
名称,但并不是同一个概念,它只出现以下在三个方法中:PropertyChanged 方法、ReadProperty 方法
以及 WriteProperty 方法中,你完全可以另外取个名,但在这三个方法中必须是完全同名的。你可以这
样理解:“TxtEffect”是一个文件名,而 mTxtEffect 则是一个变量,你要从“TxtEffect”中读出文
件内容并赋值给 mcTxtEffect,或者你要将赋了值的 mcTxtEffect 保存到“TxtEffect”去。

  还有一个需要注意的地方: 使用 ReadProperty/WriteProperty 方法读写数据时,被读写的变量不
能是数组。例如,在 MyMenu 菜单控件中,mCaption 是一维数组,但"sCaption"不可能是数组,所以你
不能这样编写代码:

For i = 1 To mItemSum: .WriteProperty "sCaption"(i), mCaption(i), "": Next

  而只能这样:
For i = 1 To mItemSum: .WriteProperty "sCaption" & i, mCaption(i), "": Next

  实际上就是:
.WriteProperty "sCaption1", mCaption(1), ""
.WriteProperty "sCaption2", mCaption(2), ""
  ……
.WriteProperty "sCaptionN", mCaption(N), ""  'N= mItemSum

  对于另一个菜单控件 muchMenu 控件来说,mCaption 是二维数组,所以只能这样编写:

For j = 1 To sRep
  For i = 1 To mItemSum(j)
    .WriteProperty "sCaption" & j * 10 & i, mCaption(j, i), ""
  Next
Next

  实际上就是:
.WriteProperty "sCaption101", mCaption(1, 1), ""
.WriteProperty "sCaption102", mCaption(1, 2), ""
  ……
.WriteProperty "sCaption10N", mCaption(1, N), ""  'N=mItemSum(j)
.WriteProperty "sCaption201", mCaption(2, 1), ""
.WriteProperty "sCaption202", mCaption(2, 2), ""
  ……
.WriteProperty "sCaption20N", mCaption(2, N), "" 
  ……
.WriteProperty "sCaptionM01", mCaption(M, 1), ""  'M=sRep
.WriteProperty "sCaptionM02", mCaption(M, 2), ""
  ……
.WriteProperty "sCaptionM0N", mCaption(M, N), "" 


4.只读属性
  前面已经说到,用公用变量法定义的属性是只读的,而且一般不论在设计模式还是运行模式都是只
读的。用标准过程法定义的属性也可以定义为在运行时只读的属性(当然也可以定义为在设计时只读或
者在设计和运行时都只读,不过那有什么意义呢?故我们不加讨论)。
  最简单的方法,就是不在 Let 或 Set 属性过程中加入任何代码,但通常这会带来诸多不便之处,
一般不宜采用。比较适宜的办法就是使用 AmbientProperties 对象。这个对象共有16个属性,都是用户
控件的环境信息。比如该对象的 DisplayName 属性就是取得控件的默认名称,中间用户将用户控件画到
窗体上时,系统就会自动为控件的 Caption 属性赋值这个默认名称。我们要实现运行时只读属性,要用
到的是该对象的 UserMode 属性。当控件处于运行模式时,UserMode=True,当控件处于设计模式时,
UserMode=False。我们在 Let 过程中对 UserMode 属性加以检测,就可以很容易地实现运行时的只读
属性了。选项卡控件中有一段代码:

Public Property Let Tabs(ByVal newVal As Integer)
If newVal > 2 And newVal < 9 And Ambient.UserMode = False Then '如果是设计模式
  If newVal = 5 Then newVal = 6
  If newVal = 7 Then newVal = 8
  propTabCount = newVal
  ReDim Preserve propCaption(1 To propTabCount) 
  PropertyChanged "Tabs"
  DrawTabs
End If
End Property

  Tabs 属性表示的是选项卡的按纽数目,按纽数目只允许在设计模式时修改,在运行模式时不允许修
改。从代码中可以看出,如果运行时企图修改 Tabs 属性是不可能的,换言之,该属性在运行时只读。


5.属性说明
  中间用户把用户控件画到窗体后,想在属性窗口查看它的属性说明,却发现只有简单的“Tabs”之
类的几个英文字符,就会弄得头大了。所以,我们有必要对属性加以描述。在用户控件页面,点击“工
具→过程属性”菜单项,这时会跳出一个对话框,我们在“名称”下拉框中选中需要说明的属性,在下
面的“描述”框中就可以输入对这个属性的说明文字了。你可以仿照微软控件的“返回/设置……”之类
的说明词加以描述,然后点“确定”就行了。再到窗体页面的属性窗口看看,呵呵,正是我们刚才输入
的那几个字!
  后面我还会讲到按纽控件的事件和方法,对于它们的描述也照此办理,不过事件和方法的描述要在
“对象浏览器”中才看得到。如果没有描述,“对象浏览器”中有关项目就只有“****工程的成员”的
简单说明,为了使中间用户明白你定义的属性、事件和方法的意义,我建议用汉字将所有的属性、事件
和方法都进行描述,免去中间用户翻译、猜测、反复试验的麻烦。


二、事件
  就象定义属性一样,我们首先要在用户控件页面代码窗口的 Option Explicit 节中声明要产生的
事件(注意声明事件也必须是公用的)。单击“工具→添加过程”,在弹出的对话框名称栏中输入事件
名称(例如“Click”),在“类型”单选按纽中选择“事件”,点击“确定”,于是声明节中就多了这
么一行:

Public Event Click()

  然后再输入对此事件的处理过程代码(以后你为控件添加的任何事件都必须有类似的代码):

Private Sub UserControl_Click()
RaiseEvent Click '触发Click事件
End Sub

  RaiseEvent的功能是把用户控件或其上的子控件的事件进行转发。上面的代码的意思是:当你单击
窗体上的用户控件时,VB 就转发出一个单击事件,这个事件发给谁呢?呵呵,当然是发给窗体代码页中
相关控件的 Click 事件啦,你再在这个 Click 事件过程中编写代码就行了。是不是很简单?
  要是你还想让这个事件携带参数,那也很容易实现,以 MyMenu 菜单控件为例:

Public Event Click(SelectedItem As Integer) '在声明节定义菜单项单击事件

Private Sub mLabel_Click(Index As Integer)
If left(mLabel(Index).Caption, 1) <> "-" Then RaiseEvent Click(Index) '转发单击事件
End Sub

  菜单项的文本是显示在标签上的,而标签是一个控件数组,mLabel_Click 过程代码中的 Index 是
选中的控件数组的编号,也就是菜单项的编号,这个号码必须返回给主程序中的相应变量,以便作进一
步的处理,所以这个编号作为 Click 的参数就被传送出去了。
  现在,我们在窗体的代码窗口上面的下拉框中找到该菜单控件的单击事件,点击一下,窗口中出现
了以下过程代码:

Private Sub MyMenu1_Click(SelectedItem As Integer)

End Sub

  看看,返回参数的变量名与在控件代码页声明节中定义的单击事件中的变量名完全一样。在这个过
程中,再输入对返回参数的处理代码,你可以用 X=SelectedItem 的句式获取其值,但最好采用下面这
样的代码:

Private Sub MyMenu1_Click(SelectedItem As Integer)
MyMenu1.Visible = False '使菜单控件不可见,这是必须的
Select Case SelectedItem
  Case 1: '去打开文件模块
  Case 2: '去保存文件模块
  .....
End Select
End Sub

回复列表 (共7个回复)

沙发

谢谢,加精以示鼓励。

板凳

[quote]1.公用变量法:

public 变量名称 as 类型 

  这里的变量名称就是属性名称。这样定义的属性一般不会保存属性值,所以是只读属性,常用于对
主程序返回一个必要的值。例如“四则运算”控件中的“ComputeAnswer”属性:

Public ComputeAnswer As String

  它返回的是计算结果,而计算结果是不需要保存在控件中的,所以把它用公用变量法定义。再例如
菜单控件“MenuControl”中的“SelectedItem”属性:

Public SelectedItem As String

  它返回用户选中的菜单项的编号,这个编号也只需要在主程序中处理,而无需保存在控件中,所以
也用公用变量法定义成只读属性。[/quote]
这个变量既然是Public,应该不是只读的吧?

3 楼

标记一下

4 楼

答2楼:确实是只读的。至于用Public来定义,是因为属性必须如此,要是用Private来定义,那就不是属性了。

5 楼

[code=c]Option Explicit

Public test As String

Private Sub UserControl_Initialize()
    test = "哈哈,你能改变我么"
End Sub
[/code]
[code=c]Option Explicit

Private Sub Form_Load()
    Debug.Print UserControl11.test
    UserControl11.test = "被改变啦,还是只读么?"
    Debug.Print UserControl11.test
End Sub
[/code]
[quote]哈哈,你能改变我么
被改变啦,还是只读么?[/quote]

6 楼

= =;
访问权限都没搞清楚?

7 楼

哈哈,我还真的无话可说了

我来回复

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