Differentiate between entering 0 and pressing cancel in application.inputbox - vba

I have a macro which takes a value from Application.InputBox and then tries to ensure that a numerical value has been put into it, and that cancel has not been pressed.
Sub test()
Dim testvalue As Variant
testvalue = Application.InputBox(Prompt:="Whatever", Title:="Whatever", Default:=10, Type:=1)
If testvalue <> False Then
MsgBox ("Test != False")
Else
MsgBox ("Test = False")
End If
If VarType(testvalue) = vbInteger Or VarType(testvalue) = vbLong Then
MsgBox ("Numerical value")
Else
MsgBox ("Nonnumerical value")
End If
End Sub
However, it seems to have trouble differentiating between having 0 put into the InputBox and cancel being pressed. In both cases, the if-statements will return that Test = False and that Test is a nonnumerical value. To be fair, the second if-statements does the same for other integers as well, but I thought there might be some small chance of differentiating between integers stored in the variant and a boolean value stored in it using the VarType method.
Is there any way to differentiate between 0 and cancel being the return-value for the inputbox, or will I have to make my own custom form?
Trying to use VBA's InputBox as suggested in the comments returns "String", no matter what value is entered into it, or what button is pressed:
Sub test2()
Dim testvalue As Variant
testvalue = InputBox("Test")
Select Case VarType(testvalue)
Case vbBoolean:
Debug.Print "Boolean"
Case vbInteger:
Debug.Print "Integer"
Case vbLong:
Debug.Print "Long"
Case vbString:
Debug.Print "String"
Case vbVariant:
Debug.Print "Variant"
End Select
End Sub

Apparently double is a numerical type as well >_> Chalk this one up to me being stupid, checking for the wrong thing, and making the wrong assumptions. Thanks to rory in the comments for getting me to check further on what types the various functions returned.
To be more explicit, when a numerical value is entered into Application.InputBox, it returns a Double, so that is what I should check for when wanting to ensure a numerical value.

Instead of VarType you can use StrPtr which will only give 0 if the input box was cancelled, although I'm not sure how it reacts to Application.InputBox(). If you are using just the standard InputBox() however it works perfectly, therefore to detect a cancelled InputBox you would add this into your code:
If StrPtr(testvalue) = 0 Then
Call MsgBox("Input cancelled.", "Cancelled")
End If
*Something to be aware of when using this method is that it relies on buggy behaviour in InputBox. Microsoft aren't going to fix it as it would break a lot of people's code but it is something to take note of.

You could adjust your type to 1+4 and check for the Vartype
Sub test()
Dim testvalue As Variant
testvalue = Application.InputBox(Prompt:="Whatever", Title:="Whatever", Default:=10, Type:=5)
If VarType(testvalue) <> vbBoolean Then
MsgBox ("Test != False")
Else
MsgBox ("Test = False")
End If
End Sub

Related

VBA Access Message Box woes

I followed a few simple comments on how to pop a confirmation box before executing a script, but sadly, if I press yes, the script doesn't run.
Private Sub Overwrite_Btn_Click()
If MsgBox("Yes?", vbOKCancel) = ok Then
Me.Product_Quantity = Me.Quantity_Input
Else
Exit Sub
End If
End Sub
I'm trying to set Product_Quantity equaling Quantity_Input, and although it works without the MsgBox command, it doesn't with it.
What am I doing wrong?
Instead of If MsgBox("Yes?", vbOKCancel) = ok Then try: If MsgBox("Yes?", vbOKCancel) = vbOK Then
Typically the interactions with forms will return one constant from a set of several constants. Those are catalogged in enums. In this case you have several constants in the VbMsgBoxResult class, and vbOK is a constant with value 1, which is returned from clicking the ok button.
Actually, If MsgBox("Yes?", vbOKCancel) = 1 Then would work as well, but it is harder to remember that clicking Ok returns 1 then simply stating a constant named vbOK
In object explorer (F2 on the VBE), searching for VbMsgBoxResult will give all possible results that comes from interacting with a message box.
https://www.techonthenet.com/access/constants/msgbox_ret.php
1) Dim a variable as integer.
2) Check for value of integer equal to 6, or check for vbYess
3) ?????
4) Profit
borrowed from link
Dim LResponse As Integer
LResponse = MsgBox("Do you wish to continue?", vbYesNo, "Continue")
If LResponse = vbYes Then
{...statements...}
Else
{...statements...}
End If
Single line:
If MsgBox("Yes?", vbOKCancel) <> vbOk then Exit Sub
'continue code here.
More Information:
MSDN : MsgBox Function (Office/VBA)

Advanced customization of InputBox edit control

So I have some basic VBA code:
Sub Test()
' Set error handler
On Error GoTo ErrorHandler
Dim strElevation As String
strElevation = InputBox("Enter elevation difference:", "Create Cross Lines", 0.5)
Exit Sub
ErrorHandler:
Call ReportError("Test")
End Sub
And it looks fine:
Is it possible to extend this so that the edit box will only allow a numeric value to 2 decimal places? Or is it simply too much work?
I know how to format text itself, eg: Format("1234.5678", "#.00"). But can the actual edit control have any customization itself?
You basically have three options here... In order of difficulty:
1. Validate the input
This uses the native InputBox() function as you have in your code sample above. You can return the value into a string variable, then do your validation at that point to make sure the data is formatted the way you want. If it doesn't pass, then display the input box again.
2. Custom VBA form
If you create your own VBA User Form, you can customize the text box to use a specific format, and perform the validation before the form accepts the input and closes. This is probably the most user-friendly approach, but involves a little more code than the first method.
Example:
Create sample VBA form with two input boxes and a command button. Name them txtDiff1, txtDiff2, and cmdOK respectively.
Double-click one of the controls, and add the following code to the code module behind the form:
Option Explicit
Private Sub cmdOK_Click()
MyElevationDifference = txtDiff1 ' (or txtDiff2)
Unload Me
End Sub
Private Sub txtDiff1_AfterUpdate()
Dim dblValue As Double
If IsNumeric(txtDiff1) Then
' Determine rounded amount
dblValue = Round(txtDiff1, 2)
' Automatically round the value
If dblValue <> CDbl(txtDiff1) Then txtDiff1 = dblValue
Else
MsgBox "Please enter a numeric value", vbExclamation
End If
End Sub
Private Sub txtDiff2_BeforeUpdate(ByVal Cancel As MSForms.ReturnBoolean)
Dim dblValue As Double
If IsNumeric(txtDiff2) Then
' Determine rounded amount
dblValue = Round(txtDiff2, 2)
' Require a max of 2 decimal places
If dblValue <> CDbl(txtDiff2) Then
Cancel = True
MsgBox "Please only use 2 decimal places", vbExclamation
End If
Else
MsgBox "Please enter a numeric value", vbExclamation
' Cancel change
Cancel = True
End If
End Sub
Paste the following into a regular code module. (This is how you can get the input in your main code through the custom form. Essentially the form assigns a value to the global variable, and you reference that after showing the form.)
Option Explicit
Public MyElevationDifference As Double
Public Sub GetElevationDifference()
UserForm1.Show
MsgBox "Elevation difference: " & MyElevationDifference, vbInformation
End Sub
Now when you run GetElevationDifference(), you will see a couple different approaches demonstrated on the user form. The first text box automatically rounds the input, while the second text box does not allow the user to continue unless they correct the input to use two decimal places or less.
Of course you will want to add some error handling and make the form look nice, but this gives you a simple example of how to use a VBA form to get user input. They involve a little more code, but obviously provide a huge level of additional flexibility over the simple InputBox() function.
3. Windows API calls
Just for completeness, there are ways to use Windows API calls to actually affect the controls on an input box, but this would end up being far more complex than the first two approaches, and I would not recommend it for something like this.
this is how you can restrict to input box to allow only numeric values:
strElevation = Application.InputBox(prompt:="Enter elevation difference:", Title:="Create Cross Lines", Default:=0.5, Type:=1)
https://msdn.microsoft.com/en-us/vba/excel-vba/articles/application-inputbox-method-excel
To validate the lenght, you can use the following code:
Do
strElevation = Application.InputBox(prompt:="Enter elevation difference:", Title:="Create Cross Lines", Default:=0.5, Type:=1)
If Len(strElevation) > 2 Then MsgBox "You typed in too many characters... 2 maximum!"
Loop While Len(strElevation) > 2
Private Sub TextBox1_AfterUpdate()
If InStr(1, Me.TextBox1.Value, ".") > 0 Then
If Len(Mid(Me.TextBox1.Value, _
InStr(1, Me.TextBox1.Value, "."), _
Len(Me.TextBox1.Value) - InStr(1, Me.TextBox1.Value, "."))) > 2 Then
Me.TextBox1.SetFocus
MsgBox "cannot have more than 2 decimal places"
End If
End If
End Sub
Apply to your situation but this gets you there
Sub Test()
' Set error handler
On Error GoTo ErrorHandler
Dim strElevation As String
strElevation = InputBox("Enter elevation difference:", "Create Cross Lines", 0.5)
If InStr(1, strElevation, ".") > 0 Then
If Len(Mid(strElevation, InStr(1, strElevation, "."), Len(strElevation) - InStr(1, strElevation, "."))) > 2 Then
MsgBox "cannot have more than 2 decimal places"
End If
End If
Exit Sub
ErrorHandler:
Call ReportError("Test")
End Subc

The weirdest VBA issue I have ever seen (VBASigned Possible Bug on Boolean Condition)

In MS Word I added some code to see if a document is missing its digital signature, or at least I thought I did. I've decided to share before testing this on other systems.
Sub test()
If Not ThisDocument.VBASigned Then
Debug.Print "I am NOT signed"
End If
End Sub
Problem: The code above produces the same result irrespective of whether or not the document has a digital signature. If I modify the code by removing the Not I still get unexpected results.
I've tried to coerce things by doing things like:
If Not CBool(ThisDocument.VBASigned) Then
But more surprisingly, the following code also fails:
Sub test()
Dim isSigned As Boolean
isSigned = ThisDocument.VBASigned
If Not isSigned Then
Debug.Print "I am NOT signed"
End If
End Sub
Even though ThisDocument.VBASigned AND isSigned are both TRUE...
but if change isSigned = ThisDocument.VBASigned to isSigned = True then everything works as expected.
Can anyone confirm this?
Any thoughts?
Edits below answer some of the questions:
Yes, using Option Explicit, Yes also tried Debug.
This code:
Option Explicit
Sub test()
Dim isSigned As Boolean
isSigned = ThisDocument.VBASigned
Debug.Print ThisDocument.VBASigned
If Not isSigned Then
Debug.Print "I am NOT signed"
End If
End Sub
Produces this output:
True
I am NOT signed
Testing: True * 0 - 1.
Sub test()
Dim isSigned As Boolean
isSigned = ThisDocument.VBASigned
Debug.Print ThisDocument.VBASigned
If Not (isSigned * 0 - 1) Then
Debug.Print "I am NOT signed"
End If
End Sub
Produces this (expected) output:
True
Edit: interesting article by Raymond Chen that might provide some further insights as to how this happened: https://blogs.msdn.microsoft.com/oldnewthing/20041222-00/?p=36923
In short, as the Windows operating system evolved it included different types of booleans: int > byte > variant
After playing with a digitally signed version of a document that SlowLearner supplied me, I have determined that Word's VBASigned is returning a 1 when it is signed.
This then leads to problems in the If statement, because Not 1 is equal to -2, and Not 0 is equal to -1 - thus leading to Not VBASigned returning a non-zero (i.e. non-False) value in all cases.
The MSDN documentation states that VBASigned is a read-only Boolean, and the type of variable returned has been confirmed (by TypeName(ThisDocument.VBASigned)) to be Boolean, but it appears it should be treated as a numeric value instead.
An additional interesting fact is that CBool(ThisDocument.VBASigned) * 1 gives an answer of 1, while a CBool(1) * 1 gives an answer of -1. So it seems that, when VBA decides that a value is already a Boolean (such as ThisDocument.VBASigned is meant to be), it doesn't bother to do any conversions. But, when the parameter to CBool is not a Boolean, it converts a non-zero value to be -1.
Code that would work:
Sub test()
Dim myVBASigned As Integer
Dim isSigned As Boolean
myVBASigned = ThisDocument.VBASigned 'Store as Integer
isSigned = myVBASigned 'Convert to a "true" Boolean
If Not isSigned Then 'Use the "true" Boolean
Debug.Print "I am NOT signed"
End If
End Sub
Testing this against Excel show that this is a bug in Word.
Assuming we have new documents with VBA enabled and VBA signed:
Using in Excel:
Sub testExcel()
Dim isSigned As Boolean
isSigned = ThisWorkbook.VBASigned
Debug.Print ThisWorkbook.VBASigned
If Not isSigned Then
Debug.Print "I am NOT signed"
Else
Debug.Print "I AM signed"
End If
End Sub
Results in
True
I AM signed
But using the same in Word
Sub testWord()
Dim isSigned As Boolean
isSigned = ThisDocument.VBASigned
Debug.Print ThisDocument.VBASigned
If Not isSigned Then
Debug.Print "I am NOT signed"
Else
Debug.Print "I AM signed"
End If
End Sub
Results in
True
I am NOT signed
This clearly shows that there is a bug in Word.
This was tested with
Windows 10 x64
Office Professional Plus 2016
Version 1703 Build 7967.2139
German
64 Bit Office

Edge cases in IsNumeric- is this overthinking it

I have code which looks like this:
Select Case IsNumeric(somevariable)
Case True
Resume Next
Case False
Call notnum
Else
Call MyErrorHandler
End Select
Is this overthinking it? Is there a chance IsNumeric will return something other than True or False here or is this bad programming practice?
Don't need the else as it will be true or false however, just a note the Else should be Case Else (moot point though as you are about to delete it)
Based on this though I wouldn't use a case for only 2 options:
If IsNumeric(somevariable) then
Resume Next
Else
Call MyErrorHandler
End if
Edit: Here is how error checking works:
Sub SheetError()
Dim MySheet As String
On Error GoTo ErrorCheck
MySheet = ActiveSheet.name
Sheets.Add
ActiveSheet.name = MySheet
MsgBox "I continued the code"
ActiveSheet.name = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
MsgBox "I will never get to here in the code"
End
ErrorCheck:
If Err.Description = "Cannot rename a sheet to the same name as another sheet, a referenced object library or a workbook referenced by Visual Basic." Then
Resume Next
Else
MsgBox "Error I am not designed to deal with"
End If
End Sub
Copy and paste this module to your personal workbook or to a new workbook and run it, step through line by line using F8 to see how it is actually dealing with the error.
From OP's comment I'm not using my error handler. I want to do stuff with the hopefully numeric output
Sub demo()
Dim inputs As Variant
inputs = InputBox("Prompt", "Title", "Default")
If Not IsNumeric(inputs) Then
notnum
Else
' Do what you want with numeric input inside the Else
End If
' Maybe do more stuff irrespective of input
End Sub
Sub notnum()
' do not numeric stuff here
End Sub
Or if you want to keep prompting for numeric input until the users gets it right or cancels
Sub demo2()
Dim inputs As Variant
Do
inputs = InputBox("Enter something Numeric", "Title", "Default")
Loop Until IsNumeric(inputs) Or inputs = vbNullString
If Not inputs = vbNullString Then
' Do wht you want with numeric input inside the Else
End If
' Maybe do more stuff irrespective of input
End Sub
Input box can have different type of input validation. Try this
something = Application.InputBox("Pls Insert the Number", Type:=1)
If something = False Then Exit Sub
'Type:=0 A formula
'Type:=1 A number
'Type:=2 Text (a string)
'Type:=4 A logical value (True or False)
'Type:=8 A cell reference, as a Range object
'Type:=16 An error value, such as #N/A
'Type:=64 An array of values

How to clear userform textbox without calling the _Change function?

I have a userform in Excel with textboxes meant for numeric data only. I want to clear the textbox when it detects bad entry and gives an error message, but I don't want to have the textbox's _Change function called again or else the message pops up twice because I change the text to "". I didn't see a built in clear function.. is there a better way to do this?
Private Sub txtbox1_Change()
txt = userform.txtbox1.Value
If Not IsNumeric(txt) Then
disp = MsgBox("Please only enter numeric values.", vbOKCancel, "Entry Error")
txtbox1.Text = ""
End If
End Sub
A simple way to achieve this is to use the _Exit() Function:
Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean)
If Not IsNumeric(TextBox1.Value) Then
MsgBox "Please only enter numeric values.", vbCritical, "Error"
End If
End Sub
This triggers as soon as the text box looses Focus.
prevent user from typing Alpha chars:
Private Sub TextBox1_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
Select Case KeyAscii
Case Asc("0") To Asc("9")
Case Asc("-")
If Instr(1,Me.TextBox1.Text,"-") > 0 Or Me.TextBox1.SelStart > 0 Then
KeyAscii = 0
End If
Case Asc(".")
If InStr(1, Me.TextBox1.Text, ".") > 0 Then
KeyAscii = 0
End If
Case Else
KeyAscii = 0
End Select
End Sub
Hope this helps!
-Hugues
You can do this way, as shown here
Private Sub TextBox1_Change()
OnlyNumbers
End Sub
Private Sub OnlyNumbers()
If TypeName(Me.ActiveControl) = "TextBox" Then
With Me.ActiveControl
If Not IsNumeric(.Value) And .Value <> vbNullString Then
MsgBox "Sorry, only numbers allowed"
.Value = vbNullString
End If
End With
End If
End Sub
You can add this line at the very beginning
sub txtbox1_Change()
If txtbox1.Text = "" Or txtbox1.Text = "-" Then Exit Sub '<~~~
Alternatively, I found this even shorter and interesting:
Private Sub txtbox1_Change()
If Not IsNumeric(txtbox1.Text & "0") Then
disp = MsgBox("Please only enter numeric values.", vbOKCancel, "Entry Error")
txtbox1.Text = ""
End If
End Sub
The interesting part is that it accepts to enter things like ".2", "-3.2", and also "5e3", the last case being not allowed by the other methods!
Turning it into a while loop can remove only the last bad typed character(s):
Private Sub txtbox1_Change()
t = txtbox1.Text
Do While t <> "" And Not IsNumeric(t) And Not IsNumeric(t & "0")
t = Mid(t, 1, Len(t) - 1)
Loop
txtbox1.Text = t
End Sub
Seems since there is nothing built in that can do what I want, this would be the simplest way to handle the problem:
Private Sub txtbox1_Change()
txt = userform.txtbox1.Value
If (Not IsNumeric(txt)) And (txt <> "") Then
disp = MsgBox("Please only enter numeric values.", vbOKCancel, "Entry Error")
txtbox1.Text = ""
End If
End Sub
Declare a global boolean and at the beginning of each sub, add an if statement which exits the sub if the boolean is true. When you get an error message, set the value to true, and nothing will happen. Then set it to false again.
Dim ufEventsDisabled As Boolean
Private Sub txtbox1_Change()
'repeat the following line everywhere that you don't want to update
if ufeventsdisabled then exit sub
txt = userform.txtbox1.Value
If Not IsNumeric(txt) Then
disp = MsgBox("Please only enter numeric values.", vbOKCancel, "Entry Error")
ufeventsdisabled = true
txtbox1.Text = ""
ufeventsdisabled = false
End If
End Sub
*Credit goes to mikerickson from mrexcel.com
You can't stop the _Changed event from firing. I would advise you to back up a couple of steps in your design and ask if you can get the job done without having to clear it in the first place. In FoxPro we would set the 'format' to 9999.99 and it would automatically prevent users from typing alpha characters, but I think that particular field was unique to FP. You can hook the _Changed event and perform your own validation there. I would suggest not filtering individual key strokes, but validating the whole value each time it's changed.
If Text1.Value <> str(val(Text1.Value)) Then
Text1.Value = previousValue
EndIf
... which will require keeping a backup variable for the previous value, but I'm sure you can figure that out. There may be certain edge cases where VB's string-number conversion functions don't exactly match, like exponential notation as you mentioned, so you may need a more sophisticated check than that. Anyway, this will make it impossible to even enter a bad value. It also provides a better user experience because the feedback is more immediate and intuitive. You may notice that the value is being changed inside the _Changed event, which should raise a knee jerk red flag in your mind about infinite loops. If you do this, make sure that your previous value has already been validated, keeping in mind that the initial value will be an empty string. As such, the recursive call will skip over the If block thus terminating the loop. In any case, what you would consider "better" may differ depending on who you ask, but hopefully I've given you some food for thought.