Dans Excel VBA, si une variable est Excel.Range et que la plage à laquelle elle fait référence est supprimée, elle perd sa référence. Toute tentative d'accès à la variable entraîne une Erreur d'exécution 424: objet requis
.
Dim rng As Range Set rng = Sheet1Range("A1") Sheet1.Rows(1).Delete 'Range has been deleted. Debug.Print rng.Address() 'Any access attempt now raises runtime error 424.
Existe-t-il un moyen de tester cet état de "référence perdue" sans erreur handler ..?
Tester Rien
, Vartype () et Typename () n'étaient pas utiles car la variable est toujours un Range. J'ai lu littéralement tout Excel.Application dans le navigateur d'objets, mais je n'ai rien trouvé. Il y a peut-être quelque chose que je néglige ...? Comme l'une de ces étranges fonctions résiduelles des versions préhistoriques d'Excel, comme ExecuteExcel4Macro () ..?
J'ai recherché sur Google la réponse à cette question, mais je n'ai rien trouvé d'utile. p>
EDIT:
Certains ont demandé pourquoi j'essayais d'éviter un gestionnaire d'erreurs. C'est ma philosophie de programmation normale pour plusieurs raisons:
3 Réponses :
Voici une approche qui devrait permettre de contourner le problème, bien que ce ne soit pas une excellente solution pour vérifier s'il a été supprimé par lui-même. Je pense que la gestion des erreurs est probablement votre meilleure approche.
Private getRange As Range Sub Example() Dim foo As Range Dim cellCount As Long Set foo = Sheet1.Range("A1") cellCount = GetCellCountInUnion(foo) Sheet1.Rows(1).Delete If Not cellCount = getRange.Cells.Count Then Debug.Print "The cell was removed!" Else Debug.Print "The cell still exists!" End If End Sub Private Function GetCellCountInUnion(MyRange As Range) As Long Set getRange = Union(MyRange, MyRange.Parent.Range("A50000")) âsecond cell in union is just a cell that should exist GetCellCountInUnion = getRange.Cells.Count End Function
En outre, voici une approche plus orientée fonction qui peut être une approche légèrement meilleure à ajouter à votre base de code. Encore une fois, pas idéal, mais cela ne devrait pas nécessiter de gestionnaire d'erreurs.
Sub Example() Dim foo1 As Range Dim foo2 As Range Dim foo3 As Range Dim numberOfCells As Long Set foo1 = Sheet1.Range("A1") Set foo2 = foo1.Offset(1, 0) 'Get the next row, ensure this cell exists after row deletion! Set foo3 = Union(foo1, foo2) numberOfCells = foo3.Cells.Count Debug.Print "There are " & numberOfCells & " cells before deletion" Sheet1.Rows(1).Delete Debug.Print "There are now " & foo3.Cells.Count & " cells" If foo3.Cells.Count <> numberOfCells Then Debug.Print "One of the cells was deleted!" Else Debug.Print "All cells still exist" End If End Sub
Un exemple utilisant un nom de plage:
Private IdAr As Variant, tbRows As Integer, myCount As Integer, Cancelado As Boolean Private Sub Worksheet_SelectionChange(ByVal Target As Range) If Target.Rows.Count = Me.Rows.Count Then Exit Sub On Error GoTo ExceptionHandling If Not Application.Intersect(Target, Me.ListObjects("Table1").DataBodyRange) Is Nothing Then Dim tblRow As Long, y As Integer, i As Integer tblRow = Target.Row - Me.ListObjects("Table1").HeaderRowRange.Row y = Target.Rows.Count If y > 1 Then ReDim IdAr(0 To y - 1) For i = 0 To y - 1 IdAr(i) = Me.ListObjects("Table1").ListColumns("ID").DataBodyRange(tblRow + i) Next i Else 'If Application.CutCopyMode = False Then IdAr = Me.ListObjects("Table1").ListColumns("ID").DataBodyRange(tblRow).Value 'End If End If tbRows = Me.ListObjects("Table1").ListRows.Count End If CleanUp: On Error Resume Next Exit Sub ExceptionHandling: MsgBox "Error: " & Err.Description Resume CleanUp End Sub Private Sub Worksheet_Change(ByVal Target As Range) On Error GoTo ExceptionHandling Application.EnableEvents = False If Not Application.Intersect(Target, Me.ListObjects("Table1").DataBodyRange) Is Nothing Then Cancelado = False Dim myCell As Range For Each myCell In Target If Not Application.Intersect(myCell, Me.ListObjects("Table1").ListColumns("ID").DataBodyRange) Is Nothing Then If Me.ListObjects("Table1").ListRows.Count > tbRows Then Cancelado = True Else If Me.ListObjects("Table1").ListRows.Count = tbRows Then If myCell.Text = vbNullString Then Debug.Print "Selected ListObject Row and Cleared Contents" Cancelado = True Delete_record myCount = myCount + 1 End If Else Cancelado = True Debug.Print "ListObject Row Deleted" Delete_record myCount = myCount + 1 End If End If Else If Cancelado = False Then If Not Application.Intersect(myCell, Me.Range("Table1[[FIELD1]:[FIELD3]]")) Is Nothing Then Update_record myCell End If End If Next myCell End If CleanUp: On Error Resume Next myCount = 0 Application.EnableEvents = True Exit Sub ExceptionHandling: MsgBox "Error: " & Err.Description & vbLf & Err.Number Resume CleanUp Resume 'for debugging End Sub Sub Update_record(myCell As Range) On Error GoTo ExceptionHandling Dim tblRow As Long, IdTbl As Long, sField As String, sSQL As String sField = Me.ListObjects("Table1").HeaderRowRange(myCell.Column) tblRow = myCell.Row - Me.ListObjects("Table1").HeaderRowRange.Row IdTbl = Me.ListObjects("Table1").ListColumns("ID").DataBodyRange(tblRow).Value 'Dim cnStr As String 'cnStr = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & sPath & ";Jet OLEDB:Database Password=123" 'Dim cn As ADODB.Connection 'Set cn = New ADODB.Connection 'cn.CursorLocation = adUseServer 'cn.Open cnStr If IdTbl > 0 Then sSQL = "UPDATE MYTABLE SET " & sField & " = '" & myCell.Value & "' WHERE ID = " & Me.ListObjects("Table1").ListColumns("ID").DataBodyRange(tblRow).Value MsgBox sSQL 'Dim cmd As ADODB.Command 'Set cmd = New ADODB.Command 'Set cmd.ActiveConnection = cn 'cmd.CommandText = sSQL 'cmd.Execute , , adCmdText + adExecuteNoRecords ''cn.Execute sSQL, RecsAffected 'alternative to Command ''Debug.Print RecsAffected Else sSQL = "SELECT ID, " & sField & " FROM MYTABLE" MsgBox sSQL 'Dim rst As ADODB.Recordset 'Set rst = New ADODB.Recordset 'rst.Open sSQL, cn, adOpenForwardOnly, adLockOptimistic, adCmdText 'cn.BeginTrans 'rst.AddNew 'rst(sField).Value = myCell.Value 'rst.Update 'IdTbl = rst(0).Value 'MsgBox "New Auto-increment value is: " & IdTbl 'Me.ListObjects("Table1").ListColumns("ID").DataBodyRange(tblRow) = IdTbl 'rst.Close 'cn.CommitTrans End If CleanUp: On Error Resume Next If Not cn Is Nothing Then If cn.State = adStateOpen Then cn.Close End If 'DriveMapDel 'https://codereview.stackexchange.com/questions/143895/making-repeated-adodb-queries-from-excel-sql-server '... get rid of the redundant assignments to Nothing; the objects are going out of scope at End Sub, they're being destroyed anyway. 'Set rst = Nothing 'Set cmd = Nothing 'Set cn = Nothing Exit Sub ExceptionHandling: MsgBox "Error: " & Err.Description & vbLf & Err.Number Resume CleanUp Resume 'for debugging End Sub Sub Delete_record() Dim sSQL As String If IsArray(IdAr) Then sSQL = "DELETE FROM MYTABLE WHERE ID = " & IdAr(myCount) MsgBox sSQL Else sSQL = "DELETE FROM MYTABLE WHERE ID = " & IdAr MsgBox sSQL End If End Sub
Ci-dessous un exemple de module de feuille à synchroniser depuis un objet de liste Excel vers une table de base de données (accès en ms).
UPDATE Jul 05, 20 ': certains tests avec le code ci-dessous semblent montrer une perte d'informations sur le compteur de lignes / colonnes sélectionnées dans le panneau de la fenêtre de l'éditeur "noms" (en haut à gauche, à côté de l'éditeur de formule) en cas de cellules multiples sélections.
Private IdAr As Variant, myCount As Integer Private Sub Worksheet_Activate() Names.Add Name:="myName", RefersTo:=Selection, Visible:=False End Sub Private Sub Worksheet_SelectionChange(ByVal Target As Range) If Target.Rows.Count = Me.Rows.Count Then Exit Sub On Error GoTo ExceptionHandling Names.Add Name:="myName", RefersTo:=Target, Visible:=False If Not Application.Intersect(Target, Me.ListObjects("Table2").DataBodyRange) Is Nothing Then Dim tblRow As Long, y As Integer, i As Integer tblRow = Target.Row - Me.ListObjects("Table2").HeaderRowRange.Row y = Target.Rows.Count If y > 1 Then ReDim IdAr(0 To y - 1) For i = 0 To y - 1 IdAr(i) = Me.ListObjects("Table2").ListColumns("ID").DataBodyRange(tblRow + i) Next i Else 'If Application.CutCopyMode = False Then IdAr = Me.ListObjects("Table2").ListColumns("ID").DataBodyRange(tblRow).Value 'End If End If End If CleanUp: On Error Resume Next Exit Sub ExceptionHandling: MsgBox "Error: " & Err.Description Resume CleanUp End Sub Private Sub Worksheet_Change(ByVal Target As Range) On Error GoTo ExceptionHandling Application.EnableEvents = False If Not Application.Intersect(Target, Me.ListObjects("Table2").DataBodyRange) Is Nothing Then Dim myCell As Range For Each myCell In Target If Not Application.Intersect(myCell, Me.ListObjects("Table2").ListColumns("ID").DataBodyRange) Is Nothing Then If InStr(1, Names("myName").RefersTo, "#") > 0 Then Debug.Print "Lost reference" Delete_record myCount = myCount + 1 Cancelado = True Else If myCell.Text = vbNullString Then Debug.Print "Selecting listObject row and clear contents" Delete_record myCount = myCount + 1 Cancelado = True End If End If Else If Cancelado = False Then If Not Application.Intersect(myCell, Me.Range("Table2[[FIELD1]:[FIELD3]]")) Is Nothing Then Update_record myCell End If End If Next myCell End If CleanUp: On Error Resume Next myCount = 0 Application.EnableEvents = True Exit Sub ExceptionHandling: MsgBox "Error: " & Err.Description Resume CleanUp End Sub Sub Update_record(myCell As Range) On Error GoTo ExceptionHandling Dim tblRow As Long, IdTbl As Long, sField As String, sSQL As String sField = Me.ListObjects("Table2").HeaderRowRange(myCell.Column) tblRow = myCell.Row - Me.ListObjects("Table2").HeaderRowRange.Row IdTbl = Me.ListObjects("Table2").ListColumns("ID").DataBodyRange(tblRow).Value 'Dim cnStr As String 'cnStr = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & sPath & ";Jet OLEDB:Database Password=123" 'Dim cn As ADODB.Connection 'Set cn = New ADODB.Connection 'cn.CursorLocation = adUseServer 'cn.Open cnStr If IdTbl > 0 Then sSQL = "UPDATE MYTABLE SET " & sField & " = '" & myCell.Value & "' WHERE ID = " & Me.ListObjects("Table2").ListColumns("ID").DataBodyRange(tblRow).Value MsgBox sSQL 'Dim cmd As ADODB.Command 'Set cmd = New ADODB.Command 'Set cmd.ActiveConnection = cn 'cmd.CommandText = sSQL 'cmd.Execute , , adCmdText + adExecuteNoRecords ''cn.Execute sSQL, RecsAffected 'alternative to Command ''Debug.Print RecsAffected Else sSQL = "SELECT ID, " & sField & " FROM MYTABLE" MsgBox sSQL 'Dim rst As ADODB.Recordset 'Set rst = New ADODB.Recordset 'rst.Open sSQL, cn, adOpenForwardOnly, adLockOptimistic, adCmdText 'cn.BeginTrans 'rst.AddNew 'rst(sField).Value = myCell.Value 'rst.Update 'IdTbl = rst(0).Value 'MsgBox "New Auto-increment value is: " & IdTbl 'tbl.ListColumns("ID").DataBodyRange(Fila) = IdTbl 'rst.Close 'cn.CommitTrans End If CleanUp: On Error Resume Next cn.Close Exit Sub ExceptionHandling: MsgBox "Error: " & Err.Description & vbLf & Err.Number Resume CleanUp Resume 'for debugging End Sub Sub Delete_record() Dim sSQL As String If IsArray(IdAr) Then sSQL = "DELETE FROM MYTABLE WHERE ID = " & IdAr(myCount) MsgBox sSQL Else sSQL = "DELETE FROM MYTABLE WHERE ID = " & IdAr MsgBox sSQL End If End Sub
MISE À JOUR août 02 '20 Enfin, j'utilise le code ci-dessous pour détecter les lignes supprimées et synchroniser vers le haut d'une table Excel ListObject à une table de base de données: p >
Dim ws As Worksheet, rng As Range, nm As Name Set ws = ActiveSheet Set rng = ws.Range("A2") Names.Add Name:="testName", RefersTo:=rng Set nm = Application.Names("testName") ws.Rows(2).Delete 'Range has been deleted. If InStr(1, nm.RefersTo, "#REF!") > 0 Then 'If InStr(1, Names("testName").RefersTo, "#REF!") > 0 Then Debug.Print "lost reference" Else Debug.Print rng.Address() End If nm.Delete 'Names.Add Name:="testName", RefersTo:=""
Juste au cas où quelqu'un aurait besoin d'une solution pour ce problème et que cela ne dérangerait pas d'utiliser le gestionnaire d'erreurs.
Option Explicit Public Sub Example() Dim rng1 As Range, rng2 As Range Set rng1 = Range("A1") Set rng2 = Range("A2") ActiveSheet.Rows(1).Delete ' rng1 will loose its reference Debug.Print "rng1 has reference? : " & RangeHasReference(rng1) Debug.Print "rng2 has reference? : " & RangeHasReference(rng2) End Sub Private Function RangeHasReference(rng As Range) As Boolean Dim Creator As Long On Error Resume Next Creator = rng.Creator ' try access some property RangeHasReference = (Err.Number <> 424) End Function
Cela semble être un bogue Excel - il devrait décrémenter le nombre de références lorsque l'objet sous-jacent est détruit.
Cela semble vrai si la référence de plage pointe vers une cellule quelque part au milieu de la feuille. Si une ligne ou une colonne est supprimée, la cellule se déplace simplement vers le haut, le bas, la gauche ou la droite. Mais ... si la cellule est dans ce qui a été supprimé, elle ne bouge pas simplement ... elle est partie . Alors quoi ..? Comment serait-il incrémenté ou décrémenté n'importe où ..? Ce serait la pointer vers une cellule différente . Par exemple, que se passe-t-il si la cellule supprimée avait une formule
Sum ()
et que celle vers laquelle elle est redirigée aAvg ()
..? Tout se détacherait dans cette feuille.N'est-ce pas ainsi que les erreurs
#REF!
se produisent?Oui, exactement. J'ai eu un moment d'illumination lorsque vous avez dit cela tout à l'heure, mais en vain. J'ai essayé de tester
Range.Errors
pourxlEmptyCellReferences
=True
mais cela n'a pas fonctionné non plus. Je suppose que l'objetErreurs
a disparu avec tout le reste lorsque la variable a perdu sa référence. Si c'était une cellule qui était toujours là, assise avec la valeurREF!
, cela fonctionnerait.Je doute que quoi que ce soit d'autre que la gestion des erreurs puisse y remédier.
Je soupçonne que @Comintern a raison de dire qu'il s'agit d'un bogue pur et simple dans Excel VBA. Si tel est le cas, il semble peu probable que quelque chose d'autre que le piégeage d'erreur puisse s'en prémunir.
Juste par curiosité: pourquoi voudriez-vous empêcher le piégeage des erreurs? Est-ce considéré comme une mauvaise habitude?
peut-être / peut-être pas un bogue - la seule gestion d'erreur nécessaire est de vérifier s'il s'agit de la première ligne car par défaut, Excel déplace les lignes vers le haut une fois supprimées. Donc, si vous supprimez A2, votre ligne devient A1, il n'y a pas de ligne A0.
@Sorceri J'ai répliqué cela en supprimant d'autres lignes, c'est-à-dire
Set foo = [2: 2]: Me.Rows (2) .Delete: Debug.Print foo.Address
. Lefoo.Address
renvoie un 424.Oui, je ne pense pas que ce soit une erreur; Je pense que c'est WAD. Voir mon premier commentaire de suivi ci-dessus. @Joost J'évite les gestionnaires d'erreurs pour la fonctionnalité, l'élégance et l'éducation. Voir les paragraphes supplémentaires à ce sujet que j'ai ajoutés à la fin de mon OP.
Voici peut-être un autre exemple classique du cas, que j'ai rencontré en parcourant le post J'obtiens toujours un objet requis sur mon code. Pourquoi? . Au fur et à mesure que je m'intéresse au sujet, quelqu'un pourrait-il me référer à de bonnes lectures sur le sujet.
Pour référence, ce n'est pas un bug, c'est une fonctionnalité. Les instances
Range
suivent leurs cellules lorsqu'elles se déplacent dans la feuille; naturellement, lorsque les cellules quittent la feuille, l'instanceRange
devient inutilisable.