
64-битный Office и старый код: Declare PtrSafe, LongPtr и совместимость с VB6
С 2019 года Microsoft ставит 64-битный Office по умолчанию, и макросы с вызовами Windows API начинают падать ещё на этапе компиляции. Разбираем, как править объявления, что делать с общим кодом VB6 и VBA и какие поломки не лечатся одной правкой.
Почему проблема вылезла именно сейчас
Долгие годы корпоративный стандарт был простым: 32-битный Office на любой Windows. Начиная с Office 2019 и Microsoft 365 установщик по умолчанию ставит 64-битную редакцию, и при плановой замене ПК или переустановке пакета макросы, которые работали десятилетиями, перестают открываться.
Сам VBA при этом почти не изменился. Обычный код с циклами, ячейками и коллекциями переносится без правок. Ломаются три вещи: объявления функций Windows API через Declare, всё, что связано с указателями и дескрипторами, и внутрипроцессные компоненты, собранные под 32 бита, включая ActiveX DLL и OCX, написанные на VB6.
Declare без PtrSafe и ошибка компиляции
В 64-битном Office работает VBA 7 (он же есть и в 32-битном Office начиная с 2010). В нём любое объявление Declare обязано содержать ключевое слово PtrSafe. Без него проект не компилируется с сообщением о том, что код нужно обновить для 64-битных систем.
Важно понимать, что PtrSafe ничего не исправляет автоматически. Это обещание разработчика: «я проверил типы параметров». Если просто дописать PtrSafe и оставить Long там, где API ждёт указатель или дескриптор, компиляция пройдёт, а на вызове вы получите неверные данные или падение Excel. Поэтому каждое объявление нужно сверить с документацией функции.
Правило перевода простое: дескрипторы окон и объектов (HWND, HANDLE, HDC, HKEY), указатели (LPVOID, LPSTR, если передаются как адрес) и поля размера, которые в C объявлены как SIZE_T, ULONG_PTR или LONG_PTR, становятся LongPtr. Обычные числа DWORD, UINT, BOOL и INT остаются Long, потому что в Windows x64 они по-прежнему 32-битные.
Условная компиляция для общего кода
Модули часто живут в двух мирах сразу: часть пользователей ещё на 32-битном Office 2016, часть уже на 64-битном Microsoft 365, а тот же .bas иногда подключается в проект VB6. Для этого есть константы условной компиляции VBA7 и Win64.
VBA7 истинна в Office 2010 и новее независимо от разрядности, Win64 истинна только в 64-битном Office. В VB6 обе константы не определены, поэтому компилятор берёт ветку #Else. Неактивные ветки редактор подсвечивает красным, но в сборку они не попадают, и это нормально.
#If VBA7 Then
' Office 2010+ (32 и 64 бита)
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
#If Win64 Then
Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongPtrA" _
(ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
#Else
Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongA" _
(ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
#End If
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(ByRef Destination As Any, ByRef Source As Any, ByVal Length As LongPtr)
#Else
' VB6 и Office до 2010
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" _
(ByVal hWnd As Long, ByVal nIndex As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
#End IfУказатели, структуры и LongLong
Тип LongPtr не самостоятельный: в 32-битном Office он превращается в Long, в 64-битном в LongLong. Сам LongLong существует только в 64-битной сборке, и код, который объявляет переменные As LongLong без проверки #If Win64, не скомпилируется в 32-битном Office.
Самые коварные ошибки сидят в пользовательских типах, которые передаются в API. Структуры вроде OPENFILENAME, BROWSEINFO или NOTIFYICONDATA содержат дескрипторы и указатели, и в 64-битном процессе их размер и выравнивание меняются. Если поле hwndOwner осталось Long, все следующие поля съезжают, а функция возвращает ошибку или портит память.
Отдельно проверьте все места, где размер структуры передаётся вручную числом. Константа 76, честно посчитанная когда-то для 32 бит, в 64-битном процессе неверна. Вычисляйте размер через LenB(переменная), а функции VarPtr, ObjPtr и StrPtr в VBA 7 возвращают LongPtr, поэтому результат нужно хранить в переменной того же типа.
- найдите все Declare поиском по проекту и заведите список функций с исходными сигнатурами из документации;
- отдельно выпишите Type, которые уходят в API, и сверьте каждое поле;
- замените жёстко заданные размеры на LenB;
- прогоните макрос на обеих разрядностях, а не только скомпилируйте его.
Что не исправить правкой объявлений
Процесс одной разрядности не может загрузить внутрипроцессный компонент другой разрядности. Отсюда целый класс поломок, где PtrSafe не поможет. COM-надстройки и ActiveX DLL, собранные на VB6, всегда 32-битные и в 64-битный Excel не загрузятся. То же касается OCX на пользовательских формах: в 64-битном Office нет MSCOMCTL.OCX, поэтому TreeView, ListView, ProgressBar и ImageList из Windows Common Controls 6.0 исчезают с форм, а календарь MSCAL.OCX отсутствует ещё с Office 2010.
Доступ к данным ломается похожим образом. Старый провайдер Microsoft.Jet.OLEDB.4.0 существует только в 32-битном варианте, в 64-битном Office нужно использовать Microsoft.ACE.OLEDB.12.0 или 16.0 соответствующей разрядности. Обратная ситуация тоже частая: 32-битной программе на VB6 нужен 32-битный ACE, а на машине с 64-битным Office штатный установщик Access Database Engine другой разрядности ставиться отказывается.
Как планировать переход
Начните с инвентаризации: соберите все книги с макросами и надстройки, найдите в них Declare, ссылки на OCX (в разделе References и на формах) и строки подключения с Jet. Этот список сразу делится на две группы: то, что лечится правкой кода, и то, что требует замены компонента.
Для первой группы хватит аккуратной работы по чек-листу выше. Для второй варианты такие: заменить контролы стандартными элементами MSForms или вынести интерфейс из Office, переписать COM-надстройку VB6 на .NET с регистрацией в обеих разрядностях, а доступ к данным перевести на ACE нужной разрядности или на внешний сервис. Пока работа идёт, держите 32-битный Office на тех рабочих местах, где без старых надстроек не обойтись, и фиксируйте это в документации к приложению.
Как это делают сегодня
Вопросы и ответы
Достаточно ли дописать PtrSafe ко всем Declare?
Нет. Код скомпилируется, но параметры-указатели и дескрипторы, оставшиеся Long, приведут к неверным результатам или падению Office. Типы нужно проверить по документации каждой функции.
Как узнать разрядность Office из макроса?
Проверьте константу #If Win64 в коде. В интерфейсе это видно в разделе Файл, Учётная запись, О программе: там указано «32-разрядная» или «64-разрядная».
Будет ли VB6-программа работать с 64-битным Excel?
Да, если она управляет Excel через CreateObject или GetObject. Не будет работать загрузка 32-битных DLL и OCX внутрь самого Excel.