I know we can detect if a key is pressed e.g. KeyAscii = 8, however I have only seen this done on events whilst the userform app is the window in operation.
Is it possible to have the window in the background e.g. whilst I am using IE, I can press CTRL + 0 and my app recognizes it and does an action?
At least to my own knowledge the answer is - it's not possible, but..
1. Things we need to know
Let's get some facts straight before we do any coding:
The excel application will not be able to execute any code while the Window object is in xlMinimzed state
So basically as soon as the user will close, minimize or even tab out of the Application, we no longer will be able to detect any OnKey (or any other) events, as the currently active application (ie. browser) takes precedence over the excel Application
hence CTRL+0 will now zoom out of the page as it's the browser's default behaviour.
With that being said, we can do at least some things to get close to at least some form of usefulness out of this.
2. The Application Layout
Since you have not provided specific application details, I created this mockup:
A Worksheet consisting of a single CommandButton, that launches the UserForm1
Private Sub CommandButton1_Click()
UserForm1.Show
End Sub
A Module named Module1 which contains a simple hello world message (for the OnKey event)
Public Sub hello()
MsgBox "Hello world!"
End Sub
And the UserForm1 which contains the code for our OnKey handling
Private Sub UserForm_Activate()
With ActiveWindow
.WindowState = xlNormal
' we need to change window state to xlNormal
' xlMaximized would result in width/height property changing error
' and with xlMinimized our Onkey would not work
.Width = 0
.Height = 0
End With
Range("A1").Select
' we need a range selected, otherwise Application OnKey will not fire
Application.OnKey "^0", "Module1.hello" ' launches hello world procedure
Me.Hide
End Sub
Now that's enough to have the window (almost) minimized and respond to a keypress
3. If we want to show the UserForm while maintaing functionality
Now let's say we want the UserForm while hiding the rest of the excel Application in the background.
In order to do this, we need to:
Change the UserForm to vbModeless
To do this, select the UserForm object and show properties (F4)
Remove the Me.Hide line from our UserForm1 code
Private Sub UserForm_Activate()
With ActiveWindow
.WindowState = xlNormal
.Width = 0
.Height = 0
End With
Range("A1").Select
Application.OnKey "^0", "Module1.hello"
' Me.Hide <- remove me
End Sub
That leaves us with the following fucntionality
If there are any suggestions for improvement / optimization I'd be
happy to know as this question intrigued me quite a bit.
I'll try to keep the answer updated!
Related
I have researched several questions and answers, but was unable to solve my problem.
I have a workbook that when it opens then a UserForm is shown, let's call it the main form.
On the main form there are several controls, one specific checkbox when clicked and it's value is changed to TRUE then it opens another UserForm, let's call this one the secondary form.
This secondary form is a simple password prompt form, and it's code name is pswdPrompt.
The main form code name is usf_TDM.
Now the code inside the pwsdPrompt is the following:
Option Explicit
Private Sub btn_input_Click()
Call passwordSubmit
Me.Hide
End Sub
Private Sub UserForm_Initialize()
With usf_TDM
Me.Top = .Top + .Height / 2
Me.Left = .Left + .Width / 2
End With
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
Call closePswdForm
End If
End Sub
The called subs passwordSubmit and closePswdForm are inside a module:
Public Sub passwordSubmit()
Application.enableEvents = False
With pswdPrompt
If .txt_password.Value = unblockPassword Then
usf_TDM.c_unblockForced.Value = True
Else
usf_TDM.c_unblockForced.Value = False
End If
.txt_password.Value = ""
End With
Application.enableEvents = True
End Sub
Public Sub closePswdForm()
pswdPrompt.Hide
End Sub
When I get to the pswdPrompt.Hide command, the secondary form dissapears BUT after inserting a breakpoint on the QueryClose event of the main form, I discovered that after the secondary form dissapears somehow the code continues to the QueryClose event of the main form even if no code called for the closure of the main form.
Furthermore, upon getting there the CloseMode variable of the event equals 1, which according to the documentation it means that "The Unload statement is invoked from code."
However at no point no Unload command was used.
So I'm unable to understand what is going on, and also this disrupt the purpose of the main form also.
Can anyone decipher what is going on here please?
UPDATE
After reading through the information, I have changed the code.
But before changing the code, I noticed that the problem I had dissapeared even though the default instances were being used, so I'm thinking it was some other context error.
I have however changed the code to use explicit instances of each user form.
I didn't use encapsulation nor composition as suggested on the rubberduck article because the software I wrote isn't that big and doesn't need to be scalable.
I find myself now with another problem, I have been searching for a way to identify each instance of each user form.
So far, and this is the wrong way to do it, I have been using the vba.Userforms with the index.
Is there another way to identify each instance?
Excel allows to start with a Modeless form and then display a Modal (but not the other way around)
I have an app with 4 Userforms : UF1 - Select a partner, UF2 - List existing transactions, UF21 - Display an existing transaction, UF22 - Make a new transaction. UF21 and UF22 both stem from UF2.
UF21 needs to be Modeless in order to display more than one transactions and compare side by side, therefore UF1, UF2 and UF21 are all Modeless. But I want UF22 to be Modal in order to issue one new transaction at a time.
My problem is that after I close UF22, even just ESCaping from the form right off the bat, all previous forms close. I should be able to return to UF2. If I make UF22 Modeless all is ok.
I have written a function to traverse the UserForms Collection and I am able to get a reference to the object of the Form I want to activate. So, I am able to return (in debug mode) to UF2 which is a listbox, activate the list box, but after the last pending statement both UF2 and UF1 close.
Is what I am trying to do impossible due to the nature of the Modal and Modeless forms or should I keep pushing for the correct code?
Since my original question is still open and my tested implementation of the proposed solution by #PeterT is not working properly, I include the code I have for the moment, based on #PeterT 's suggestion.
'===============
' Form UF1
'===============
Private Sub UserForm_Activate()
If ActivateUF22(FormID) = True Then Exit Sub
'.... more commands
End Sub
'============
' Form UF2
'============
Private Sub UserForm_Activate()
If ActivateUF22(FormID) = True Then Exit Sub
'.... more commands
End Sub
'----------------
Private Sub Cbn_OpenUF22_Click()
If ActivateUF22() = True Then
Exit Sub
Else
With New UF22
.Show vbModeless
End With
End If
End Sub
'================
' In a Module...
'================
Public Function ActivateUF22() As Boolean
Dim frm As Object
Set frm = GetFormFromID("UF22*") ' Custom function to get a form Object based on
' some criterion (FormID in a hidden TextBox)
If Not frm Is Nothing Then
' the only way I know to *Activate* an already .Show(n) form and compensate
' for the fact that the Close CommandButton may already have Focus
frm.TBx_UF22_CODE.SetFocus
frm.CBn_UF22_CLOSE.SetFocus
ActivateUF22 = True
Else
ActivateUF22 = False
End If
End Function
Well I finally managed to get the workaround to behave.
The remaining problem was the fact that clicking twice in a row on the same userform, besides the "Modal" one, would succeed and allow the user to break out.
I even tried the "AppActivate Application.caption" approach found in another SO thread but that didn't work either.
The only solution that works and does not bother me is to insert a MsgBox with a warning to the user, as such:
Public Function ActivateUF22() As Boolean
Dim frm As Object
Set frm = GetFormFromID("UF22*") ' Custom function to get a form Object based on
' some criterion (FormID in a hidden TextBox)
If Not frm Is Nothing Then
' the only way I know to *Activate* an already .Show(n) form and compensate
' for the fact that the Close CommandButton may already have Focus
frm.TBx_UF22_CODE.SetFocus
frm.CBn_UF22_CLOSE.SetFocus
ActivateUF22 = True
MsgBox("You cannot move away from this form until it is either completed or cancelled")
Else
ActivateUF22 = False
End If
End Function
Displaying the MsgBox does the trick internally, switches the focus to a different form from the one clicked and, upon return, the UserForm.Activate event fires normally and the ActivateUF22 function prevents the user from escaping the Pseudo-Modal form.
Thanks #PeterT for pointing me to a workaround. I managed to do what I set out to do, albeit in a different manner.
PS I still believe that there is a way to switch from a Modeless form to a Modal one. After all the MsgBox I use is obviously a Modal form and works just as I would like ;-)
Problem: I am building a Userform that has a 'Submit' and 'Cancel' button. I want the entire form to clear any entered data and close the form if the user hits the 'Cancel' button but I also am trying to build in the same functionality if the user hits the red 'X' in the top right corner. I'm unclear where I need to unload the form. I currently have it placed within the btnCancel_Click() method and I'm able to launch the form, enter some data and hit Cancel and it will close the form down.
But when I try to re-launch the form a 2nd time I get an error (I attached a picture of that message) that says
"Run-Time error '-2177418105 (80010007): Automation Error - The Callee (server [not server application]) is not available and disappeared; all connections are invalid. The call may have executed.
If I remove Unload Me from btnCancel_Click() then the form can close and re-open just fine, but any data I entered the first time will still be on the form and isn't cleared properly. I'm wondering if this is an Unload Me error or do I need to reset all form controls when I initialize the form?
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
' how was the form closed?
' vbFormControlMenu = X in corner of title bar
If CloseMode = vbFormControlMenu Then
' cancel normal X button behavior
Cancel = True
' run code for click of Cancel button
btnCancel_Click
End If
End Sub
'******************************************************************
Private Sub btnCancel_Click()
mbCancel = True
Me.Hide
Unload Me
End Sub
'*********************************************************************
Private Sub UserForm_Initialize()
'Populate values for 2 combo boxes
lastEmp = Sheets("Form_Ref").Cells(Rows.Count, 1).End(xlUp).Row
Me.cmbBoxEmpName.List = Sheets("Form_Ref").Range("A2:A" & lastEmp).Value
lastBld = Sheets("Form_Ref").Cells(Rows.Count, 2).End(xlUp).Row
Me.cmbBoxBuildingName.List = Sheets("Form_Ref").Range("B2:B" & lastBld).Value
End Sub
'******************************************************************
Public form As New CheckOutForm
Sub testFormOptions()
'Button pressed within Excel will start program and show the userform
form.Show
End Sub
This is the easiest quick and dirty solution:
Delete Public form As New CheckOutForm from the code. Then add it in the testFormOptions():
Sub testFormOptions()
Dim form As New CheckOutForm
form.Show
End Sub
Some not-that-good VBA books/tutorials would even go a bit like this, but this is brutal:
Sub testFormOptions()
CheckOutForm.Show
End Sub
Anyway, now the problem with the predefined values in the form is solved.
For the clean and not-so-easy solution, consider writing a MVC framework around the form:
https://codereview.stackexchange.com/questions/154401/handling-dialog-closure-in-a-vba-user-form
this blogpost (disclaimer - mine!), which pretty much says what the above link proposes, but it does not have the errors from the question.
the old StackOverflow tutorial for UserForms
If you execute Unload, you destroy the form object. With other words, your (global) variable form gets invalid and if you issue a new form.show, you get the runtime error.
When, on the other hand, you just unhide the form, the form-object stays valid (it's just currently not visible) and all controls keep their value.
Either you do some housekeeping by resetting all controls when a form is displayed (use the UserForm_Activate-event), or you have to create a new form-object every time you want to display it (I would strongly advice not to use the name form as variable name to avoid confusion).
Sub testFormOptions()
dim myForm as CheckOutForm
myForm.Show
End Sub
A button on a worksheet launches a macro that opens a userform (say, userform1). Userform1 is loaded non-modal in order for the user to use both userform1 and the worksheet (i.e., click cells) for input. There is a button on userform1 that, when clicked, opens another userform (say, userform2). Userform2 is modal. Clicking a Cancel button on userform2 unloads userform2 as it is supposed to; however, it, for some reason, also unloads userform1, which I do not want. If I make userform1 modal, then unloading userform2 does not unload userform1; however, the user can no longer use (i.e., click) the cells in the worksheet. I cannot find any info that will give me a clue as to why unloading one userform unloads both.
I am very happy I just stumbled upon this old thread.
What I discoverd is that the problem goes away when the VBA-editor window is closed. You really have to CLOSE it, minimizing the window is not enough. It also does not matter if the window is opend on the same screen or not. Only closing it did the trick for me. What I discoverd is that as soon as Form2 was unloaded, the VBA-editor shows Form2, no matter what other code-module I was just in.
I aimed to isolate the problem in a TestWorkbook.xlsm(review code below). I tried out the suggestions DoEvents and a line of code after Form2.Show and both helped. In my DevelopmentAddIn.xlam they did not help, the 1st form still closes. So the problem could still lay in the more complex code of my AddIn.
But like I said, closing the VBA-editor window does the trick, all though I still do not understand why.
TestWorkbook.xslm (two userforms Form1 and Form2 and a code module mLoad)
mLoad:
Option Explicit
Public Changed As Integer
'***Load the 1st form
Public Sub LoadFirstForm()
Load Form1
'allow user to change the active workbook
Form1.Show vbModeless
End Sub
'***Load a 2nd form (from Form1)
Public Sub LoadSecondForm()
Dim a As Integer
Load Form2
'continue after 2nd form closes
Form2.Show vbModal
'suggestions
a = 1
DoEvents
'2nd form was changed
If Changed = 1 Then
Form1.InfoBox.Value = "Changed"
'process changes
'2nd form is unchanged
Else
Form1.InfoBox.Value = "Unchanged"
End If
End Sub
Form1 (with button cmdLoad and textbox InfoBox)
'***Load the 2nd form (from Form1)
Private Sub cmdLoad_Click()
LoadSecondForm
End Sub
Form2: (with button cmdOK)
Option Explicit
'***Initial status is 'unchanged'
Private Sub UserForm_Initialize()
Changed = 0
End Sub
'***Status is 'changed'
Private Sub cmdOk_Click()
Changed = 1
'close 2nd form and continue
Unload Me
End Sub
UserForm2.Show
a = 1 ' only 1 more line of any code to execute
this will do the trick. At least this worked for me with the same issue...
this one may be impossible to solve in VBA but I'd like to see what you experts have to say about it.
I have a textbox on a userform that triggers a macro within a TextBox1_Change() type of sub.
If the user types "ABC" in the textbox, the macro gets triggered 3 times: once for "A", once for "AB" and once for "ABC". This macro is actually kind of heavy, so I would like it to run only when the user is actually done typing, and not inbetween single key strokes.
I know I can make the user "press enter" or whatever and only then run the macro, but this is not what I'm looking for. I want him to type freely and see the results of his typing dynamically show up, with no other type of interaction required.
So, I came up with the idea of making the change event wait and see if another change event gets triggered within, say, 1 second from the first. If that happens, the first change event aborts.
Now this would work, and I think I would know how to code it, except that I don't know how to give the user the power to keep typing even when the first change event is running.
What I mean is that when the first macro runs, it "freezes" everything. Waiting to see if another change event triggers will therefore not work, as nothing is going to trigger until the first macro is done running.
Do you guys see my problem here? How would you go about this? Any chance I can achieve the results I'd like?
Any help is greatly appreciated, thanks guys!
I tested the following, and it works (assuming I correctly understand what you're trying to do).
In a code module, write this:
Public aRunIsScheduled As Boolean
Public nextRunTime As Variant
Sub MyMacro()
'Flag macro as having been run, no longer scheduled.
aRunIsScheduled = False
'Place your macro code here.
'I'll just use some dummy code:
MsgBox "MyMacro is running!"
End Sub
In your sheet module:
Private Sub CommandButton1_Click()
If aRunIsScheduled Then
' Cancel the previously scheduled run.
Application.OnTime EarliestTime:=nextRunTime, _
Procedure:="MyMacro", Schedule:=False
aRunIsScheduled = False
End If
' Schedule a new run 3 seconds from now:
nextRunTime = Now + TimeValue("00:00:03")
Application.OnTime EarliestTime:=nextRunTime, _
Procedure:="MyMacro", Schedule:=True
aRunIsScheduled = True
End Sub
I put a Commandbutton in my sheet and here I'm using its change event, but you can put this code in your TextBox1_Change() event instead, in exactly the same way.
reference: http://www.cpearson.com/excel/SuppressChangeInForms.htm
To suppress events in a form, you can create a variable at the form's module level called "EnableEvents" and set that to False before changing a property that will cause an event to be raised.
Public EnableEvents As Boolean
Private Sub UserForm_Initialize()
Me.EnableEvents = True
End Sub
Sub Something()
Me.EnableEvents = False
' some code that would cause an event to run
Me.EnableEvents = True
End Sub
Then, all of the controls on form should have a test if that variable as their order of business in any event code. For example,
Private Sub ListBox1_Change()
If Me.EnableEvents = False Then
Exit Sub
End If
MsgBox "List Box Change"
End Sub
You can declare the EnableEvents as Private if only procedures with that form need to suppress events. However, if you have forms that are programmatically linked together, such UserForm2 adding an item to a ListBox on UserForm1, you should declare the variable as Public and set it for another form with code like the following:
UserForm1.EnableEvents = False
'
' change something on UserForm1
'
UserForm1.EnableEvents = True
The primary difference between the EnableEvents property and code shown above and the Application.EnableEvents property is that with a UserForm EnableEvents, all control on the form must have code to exit if EnableEvents is True. In other words, all the form's controls must cooperate and respect the setting of EnableEvents.