I have the following portion of the code for a given active worksheet:
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Dim sh As Worksheet
Dim myMessage
If ActiveSheet.Range("I5").Value > 0 Then
MsgBox "Some cells are empty" _
& Chr(13) & Chr(13) & "Please, fill in the empty cells to proceed", _
vbExclamation + vbOKOnly, "ERROR!"
Cancel = True
Else
ThisWorkbook.Unprotect ("123")
Worksheets("Instructions").Visible = True
Worksheets("Instructions").Activate
For Each sh In Worksheets
If sh.Name <> "Instructions" Then
sh.Visible = xlVeryHidden
End If
Next sh
ThisWorkbook.Protect ("123")
myMessage = MsgBox("Do you want to save and close this file?", vbQuestion + vbYesNo, "Workbook 1")
If myMessage = vbYes Then
Application.DisplayFormulaBar = True
ThisWorkbook.Close True
ElseIf myMessage = vbNo Then
Cancel = True
Else
'nothing
End If
End If
End Sub
In general, upon pressing the close button on the workbook itself, the idea is to check a given cell of a given active worksheet for a non-zero value. If it is non-zero, the user has to fill in certain empty cells first and the workbook is not closed. Alternatively, the "Instruction" worksheet is activated and the user receives a message box of what to do next (by doing this, instead of simply no msgbox and sticking with the usual system "SAVE/NO" prompt, is that I want to eliminate the possibility of a user pressing the "No" button, which will lead to no save being made). The only problem I have is the one-time loop I get when a user selects "Yes" answer: the same message box re-appears (apparently, because the procedure goes through the code line again!).
I would appreciate if you could suggest me the way I could avoid this loop.
If you are not going to allow the user not to save then why not force a save. You can replace the entire message box prompt section:
myMessage = MsgBox("Do you want to save and close this file?", vbQuestion + vbYesNo, "Workbook 1")
If myMessage = vbYes Then
Application.DisplayFormulaBar = True
ThisWorkbook.Close True
ElseIf myMessage = vbNo Then
Cancel = True
Else
'nothing
End If
with
ThisWorkbook.Save
This will force the save and close the workbook.
Related
This question already has an answer here:
How to handle the InputBox cancel button?
(1 answer)
Closed 1 year ago.
The below code unprotects all sheets in the workbook and prompts for the password only once.
What I am trying to achieve is:
If user presses "cancel" on the password input window, the sub exits.
If user presses "ok" without entering anything, it should behave in the same way as entering the wrong password i.e. go to the error popup.
The issue is on pressing "ok" or "cancel" it doesnt behave as above, rather, in both cases, it brings up the default password prompt 3 more times, 1 for each sheet.
I am struggling with perfecting the if/then logic and have swapped things around many times, almost getting there but never quite.
Sub UnprotectAllSheets()
Dim ws As Worksheet
Dim pass As String
If ActiveSheet.ProtectContents = False Then
MsgBox "Already Unprotected"
Exit Sub
Else
pass = InputBox("Password?")
On Error GoTo Popup:
For Each ws In ThisWorkbook.Worksheets
ws.Unprotect pass
Next ws
If ActiveSheet.ProtectContents = False Then
MsgBox "Sheets now Unprotected"
ElseIf StrPtr(pass) = "" Then 'if press OK on blank entry
MsgBox "Incorrect Password", vbCritical, "Admin"
ElseIf pass = 0 Then 'if press CANCEL
Exit Sub
End If
End If
Popup:
If err.Number = 1004 Then
MsgBox "Incorrect Password", vbCritical, "Admin"
End If
End Sub
InputBox behaves similarly for cases of pressing Cancel, inputting an empty string or pressing window corner x, in terms of String return.
The next sub gives a possibility to separate the above cases:
Sub testInputBox()
Dim pass
pass = InputBox("Password?")
'standard behavior without checking the result:
MsgBox "Pass is " & pass 'it will be an empty string in case of Cancel, empty string, pressing window corner X
If StrPtr(pass) = 0 Then
MsgBox ("Cancel pressed...") 'the same message if window corner `X` is pressed
ElseIf pass = vbNullString Then
MsgBox ("Empty string..") 'OK for an empty string
Else
MsgBox ("You entered " & pass)
End If
End Sub
No need of error handling if you use the above way of identification.
And I should modify your code to act in this way:
Sub UnprotectAllSheets()
Dim ws As Worksheet, pass As String, myPass As String, i As Long
myPass = "1234"
TryAgain:
pass = InputBox("Password?")
If StrPtr(pass) = 0 Then Exit Sub 'for Cancel and window corner 'X' pressed
If pass = vbNullString Then MsgBox "You did not enter any password!", vbCritical, "Admin": Exit Sub
If pass <> myPass Then
MsgBox "Incorrect Password" & vbCrLf & _
IIf(i < 3, "Please, try again!", "We stop here..."), vbCritical, "Admin"
If i >= 3 Then Exit Sub
i = i + 1
GoTo TryAgain
End If
For Each ws In ThisWorkbook.Worksheets
If ws.ProtectContents = True Then
ws.Unprotect pass
End If
Next ws
End Sub
I'm trying to write a code to trace every change made by the user on any worksheet. The user will input data and from time to time will erase said data and/or correct the original value they inserted. If the change is either deletion or modification, an Userform will pop up and the user will include a reason for that change. Right now I'm able to display the form everytime the user makes one of the changes mentioned before, but I'm not able to retrieve the reason, could you guys help me?
This is what I have for the UserForm
Private Sub CommandButton1_Click()
Dim msgvalue As VbMsgBoxResult
Dim value As String
msgvalue = MsgBox("Do you wish to save the change?", vbYesNo + vbInformation, "Confirm")
If msgvalue = vbNo Then GoTo Command
If msgvalue = vbYes Then
value = UserForm1.txtCmmt.Value
If value = "" Then GoTo Command
End
End If
Command:
MsgBox ("A reason must be provided")
With UserForm1
.txtCmmt.Value = ""
End With
End Sub
So if a user tries to delete a value, the code is the following:
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
Dim sLastAction As String
Dim Cell As Range
sLastAction = Application.CommandBars("Standard").Controls("&Undo").List(1)
For Each Cell In Target
If sLastAction = "Clear" Or sLastAction = "Delete" Or Left(sLastAction, 9) = "Typing ''" Then
UserForm1.Show 'this is where I'm stuck, I'm not sure how to retrieve the value from the form
End If
'the code continues to retrieve other info from the changes made, including the "reason"
Thanks for the help!
Try the next way, please:
Let us say that your text box where the comment will be written is named "txtComment".
Put this code in its Exit event:
Private Sub txtComment_Exit(ByVal Cancel As MSForms.ReturnBoolean)
If Me.txtComment.text <> "" Then
If ActiveSheet.Name <> "LogDetails" Then
Application.EnableEvents = False
Sheets("LogDetails").Range("A" & rows.count).End(xlUp).Offset(0, 5).Value = Me.txtComment.text
Application.EnableEvents = True
Unload Me
End If
End If
End Sub
Let the existing Worksheet_Change event as it is, only launching the form and maybe making a Public boolean variable (from a standard module) True (something boolStop) which will not allow changing anything in any worksheet until it is not False.
Then fill the text you need in the text box ("txtComment", or however you named it) and press Enter. If my above suggestion looks interesting for you, the last line of the text box event will be boolStop = False.
If you understand how to implement the above simple solution, you maybe will forget about a user form and use a simple InputBox, as in the next example:
Private Sub Worksheet_Change(ByVal Target As Range)
If Target.Address(0, 0) <> "E1" Then Exit Sub
Dim sLastAction As String, comValue As String
Dim Cell As Range
sLastAction = Application.CommandBars("Standard").Controls("&Undo").list(1)
For Each Cell In Target
If sLastAction = "Clear" Or sLastAction = "Delete" Or left(sLastAction, 9) = "Typing ''" Then
WritePlease:
comValue = InputBox("Please write the reason for cell """ & Cell.Address & """ (" & sLastAction & ").", "Reason, please")
If comValue = "" Then GoTo WritePlease
If ActiveSheet.Name <> "LogDetails" Then
Application.EnableEvents = False
'Stop
Sheets("LogDetails").Range("A" & rows.count).End(xlUp).Offset(0, 5).Value = comValue
Application.EnableEvents = True
End If
End If
Next
End Sub
I've been trying to get the typical hide sheets/unhide sheets code to work for encouraging Macro enabling. Since I lock down saving, I needed to do it slightly differently than putting it in the Workbook_BeforeClose Sub as usually is done.
But...my hide sub
Sub HideSheets()
'Error Handling when workbook is unprotected.
On Error GoTo EH
'Unprotect the workbook to allow conditional formatting changes.
ThisWorkbook.Sheets("Field Service Report").Unprotect Password:="x"
ThisWorkbook.Sheets("Prompt").Unprotect Password:="x"
'Main Sub Code
Application.EnableCancelKey = xlDisabled
Sheets("Prompt").Visible = xlSheetVisible
Sheets("Field Service Report").Visible = xlSheetVeryHidden
Application.EnableCancelKey = xlInterrupt
'Reprotect worksheet before ending sub.
ThisWorkbook.Sheets("Field Service Report").Protect Password:="x", UserInterfaceOnly:=True
ThisWorkbook.Sheets("Prompt").Protect Password:="x"
Exit Sub
EH:
Call EH
Resume Next
End Sub
and my unhide sub
Sub UnhideSheets()
'Error Handling
On Error GoTo EH
'Unprotect the workbook to allow conditional formatting changes.
ThisWorkbook.Sheets("Field Service Report").Unprotect Password:="x"
ThisWorkbook.Sheets("Prompt").Unprotect Password:="x"
'Main Sub Code
Application.EnableCancelKey = xlDisabled
Sheets("Field Service Report").Visible = xlSheetVisible
Sheets("Prompt").Visible = xlSheetVeryHidden
Application.EnableCancelKey = xlInterrupt
'Reprotect worksheet before ending sub.
ThisWorkbook.Sheets("Field Service Report").Protect Password:="x", UserInterfaceOnly:=True
ThisWorkbook.Sheets("Prompt").Protect Password:="x"
Exit Sub
EH:
Call EH
Resume Next
End Sub
....works fine when called form the immediate window. Sheets hide and unhide appropriately.
But, when I step through the sub it doesn't actually do anything. The idea is to set the sheets to the "prompt" sheet before saving, save, then revert to usable after saving. But I can't even see if that code is working correctly (it appears to) because stepping through the actual hide/unhide subs doesn't do anything.
Edit: No errors, just doesn't change any settings to hide or unhide sheets.
Thoughts?
Edit: So given the comments below, my subs work when run from the immediate window and when stepped through via debugger. They hide and unhide the worksheets appropriately. So, the only thing that can be wrong is the code that calls these subs. So, here are two more subs. One is the button code for a save button, and the other is the Workbook_BeforeSave Sub.
Sub Save_Form()
'Error Handling ...
On Error GoTo EH
'Unprotect the workbook ...
ThisWorkbook.Sheets("Field Service Report").Unprotect Password:="x"
'Variable to disable any other save but this button.
Module1.SaveChk = 1
'Code to automatically save a copy ...
Module1.UserPath = Environ("USERPROFILE")
Module1.Path = UserPath & "\Desktop\"
If Module1.EditChk = "Y" Then
Module1.SaveName = "FSR Master"
Else
Module1.SaveName = Range("AF6").Value
End If
ThisWorkbook.SaveAs _
Filename:=Path & SaveName & ".xlsm", _
FileFormat:=52
If Module1.SaveError <> 1 Then
'User Display of Save Success
MsgBox "Filename = " & SaveName & vbNewLine _
& "File is saved to your desktop."
Else
Module1.SaveError = 0
End If
'Reset SaveChk variable
Module1.SaveChk = 0
'Reprotect Worksheet
ThisWorkbook.Sheets("Field Service Report").Protect Password:="x", UserInterfaceOnly:=True
Exit Sub
EH:
Call ErHa
Resume Next
End Sub
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
'Error Handling ...
On Error GoTo EH
'Save Initiation Check
If Module1.SaveChk <> 1 Then
Cancel = True
MsgBox "Please Use Form Save Button", vbOKCancel + vbExclamation, "SAVE CANCELLED"
Exit Sub
End If
If Module1.EditChk <> "Y" Then 'Skips the whole block if EditChk = Y
'Create the final range of cells for checking
Set Module1.Required = Application.Union(Module1.Fixed, Module1.Drive, Module1.Stage)
'Check if all required cells are filled in
If WorksheetFunction.CountA(Module1.Required) < Module1.Required.Count Then
Cancel = True
MsgBox "Please Completed Shaded Cells!", vbOK + vbExclamation, "SAVE CANCELLED"
Module1.SaveError = 1
Else
'Set the report date before saving
Application.EnableEvents = False
Range("AE59") = Format(Now(), "mm-dd-yyyy hh:mm:ss AM/PM")
Application.EnableEvents = True
End If
End If
'Renable Macro Splash Screen Before Save
Call HideSheets
Exit Sub
EH:
Call ErHa
Resume Next
End Sub
I thought this would be a readily used function in Excel but it's surprisingly difficult to implement a simple process of restricting access to specific worksheets within a larger workbook.
There's a few methods that prompt an initial password to open various versions of the same workbook. But I want to keep the workbook identical for all users but restrict access to certain sheets. Surely there's a password protect function that requires the user to enter a password to view a sheet. Rather than create multiple versions of the same workbook based on different users.
I have tried the following but it doesnt prompt a password to access the sheet
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
Dim MySheets As String, Response As String
Dim MySheet As Worksheet
MySheet = "COMMUNICATION"
If ActiveSheet.Name = MySheet Then
ActiveSheet.Visible = False
Response = InputBox("Enter password to view sheet")
If Response = "MyPass" Then
Sheets(MySheet).Visible = True
Application.EnableEvents = False
Sheets(MySheet).Select
Application.EnableEvents = True
End If
End If
Sheets(MySheet).Visible = True
End Sub
Am I doing this right?
It sounds like according to the comments that this isn't as much as a security issue as it is a convenience issue. So please bear in mind when considering implementing this into your project that this is easily breakable if there is any malicious intent to gain unauthorized access.
First, I would recommend a common landing zone. A main worksheet that is displayed immediately after opening a workbook. To do this, we would use the Workbook_Open() event and activate a sheet from there.
This can be a hidden sheet if desired, that will be up to you.
Option Explicit
Private lastUsedSheet As Worksheet
Private Sub Workbook_Open()
Set lastUsedSheet = Me.Worksheets("MainSheet")
Application.EnableEvents = False
lastUsedSheet.Activate
Application.EnableEvents = True
End Sub
Next, we should decide on what should occur when there's an attempt to access a new sheet. In the below method, once a sheet is activated it will automatically redirect the user back to the last used sheet until a successful password attempt has been made.
We can track the last used sheet in a module-scoped variable, which in this example will be named lastUsedSheet. Any time a worksheet is successfully changed, this variable will be set to that worksheet automatically - this way when when someone attempts to access another sheet, it will redirect them back to the prior sheet until the password is successfully entered.
Private Sub Workbook_SheetActivate(ByVal Sh As Object)
On Error GoTo SafeExit
Application.EnableEvents = False
' Error protection in case lastUsedSheet is nothing
If lastUsedSheet Is Nothing Then
Set lastUsedSheet = Me.Worksheets("MainSheet")
End If
' Allow common sheets to be activated without PW
If Sh.Name = "MainSheet" Then
Set lastUsedSheet = Sh
Sh.Activate
GoTo SafeExit
Else
' Temporarily send the user back to last sheet until
' Password has been successfully entered
lastUsedSheet.Activate
End If
' Set each sheet's password
Dim sInputPW As String, sSheetPW As String
Select Case Sh.Name
Case "Sheet1"
sSheetPW = "123456"
Case "Sheet2"
sSheetPW = "987654"
End Select
' Create a loop that will keep prompting password
' until successful pw or empty string entered
Do
sInputPW = InputBox("Please enter password for the " & _
"worksheet: " & Sh.Name & ".")
If sInputPW = "" Then GoTo SafeExit
Loop While sInputPW <> sSheetPW
Set lastUsedSheet = Sh
Sh.Activate
SafeExit:
Application.EnableEvents = True
If Err.Number <> 0 Then
Debug.Print Time; Err.Description
MsgBox Err.Description, Title:="Error # " & Err.Number
End If
End Sub
Side note, disabling events is necessary due to the fact that your Workbook_SheetActivate event will continue to fire after a successful sheet change.
Preventing file type changes during SaveAs1
You can further protect the accidental removal of VBA code by restricting the file save type. This can be accomplished using the Workbook_BeforeSave() event. The reason this is a potential problem is that saving as a non-macro enabled workbook will erase the code, which will prevent the password protection features you just implemented above.
First, we need to check if this is a Save or SaveAs. You can accomplish this using the Boolean property SaveAsUI that is included with the event itself. If this value is True, then it's a SaveAs event - which means we need to perform additional checks to ensure that the file type isn't accidentally changed from the save dialog box. If the value is False, then this is a normal save, and we can bypass these checks because we know the workbook will be saved as type .xlsm.
After this initial check, we will display the dialog box using Application.FileDialog().Show.
Afterwards, we will check if the user canceled the operation .SelectedItems.Count = 0 or clicked Save. IF user clicked cancel, then we simply set Cancel = True and the workbook will not save.
We proceed to check the type of extension selected by the user using this line:
If Split(fileName, ".")(UBound(Split(fileName, "."))) <> "xlsm" Then
This will split the file path by a period ., and will grab the last instance of the period (UBound(Split(fileName, "."))) in the event a file name may contain additional periods. If the extension does not match xlsm, then we abort the save operation.
Finally, after all checks passed, you can save the document:
Me.SaveAs .SelectedItems(1), 52
Since we already saved it with the above line, we can go ahead and set Cancel = True and exit the routine.
The full code (to be placed in the Worksheet obj module):
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
On Error GoTo SafeExit
If SaveAsUI Then
With Application.FileDialog(msoFileDialogSaveAs)
.Show
If .SelectedItems.Count = 0 Then
Cancel = True
Else
Dim fileName$
fileName = .SelectedItems(1)
If Split(fileName, ".")(UBound(Split(fileName, "."))) <> "xlsm" Then
MsgBox "You must save this as an .xlsm document. Document has " & _
"NOT been saved", vbCritical
Cancel = True
Else
Application.EnableEvents = False
Application.DisplayAlerts = False
Me.SaveAs .SelectedItems(1), 52
Cancel = True
End If
End If
End With
Else
Exit Sub
End If
SafeExit:
Application.EnableEvents = True
Application.DisplayAlerts = True
If Err.Number <> 0 Then
Debug.Print Time; Err.Description
MsgBox Err.Description, Title:="Error # " & Err.Number
End If
End Sub
1 Shoutout to PatricK for the suggestion
If you want to restrict access to a worksheet, you can just hide it:
ActiveWorkbook.Sheets("YourWorkSheet").Visible = xlSheetVeryHidden
I concur with Mathieu Guindon that any VBA attempt to "Restrict viewing access to an Excel worksheet" will be flimsy as explained by Mathieu Guindon. Moreover, If the file is opened with Excel option Macro security level other than the lowest, any VBA code including this is bound to fail.
However just for shake of simplicity I prefer to use workbook open event and Sheet Activate of the restricted sheet. Using Workbook Sheet Activate event will trigger password prompt even during switching between sheets by user with viewing access.
Private Sub Workbook_Open()
Sheets("COMMUNICATION").Visible = xlSheetHidden
End Sub
Public ViewAccess As Boolean 'In restricted sheet's activate event
Private Sub Worksheet_Activate()
If ViewAccess = False Then
Me.Visible = xlSheetHidden
response = Application.InputBox("Password", xTitleId, "", Type:=2)
If response = "123" Then
Me.Visible = xlSheetVisible
ViewAccess = True
End If
End If
End Sub
I have an input box asking user to enter a date. How do I let the program know to stop if the user click cancel or close the input dialog instead of press okay.
Something like
if str=vbCancel then exit sub
Currently, user can hit OK or Cancel but the program still runs
str = InputBox(Prompt:="Enter Date MM/DD/YYY", _
Title:="Date Confirmation", Default:=Date)
If the user clicks Cancel, a zero-length string is returned. You can't differentiate this from entering an empty string. You can however make your own custom InputBox class...
EDIT to properly differentiate between empty string and cancel, according to this answer.
Your example
Private Sub test()
Dim result As String
result = InputBox("Enter Date MM/DD/YYY", "Date Confirmation", Now)
If StrPtr(result) = 0 Then
MsgBox ("User canceled!")
ElseIf result = vbNullString Then
MsgBox ("User didn't enter anything!")
Else
MsgBox ("User entered " & result)
End If
End Sub
Would tell the user they canceled when they delete the default string, or they click cancel.
See http://msdn.microsoft.com/en-us/library/6z0ak68w(v=vs.90).aspx
Following example uses InputBox method to validate user entry to unhide sheets:
Important thing here is to use wrap InputBox variable inside StrPtr so it could be compared to '0' when user chose to click 'x' icon on the InputBox.
Sub unhidesheet()
Dim ws As Worksheet
Dim pw As String
pw = InputBox("Enter Password to Unhide Sheets:", "Unhide Data Sheets")
If StrPtr(pw) = 0 Then
Exit Sub
ElseIf pw = NullString Then
Exit Sub
ElseIf pw = 123456 Then
For Each ws In ThisWorkbook.Worksheets
ws.Visible = xlSheetVisible
Next
End If
End Sub
The solution above does not work in all InputBox-Cancel cases. Most notably, it does not work if you have to InputBox a Range.
For example, try the following InputBox for defining a custom range ('sRange', type:=8, requires Set + Application.InputBox) and you will get an error upon pressing Cancel:
Sub Cancel_Handler_WRONG()
Set sRange = Application.InputBox("Input custom range", _
"Cancel-press test", Selection.Address, Type:=8)
If StrPtr(sRange) = 0 Then 'I also tried with sRange.address and vbNullString
MsgBox ("Cancel pressed!")
Exit Sub
End If
MsgBox ("Your custom range is " & sRange.Address)
End Sub
The only thing that works, in this case, is an "On Error GoTo ErrorHandler" statement before the InputBox + ErrorHandler at the end:
Sub Cancel_Handler_OK()
On Error GoTo ErrorHandler
Set sRange = Application.InputBox("Input custom range", _
"Cancel-press test", Selection.Address, Type:=8)
MsgBox ("Your custom range is " & sRange.Address)
Exit Sub
ErrorHandler:
MsgBox ("Cancel pressed")
End Sub
So, the question is how to detect either an error or StrPtr()=0 with an If statement?
If your input box is an array, it does not work. I have solved it by adding a check for if it is an array first.
Dim MyArrayCheck As String
Dim MyPlateMapArray as variant
MyPlateMapArray = Application.InputBox("Select ....", Type:=8)
MyArrayCheck = IsArray(MyPlateMapArray)
If MyArrayCheck = "False" Then
Exit Sub
End If
I have solved it with a False like below
MyLLOQ = Application.InputBox("Type the LLOQ number...", Title:="LLOQ to be inserted in colored cells.", Type:=1)
If MyLLOQ = False Then Exit Sub
If user click cancel the sub will exit.
Another suggestion.
Create a message box when inputbox return null value. Example:
Dim PrC as string = MsgBox( _
"No data provided, do you want to cancel?", vbYesNo+vbQuestion, "Cancel?")
Sub TestInputBox()
Dim text As String
text = InputBox("Type some text")
If text = "" Then
MsgBox "button cancel pressed or nothing typed"
Else
MsgBox text
End If
End Sub
Inputbox send a boolean False value when Cancel is pressed.
contenidoy = Application.InputBox("Cantidad = ", titulox, contenidox, , , , , Type:=1)
'ESC or CANCEL
If contenidoy = False Then
MsgBox "Cancelado"
Else
MsgBox "EdiciĆ³n aceptada"
'End If