🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

VBA Table (ListObject) en Excel — añade filas y deja de perseguir la última fila

|

VBA Table (ListObject) en Excel — añade filas y deja de perseguir la última fila

TL;DR — Una tabla de Excel es un ListObject: un rango que sigue la pista de su propio encabezado, su cuerpo de datos y sus bordes, y se autoexpande cuando añades datos. Crea una con ws.ListObjects.Add(xlSrcRange, range, , xlYes) y ponle nombre. Direcciona sus partes por nombre — .HeaderRowRange, .DataBodyRange, .ListColumns("Amount").DataBodyRange — en lugar de calcular direcciones. Añade una fila con lo.ListRows.Add, que da formato y extiende las fórmulas automáticamente. La única trampa: .DataBodyRange es Nothing en una tabla vacía (solo encabezado), así que protege tus bucles con If Not lo.DataBodyRange Is Nothing.

Dim lo As ListObject, r As ListRow

Set lo = Worksheets("Data").ListObjects.Add( _
    SourceType:=xlSrcRange, _
    Source:=Worksheets("Data").Range("A1").CurrentRegion, _
    XlListObjectHasHeaders:=xlYes)
lo.Name = "tblSales"

Set r = lo.ListRows.Add                       ' anade una fila nueva al final
r.Range.Cells(1, 1).Value = "North"
r.Range.Cells(1, 2).Value = 1200
' una columna de formula se rellena sola, usando referencias estructuradas
lo.ListColumns("Total").DataBodyRange.Formula = "=[@Qty]*[@Price]"

La mitad de todo el VBA «de verdad» son las mismas tres líneas: encontrar la última fila usada, escribir debajo, extender las fórmulas. Cada una de esas líneas eres tú haciendo una contabilidad que la hoja podría hacer sola. Ese es todo el sentido de una tabla de Excel: una Table es un rango que conoce sus propios bordes. Sabe dónde acaba su encabezado y dónde empiezan sus datos, crece cuando añades una fila, y mantiene sus propias columnas con nombre. Convierte un rango en un ListObject una vez y la caza de la última fila, la frágil construcción de cadenas A2:A" & lastRow, y el bug de «inserté una fila y la fórmula no la siguió» desaparecen todos a la vez. Todo lo de abajo es esa única idea aplicada.

Lo que aprenderás

  • El modelo mental — una Table (ListObject) conoce sus propios bordes y se autoexpande
  • Convertir un rango en una Table con ListObjects.Add y ponerle nombre
  • Direccionar las partes por nombre en lugar de calcular direcciones
  • Añadir y borrar filas de la forma fiable con ListRows
  • La trampa de que DataBodyRange es Nothing en una tabla vacía
  • Fórmulas con referencias estructuradas que sobreviven a las inserciones, y recorrer las filas

El modelo mental: un rango que conoce sus propios bordes

Un rango normal es tonto respecto a su propia extensión — Range("A1:D100") son 100 filas porque tú lo dijiste, y sigue siendo 100 filas por mucho que añadas. Un ListObject es distinto: es un rango con conciencia de sí mismo. Recuerda que la fila 1 es un encabezado, que el cuerpo de datos es todo lo que hay debajo, y que su borde derecho es la última columna. Cuando escribes una fila al final, la absorbe dentro de sí mismo y extiende su formato y sus fórmulas para que encajen.

Dim lo As ListObject
Set lo = Worksheets("Data").ListObjects("tblSales")

Debug.Print lo.ListRows.Count          ' filas de datos - sin End(xlUp)
Debug.Print lo.Range.Address           ' toda la tabla, del encabezado a la ultima fila
Debug.Print lo.DataBodyRange.Address   ' solo los datos, sin encabezado

Como el objeto sigue la pista de sus propios bordes, nunca vuelves a preguntar «¿cuántas filas tiene esto?» — se lo preguntas a la Table. Ese único cambio es lo que quita el ritual de End(xlUp) de tu código, y es por lo que una Table es el cimiento correcto debajo de una tabla dinámica o un gráfico: esos objetos leen un origen que ahora crece solo.

Convertir un rango en una Table

Convierte un rango existente en una Table con ListObjects.Add, y luego ponle nombre para que tanto tu código como cualquier fórmula puedan referirse a él con claridad:

Dim lo As ListObject
Set lo = Worksheets("Data").ListObjects.Add( _
    SourceType:=xlSrcRange, _
    Source:=Worksheets("Data").Range("A1").CurrentRegion, _
    XlListObjectHasHeaders:=xlYes)
lo.Name = "tblSales"                 ' ahora referenciable como tblSales en todas partes

CurrentRegion es el truco útil para Source — agarra todo el bloque contiguo alrededor de A1, así no codificas a mano la extensión. xlYes le dice a Excel que la primera fila son encabezados (equivócate en esto y tus nombres de columna se convierten en "Column1", "Column2"). El nombre no es decoración: tblSales es como alcanzas la Table desde otra hoja (Range("tblSales")), como leen las referencias estructuradas (tblSales[Amount]), y como la apunta un caché de tabla dinámica. Ponle nombre a cada Table que crees en código; un Table1 sin nombre es un número mágico esperando a romperse.

Direccionar las partes por nombre

Aquí es donde una Table se gana el sueldo. Cada parte de ella tiene una propiedad con nombre, así que dejas de traducir «los importes» a D2:D" & lastRow:

lo.HeaderRowRange                          ' las celdas de encabezado
lo.DataBodyRange                           ' todas las filas de datos, sin encabezado
lo.ListColumns("Amount").DataBodyRange     ' los datos de una columna
lo.ListRows(1).Range                       ' una fila entera
lo.Range                                   ' todo, encabezado + cuerpo (+ totales)

Compara lo.ListColumns("Amount").DataBodyRange con el equivalente de rango normal — encontrar la columna «Amount» rastreando el encabezado, encontrar la última fila, y coser una cadena de dirección. La versión con Table sigue funcionando después de que insertes una columna, no renombres nada, y añadas mil filas. Activa una fila de totales con lo.ShowTotals = True y aparece también lo.TotalsRowRange; fija el total de una columna con lo.ListColumns("Amount").TotalsCalculation = xlTotalsCalculationSum.

Añadir y borrar filas de la forma fiable

Para añadir una fila al final, usa ListRows.Add — nunca escribas en la celda de debajo de la tabla con la esperanza de que la absorba:

Dim r As ListRow
Set r = lo.ListRows.Add                 ' anade al final, devuelve la fila nueva
r.Range.Cells(1, 1).Value = "North"
r.Range.Cells(1, 2).Value = 1200
' insertar en medio en su lugar: lo.ListRows.Add Position:=3

ListRows.Add devuelve el nuevo ListRow, le extiende encima el formato y las fórmulas de la Table, y hace crecer cualquier tabla dinámica o gráfico ligado a la Table. Escribir en la celda de debajo de la Table con Cells es la trampa clásica: a veces la Table se autoexpande para tragársela, a veces no (depende de la opción «Incluir en la tabla» y de si la fila es de verdad adyacente), así que el comportamiento es no determinista de una máquina a otra. Borra por índice de la misma manera limpia — lo.ListRows(2).Delete quita una fila de datos y cierra el hueco, a diferencia de desplazar celdas a mano.

La trampa de la tabla vacía y recorrer las filas

Aquí está la trampa del ListObject que lanza el error de tiempo de ejecución 91: cuando una Table solo tiene su fila de encabezado y ningún dato, .DataBodyRange es Nothing, no un rango vacío. Recórrelo a ciegas y te estrellas:

' SEGURO - protege antes de tocar DataBodyRange
If Not lo.DataBodyRange Is Nothing Then
    Dim cell As Range
    For Each cell In lo.ListColumns("Amount").DataBodyRange
        cell.Value = cell.Value * 1.1
    Next cell
End If

Protege cualquier código que lea DataBodyRange en una Table que pueda estar vacía. Para recorrer las filas como objetos, itera ListRows — que está vacío de forma natural (cero iteraciones) en una tabla vacía, así que no necesita protección:

Dim lr As ListRow
For Each lr In lo.ListRows
    Debug.Print lr.Range.Cells(1, 1).Value
Next lr

Para las fórmulas de columna, escribe una sola vez en toda la columna con una referencia estructurada y deja que se rellene: lo.ListColumns("Total").DataBodyRange.Formula = "=[@Qty]*[@Price]". La sintaxis [@Qty] significa «la Qty de esta fila», sobrevive a las filas insertadas y a las columnas reordenadas, y se lee mucho mejor que =B2*C2. Consulta VBA Formula para saber cómo se comportan las referencias estructuradas en .Formula, y VBA Range para los fundamentos de rangos que hay debajo de una Table.

Cómo ayuda ExcelMaster

Los errores de Table que hacen perder tiempo son sutiles: el bucle que se estrelló en una tabla vacía, la fila escrita debajo de la Table que no se absorbió, el informe que aún busca la última fila con End(xlUp) cuando la Table ya la sabía. Ninguno es una errata — son código que funciona peleándose con una estructura que estaba pensada para ayudar.

ExcelMaster trata una Table como el objeto que es. Pídele «añade las filas de este mes y suma la columna de importes», y añade con ListRows.Add, protege DataBodyRange antes de recorrerlo, escribe las fórmulas de columna como referencias estructuradas que sobreviven a las inserciones, y direcciona las columnas por nombre para que una hoja reordenada no rompa la macro. Tú describes el cambio en los datos; él usa los propios bordes de la Table en lugar de recalcularlos — y cada tabla dinámica y gráfico que apunta a esa Table se actualiza gratis.

Preguntas frecuentes

¿Cómo creo una tabla (ListObject) en VBA?

Convierte un rango con ListObjects.Add: Set lo = ws.ListObjects.Add(SourceType:=xlSrcRange, Source:=ws.Range("A1").CurrentRegion, XlListObjectHasHeaders:=xlYes), y luego ponle nombre con lo.Name = "tblSales". CurrentRegion agarra todo el bloque contiguo para que no codifiques a mano la extensión, y xlYes le dice a Excel que la primera fila contiene encabezados.

¿Cómo añado una fila a una tabla de Excel en VBA?

Usa lo.ListRows.Add, que añade una fila al final, la devuelve como ListRow, y extiende el formato y las fórmulas de la Table: Set r = lo.ListRows.Add: r.Range.Cells(1, 2).Value = 1200. Inserta en medio con lo.ListRows.Add Position:=3. No escribas en la celda de debajo de la Table con Cells — que se absorba o no es no determinista.

¿Por qué DataBodyRange lanza un error en VBA?

Porque una Table con solo una fila de encabezado y ningún dato tiene DataBodyRange igual a Nothing, no un rango vacío, así que cualquier código que lo toque lanza el error de tiempo de ejecución 91. Protégelo: If Not lo.DataBodyRange Is Nothing Then ... End If. Como alternativa, itera lo.ListRows, que simplemente itera cero veces en una tabla vacía y no necesita protección.

¿Cómo referencio una columna de una tabla en VBA?

Usa ListColumns: lo.ListColumns("Amount").DataBodyRange son los datos de esa columna sin el encabezado, y lo.ListColumns("Amount").Range incluye el encabezado. En las fórmulas, usa referencias estructuradas — lo.ListColumns("Total").DataBodyRange.Formula = "=[@Qty]*[@Price]" — donde [@Qty] significa «la Qty de esta fila» y sobrevive a las filas insertadas y a las columnas reordenadas.

¿Cómo dejo de usar End(xlUp) para encontrar la última fila?

Convierte el rango en una Table. Un ListObject sigue la pista de sus propios bordes, así que lo.ListRows.Count da el número de filas, lo.DataBodyRange da los datos, y lo.ListRows.Add añade sin ninguna aritmética de direcciones. La Table se autoexpande a medida que añades filas, que es exactamente la contabilidad que End(xlUp) hacía a mano.

Probado en

Probado en: Excel 365 (Windows 11), VBA 7.1 — última verificación el 02/09/2026.

Guías relacionadas: VBA Pivot Table · VBA Chart · VBA Range · VBA Formula · VBA AutoFilter