Scenario
Create a blank workbook and add a ToggleButton and an InkPicture control (the latter inserted by means of ActiveX Controls -> More Controls). In the SizeChanged event of the InkPicture control, add the line:
Sheet1.ToggleButton1.Caption = "foo bar"
Problem
A "Method or data member not found" error in the line above that highlights the occurence of .ToggleButton1 from that line.
Reason
The ink picture's SizeChanged event happens before the toggle button is created, and in some cases, the event occurs before the workbook's Open event! You can Debug.Print relevant messages in the Immediate Window to see that.
Question
How can I postpone the execution of the ink picture's SizeChanged code until the toggle button is created and can be accessed as a property of the worksheet? I can, of course, workaround the issue by creating a Boolean variable to avoid the InkPicture's SizeChanged event's execution until the end of the workbook's Open one, but this is only paper over the cracks and doesn't solve the actual issue.
What I've tried
stopping the debugger, re-compiling the VBA project and saving it
afterwards
add DoEvents before the line above
set the toggle button's AutoLoad property to True, in an attempt
to create the toggle button before the ink picture
Every attempt appeared to work after saving and reopening the workbook, only to fail on subsequent saves and openings.
Note: I've edited my post to be more concise and easy to read, so try not to be overzealous and downvote a legitimate question...
Depending on which properties you need to access, you can try and grab a reference to your control via the Worksheet.OLEObjects collection.
Example:
Private Sub InkPicture1_Resize(Left As Long, Top As Long, Right As Long, Bottom As Long)
Dim oleObj As OLEObject
Set oleObj = Sheet1.OLEObjects("ToggleButton1")
oleObj.Left = 1
oleObj.Top = 1
'...
End Sub
In this way, you are interacting with the control through the OLEObject Interface. As such, some of the control properties may not be available. Normally, you can interact with the control through its specific control interface (ie, the ToggleButton interface) by grabbing the OLEObject.Object property.
Private Sub InkPicture1_Resize(Left As Long, Top As Long, Right As Long, Bottom As Long)
Dim tb As MSForms.ToggleButton
Set tb = Sheet1.OLEObjects("ToggleButton1").Object
tb.Caption = "foo"
End Sub
However, this seems to cause an error when the workbook is starting up. I believe it is a security thing. But, at least this error is a runtime that you can catch and handle, as oppose to the unhandlable Method or data member not found compiler error you are seeing right now.
Hope this helps!
Related
I have a bunch of TextBox-Button pairs on a form. When the button is clicked I want to insert the value of the text box into a database. The name TextBoxes and Buttons follow a naming standard, for example Value1Tb - Value1Cmd and Value2Tb - Value2Cmd.
My problem is that since I want to do the same for every button I would like the possibility to write a Sub like:
Private Sub AnyButton_Click(sender As CommandButton)
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
End Sub
But I cannot find a way to point the Click-event of a Button to a different sub than the standard Name_Click().
Anybody know a way around this, that doesn't involve me writing 50 or so different Name_Click() subs?
If you are OK to use Form Controls rather that ActiveX, as it looks as though you may be at the moment, then Chris' solution seems good.
However if you need ActiveX CommandButtons then you are unable (as the VBA compiler will tell you, "Procedure declaration does not match...") to have parameters in the callback for the click event, and you are unable to raise the event from multiple objects, although you do of course know which button raised the event (since the relationship is 1 CommandButton = 1 Sub).
So... I would go with something like:
Private Sub Value1Cmd_Click()
Call TheMethod(Value1Cmd)
End Sub
Private Sub Value2Cmd_Click()
Call TheMethod(Value2Cmd)
End Sub
Private Sub TheRealMethod(sender As CommandButton)
' Do your thing '
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
' Etcetera... '
End Sub
Requires a stub for each button, so some copying and pasting to begin with, but then easy to maintain etcetera as all _Click event callbacks are pointing at the same method...
Edit:
E.g.
Sub AutoWriteTheStubs()
Dim theStubs As String
Dim i As Long
For i = 1 To 10
theStubs = theStubs & "Private Sub Value" & CStr(i) & "Cmd_Click()" & vbCrLf _
& " Call TheMethod(Value" & CStr(i) & "Cmd)" & vbCrLf _
& "End Sub" & vbCrLf & vbCrLf
Next i
Debug.Print theStubs
End Sub
It seems that what you want is to get the name of the clicked button. If you are creating buttons like this:
(where 'i' increments in a loop)
Set btn = Sheet1.Buttons.Add( , , , ,)
With btn
.OnAction = "btnSub"
.Caption = "Upadate"
.Name = "btn" & CStr(i) & "Cmd"
End With
and then defining a generic "private sub btnSub()" for all the buttons, you could at least get the name of the button that was clicked using Application.Caller. Something like:
Private Sub btnSub()
Dim ButtonName As String
ButtonName = Application.Caller
MsgBox ("Hello:" & ButtonName)
End Sub
Hope it helps!
I decided to make this an answer because I am doing something similar and I confirmed that it works.
You can store the OLEobjects in a Collection, of arbitrary size, containing Custom Class Objects that include the OLEobjects and associations and the events that you need. Thus you can completely avoid any code stubs.
Create a Custom Class to bind the Button and TextBox pairs.
Declare the Button object WithEvents.
Include your call-back in the exposed button event handler in the Class Module.
Put a Public routine in a Standard Module to initialise a Collection of these Custom Class objects by spanning the Form Controls. You can also use this to Add the controls programmatically as a 'reBuild' option. The Collection can be inside another Class Module with all of the management routines, but it needs to be Instantiated and loaded in a Standard Module.
Put a public routine in a standard module to receive the call-backs with whatever context you need. This can also be in a Worksheet Module if it makes for better encapsulation. You can use late binding to reference the callback or CallByName.
You need to bear in mind that the Module of the Form will recompile every time you add a control, so you have to be careful where you put your code.
My application has the controls directly on the Worksheet Surface, so I can't put the the Collection Class in, or source any initialisation of the Collection from the Worksheet module. This would amount to self modifying code and it grinds excel to a halt.
I dreamed this idea up through bloody-minded idealism (not necessarily a good thing) but, of course, I was not the first one to think of it as you can see here. #Tim Williams explains it in his answer. You can also google VBA Control Array Events to see plenty of similar examples including an excellent article by #SiddharthRout. In line with the VB6 analogy, he uses an Array instead of a Collection to achieve the same result.
I'll try to post some code later. My application is a bit different so it will take a lot of work to trim it down, but the principle is the same.
The other thing to bear in mind is that VBE really struggles with this type of thing so don't worry if it is loading up you processors. After you re-start with VBE off, all will be fine.
I have this same situation, and I just have a click event for every button that is a wrapper to the function I want to call. This also allows you to pass sheet-specific parameters if you need to.
Example:
Public Sub StoreButton_Click()
' Store values for transaction sheet 3/27/09 ljr
Call StoreTransValues(ActiveSheet)
End Sub
I just published (Open Source) the Event Centralizer for MSForms.
Citation: "The Event Centralizer for MSForms is a VBA programming tool that allows all sorts of custom grouping when writing handlers for the events occurring in UserForms.
With the Event Centralizer for MSForms, it is easy for example to have all TextBoxes react the same way when the Enter event occurs, or all except one, or only those with a particular Tag value.
Thanks to its events logs system, the Event Centralizer for MSForms is a powerful learning and debugging help."
I can't explain here how it works. I tried to do it on the site.
Set the event to =FunctionName(parameter).
A bit late but this may help someone else:
If you have a function called OpenDocumentById(docID as integer), then each control calling the function would have in the event the following:
cmd1's On Click event:
=OpenDocumentById([DocID])
cmd2's On Click event:
=OpenDocumentById([DocID])
etc...
I have a progress using a userform. I show the userform when a worksheet button is clicked. The button calls startProgressIndicator()
Sub startProgressIndicator()
UserForm1.Show
End Sub
I have this as my userform code:
Private Sub UserForm_Activate()
Call myCode
End Sub
I then want to hide the progress bar and ask for user input. This occurs in myCode. I inlcude UserForm1.Hide in beginning of myCode.
After getting the user input, I want to show the progress indicator again.
I try UserForm1.Show, however, this just calls myCode all over again. I just want the same userform to be visible again.
I tried using UserForm1.Visible = False, but then I get this error
Function or interface marked as restricted, or the function uses an
Automation type not supported in Visual Basic
Short answer is to rewrite myCode to not include .Hide. Break myCode into logical chunks.
However, you should separate the logic from the display (see Rubberduck UserForm1.Show (*)). By doing so - you would only call .Hide from the form when you want to (e.g. on the 'Close' button click).
#ChrisNeilsen suggested using _Initialize and this will solve the immediate problem but will not set you up for better programming practices in the future. lso, if you decide to modify myCode you may get bugs that are harder to identify.
#ChrisNeilsen: Use 'UserForm_Initialize' rather than 'UserForm_Activate'
(*) No disclaimer required, I am in no way affiliated with Rubberduck, but it does make good reading!
Are there disadvantages in putting code into a VBA Userform instead of into a "normal" module?
This might be a simple question but I have not found a conclusive answer to it while searching the web and stackoverflow.
Background: I am developing a Front-End Application of a database in Excel-VBA. To select different filters I have different userforms. I ask what general program design is better: (1) putting the control structure into a separate module OR (2) putting the code for the next userform or action in the userform.
Lets make an example. I have a Active-X Button which triggers my filters and my forms.
Variant1: Modules
In the CommandButton:
Private Sub CommandButton1_Click()
call UserInterfaceControlModule
End Sub
In the Module:
Sub UserInterfaceControllModule()
Dim decisionInput1 As Boolean
Dim decisionInput2 As Boolean
UserForm1.Show
decisionInput1 = UserForm1.decision
If decisionInput1 Then
UserForm2.Show
Else
UserForm3.Show
End If
End Sub
In Variant 1 the control structure is in a normal module. And decisions about which userform to show next are separated from the userform. Any information needed to decide about which userform to show next has to be pulled from the userform.
Variant2: Userform
In the CommadButton:
Private Sub CommandButton1_Click()
UserForm1.Show
End Sub
In Userform1:
Private Sub ToUserform2_Click()
UserForm2.Show
UserForm1.Hide
End Sub
Private Sub UserForm_Click()
UserForm2.Show
UserForm1.Hide
End Sub
In Variant 2 the control structure is directly in the userforms and each userform has the instructions about what comes after it.
I have started development using method 2. If this was a mistake and there are some serious drawbacks to this method I want to know it rather sooner than later.
Disclaimer I wrote the article Victor K linked to. I own that blog, and manage the open-source VBIDE add-in project it's for.
Neither of your alternatives are ideal. Back to basics.
To select different filters I have differnt (sic) userforms.
Your specifications demand that the user needs to be able to select different filters, and you chose to implement a UI for it using a UserForm. So far, so good... and it's all downhill from there.
Making the form responsible for anything other than presentation concerns is a common mistake, and it has a name: it's the Smart UI [anti-]pattern, and the problem with it is that it doesn't scale. It's great for prototyping (i.e. make a quick thing that "works" - note the scare quotes), not so much for anything that needs to be maintained over years.
You've probably seen these forms, with 160 controls, 217 event handlers, and 3 private procedures closing in on 2000 lines of code each: that's how badly Smart UI scales, and it's the only possible outcome down that road.
You see, a UserForm is a class module: it defines the blueprint of an object. Objects usually want to be instantiated, but then someone had the genius idea of granting all instances of MSForms.UserForm a predeclared ID, which in COM terms means you basically get a global object for free.
Great! No? No.
UserForm1.Show
decisionInput1 = UserForm1.decision
If decisionInput1 Then
UserForm2.Show
Else
UserForm3.Show
End If
What happens if UserForm1 is "X'd-out"? Or if UserForm1 is Unloaded? If the form isn't handling its QueryClose event, the object is destroyed - but because that's the default instance, VBA automatically/silently creates a new one for you, just before your code reads UserForm1.decision - as a result you get whatever the initial global state is for UserForm1.decision.
If it wasn't a default instance, and QueryClose wasn't handled, then accessing the .decision member of a destroyed object would give you the classic run-time error 91 for accessing a null object reference.
UserForm2.Show and UserForm3.Show both do the same thing: fire-and-forget - whatever happens happens, and to find out exactly what that consists of, you need to dig it up in the forms' respective code-behind.
In other words, the forms are running the show. They're responsible for collecting the data, presenting that data, collecting user input, and doing whatever work needs to be done with it. That's why it's called "Smart UI": the UI knows everything.
There's a better way. MSForms is the COM ancestor of .NET's WinForms UI framework, and what the ancestor has in common with its .NET successor, is that it works particularly well with the famous Model-View-Presenter (MVP) pattern.
The Model
That's your data. Essentially, it's what your application logic need to know out of the form.
UserForm1.decision let's go with that.
Add a new class, call it, say, FilterModel. Should be a very simple class:
Option Explicit
Private Type TModel
SelectedFilter As String
End Type
Private this As TModel
Public Property Get SelectedFilter() As String
SelectedFilter = this.SelectedFilter
End Property
Public Property Let SelectedFilter(ByVal value As String)
this.SelectedFilter = value
End Property
Public Function IsValid() As Boolean
IsValid = this.SelectedFilter <> vbNullString
End Function
That's really all we need: a class to encapsulate the form's data. The class can be responsible for some validation logic, or whatever - but it doesn't collect the data, it doesn't present it to the user, and it doesn't consume it either. It is the data.
Here there's only 1 property, but you could have many more: think one field on the form => one property.
The model is also what the form needs to know from the application logic. For example if the form needs a drop-down that displays a number of possible selections, the model would be the object exposing them.
The View
That's your form. It's responsible for knowing about controls, writing to and reading from the model, and... that's all. We're looking at a dialog here: we bring it up, user fills it up, closes it, and the program acts upon it - the form itself doesn't do anything with the data it collects. The model might validate it, the form might decide to disable its Ok button until the model says its data is valid and good to go, but under no circumstances a UserForm reads or writes from a worksheet, a database, a file, a URL, or anything.
The form's code-behind is dead simple: it wires up the UI with the model instance, and enables/disables its buttons as needed.
The important things to remember:
Hide, don't Unload: the view is an object, and objects don't self-destruct.
NEVER refer to the form's default instance.
Always handle QueryClose, again, to avoid a self-destructing object ("X-ing out" of the form would otherwise destroy the instance).
In this case the code-behind might look like this:
Option Explicit
Private Type TView
Model As FilterModel
IsCancelled As Boolean
End Type
Private this As TView
Public Property Get Model() As FilterModel
Set Model = this.Model
End Property
Public Property Set Model(ByVal value As FilterModel)
Set this.Model = value
Validate
End Property
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Private Sub TextBox1_Change()
this.Model.SelectedFilter = TextBox1.Text
Validate
End Sub
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub Validate()
OkButton.Enabled = this.Model.IsValid
End Sub
Private Sub CancelButton_Click()
OnCancel
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
OnCancel
End If
End Sub
Private Sub OnCancel()
this.IsCancelled = True
Me.Hide
End Sub
That's literally all the form does. It isn't responsible for knowing where the data comes from or what to do with it.
The Presenter
That's the "glue" object that connects the dots.
Option Explicit
Public Sub DoSomething()
Dim m As FilterModel
Set m = New FilterModel
With New FilterForm
Set .Model = m 'set the model
.Show 'display the dialog
If Not .IsCancelled Then 'how was it closed?
'consume the data
Debug.Print m.SelectedFilter
End If
End With
End Sub
If the data in the model needed to come from a database, or some worksheet, it uses a class instance (yes, another object!) that's responsible for doing just that.
The calling code could be your ActiveX button's click handler, New-ing up the presenter and calling its DoSomething method.
This isn't everything there is to know about OOP in VBA (I didn't even mention interfaces, polymorphism, test stubs and unit testing), but if you want objectively scalable code, you'll want to go down the MVP rabbit hole and explore the possibilities truly object-oriented code bring to VBA.
TL;DR:
Code ("business logic") simply doesn't belong in forms' code-behind, in any code base that means to scale and be maintained across several years.
In "variant 1" the code is hard to follow because you're jumping between modules and the presentation concerns are mixed with the application logic: it's not the form's job to know what other form to show given button A or button B was pressed. Instead it should let the presenter know what the user means to do, and act accordingly.
In "variant 2" the code is hard to follow because everything is hidden in userforms' code-behind: we don't know what the application logic is unless we dig into that code, which now purposely mixes presentation and business logic concerns. That is exactly what the "Smart UI" anti-pattern does.
In other words variant 1 is slightly better than variant 2, because at least the logic isn't in the code-behind, but it's still a "Smart UI" because it's running the show instead of telling its caller what's happening.
In both cases, coding against the forms' default instances is harmful, because it puts state in global scope (anyone can access the default instances and do anything to its state, from anywhere in the code).
Treat forms like the objects they are: instantiate them!
In both cases, because the form's code is tightly coupled with the application logic and intertwined with presentation concerns, it's completely impossible to write a single unit test that covers even one single aspect of what's going on. With the MVP pattern, you can completely decouple the components, abstract them behind interfaces, isolate responsibilities, and write dozens of automated unit tests that cover every single piece of functionality and document exactly what the specifications are - without writing a single bit of documentation: the code becomes its own documentation.
When a user presses the ESC key, I need to display a prompt.
How can I do this in Lotus Notes client?
Can you elaborate? Is this for one application, one form or the whole Lotus client? Why would you want to disable the esc key?
In the Queryclose event you can get a handle to the close event. Continue = false will prevent the form from closing:
Sub Queryclose(Source As Notesuidocument, Continue As Variant)
msgbox "the message"
End Sub
** The original question changed (by a moderator) from it's original intent, which asked to detect AND ignore ESC key press in Lotus Notes. **
You need to use the "QueryClose" event (as others have mentioned), but how do you identify a "legitimate" way to close the form ? We need some logic to distinguish between someone actually clicking a button to "legitimately" close the form, and if someone hits the escape key or the "X" button in the window bar.
So, you need to use 2 form events, and an action button to do this.
In the QueryOpen event of the form
Sub Queryopen(Source As Notesuidocument, Mode As Integer, Isnewdoc As Variant, Continue As Variant)
Dim session As New NotesSession
Call session.SetEnvironmentVar("CloseDoc",0,True)
End Sub
Your QueryClose event needs to look like this
Sub Queryclose(Source As Notesuidocument, Continue As Variant)
Dim session As New NotesSession
Dim iCloseDoc As Integer
iCloseDoc = session.GetEnvironmentValue("CloseDoc", True)
If iCloseDoc <> 1 Then
continue = False
End If
End Sub
And you need to have an action button called "Close", on the form with this in it.
#SetEnvironment("CloseDoc";"1");
#PostedCommand([FileCloseWindow])
The LotusScript alternative looks like this
Sub Click(Source As Button)
Dim ws As New notesUIWorkspace
Dim session As New NotesSession
Call session.SetEnvironmentVar("CloseDoc",1,True)
Call ws.CurrentDocument.Close
End Sub
Now what's going on ? When you open the form, I set an environment variable for "CloseDoc", (QueryOpen event). If the user hits the "ESC" key or even clicks the "X" on the form to close the window the QueryClose event triggers.
When a request to close the form is detected, the QueryClose event then runs. I retrieve the "CloseDoc" environment variable which, in this case is still 0 (zero). If it's not equal to 1 then the close form will be aborted. You can add a messagebox there if you like.
Now the only way for the user to successfully close the form is for them to press the "Close" button. Which first sets the CloseDoc environment variable to 1, then call NotesUIDoc.close(). Although I am not a big fan of environment variables, it is handy in the case as you are able to record user activity without modifying the current document or another document. (That would be even messier, because you would have to clean up temporary documents, or you are forced to make changes to the document which won't work if the user only has reader access to the database or the current document).
This is relatively clean as the control is managed via a discrete envrionment variable that adds no overhead to the database performance and will not get in the way of any other functionality of applications. Some suggestions in the comments advises the use of global variables instead of environment variables and this quite is appropriate if you're using LotusScript exclusively, but if you have a mixture of formula and lotusScript, environment variables are common to both. But it's upto the developer to determine which.
This is one way I have over the years forced the user to click a specific button to close the document, and ignore "ESC" keys or the "X" button int he form window bar without any annoying messages and is free of performance issues.
This is a vba question. I found the question:
"Adding controls to a frame in an Excel userform with VBA"
and used its method to add commandbuttons to my frame in my user form. Since I added four commandbuttons, I ran the code in a loop, and used the
With ...
.Name = "commandbutton" & x
to give each command button its own name. I included in my code macros for each commandbutton (since I renamed them I know what the name of each cb is) but when I click on the button, nothing happens.
Alternatively, if someone could explain to me how use code to place controls on a form into a frame, I could solve my problem a different way.
Thanks,
Michael
It seems that events are only handled for object variables which were declared when the VBA program is compiled (i.e. at design time.) If you dynamically create objects in the program (i.e. at run-time) then you need to assign references to those objects to appropriate object variables which have already been declared.
As per your example, in the module which relates to your UserForm, you would need to have:
Public WithEvents btn1 As MSForms.CommandButton
Public WithEvents btn2 As MSForms.CommandButton
Public WithEvents btn3 As MSForms.CommandButton
Public WithEvents btn4 As MSForms.CommandButton
(The MSForms prefix to the variable type might not be strictly necessary)
then in your With statement to create the controls you would need to assign each control to one of the variables which ends up being fairly messy.
Better solutions:
create the maximum number of possible buttons at design time and just toggle the Visible property to show the correct ones
if the function of each button would be determined at run-time then use some property of the CommandButton object to determine this (e.g. Caption property, Tag property) and then have the event handler for that button call the appropriate function based on that property
change the UI and use a ListBox or ComboBox instead of buttons