Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Monitor class events from userform

I have a userform which assembles itself at runtime, by looking in a folder and extracting all the pictures from it into image-controls on my form. What makes the process a little more complex is that I'm also using the image-controls' events to run some code.

As a simplified example - I have a form which creates a picture at runtime, the picture has an on-click event to clear its contents. To do this I have a custom class to represent the image object

In a blank userform called "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

In a class module called "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

The problem is, this doesn't display the changes, and I realised I needed a imgForm.Repaint in there somewhere - the question is, where?

Attempts

First option is to put it in the Init() sub of clsImg. (ie. have a line imgForm.Repaint at the end of the click event) That works, but not ideal as the class can then only be used with the userform of the correct name.


A 2nd idea was to pass the userform as an argument to 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

And called with

oneImg.Init Me

That works too, but would mean that wherever I require a repaint, I would have to pass the parameter which is also not ideal - the code is in reality a lot more complex than is shown here, so I don't want to have to add in this extra parameter unless necessary


The third option which I'm currently using is to pass the userform object to the class and save it there.

So with a Public myForm As UserForm at the top of my class module, I can pass the userform with the Init(uf As UserForm) and have a

Set myForm = uf 'Works with a private "myForm"/ class Property

Or I can set it directly from the userform code with a

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

But what does this do for memory - does saving the userform as a variable in my class take up a lot of memory? Bear in mind that in my real code I declare an array of clsImgs that can be of the order of >100 instances so I don't really want to be making copies of the UF in each class if that's what this method does. Also, it's ugly

What I really want...

... is a way of telling the userform that it needs to repaint, rather than directly repainting from within the class. To me this says I need an event to occur in my class, which the userform hears with some custom event handler. Exactly how Worksheet_Change works, the sheet object raises a change event, the sheet class code handles it.

Is such a thing possible (I suppose I would have to declare clsImg WithEvents - can you do that for an array?), or is there a better alternative. I'm looking for a method which does not impede performance with a large number of classes declared, as well as one which is portable and easily readable. This is my first use of Classes so I may be missing something really obvious!

like image 668
Greedo Avatar asked Jun 21 '17 09:06

Greedo


Video Answer


1 Answers

Since good practice is that classes are self-contained (as you obviously know) the clsImg should indeed not have to be aware of the UserForm and thus shouldn't tell the UserForm to repaint.

What this calls for, is indeed that the clsImg raises an event that the UserForm hooks into, so it repaints based on that event, or, in your own words: "a way of telling the userform that it needs to repaint."

I replicated your Custom Class (clsImg) as follows (wanted to use a proper Setter / Getter, functionality doesn't really change)

clsImg Code:

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

Next, in the UserForm we hook into this NeedToRepaint Event that's raised during the Event Handler of the MouseDown of the picture.

UserForm1 Code:

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

The second part of your question is whether you can use this in an array. The short answer is "no" - Each object would have to have it's own Event Handler. However, there are ways to work around this limitation by using a Collection or some similar approach. Still, this wrapper will have to be "UserForm aware" since that's where you'll be repainting. The approach would be something like in this article

EDIT: A solution / workaround for not being able to use an Array:

Since I really liked this question - Here's another approach. We can apply somewhat of a PubSub pattern as follows: I did a quick build for CommandButtons, but no reason that it can not be made for other classes of course.

Publisher class:

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

In a regular class, I setup a factory routine to keep this specific Publisher a singleton (as in: It will always be the very same in memory object that you're pointing at):

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

Next, we have the UserForm (I just made one with 4 buttons) and the button class to utilize this Publisher. The Userform will just subscribe to the event it raises: Userform code:

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

Last we have the cButton class, that centralizes the button events (through the array). Instead of handling each event individually, we just tell the publisher that an Event has been raised.:

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

With this approach we have one "Publisher" that can handle any event from specific classes that register the right event with it. You could also add image events, workbook events, etc. The publisher itself raises the events we need based on what gets passed to it. This way the UserForm can be agnostic of the button class and vice versa. Based on what is supported in VBA, I'm quite confident this is the cleanest approach for your scenario. If anyone has a better idea, I'd love to see another answer.

like image 102
Rik Sportel Avatar answered Sep 21 '22 03:09

Rik Sportel