从用户窗体监视类事件

6
我有一个用户表单,它可以在运行时自动组装。它会查找文件夹并将其中的所有图片提取到我的表单上的图像控件中。这个过程变得更加复杂的地方是,我还使用了图像控件的事件来运行一些代码。
举个简单的例子 - 我有一个表单,在运行时动态创建一个图片,该图片具有清除其内容的“单击”事件。为此,我编写了一个自定义类来表示该图像对象。
在一个名为“imgForm”的空白用户表单中。
Dim oneImg As New clsImg 'our custom class

Private Sub UserForm_Initialize()
Set oneImg.myPic = Me.Controls.Add("Forms.Image.1") 'set some property of the class
oneImg.Init 'run some setup macro of the class
End Sub

在名为“clsImg”的类模块中
Public WithEvents myPic As MSForms.Image

Public Sub Init() 'can't put in Class_Initialise as it is called before the set statement - so myPic is still empty at that point
myPic.Picture = LoadPicture(path/image)
End Sub

Public Sub myPic_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
onePic.Picture = Nothing
End Sub

问题是,这不会显示更改,并且我意识到需要在某个地方加上imgForm.Repaint。问题是,在哪里加呢?
尝试一:将其放在clsImgInit()子程序中(即在单击事件的末尾加上一行)。 这可以工作,但并不理想,因为该类只能与正确名称的用户窗体一起使用。
第二个想法是将用户窗体作为参数传递给Init()
Public Sub Init(uf As UserForm) 'can't put in Class_Initialise as it is called before the set statement - so myPic is still empty at that point
myPic.Picture = LoadPicture(path/image)
uf.Repaint
End Sub

并称为

oneImg.Init Me

那种方法也可以,但这意味着无论在哪里需要重绘,我都必须传递参数,这也不理想——实际上,代码比这里展示的要复杂得多,所以我不想添加这个额外参数,除非必要。
第三个选项是我目前正在使用的,即将用户窗体对象传递到类中并在其中保存。
因此,通过在类模块的顶部使用 Public myForm As UserForm ,我可以使用 Init(uf As UserForm) 传递用户窗体并进行保存。
Set myForm = uf 'Works with a private "myForm"/ class Property

或者我可以直接在用户表单代码中设置它。

Set clsImg.myForm = Me 'only if "myForm" is Public

但这对于内存有何作用 - 将用户表单保存为我的类中的变量会占用大量内存吗?请记住,在我的实际代码中,我声明了一个可以超过100个实例的clsImg数组,因此如果该方法是在每个类中制作副本,我并不想这样做。另外,这很丑。

我真正想要的是...

...一种告诉用户表单需要重新绘制的方法,而不是直接从类中重新绘制。对我来说,这意味着我的类需要发生某些事件,而用户表单则通过一些自定义事件处理程序听到。就像Worksheet_Change一样,工作表对象引发一个更改事件,工作表类代码处理它。

是否可能实现这样的方法(我想我必须声明clsImg WithEvents - 你可以为数组这样做吗?),或者是否有更好的选择。我正在寻找一种不会影响性能的方法,可以声明大量类,并且易于移植和阅读。这是我第一次使用类,所以我可能会错过一些非常明显的东西!


1
如果您在初始化时将图像作为控件传递,则可以使用.parent属性获取表单,请参见下面的答案。 - Nathan_Sav
2个回答

5
由于良好的实践是类应该是自包含的(正如您所知),因此clsImg确实不必意识到UserForm,因此不应该告诉UserForm重新绘制。
这需要的是clsImg引发一个事件,而UserForm则钩入该事件,因此根据该事件重新绘制,或者用您自己的话说:“一种告诉userform它需要重新绘制的方法。”
我按照如下方式复制了您的自定义类(clsImg)(想使用适当的Setter / Getter,但实际上功能并没有改变) clsImg代码:
Private WithEvents myPic As MSForms.Image 'Because we need the click event.
Public Event NeedToRepaint() 'Because we need to raise an event that the UserForm can hook into.
Public Property Let picture(value As MSForms.Image)
    Set myPic = value
End Property
Public Property Get picture() As MSForms.Image
    Set picture = myPic
End Property
Public Sub myPic_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
    myPic.picture = Nothing
    RaiseEvent NeedToRepaint
End Sub

接下来,在用户窗体中,我们会钩入此NeedToRepaint事件,该事件在图片的MouseDown事件处理程序期间触发。

UserForm1代码:

Private WithEvents oneImg As clsImg 'Our custom class
Private Sub oneImg_NeedToRepaint() 'Handling the event of our custom class
    Me.Repaint
End Sub
Private Sub UserForm_Initialize()
    Dim tmpCtrl As MSForms.Image
    Set oneImg = New clsImg
    Set tmpCtrl = Me.Controls.Add("Forms.Image.1")
    tmpCtrl.picture = LoadPicture("C:\Path\image.jpg")
    oneImg.picture = tmpCtrl
End Sub

您的问题的第二部分是关于是否可以在数组中使用它。 简短的答案是“不行”-每个对象都必须有自己的事件处理程序。但是,通过使用集合或类似方法可以解决这个限制。仍然需要使该包装器“UserForm感知”,因为那里将重新绘制。方法类似于本文所述。 编辑:无法使用数组的解决方案/解决方法: 由于我真的很喜欢这个问题-以下是另一种方法。 我们可以应用某种PubSub模式,如下所示: 我快速构建了CommandButtons,但当然也可以制作其他类型的类。
Publisher类:
Public Event ButtonClicked(value As cButton)
Public Sub RegisterButtonClickEvent(value As cButton)
    RaiseEvent ButtonClicked(value)
End Sub
'Add any other events + RegisterSubs.

在普通的类中,我设置了一个工厂例程来保持这个特定的发布者(Publisher)是单例的(也就是说:它将始终是你指向的相同内存对象):
Private pub As Publisher
Public Function GetPublisher() As Publisher
    If pub Is Nothing Then
        Set pub = New Publisher
    End If
    Set GetPublisher = pub
End Function

接下来,我们有一个UserForm(我制作了一个带有4个按钮的UserForm)和一个按钮类,以利用此发布者。 Userform将仅订阅它所引发的事件:Userform 代码:

Private WithEvents pPub As Publisher 'Use the Publishers events.
Private button() As cButton 'Custom button array
Private Sub pPub_ButtonClicked(value As cButton) 'Hook into Published event.
    MsgBox value.button.Caption
End Sub
Private Sub UserForm_Initialize()
    Set pPub = GetPublisher 'Private publisher for getting it's event. Will be always the same object as long as you use "GetPublisher"

    Dim i As Integer
    Dim btn As MSForms.CommandButton

    'Create an array of the buttons:
    i = -1
    For Each btn In Me.Controls
        i = i + 1
        ReDim Preserve button(0 To i)
        Set button(i) = New cButton
            button(i).button = btn
    Next btn
End Sub

最后我们有 cButton 类,它通过数组集中了按钮事件。我们不需要逐个处理每个事件,只需告诉发布者已经触发了一个事件即可。
Private WithEvents btn As MSForms.CommandButton
Private pPub As Publisher
Public Event btnClicked()
Private Sub btn_Click()
    pPub.RegisterButtonClickEvent Me 'Pass the events to the publisher.
End Sub
Public Property Let button(value As MSForms.CommandButton)
    Set btn = value
End Property
Public Property Get button() As MSForms.CommandButton
    Set button = btn
End Property
Private Sub Class_Initialize()
    Set pPub = GetPublisher
End Sub

通过这种方式,我们有一个“发布者”,可以处理特定类别的任何事件,并向其注册正确事件。您还可以添加图像事件、工作簿事件等等。 发布者本身基于传递给它的内容触发所需的事件。 这样,用户表单可以忽略按钮类,反之亦然。 根据VBA支持的内容,我非常自信这是您场景中最干净的方法。如果有更好的想法,我很愿意看到其他答案。


抱歉,我有点困惑。你的主要回答内容非常清晰,感谢你提供关于Letting/Getting的提示。但正如你所强调的,这种方法对于未定义的数组长度是行不通的,因为每个数组都需要自己的硬编码事件处理程序 - 我理解得对吗?你提到了一种解决方法“使用集合或类似的方法”,但你提供的文章只是描述了一种将控件分配给类数组的方法。我已经拥有完全相同的东西(尽管是运行时创建的控件而不是现有的控件),但我不明白它与任何解决方法有什么关联? - Greedo
我猜你的意思是我创建另一个类,该类采用我的第一个类的WithEvents版本,并且第二个类包含重绘代码。这样,您可以将picture->firstclass和firstclass->second class分配,每个对象处理其下面的事件。这就是你的意思吗?因为我真正需要的是以某种方式收集第一类中的所有事件到单个事件中(例如具有paramArray输入的某些OR语句;我在此思考),然后将该集体事件传递给我的用户窗体。是否有可能做到这一点? - Greedo
1
不幸的是,这是不可能的。你可以调用一个常规模块代替触发事件,并在用户窗体中添加自定义类的数组UserForm1.Repaint。事实上,你可以很好地链式调用事件,但某个地方你仍需要为对象提供特定的处理程序。我们所需要的基本结构是 Private WithEvents images() as clsImg,但据我所知,VBA不支持此项功能。我会看看是否能找到更清晰的解决方案。 - Rik Sportel
@Greedo 请看我的编辑。我认为这是最干净的方法,也得到了VBA的支持。如果有其他想法 --> 我很乐意听取。 - Rik Sportel
1
“GetPublisher” 无法强制转换为“Publisher”类 - 类本身确实无法实例化。当然,您可以在工作簿中进行初始实例化,但是任何想要使用它的类仍然需要该工厂程序。至于在事件中传递按钮 - 在引发任何事件时,您可以自行决定要传递哪些变量(如果有) - 根据需要进行更改。拥有某个发布者的想法是一种设计模式,首先由3个类组成。在VBA中,您不真正具备执行全面的“Subscribe()”的能力。 - Rik Sportel
显示剩余3条评论

0
我做了以下操作,如果您将控件作为控件传递,您可以使用其父级。
在我的表单中。
Public c As Collection

Private Sub UserForm_Initialize()

Dim ctl As Control
Dim cls As clsCustomImage

    Set c = New Collection
    For Each ctl In Me.Controls
        If TypeName(ctl) = "Image" Then
            Set cls = New clsCustomImage
            cls.init  ctl
            c.Add cls, CStr(c.Count)
        End If
    Next ctl

End Sub

在我的类中,clsCustomImage

Private WithEvents i As MSForms.Image
Private frm As UserForm
public event evtRepaint
Public Sub init(c As control)
    Set frm = c.parent
    Set i = c
End Sub

Private Sub Class_Initialize()

End Sub

Private Sub Class_Terminate()
    Set frm = Nothing
    Set i = Nothing
End Sub

'

Private Sub i_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
    i.Picture = Nothing
    frm.Repaint
    raiseevent evtRepaint
End Sub

编辑

要拥有一个单一的处理程序,你需要看看一些类似于以下内容的东西,在一个名为clsHoldAndHandle的类中:

Private c As Collection
Private f As UserForm
Private WithEvents cls As clsCustomImage

Public Sub AddControl(ctl As Control)
    Set cls = new clsCustomImage
    If f Is Nothing Then Set f = ctl.Parent
    cls.init ctl
    c.Add cls, CStr(c.Count)
End Sub

Private Sub Class_Initialize()
    Set c = New Collection
End Sub

Private Sub cls_evtRepaint()
    f.Repaint
End Sub

实际上你仍然可以使用 MSForms.Image 而不仅仅是 control。然后无论何时当我想在我的类中说 imgForm.Repaint,我可以改为说 myPic.Parent.Repaint - 或者正如你所做的那样,在 Init() 子程序中将 myPic.Parent 保存为一个 Userform 对象。但是你删除的部分呢,关于声明公共事件?那可能是另一种方式吗 - 如果我有一个集合/数组的类都引发相同的公共事件,并且我的用户窗体包含它的事件处理程序?我该如何编写代码? - Greedo
1
使用事件时,需要调用相同的代码,因此您需要在repaint所在的位置使用raiseevent,并声明每个图像类的单个实例。或者有另一个类处理它,但是仍然需要.parent。 - Nathan_Sav
所以,根据我在Rik Sportel的回答中的评论,难道没有办法将所有事件收集到一个单一的事件中吗?是否有某种OR函数可以处理未定义数量的事件输入? - Greedo
你可以编写自己的集合类,并使用父类的一个实例,这样就有了一个包含集合和用户窗体的类,设置一次用户窗体,然后以这种方式调用,但似乎你需要一个控制自定义类的类。 - Nathan_Sav
请纠正我,但这只是监听从集合中添加的最后一个控件的事件吗?我认为这种方法有一些优势,它创建了一个接受参数的函数事件处理程序,但看起来相当复杂! - Greedo
嗨,是的,我放了类似的东西。我认为不会有你想要的解决方案。最好还是使用可行的解决方案 :( - Nathan_Sav

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接