Как писать без Select и Activate

Записанный макрос щёлкает по листу, как рука с мышкой. Убираем Select и Activate — и код становится короче, надёжнее и в десятки раз быстрее.

Select — выделить объект (это ровно то, что делает щелчок мышью), Activate — сделать лист или книгу активными. VBA умеет менять ячейку, не выделяя её. Именно так и надо писать в 95% случаев.

Откуда вообще берётся Select

Макрорекордер — прекрасный учитель и ужасный программист. Он записывает не ваши намерения, а ваши движения. Вы щёлкнули по A1 — он записал Range("A1").Select. Вы нажали «жирный» — он записал Selection.Font.Bold = True. Рекордер физически не может записать иначе: он видит только выделение и команды меню.

Человеку так работать приходится — чтобы что-то поменять в ячейке, надо сначала до неё дотянуться мышкой. А вот Excel — не человек. Ему не нужно «доходить» до ячейки, чтобы записать в неё значение. Он может обратиться к любой ячейке любой открытой книги напрямую, мгновенно и не трогая текущее выделение.

Отказ от Select — это главный водораздел между «я записал макрос» и «я пишу на VBA». Разберём его на конкретном примере.

Было: макрос, записанный рекордером

Задача житейская: на листе «Данные» оформить шапку, а на листе «Отчёт» поставить итоговую сумму и подпись.

Sub Макрос1()
'
' Макрос1 Макрос
'
    Sheets("Данные").Select
    Range("A1").Select
    ActiveCell.FormulaR1C1 = "Дата"
    Range("B1").Select
    ActiveCell.FormulaR1C1 = "Клиент"
    Range("C1").Select
    ActiveCell.FormulaR1C1 = "Сумма"
    Range("A1:C1").Select
    Selection.Font.Bold = True
    With Selection.Interior
        .Pattern = xlSolid
        .ThemeColor = xlThemeColorAccent1
        .TintAndShade = 0.8
    End With
    Columns("A:C").Select
    Selection.Columns.AutoFit
    Sheets("Отчёт").Select
    Range("B2").Select
    ActiveCell.FormulaR1C1 = "=SUM(Данные!C[1])"
    Range("A2").Select
    ActiveCell.FormulaR1C1 = "Итого"
    Range("A1").Select
End Sub

Двадцать с лишним строк, из которых половина — переключение внимания Excel. Плюс сюрприз: этот код работает только если начать с листа «Данные», и по дороге он утащит пользователя на другой лист, испортив ему выделение.

Стало: тот же результат, ни одного Select

Sub BuildReport()
    Dim wsData As Worksheet
    Dim wsReport As Worksheet

    ' Set — «запомни, куда смотреть». Дальше wsData и wsReport
    ' работают как пульты от конкретных листов.
    Set wsData = ThisWorkbook.Worksheets("Данные")
    Set wsReport = ThisWorkbook.Worksheets("Отчёт")

    ' шапка — одной группой
    wsData.Range("A1:C1").Value = Array("Дата", "Клиент", "Сумма")

    With wsData.Range("A1:C1")
        .Font.Bold = True
        .Interior.Color = RGB(221, 235, 247)
    End With

    wsData.Columns("A:C").AutoFit

    ' итог на другом листе — без перехода на него
    wsReport.Range("A2").Value = "Итого"
    wsReport.Range("B2").Formula = "=SUM(Данные!C:C)"
    wsReport.Range("B2").Font.Bold = True
End Sub

Одиннадцать содержательных строк вместо двадцати. Макрос запускается с любого листа, ничего не выделяет, не дёргает экран и не отбирает у пользователя место, где стоял курсор.

Построчный разбор

  • Dim wsData As Worksheet — объявляем переменную-«пульт» типа «лист». Тип нужен, чтобы редактор VBA подсказывал свойства по точке (IntelliSense), и чтобы опечатка в .Vlaue ловилась ещё до запуска.
  • Set wsData = ThisWorkbook.Worksheets("Данные")Set обязателен: мы кладём в переменную объект (ссылку на лист), а не значение. ThisWorkbook — «книга, в которой лежит этот код», о ней подробно в следующем уроке.
  • wsData.Range("A1:C1") — точка перед Range означает «Range вот этого листа». Ровно это отличает надёжный код от кода-рулетки: адрес больше не зависит от того, какой лист сейчас открыт у пользователя.
  • .Value = Array("Дата", "Клиент", "Сумма") — три ячейки заполняются одним присваиванием. Размер массива совпал с размером диапазона — Excel разложил значения по местам.
  • With ... End With — блок «всё, что начинается с точки, относится к этому объекту». Избавляет от повторения wsData.Range("A1:C1") в каждой строке. Это не оптимизация ради красоты: Excel вычисляет ссылку один раз, а не заново на каждой строке.

Насколько это быстрее

Разница не косметическая. Проверим на честном замере: запишем числа в 20 000 ячеек двумя способами и засечём время встроенной функцией Timer (она возвращает количество секунд, прошедших с полуночи).

Sub SpeedTest()
    Dim t As Double
    Dim i As Long
    Dim ws As Worksheet
    Set ws = ThisWorkbook.Worksheets("Данные")
    ws.Columns("A:B").ClearContents

    ' --- вариант рекордера: сначала выделить, потом записать ---
    ws.Activate
    t = Timer
    For i = 1 To 20000
        ws.Cells(i, 1).Select
        Selection.Value = i
    Next i
    Debug.Print "Через Select:  " & Format(Timer - t, "0.00") & " сек"

    ' --- прямое обращение ---
    t = Timer
    For i = 1 To 20000
        ws.Cells(i, 2).Value = i
    Next i
    Debug.Print "Без Select:    " & Format(Timer - t, "0.00") & " сек"
End Sub

Результат (окно Immediate, обычный офисный ноутбук — у вас цифры будут отличаться, но соотношение сохранится):

Через Select:  11,40 сек
Без Select:    0,42 сек

Разница примерно в 25 раз — и это на скромных двадцати тысячах ячеек. На реальной выгрузке в 200 000 строк макрос с Select либо будет молотить несколько минут, либо Excel просто перестанет отвечать и пользователь снимет его через диспетчер задач.

Как это работает

Почему выделение такое дорогое? Потому что Select — это не «пометить ячейку в памяти». Это полноценное событие в интерфейсе Excel, и на каждый вызов программа обязана:

  • перерисовать рамку выделения на экране и снять её со старой ячейки;
  • прокрутить лист, если ячейка вне видимой области;
  • обновить поле «Имя» слева от строки формул и саму строку формул;
  • пересчитать содержимое строки состояния (сумма, среднее, количество по выделению);
  • возбудить событие Worksheet_SelectionChange — а если в книге есть обработчик этого события, он выполнится 20 000 раз.

Прямая запись ws.Cells(i, 2).Value = i ничего этого не делает. Она меняет значение в модели данных, и всё.

Вторая, менее очевидная проблема — надёжность. Selection и ActiveCell — это, по сути, глобальные переменные, которые может изменить кто угодно: пользователь щёлкнул мышью во время работы макроса, сработал другой макрос, всплыло диалоговое окно. Код, который читает Selection, зависит от состояния всего приложения, а не только от себя.

Отсюда же растёт знаменитая ошибка «Run-time error 1004: Select method of Range class failed». Причина всегда одна: вы пытаетесь выделить ячейку на листе, который сейчас не активен. Excel не умеет выделять что-то на невидимом листе — сначала надо активировать лист, потом ячейку. Именно поэтому рекордер всегда вставляет Sheets("...").Select перед Range("...").Select. А при прямом обращении wsReport.Range("B2").Value = 100 проблемы просто не существует: значение записывается в неактивный, скрытый и даже защищённый от просмотра лист без единого возражения.

Когда Select и Activate всё-таки нужны

Полностью запрещать их — фанатизм. Есть законные случаи:

  • Вежливое завершение макроса. Оставить пользователя в удобном месте: Application.Goto ws.Range("A1"), Scroll:=True — переключит на лист и прокрутит его к началу. Один раз в конце, а не 20 000 раз в цикле.
  • Работа с окном, а не с данными. Закрепление областей требует активного листа: ws.Activate, затем ws.Range("A2").Select и ActiveWindow.FreezePanes = True. Здесь без активации никак — ActiveWindow по определению работает с активным листом. То же с масштабом (ActiveWindow.Zoom).
  • Отладка. Иногда полезно выделить проблемный диапазон, чтобы глазами увидеть, что макрос считает «данными».

Общее правило: если вы меняете данные или формат — Select не нужен. Если вы управляете тем, что видит пользователь на экране — иногда нужен.

Частые ошибки

  • Механическая замена Select на переменные, но с сохранением ActiveSheet. Строка ActiveSheet.Range("A1").Value = 1 ничем не лучше Range("A1").Value = 1: она всё так же зависит от того, какой лист открыт. Ссылайтесь на конкретный лист через переменную.
  • Копирование через выделение. Range("A1:A10").Select : Selection.Copy : Sheets("Отчёт").Select : Range("A1").Select : ActiveSheet.Paste — пять команд там, где хватит одной: wsData.Range("A1:A10").Copy Destination:=wsReport.Range("A1"). А если нужны только значения без форматов, то и копирования не требуется вовсе: wsReport.Range("A1:A10").Value = wsData.Range("A1:A10").Value.
  • Забытый Set. wsData = ThisWorkbook.Worksheets("Данные") без Set — ошибка 91 или 424. Объекты присваиваются только через Set.
  • «Скрою тормоза через ScreenUpdating». Application.ScreenUpdating = False действительно ускоряет код с Select — но не устраняет причину: события выделения всё равно возбуждаются, а надёжность не улучшается. Отключение перерисовки — полезная добавка к чистому коду, а не замена ему. И не забудьте вернуть True в конце, иначе экран останется замороженным.
  • Точка перед Range потерялась внутри With. В блоке With wsData строка Range("A1") (без точки) обращается к активному листу, а .Range("A1") — к wsData. Одна точка — и совсем другой лист.

Итоги

  • Select/Activate в коде — почти всегда наследие макрорекордера. Excel умеет менять ячейку, не выделяя её.
  • Схема чистого макроса: Dim ws As WorksheetSet ws = ThisWorkbook.Worksheets("Имя")ws.Range(...) / ws.Cells(...).
  • Выигрыш — не только в скорости (в разы и десятки раз), но и в надёжности: макрос перестаёт зависеть от того, какой лист открыт и куда пользователь ткнул мышью.
  • Selection и ActiveCell — глобальное состояние. Читать их в середине макроса — значит доверять данные случайности.
  • Ошибка 1004 «Select method of Range class failed» = попытка выделить ячейку на неактивном листе. При прямом обращении её просто не бывает.
  • Копирование: Источник.Copy Destination:=Приёмник или прямое присвоение .Value = .Value — без выделения и буфера обмена.
  • Законные применения Select остаются: Application.Goto в конце макроса и работа с окном (FreezePanes, Zoom).
Проверьте себя
1. Почему цикл, который перед записью значения делает Cells(i, 1).Select, работает в разы медленнее прямого обращения?
ASelect дополнительно сохраняет книгу на диск после каждой ячейки
BКаждое выделение заставляет Excel перерисовать экран, обновить строку формул и возбудить событие смены выделения
CSelect пересчитывает все формулы книги, а прямая запись — нет
DРазницы в скорости нет, замедление вызвано только антивирусом
2. Активен лист «Данные». Что произойдёт при выполнении строки ThisWorkbook.Worksheets("Отчёт").Range("B2").Value = 100?
AОшибка 1004: нельзя писать в неактивный лист
BЗначение запишется в B2 листа «Данные», потому что он активен
CExcel сначала переключится на лист «Отчёт», а потом запишет значение
DЗначение запишется в B2 листа «Отчёт», активный лист при этом не изменится