I'm trying to ensure that data entered into the named range of an Excel spreadsheet is valid. To do this, I've defined a static validation list for column "A" in the range, and enabled the dropdown list for that column. Based on the option selected by the user, I add a validation object in column "B" at runtime, having a list of entries constrained by the entry in column "A". Based on the entries in columns A and B, the cell in column "C" is automatically populated.
This works fine until spreadsheet protection is enabled. At that point, attempting to select an option from the droplist in column "B" generates the following error:
"The cell or chart that you are trying to change is protected and therefore read-only. ... "
However
All cells in the range in question were unlocked prior to adding
worksheet protection.
The code explicitly removes protection prior to updating the
validation object in column "B", then replaces it once the validation
object has been added.
When a list item is selected from the droplist in column "B", the
error message fires immediately before any worksheet events occur,
making it impossible to trap or debug the error.
I have code in both the spreadsheet and in a separate code module, both or which are included below. Any ideas would be greatly appreciated
Here's the code in the Worksheet_Change() event:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim strNm As String
' there will be multiple named ranges eventually. We need to be able to distinguish
' among the various ranges so that our code executes only against the data we expect
' to manipulate - not random cells
If Not Intersect(ActiveCell, ActiveWorkbook.Names("DBAddRange").RefersToRange) Is Nothing Then
Dim rng As Range
Set rng = ActiveWorkbook.Names("DBAddRange").RefersToRange
If Target.Column = 1 Then
If FLAG_CHANGE_IN_PROGRESS = True Then Exit Sub
FLAG_CHANGE_IN_PROGRESS = True
Dim VldnList As String
VldnList = getVldtnList(Target.Value)
unlockSS ActiveSheet
Range("B" & Target.row).Clear
Range("B" & Target.row).Select
With Range("B" & Target.row).Validation
.Delete
.Add Type:=xlValidateList, Operator:=xlValidateList, Formula1:=VldnList
.IgnoreBlank = False
.InCellDropdown = True
End With
lockSS ActiveSheet
Range("B" & Target.row).Select
FLAG_CHANGE_IN_PROGRESS = False
ElseIf Target.Column = 2 Then
If FLAG_CHANGE_IN_PROGRESS = True Then Exit Sub
FLAG_CHANGE_IN_PROGRESS = True
unlockSS ActiveSheet
Dim dbHost As Variant
Dim hNmRng As Range
Set hNmRng = ActiveWorkbook.Names("valid_lookups").RefersToRange
dbHost = Application.VLookup(Target.Value, hNmRng, 2, False)
Range("C" & Target.row).Value = dbHost
lockSS ActiveSheet
FLAG_CHANGE_IN_PROGRESS = False
End If
End If
If Not Intersect(ActiveCell, ActiveWorkbook.Names("HostAddRange").RefersToRange) Is Nothing Then
End If
End Sub
Code in the external module:
Sub lockSS(ByVal sheet As Sheet1)
sheet.Protect Password:=[NOT SHOWN], UserInterfaceOnly:=True, DrawingObjects:=False
Application.EnableEvents = True
End Sub
Function getVldtnList(ByVal dbName As String)
Dim vrtmatchRow As Variant
Dim rng As Range
If dbName = "" Then
getVldtnList = ""
Exit Function
End If
' this is a pre-defined range having entries for:
' DB Name - Column 1
' DB CI ID - Column 2
' DB Host - Column 3
Set rng = ActiveWorkbook.Names("valid_db_nms").RefersToRange
' find the value of the first row in the range that matches the value
' of the dbName parm. NOTE: the final 0 parm tells the match function
' to find an exact match.
vrtmatchRow = Application.Match(dbName, rng, 0)
If IsError(vrtmatchRow) Then
' NOTE: we should NEVER get here due to the way cell validation is set up.
MsgBox "The value entered was not found in the list of valid database values. See xxx for help", vbRetryCancel, "Invalid Entry"
Else
Dim row As Long
Dim strListVals As String
Set rng = ActiveWorkbook.Names("valid_db_info").RefersToRange
row = vrtmatchRow
Do
If Len(strListVals) > 0 Then strListVals = strListVals + ","
strListVals = strListVals + rng.Cells(row, 2).Value
row = row + 1
Loop While (rng.Cells(row, 1).Value = dbName)
End If
getVldtnList = strListVals
End Function
Sub unlockSS(ByVal sheet As Sheet1)
sheet.Unprotect Password:=[NOT SHOWN]
Application.EnableEvents = False
End Sub
Clearing a range will also reset the "locked" checkbox, so you need to reset that each time
Range("B" & Target.row).Clear
Related
Wondering if someone can help me reverse the below code. Essentially, I have a userform with a combobox that generates from a list of names from a worksheet column "A". Upon submit the selected items from userform are populated to the worksheet to the row of the corresponding name from the combobox.
I am hoping to somehow reverse the code below so I can place it in "UserForm_Initialize()" to regenerate saved values back to the texboxes on the form if user closes and reopens the same day. I have a current date textbox called "currentDate". So basically if Date = currentDate.Text Than...add cell value back to textboxes.
Dim dn As Worksheet: Set dn = Sheets("DailyNumbers")
Dim EmptyRow As Long
Dim FoundVal As Range
EmptyRow = dn.Cells(ws.Rows.Count, "B").End(xlUp).Row + 1
' *** Check combobox selection ***
If procNamecombobox.ListIndex > -1 Then
Set FoundVal = dn.Range("A1:A" & EmptyRow).Find (procNamecombobox.Value) 'find Combobox value in Column A
If Not FoundVal Is Nothing Then 'if found
dn.Range("B" & FoundVal.Row).Value = currentDate.Text
dn.Range("C" & FoundVal.Row).Value = completeCount.Text 'use that row to populate cells
dn.Range("D" & FoundVal.Row).Value = handledCount.Text
dn.Range("E" & FoundVal.Row).Value = wipCount.Text
dn.Range("F" & FoundVal.Row).Value = suspendCount.Text
Else 'if not found use EmptyRow to populate Cells
dn.Range("A" & EmptyRow).Value = procNamecombobox.Value
dn.Range("B" & EmptyRow).Value = currentDate.Text
dn.Range("C" & EmptyRow).Value = completeCount.Text
dn.Range("D" & EmptyRow).Value = handledCount.Text
dn.Range("E" & EmptyRow).Value = wipCount.Text
dn.Range("F" & EmptyRow).Value = suspendCount.Text
End If
Else
MsgBox "Please select your name"
End If
Thank you!
I guess you could use something like this
Option Explicit
Private Sub UserForm_Initialize()
Dim f As Range
With Worksheets("DailyNumbers") 'reference wanted sheet
Set f = .Range("B1", .Cells(.Rows.Count, "B").End(xlUp)).Find(Date, lookat:=xlWhole, LookIn:=xlValues) 'search referenced sheet column B for current date
End With
If Not f Is Nothing Then ' if current date found
With Me 'reference userform
.completeCount.Text = f.Offset(, 1).value
.handledCount.Text = f.Offset(, 2).value
.wipCount.Text = f.Offset(, 3).value
.suspendCount.Text = f.Offset(, 4).value
End With
End If
'your other code to fill listbox
With Worksheets("NamesArchive") ' just a guess...
Me.procNamecombobox.List = Application.Transpose(.Range("A1", .Cells(.Rows.Count, "A").End(xlUp))) 'fill combobox with referenced sheet column A values from rows 1 down to last not empty one
End With
End Sub
BTW, your code could be refactored as follows:
Option Explicit
Private Sub CommandButton1_Click() ' just a guess...
Dim dn As Worksheet: Set dn = Sheets("DailyNumbers")
Dim emptyRow As Long
Dim foundRng As Range
With Me
With .procNamecombobox
If .ListIndex = -1 Then
MsgBox "Please select your name"
Exit Sub
End If
emptyRow = dn.Cells(dn.Rows.Count, "B").End(xlUp).Row + 1
Set foundRng = dn.Range("A1:A" & emptyRow).Find(.value) 'find Combobox value in Column A
If foundRng Is Nothing Then 'if no entry with input name
dn.Range("A" & emptyRow).value = .value 'fill column A first empty with input name
Else 'otherwise
emptyRow = foundRng.Row 'set found cell row index as the one to write in
End If
End With
Intersect(dn.Range("B:F"), dn.Rows(emptyRow)).value = Array(.currentDate.Text, .completeCount.Text, .handledCount.Text, .wipCount.Text, .suspendCount.Text) 'fill columns B to F proper row with textboxes values
End With
End Sub
To help get you started:
A)
Determine if there is a cell in column B with the current date. If so, locate it and use the .Row property to save the row number to a variable.
(There are a couple of range functions (.Find, .Search) that you can use to locate a cell with a particular value. For date's, this link has some helpful information.)
A.5) From the above link, if the dates are in Excel as serial dates -- not text -- then you can use
Set FoundCell = Range("A1:A100").Find _
(what:=Date,lookin:=xlFormulas)
to find the current date in column A from rows 1 to 100. VBA has a function Date() which returns the current day's date. Now() returns the current date and time, while Time() returns the current time.
B)
Set the .text values of the Text/Combo boxes to the values of the cells
(These can be located with a concatenation of the correct column with the saved row variable from earlier. Similar to how you located the cells to save the values initially)
If you're stuck on how to do a particular step or process, and can't find an existing Q&A with information, you can ask for elaboration.
I've been trying to write a function that goes through an Excel worksheet to find a range of cells fulfilling a certain condition (two cells in the same row that have to be equal).
I've written the following code that goes through the Worksheet row by row and checks if the condition is fulfilled.
If a cell is found for which the condition is true I would like the address of the cell to be added to a range.
The output of the function should finally be this range which is subsequently used to populate a dropdown menu in a dialog with the entries fulfilling the condition.
Private Function DetermineRange(WorksheetName As String, Column1 As Integer, Column2 As Integer) As Range
Dim rng As Range
'Go through rows of specified worksheet
For currRow = 1 To Worksheets(WorksheetName).Cells(Rows.Count, 3).End(xlUp).Row
'Compare cells in specified columns of current row
If Worksheets(WorksheetName).Cells(currRow, Column1).Value = Worksheets(WorksheetName).Cells(currRow, Column2).Value _
And Not (Worksheets(WorksheetName).Cells(currRow, Column1).Value = "") Then
'If cells are equal, but not empty, append current adress of current cell to range
If Not rng Is Nothing Then
Set rng = Union(rng, Worksheets(WorksheetName).Cells(currRow, 2))
Else
Set rng = Worksheets(WorksheetName).Cells(currRow, 2)
End If
End If
Next currRow
If Not rng Is Nothing Then
'return found Range
Set DetermineRange = rng
MsgBox ("Range is: " & rng)
Else
'DEBUG: Throw error message if rng is empty,
MsgBox ("DEBUG DetermineRange Function:" & vbCrLf & _
"Error! No corresponding Cells found in Sheet" & WorksheetName)
End If
End Function
Cycling through the rows works fine, however I don't seem to be able to add the addresses for the cells after the condition is checked to the range object.
I have also tried the following, which results in a
Runtime error 424: Object required
'If cells are equal, but not empty, append current address of current cell to range
If Not rng Is Nothing Then
Set rng = Union(rng, Worksheets(WorksheetName).Cells(currRow, 2).Address)
Else
Set rng = Worksheets(WorksheetName).Cells(currRow, 2).Address
End If
I've been looking around, but can't seem to find much information on how to add cells to range objects however...
Maybe one of you could help! Any kind of pointer in the right direction is highly appreciated!
Thanks in advance for any kind of help!
Edit:
I am calling the function like this:
Set NameRng = DetermineRange("Features", ProjectColumn, TCGroupColumn)
cb_FcnName.RowSource = Worksheets(3).Name & "!" & NameRng.Address
But I get the following error:
Runtime Error 380: Not able to set property RowSource
One method is to capture the cell addresses. Concatenate these and use the final value to build a new range.
Example:
Public Function DetermineRange(WorksheetName As String, Column1 As Integer, Column2 As Integer) As Range
Dim rng As Range
Dim currRow As Integer
Dim targetSheet As workSheet ' Shortcut to requested sheet.
Dim matchesFound As String ' Address of matching cells.
' This line will raise an error if the name is not valid.
Set targetSheet = ThisWorkbook.Sheets(WorksheetName)
'Go through rows of specified worksheet
For currRow = 1 To targetSheet.UsedRange.Rows(targetSheet.UsedRange.Rows.Count).Row
'Compare cells in specified columns of current row
If targetSheet.Cells(currRow, Column1).Value <> "" Then
If targetSheet.Cells(currRow, Column1).Value = targetSheet.Cells(currRow, Column2).Value Then
' Capture address of matching cells.
If Len(matchesFound) > 0 Then
matchesFound = matchesFound & "," & targetSheet.Cells(currRow, Column1).Address
Else
matchesFound = targetSheet.Cells(currRow, Column1).Address
End If
End If
End If
Next currRow
' DEBUG: Throw error message if no matches found.
If Len(matchesFound) = 0 Then
Err.Raise vbObjectError + 101, "DetermineRange", "No matching cells found."
End If
' Return range.
Set DetermineRange = targetSheet.Range(matchesFound)
End Function
The code is a little rough and ready. I can't help but feel there are few too many lines. But the basic approach works.
I am trying to test the differences between two lists. My VBA code is supposed to loop through list one and use the find method to look at each item in list two. If the item is not found in list two, I use the copy , paste method to log it in my compare sheet. The code then goes back and does the reverse procedure to test list two on list one; loop through list two and look for each item in list 1.
My Results imply and inconsistent number of matching results. The number of items in list 1 minus the results from the "list1" find loop do not equal number of items in list two minus the number of items from "list2' find loop. The remaining items should only be values that where found in each list.
All items are primary keys and are unique within their respective list.
Public Sub compare_list()
Dim wsList2 As Worksheet
Dim wsList1 As Worksheet
Dim wsCompare As Worksheet
Dim found1 As Range
Set wsList2 = Worksheets("List2")
Set wsList1 = Worksheets("List1")
Set wsCompare = Worksheets("Compare")
Application.ScreenUpdating = False
'Check each value in client_id list one with list two
wsList1.Activate
wsList1.Range("a1").Select
Do Until ActiveCell.Value = ""
Set found1 = wsList2.Range("a1",_
wsList2.Range("A1048576").End(xlUp)).Find(Selection.Value)
If found1 Is Nothing Then
Selection.Copy
wsCompare.Range("A1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
ActiveCell.Offset(1, 0).Select
Loop
'Check each value in client_id list two with list one
wsList2.Activate
wsList2.Range("a1").Select
Do Until ActiveCell.Value = ""
Set found1 = wsList1.Range("a1",_
wsList1.Range("A1048576").End(xlUp)).Find(Selection.Value)
If found1 Is Nothing Then
Selection.Copy
wsCompare.Range("B1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
ActiveCell.Offset(1, 0).Select
Loop
wsCompare.Activate
Application.ScreenUpdating = True
End Sub
Just a note: I am very new to VBA, and do not come from a developer back ground. You probably can't dumb down your solution too much.
My problem was a default parameter in the find function. I needed to set the LookAt:=xlWhole
For example, list 1 = (1,2,32,142) and list 2 = (1,2,3,132)
For my purposes 32 is not in list 2 but without setting that parameter the find function returns a result when search for 32 as it finds it in the cell containing 132
The following is the code that is working for me now. Thanks for all your help
Public Sub compare_list()
Dim wsList2 As Worksheet
Dim wsList1 As Worksheet
Dim wsCompare As Worksheet
Dim found1 As Range
Dim found2 As Range
Dim myCell As Range
Dim countList2 As Integer
Dim countList1 As Integer
Dim listDiff As Integer
Dim commonList2 As Integer
Dim commonList1 As Integer
Dim diffList1 As Integer
Dim diffList2 As Integer
Set wsList2 = Worksheets("List2")
Set wsList1 = Worksheets("List1")
Set wsCompare = Worksheets("Compare")
Application.ScreenUpdating = False
Application.CutCopyMode = False
'Check each value in the client_id list created by List1 to find an equal value in List2's list
Set myCell = wsList1.Range("A1")
Do Until myCell.Value = ""
Set found1 = wsList2.Range("a1", wsList2.Range("A1048576").End(xlUp)).Find(what:=myCell.Value, LookAt:=xlWhole)
If found1 Is Nothing Then
myCell.Copy
wsCompare.Range("A1048576").End(xlUp).Offset(1, 0).PasteSpecial
Else
myCell.Copy
wsCompare.Range("G1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
Set myCell = myCell.Offset(1, 0)
Loop
'Check each value in the client_id list created by List2 to find an equesl value in List1's list
Set myCell = wsList2.Range("A1")
Do Until myCell.Value = ""
Set found2 = wsList1.Range("a1", wsList1.Range("A1048576").End(xlUp)).Find(what:=myCell.Value, LookAt:=xlWhole)
If found2 Is Nothing Then
myCell.Copy
wsCompare.Range("B1048576").End(xlUp).Offset(1, 0).PasteSpecial
Else
myCell.Copy
wsCompare.Range("F1048576").End(xlUp).Offset(1, 0).PasteSpecial
End If
Set myCell = myCell.Offset(1, 0)
Loop
Application.ScreenUpdating = True
wsCompare.Activate
'test logic of result
countList1 = wsList1.Range("a2", wsList1.Range("A1048576").End(xlUp)).Rows.count
countList2 = wsList2.Range("a2", wsList2.Range("a1048576").End(xlUp)).Rows.count
diffList1 = (wsCompare.Range("a2", wsCompare.Range("A1048576").End(xlUp)).Rows.count - 1)
diffList2 = (wsCompare.Range("b2", wsCompare.Range("b1048576").End(xlUp)).Rows.count - 1)
listDiff = Abs(countList1 - countList2)
commonList2 = (countList2 - diffList2)
commonList1 = (countList1 - diffList1)
MsgBox "List2 has " & commonList2 & " in common with List1" & vbCrLf & "List1 has " & commonList1 & " in common with List2"
End Sub
Can anyone walk me through how to write a script to delete the entire row if a cell in column D = "" on sheet 3 in range D13:D40.
Also, how to prevent the user from accidentally running the script again once those cells in the range are already deleted and other cells are now on the D13:D40 range?
Solution: This is working for me:
Sub DeleteRowsWithEmptyColumnDCell()
Dim rng As Range
Dim i As Long
Set rng = ThisWorkbook.ActiveSheet.Range("D13:D40")
With rng
' Loop through all cells of the range
' Loop backwards, hence the "Step -1"
For i = .Rows.Count To 1 Step -1
If .Item(i) = "" Then
' Since cell is empty, delete the whole row
.Item(i).EntireRow.Delete
End If
Next i
End With
End Sub
Explanation: Run a for loop through all cells in your Range in column D and delete the entire row if the cell value is empty. Important: When looping through rows and deleting some of them based on their content, you need to loop backwards, not forward. If you go forward and you delete a row, all subsequent rows get a different row number (-1). And if you have two empty cells next to each other, only the row of the first one will be deleted because the second one is moved one row up but the loop will continue at the next line.
No need for loops:
Sub SO()
Static alreadyRan As Integer
restart:
If Not CBool(alreadyRan) Then
With Sheets("Sheet3")
With .Range("D13:D40")
.AutoFilter 1, "="
With .SpecialCells(xlCellTypeVisible)
If .Areas.Count > 1 Then
.EntireRow.Delete
alreadyRan = alreadyRan + 1
End If
End With
End With
.AutoFilterMode = False
End With
Else
If MsgBox("procedure has already been run, do you wish to continue anyway?", vbYesNo) = vbYes Then
alreadyRan = 0
GoTo restart:
End If
End If
End Sub
Use AutoFilter to find blank cells, and then use SpecialCells to remove the results. Uses a Static variable to keep track of when the procedure has been run.
Here's my take on it. See the comments in the code for what happens along the way.
Sub deleterow()
' First declare the variables you are going to use in the sub
Dim i As Long, safety_net As Long
' Loop through the row-numbers you want to change.
For i = 13 To 40 Step 1
' While the value in the cell we are currently examining = "", we delete the row we are on
' To avoid an infinite loop, we add a "safety-net", to ensure that we never loop more than 100 times
While Worksheets("Sheet3").Range("D" & CStr(i)).Value = "" And safety_net < 100
' Delete the row of the current cell we are examining
Worksheets("Sheet3").Range("D" & CStr(i)).EntireRow.Delete
' Increase the loop-counter
safety_net = safety_net + 1
Wend
' Reset the loop-counter
safety_net = 0
' Move back to the top of the loop, incrementing i by the value specified in step. Default value is 1.
Next i
End Sub
To prevent a user from running the code by accident, I'd probably just add Option Private Module at the top of the module, and password-protect the VBA-project, but then again it's not that easy to run it by accident in the first place.
This code executes via a button on the sheet that, once run, removes the button from the worksheet so it cannot be run again.
Sub DeleteBlanks()
Dim rw As Integer, buttonID As String
buttonID = Application.Caller
For rw = 40 To 13 Step -1
If Range("D" & rw) = "" Then
Range("D" & rw).EntireRow.Delete
End If
Next rw
ActiveSheet.Buttons(buttonID).Delete
End Sub
You'll need to add a button to your spreadsheet and assign the macro to it.
There is no need for loops or filters to find the blank cells in the specified Range. The Range.SpecialCells property can be used to find any blank cells in the Range coupled with the Range.EntireRow property to delete these. To preserve the run state, the code adds a Comment to the first cell in the range. This will preserve the run state even if the Workbook is closed (assuming that it has been saved).
Sub DeleteEmpty()
Dim ws As Excel.Worksheet
Set ws = ActiveSheet ' change this as is appropriate
Dim sourceRange As Excel.Range
Set sourceRange = ws.Range("d13:d40")
Dim cmnt As Excel.Comment
Set cmnt = sourceRange.Cells(1, 1).Comment
If Not cmnt Is Nothing Then
If cmnt.Text = "Deleted" Then
If MsgBox("Do you wish to continue with delete?", vbYesNo, "Already deleted!") = vbNo Then
Exit Sub
End If
End If
End If
Dim deletedThese As Excel.Range
On Error Resume Next
' the next line will throw an error if no blanks cells found
' hence the 'Resume Next'
Set deletedThese = sourceRange.SpecialCells(xlCellTypeBlanks)
On Error GoTo 0
If Not deletedThese Is Nothing Then
deletedThese.EntireRow.Delete
End If
' for preserving run state
If cmnt Is Nothing Then Set cmnt = sourceRange.Cells(1, 1).AddComment
cmnt.Text "Deleted"
cmnt.Visible = False
End Sub
I've recently had to write something similar to this. I'm not sure that the code below is terribly professional, as it involves storing a value in cell J1 (obviously this can be changed), but it will do the job you require. I hope this helps:
Sub ColD()
Dim irow As long
Dim strCol As String
Sheets("sheet2").Activate
If Cells(1, 10) = "" Then
lrun = " Yesterday."
Else: lrun = Cells(1, 10)
End If
MsgBox "This script was last run: " & lrun & " Are you sure you wish to continue?", vbYesNo
If vbYes Then
For irow = 40 To 13 step -1
strCol = Cells(irow, 4).Value
If strCol = "" Then
Cells(irow, 4).EntireRow.Delete
End If
Next
lrun = Now()
Cells(1, 10) = lrun
Else: Exit Sub
End If
End Sub
I'm using a VBA script to fill a column with some data.
This script checks for the first free cell in a range and fill it with the data from another Excel worksheet.
The script starts when user double clicks on a data-cell in the other sheet.
The code of the VBA script is:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
Dim ws2 As Worksheet
Dim cognome As Range
Dim ultB As Long
Set cognome = Me.Range("A:A")
Set ws2 = ThisWorkbook.Sheets("PUBBLICO")
ultB = IIf(ws2.Range("E8").Value = "", 8, ws2.Range("E7").End(xlDown).Row + 1)
If Not Intersect(Target, cognome) Is Nothing And Target.Value <> "" Then
ws2.Range("E" & ultB).Value = Me.Range("B" & Target.Row).Value 'ANNO
ws2.Range("F" & ultB).Value = Me.Range("A" & Target.Row).Value 'COGNOME
'ws2.Range("E4").Value = Me.Range("C" & Target.Row).Value NOME SQUADRA
End If
Set ws2 = Nothing
Cancel = True
End Sub
The problem is that this script should be optimized for another use. I've another Excel sheet that into the range of cell to fill contains a cell that is always pre-filled and it is merged.
This is the example of my Excel file:
As you can see, row 19 is always pre-filled.
So, any suggestions to correct my script to jump row 19?
Consider
ultB = IIf(ws2.Range("E8").Value = "", 8, ws2.Range("E27").End(xlUp).Row + 1)
If ultB = 19 Then ultB = 20
If ultB = 27 Then
MsgBox "Form is full"
Exit Sub
End If
If Not Intersect(Target, cognome) Is Nothing And Target.Value <> "" Then
This will find the last populated cell from the bottom up instead of from the top down. It also contains some code to notify when the form is full.