Как писать без 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 Worksheet→Set 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).