监视来自用户窗体的 class 个事件

Monitor class events from userform

我有一个用户窗体,它在 运行 时自行组装,方法是查看一个文件夹并将其中的所有图片提取到我窗体上的图像控件中。让这个过程稍微复杂一点的是,我还使用图像控件的事件来 运行 一些代码。

作为一个简化示例 - 我有一个在 运行 时间创建图片的表单,图片有一个单击事件来清除其内容。为此,我有一个自定义 class 来表示图像对象

在名为 "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"

的 class 模块中
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() 子目录中。 (即在点击事件的末尾有一行 imgForm.Repaint)这行得通,但并不理想,因为 class 只能与正确名称的用户表单一起使用。


第二个想法是将用户窗体作为参数传递给 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

这也行得通,但这意味着无论我需要重绘,我都必须传递同样不理想的参数 - 实际上代码比这里显示的要复杂得多,所以我不除非必要,否则不想添加这个额外的参数


我目前使用的第三个选项是将用户窗体对象传递给 class 并将其保存在那里。

所以在我的 class 模块顶部有一个 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

但这对内存有何影响 - 将用户表单作为变量保存在我的 class 中会占用大量内存吗?请记住,在我的真实代码中,我声明了一个 clsImgs 的数组,它可以是 >100 个实例的顺序,所以我真的不想在 each 中制作 UF 的副本class 如果这是此方法的作用。还有就是丑

我真正想要的...

... 是一种 告诉 需要重新绘制的用户窗体,而不是直接从 class 内部重新绘制。对我来说,这表示我需要在我的 class 中发生一个事件,用户表单 听到 带有一些自定义事件处理程序。 Worksheet_Change 的确切工作方式,sheet 对象引发更改事件,sheet class 代码处理它。

这样的事情可能吗(我想我必须声明 clsImg WithEvents - 你能为数组这样做吗?),或者有更好的选择。我正在寻找一种方法,该方法不会因声明大量 classes 而妨碍性能,并且可移植且易于阅读。这是我第一次使用 类,所以我可能遗漏了一些非常明显的东西!

我做了以下,如果你把控件作为控件传递,你可以使用父级。

以我的形式

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

在我的 class 中,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

的 class 中
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

因为好的做法是 classes 是独立的(你显然知道)clsImg 确实不必知道 UserForm 因此应该' t 告诉用户窗体重新绘制。

这确实需要 clsImg 引发用户窗体挂钩的事件,因此它会根据该事件重新绘制,或者用您自己的话来说:"a way of telling the userform that it needs to repaint."

我复制了你的自定义 Class (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

接下来,在用户窗体中,我们挂钩到在图片 MouseDown 的事件处理程序期间引发的 NeedToRepaint 事件。

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

你问题的第二部分是你是否可以在数组中使用它。 简短的回答是 "no" - 每个对象都必须有自己的事件处理程序。但是,有一些方法可以通过使用 Collection 或一些类似的方法来解决此限制。不过,这个包装器必须是 "UserForm aware",因为这是您要重新绘制的地方。该方法类似于 this article

编辑:无法使用数组的解决方案/解决方法:

因为我真的很喜欢这个问题 - 这是另一种方法。 我们可以应用某种 PubSub 模式,如下所示: 我为 CommandButtons 做了一个快速构建,但当然没有理由不能为其他 classes 制作它。

Publisher class:

Public Event ButtonClicked(value As cButton)
Public Sub RegisterButtonClickEvent(value As cButton)
    RaiseEvent ButtonClicked(value)
End Sub
'Add any other events + RegisterSubs.

在常规 class 中,我设置了一个工厂例程来使这个特定的 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

接下来,我们有用户窗体(我刚刚制作了一个带有 4 个按钮的窗体)和按钮 class 来使用此发布者。 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 class,它集中了按钮事件(通过数组)。我们不是单独处理每个事件,而是告诉发布者已引发一个事件。:

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

通过这种方法,我们有一个 "Publisher" 可以处理来自特定 class 的任何事件,这些事件向它注册了正确的事件。您还可以添加图像事件、工作簿事件等。 发布者本身根据传递给它的内容引发我们需要的事件。 这样,用户窗体就可以与按钮 class 无关,反之亦然。 基于 VBA 中支持的内容,我非常有信心这是适合您的场景的最简洁的方法。如果有人有更好的主意,我很乐意看到另一个答案。