Excel VBA: mover una fila a otra hoja según un desplegable (Worksheet_Change)

¿Quieres que al cambiar un estado en un desplegable de Excel la fila entera “salte” automáticamente a otra hoja y se borre del origen? Aquí tienes una guía completa con código VBA listo para usar, más variantes robustas para tablas, mapeos de nombres y prevención de errores.

Índice

Qué vas a conseguir

Cuando selecciones un estado en la columna I (Estado) de la hoja Register, la fila completa (columnas A:M) se moverá automáticamente a la hoja cuyo nombre coincida con el valor elegido en el desplegable (por ejemplo, Zone1). Si más tarde cambias el estado desde la nueva hoja, el registro volverá a moverse a su nuevo destino. La fila desaparece del origen para evitar duplicados.

Requisitos previos

  • Tu columna de estado está en I (o en la que decidas).
  • Las hojas de destino existen y sus nombres coinciden exactamente con los valores del desplegable (espacios, mayúsculas, tildes).
  • El rango que deseas mover es A:M (puedes ajustarlo).
  • Has creado un desplegable (Validación de datos) para el estado.
  • El libro permite macros (guárdalo como .xlsm o .xlsb).

Opción A — Rápida: mueve filas solo cuando cambias el estado en “Register”

Esta opción actúa únicamente si el cambio sucede en la hoja Register (ideal si los estados solo se editan ahí).

  1. Abre el editor de VBA: Alt + F11.
  2. En el Explorador de proyectos, haz doble clic en la hoja Register.
  3. En los desplegables superiores, elige Worksheet y luego Change.
  4. Pega este código:
Private Sub Worksheet_Change(ByVal Target As Range)
    Const STATUS_COL As Long = 9      ' I
    Const FIRST_COL As String = "A"   ' Primer dato de la fila
    Const LAST_COL As String = "M"    ' Último dato de la fila

    On Error GoTo Salir
    If Intersect(Target, Columns(STATUS_COL)) Is Nothing Then Exit Sub
    If Target.CountLarge > 1 Then Exit Sub
    If Len(Target.Value) = 0 Then Exit Sub

    Application.EnableEvents = False

    Dim wsDest As Worksheet
    On Error Resume Next
    Set wsDest = ThisWorkbook.Worksheets(CStr(Target.Value)) ' nombre de hoja = valor del desplegable
    On Error GoTo 0
    If wsDest Is Nothing Then GoTo Salir                     ' si no existe, salir sin error

    Dim rngFila As Range
    Set rngFila = Range(FIRSTCOL & Target.Row & ":" & LASTCOL & Target.Row)

    Dim nextRow As Long
    nextRow = wsDest.Cells(wsDest.Rows.Count, "A").End(xlUp).Row + 1

    rngFila.Copy wsDest.Range("A" & nextRow)
    ' Si quieres quitar solo A:M:
    rngFila.Delete Shift:=xlUp
    ' Si prefieres borrar la fila completa, usa esta línea en su lugar:
    ' Rows(Target.Row).Delete

Salir:
    Application.EnableEvents = True
End Sub

Cuándo elegir esta opción

  • Solo editas el estado en “Register”.
  • Quieres el código más corto posible.

Opción B — Completa: mueve filas aunque cambies el estado desde cualquier hoja

Esta opción intercepta cambios en todo el libro. Si cambias el estado estando ya en Zone1, Zone2, etc., la fila volverá a moverse a la hoja cuyo nombre coincida con el nuevo estado.

  1. En el editor VBA, haz doble clic en ThisWorkbook.
  2. Selecciona Workbook y luego SheetChange.
  3. Pega este código:
Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    Const STATUS_COL As Long = 9      ' I  (cámbialo a 7 si usas la columna G)
    Const FIRST_COL As String = "A"
    Const LAST_COL As String = "M"

    On Error GoTo Fin
    If Target.CountLarge > 1 Then Exit Sub
    If Target.Column <> STATUS_COL Then Exit Sub
    If Len(Target.Value) = 0 Then Exit Sub

    Dim wsDest As Worksheet
    Application.EnableEvents = False

    On Error Resume Next
    Set wsDest = ThisWorkbook.Worksheets(CStr(Target.Value))
    On Error GoTo 0
    If wsDest Is Nothing Then GoTo Fin

    ' Si ya está en la hoja destino, no hacer nada
    If wsDest.Name = Sh.Name Then GoTo Fin

    Dim rngFila As Range
    Set rngFila = Sh.Range(FIRSTCOL & Target.Row & ":" & LASTCOL & Target.Row)

    Dim nextRow As Long
    nextRow = wsDest.Cells(wsDest.Rows.Count, "A").End(xlUp).Row + 1

    rngFila.Copy wsDest.Range("A" & nextRow)
    Sh.Rows(Target.Row).Delete

Fin:
    Application.EnableEvents = True
End Sub

Ventajas

  • Cumple el requisito de mover el registro aunque modifiques el estado desde la hoja en la que esté.
  • Evita registros duplicados, ya que borra la fila del origen.

Personalización esencial

NecesidadCambioEjemplo
Usar otra columna para el estadoModifica STATUS_COLColumna G → STATUS_COL = 7
Mover otro rango de columnasEdita FIRSTCOL y LASTCOLDe B a N → "B" y "N"
Eliminar fila completa en lugar de A:MUsa Rows(Target.Row).DeleteReemplaza la línea Delete del ejemplo
Evitar bucles de eventosMantén la envoltura Application.EnableEventsYa incluido en los ejemplos

Buenas prácticas de datos: el desplegable

  • Usa Validación de datos con una lista de estados (Register, Zone1, Zone2…).
  • Los valores deben coincidir exactamente con los nombres de las hojas destino (ojo con mayúsculas, espacios y tildes).
  • Si deseas que el usuario solo elija valores válidos, marca “Omitir blancos” y “Mostrar mensaje de error”.

Versión robusta con funciones auxiliares (reutilizable y escalable)

Para proyectos más grandes, conviene encapsular la lógica y añadir utilidades como “siguiente fila libre”, mapeo de estados a hojas con nombres distintos, e incluso creación automática de hojas.

Procedimiento central reutilizable

Pega este módulo estándar (Insert > Module) y nómbralo, por ejemplo, modMoverFilas:

Option Explicit

Public Sub MoverFilaPorEstado(ByVal Sh As Worksheet, ByVal fila As Long, _
                              ByVal statusValue As String, _
                              Optional ByVal firstCol As String = "A", _
                              Optional ByVal lastCol As String = "M", _
                              Optional ByVal colBusquedaDestino As String = "A", _
                              Optional ByVal crearSiNoExiste As Boolean = False)

    Dim nombreDestino As String
    nombreDestino = ResolverHojaDestino(statusValue) ' Permite mapeo flexible

    If Len(nombreDestino) = 0 Then Exit Sub

    Dim wsDest As Worksheet
    Set wsDest = ObtenerHoja(nombreDestino, crearSiNoExiste)
    If wsDest Is Nothing Then Exit Sub

    ' Si ya está en la hoja destino, salir
    If wsDest.Name = Sh.Name Then Exit Sub

    Dim rngFila As Range
    Set rngFila = Sh.Range(firstCol & fila & ":" & lastCol & fila)

    Dim nextRow As Long
    nextRow = SiguienteFilaLibre(wsDest, colBusquedaDestino)

    ' Copiar y borrar (valores y formatos)
    rngFila.Copy wsDest.Range("A" & nextRow)
    Sh.Rows(fila).Delete
End Sub

Public Function SiguienteFilaLibre(ByVal ws As Worksheet, ByVal col As String) As Long
    Dim last As Long
    last = ws.Cells(ws.Rows.Count, col).End(xlUp).Row
    If last = 1 And Application.WorksheetFunction.CountA(ws.Rows(1)) = 0 Then
        SiguienteFilaLibre = 1
    Else
        SiguienteFilaLibre = last + 1
    End If
End Function

' Devuelve la hoja correspondiente a un estado. Aquí puedes mapear nombres distintos.
Public Function ResolverHojaDestino(ByVal estado As String) As String
    ' Mapeo directo por defecto (estado = nombre de hoja)
    ResolverHojaDestino = CStr(estado)

    ' -- EJEMPLO DE MAPE0:
    'Select Case UCase$(estado)
    '   Case "ZONA 1": ResolverHojaDestino = "Zone1"
    '   Case "ZONA 2": ResolverHojaDestino = "Zone2"
    '   Case Else: ResolverHojaDestino = CStr(estado)
    'End Select
End Function

Public Function ObtenerHoja(ByVal nombre As String, ByVal crear As Boolean) As Worksheet
    On Error Resume Next
    Set ObtenerHoja = ThisWorkbook.Worksheets(nombre)
    On Error GoTo 0
    If ObtenerHoja Is Nothing And crear Then
        Set ObtenerHoja = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
        ObtenerHoja.Name = nombre
    End If
End Function

Ahora, desde ThisWorkbook, llama a este procedimiento desde el evento general para controlar cambios en cualquier hoja:

Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    Const STATUS_COL As Long = 9   ' I
    On Error GoTo Fin

    If Target.CountLarge > 1 Then Exit Sub
    If Target.Column <> STATUS_COL Then Exit Sub
    If Len(Target.Value) = 0 Then Exit Sub

    Application.EnableEvents = False
    Call MoverFilaPorEstado(Sh, Target.Row, CStr(Target.Value), "A", "M", "A", False)

Fin:
    Application.EnableEvents = True
End Sub

Ventajas de la versión modular

  • Permite mapear estados a hojas con nombres diferentes.
  • Opcionalmente puede crear la hoja si no existe (pon True en crearSiNoExiste).
  • Centraliza la lógica y reduce errores por duplicación de código.

Compatibilidad con Tablas (ListObjects)

Si tus datos están dentro de una Tabla de Excel, borrar filas con Rows(...).Delete elimina filas completas en la hoja y puede deformar la tabla. En su lugar, borra la fila dentro de la tabla:

Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    Const STATUS_COL As Long = 9 ' I
    On Error GoTo Salir

    If Target.CountLarge > 1 Then Exit Sub
    If Target.Column <> STATUS_COL Then Exit Sub
    If Len(Target.Value) = 0 Then Exit Sub

    Application.EnableEvents = False

    Dim wsDest As Worksheet
    Set wsDest = Nothing
    On Error Resume Next
    Set wsDest = ThisWorkbook.Worksheets(CStr(Target.Value))
    On Error GoTo 0
    If wsDest Is Nothing Then GoTo Salir

    Dim firstCol As String: firstCol = "A"
    Dim lastCol As String:  lastCol  = "M"

    Dim fila As Long: fila = Target.Row
    Dim nextRow As Long
    nextRow = wsDest.Cells(wsDest.Rows.Count, "A").End(xlUp).Row + 1

    Sh.Range(firstCol & fila & ":" & lastCol & fila).Copy wsDest.Range("A" & nextRow)

    ' Si Target está en una Tabla, borrar la fila de la tabla:
    If Not Target.ListObject Is Nothing Then
        Dim lo As ListObject, lr As ListRow
        Set lo = Target.ListObject
        Dim idx As Long
        idx = fila - lo.HeaderRowRange.Row
        If idx >= 1 And idx <= lo.ListRows.Count Then
            lo.ListRows(idx).Delete
        Else
            Sh.Rows(fila).Delete
        End If
    Else
        Sh.Rows(fila).Delete
    End If

Salir:
    Application.EnableEvents = True
End Sub

Copiar valores solamente, preservar fórmulas o mover con “Cortar”

  • Solo valores: si no quieres llevarte fórmulas al destino, usa PasteSpecial valores: rngFila.Copy wsDest.Range("A" & nextRow).PasteSpecial xlPasteValues
  • Incluir formatos: añade otro PasteSpecial para formatos: wsDest.Range("A" & nextRow).PasteSpecial xlPasteFormats
  • “Cortar” en lugar de copiar+borrar: puede ser más rápido y mantiene mejor ciertos formatos. Aun así, borra la fila origen para no dejar huecos: rngFila.Cut Destination:=wsDest.Range("A" & nextRow) Sh.Rows(Target.Row).Delete

Rendimiento y estabilidad

Para listas grandes, puedes mejorar la velocidad desactivando actualización de pantalla y cálculo mientras se ejecuta la macro:

Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
' ... tu lógica ...
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True

Recuerda restaurar siempre estos valores al final, incluso si hay errores (úsalo dentro de bloques con On Error y una etiqueta de salida).

Prevención de bucles y errores

  • Reentradas: rodea tu lógica con Application.EnableEvents = False/True. Ya está aplicado en los ejemplos.
  • Cambios masivos: los ejemplos ignoran cambios con múltiples celdas (Target.CountLarge > 1).
  • Filas de encabezado: si no quieres reacción en encabezados, añade If Target.Row = 1 Then Exit Sub.
  • Estados vacíos: se ignoran con Len(Target.Value) = 0.
  • Hojas inexistentes: el código sale silenciosamente sin error. Si lo prefieres, créala automáticamente.

Crear la hoja automáticamente si no existe

Si haces crecer la lista de estados con frecuencia, puede ser útil crear la hoja destino en tiempo real (opcional):

Set wsDest = ObtenerHoja(CStr(Target.Value), True) ' True = crear si no existe

Auditoría: registrar los movimientos

Para rastrear cuándo y desde dónde se movió cada fila, puedes llevar un log en una hoja “Historial”:

Sub RegistrarMovimiento(ByVal origen As String, ByVal destino As String, ByVal filaOrigen As Long)
    Dim ws As Worksheet, r As Long
    Set ws = ObtenerHoja("Historial", True)
    r = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1
    ws.Cells(r, "A").Value = Now
    ws.Cells(r, "B").Value = origen
    ws.Cells(r, "C").Value = destino
    ws.Cells(r, "D").Value = filaOrigen
End Sub

Invoca RegistrarMovimiento Sh.Name, wsDest.Name, Target.Row justo antes de borrar la fila origen.

Mapeo de estados a hojas con nombres distintos

Si tu Validación de datos muestra textos “bonitos” (“Zona 1”) pero tus hojas se llaman diferente (“Zone1”), usa un Select Case o un Diccionario en ResolverHojaDestino:

Public Function ResolverHojaDestino(ByVal estado As String) As String
    Select Case Trim$(UCase$(estado))
        Case "REGISTRAR", "REGISTER": ResolverHojaDestino = "Register"
        Case "ZONA 1": ResolverHojaDestino = "Zone1"
        Case "ZONA 2": ResolverHojaDestino = "Zone2"
        Case Else: ResolverHojaDestino = CStr(estado)
    End Select
End Function

Preguntas frecuentes (FAQ)

¿Se puede deshacer (Ctrl+Z) después de que la macro mueva la fila?
Por norma general, no. Las macros limpian la pila de deshacer. Usa la hoja “Historial” y realiza pruebas en una copia.

¿Qué pasa si la hoja destino está protegida?
Podrías obtener un error al pegar o al insertar filas. Quita la protección temporalmente en el procedimiento y vuelve a protegerla al final.

¿Funcionará si el estado proviene de una fórmula?
WorksheetChange no se dispara por cambios de resultados de fórmula; necesitarías WorksheetCalculate (lógica distinta) o desencadenar el cambio en una celda editable por el usuario.

¿Puedo mover más/menos columnas?
Sí. Cambia FIRSTCOL y LASTCOL.

¿Puedo mover la fila a un libro distinto?
Sí, pero requiere referenciar otro Workbook y manejar aperturas y guardados. Empieza resolviendo wsDest en el otro libro.

Checklist rápido antes de usar en producción

  • El estado está en la columna correcta (STATUS_COL coincide).
  • El rango de columnas a mover es el que necesitas (A:M por defecto).
  • Los nombres de hojas y los valores del desplegable coinciden o están mapeados.
  • La protección de hoja/libro no bloquea borrados o pegados.
  • Eventos y pantalla se restauran aunque algo falle (bloques On Error y etiquetas de salida).
  • Has probado el comportamiento con tres estados y varios registros.

Solución al caso “no pasa nada” al usar la columna G

Si cambias el estado en G y no sucede nada:

  1. Verifica que el código esté en el módulo correcto (la hoja “Register” para la Opción A, o ThisWorkbook para la Opción B).
  2. Cambia STATUS_COL a 7.
  3. Asegúrate de que el valor del desplegable exista como hoja real.
  4. No pegues múltiples celdas a la vez (el código ignora cambios masivos).
  5. Comprueba que Application.EnableEvents no está desactivado por otro procedimiento.
  6. Revisa si hay protección de hoja/libro que impida borrar filas.

Tabla comparativa: Opción A vs Opción B

CriterioOpción A (Worksheet)Opción B (Workbook)
Dónde actúaSolo en RegisterEn todo el libro
ComplejidadBajaMedia
Riesgo de efectos colateralesMenorMayor (controla más casos)
Edición desde hojas destinoNo mueveSí mueve
EscalabilidadLimitadaAlta (recomendado con versión modular)

Ejemplo completo final (libro entero, con mapeo y log opcional)

Este es un ejemplo “todo en uno” que puedes adaptar rápidamente. Pega el módulo y el evento:

Módulo estándar modMoverFilas

Option Explicit

Public Sub MoverFilaPorEstadoConLog(ByVal Sh As Worksheet, ByVal fila As Long, \_
ByVal estado As String, \_
Optional ByVal firstCol As String = "A", \_
Optional ByVal lastCol As String = "M", \_
Optional ByVal crearSiNoExiste As Boolean = False, \_
Optional ByVal registrar As Boolean = True)```
Dim destino As String
destino = ResolverHojaDestino(estado)
If Len(destino) = 0 Then Exit Sub

Dim wsDest As Worksheet
Set wsDest = ObtenerHoja(destino, crearSiNoExiste)
If wsDest Is Nothing Then Exit Sub

If Sh.Name = wsDest.Name Then Exit Sub

Dim rng As Range
Set rng = Sh.Range(firstCol & fila & ":" & lastCol & fila)

Dim nextRow As Long
nextRow = SiguienteFilaLibre(wsDest, "A")

rng.Copy wsDest.Range("A" & nextRow)
Sh.Rows(fila).Delete

If registrar Then RegistrarMovimiento Sh.Name, wsDest.Name, fila
```
End Sub

Public Function ResolverHojaDestino(ByVal estado As String) As String
Select Case Trim\$(UCase\$(estado))
Case "REGISTER": ResolverHojaDestino = "Register"
Case "ZONE1", "ZONA 1": ResolverHojaDestino = "Zone1"
Case "ZONE2", "ZONA 2": ResolverHojaDestino = "Zone2"
Case Else: ResolverHojaDestino = CStr(estado)
End Select
End Function

Public Function ObtenerHoja(ByVal nombre As String, ByVal crear As Boolean) As Worksheet
On Error Resume Next
Set ObtenerHoja = ThisWorkbook.Worksheets(nombre)
On Error GoTo 0
If ObtenerHoja Is Nothing And crear Then
Set ObtenerHoja = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
ObtenerHoja.Name = nombre
End If
End Function

Public Function SiguienteFilaLibre(ByVal ws As Worksheet, ByVal col As String) As Long
Dim last As Long
last = ws.Cells(ws.Rows.Count, col).End(xlUp).Row
If last < 1 Then last = 1
SiguienteFilaLibre = IIf(Application.WorksheetFunction.CountA(ws.Rows(1)) = 0, 1, last + 1)
End Function

Public Sub RegistrarMovimiento(ByVal origen As String, ByVal destino As String, ByVal filaOrigen As Long)
Dim ws As Worksheet, r As Long
Set ws = ObtenerHoja("Historial", True)
r = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row + 1
ws.Cells(r, "A").Value = Now
ws.Cells(r, "B").Value = origen
ws.Cells(r, "C").Value = destino
ws.Cells(r, "D").Value = filaOrigen
End Sub </code></pre>

<h3>Evento del libro <em>ThisWorkbook</em></h3>
<pre><code class="language-vba">Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
    Const STATUS_COL As Long = 9 ' I
    On Error GoTo Fin```
If Target.CountLarge &gt; 1 Then Exit Sub
If Target.Column &lt;&gt; STATUS_COL Then Exit Sub
If Len(Target.Value) = 0 Then Exit Sub

Application.EnableEvents = False
Application.ScreenUpdating = False

' Si trabajas con tablas y necesitas borrar dentro de la tabla, puedes adaptar aquí
Call MoverFilaPorEstadoConLog(Sh, Target.Row, CStr(Target.Value), "A", "M", False, True)
```
Fin:
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub 

Casos de uso recomendados

  • Embudo de atención al cliente: Register → Asignaciones por zona.
  • Gestión de tareas: Backlog → En curso → Hecho, con hojas por estado.
  • RR. HH.: Candidatos → Entrevista → Oferta → Contratados.
  • Logística: Pedidos → Preparación → En tránsito → Entregados.

Resumen

Con el evento Change a nivel de hoja o libro y unas pocas líneas de VBA, puedes automatizar el movimiento de registros en Excel según un estado elegido en un desplegable. Empieza con la Opción A si solo editas en Register; pasa a la Opción B (o a la versión modular) cuando necesites que el sistema funcione desde cualquier hoja, con mapeos, tablas y registro de auditoría.


Plantilla rápida de decisiones

  • ¿Editas solo en Register? → Opción A.
  • ¿Editas desde cualquier hoja? → Opción B o versión modular.
  • ¿Estados ≠ nombres de hojas? → Mapeo en ResolverHojaDestino.
  • ¿Datos en Tabla? → Borrado con ListRows.Delete.
  • ¿Necesitas historial? → Usa RegistrarMovimiento.
Índice