
Как распространять VB6-приложение, использующее Excel: ссылки, позднее связывание, версии Office
Программа с выгрузкой в Excel отлично работает у разработчика и падает у пользователя с другой версией Office. Разбираем, как устроена ссылка на библиотеку Excel и как сделать дистрибутив, независимый от версии.
Откуда берётся проблема
Когда в проект добавляется ссылка Microsoft Excel 11.0 Object Library, VB6 записывает в файл .vbp идентификатор библиотеки типов Excel и её версию. Компилятор использует описания интерфейсов из этой библиотеки, и итоговый EXE знает о конкретных методах и их номерах.
Интерфейсы Excel устроены так, что новые версии сохраняют совместимость со старыми. Поэтому программа, собранная со ссылкой на Excel 2003, как правило, работает и с Excel 2010. Обратное неверно: если собрать проект на машине с Excel 2016 и использовать появившиеся там методы, у пользователя с Excel 2003 программа упадёт с ошибкой 438 или вообще не создаст объект.
Ещё хуже ситуация в среде разработки: если открыть проект на компьютере с другой версией Office, VB6 пометит ссылку как MISSING, и компиляция остановится на первом же встроенном вызове вроде Left$ с сообщением «Can't find project or library».
Что нельзя класть в дистрибутив
Package and Deployment Wizard, анализируя зависимости, может предложить включить в пакет EXCEL.EXE или файлы библиотеки типов Excel. Делать этого нельзя ни технически, ни юридически. Excel не является распространяемым компонентом, его лицензия не разрешает поставку в составе вашего установщика, а подмена системных файлов Office у пользователя ломает сам Office.
Правильная позиция дистрибутива: Excel это внешнее требование, как операционная система. Установщик проверяет его наличие или программа сообщает об отсутствии при первой попытке экспорта, а сами файлы Office в пакет не попадают.
Раннее и позднее связывание
При раннем связывании переменная объявляется конкретным типом Excel.Application. Это даёт IntelliSense, проверку при компиляции и немного более быстрые вызовы. Цена: зависимость от библиотеки типов и проблемы с версиями.
При позднем связывании переменная имеет тип Object, а объект создаётся через CreateObject("Excel.Application"). Программа находит Excel по ProgID в реестре во время выполнения и работает с любой установленной версией. Скорость вызовов чуть ниже, но для отчётов это незаметно, особенно если данные пишутся массивом.
Именованные константы Excel при позднем связывании недоступны, поэтому их нужно объявить самостоятельно. Лучший рабочий компромисс: разрабатывать с ранним связыванием ради подсказок, а собирать релиз с поздним. Условная компиляция позволяет переключаться одной константой проекта.
' В свойствах проекта: Conditional Compilation Arguments = EARLY_BIND = 1
' Для релиза: EARLY_BIND = 0 и ссылку на Excel из проекта убрать
#If EARLY_BIND Then
Private m_xl As Excel.Application
#Else
Private m_xl As Object
#End If
' Константы Excel объявляем сами: при позднем связывании их нет
Public Const xlWorkbookNormal As Long = -4143 ' .xls в Excel 2003 и старше
Public Const xlExcel8 As Long = 56 ' .xls в Excel 2007 и новее
Public Const xlOpenXMLWorkbook As Long = 51 ' .xlsx
Public Const xlCenter As Long = -4108Проверка наличия Excel и его версии
Если Excel не установлен или повреждена его регистрация, CreateObject вызывает ошибку 429 «ActiveX component can't create object». Перехватите её и покажите понятное сообщение вместо системного.
Свойство Application.Version возвращает строку с номером версии: 11.0 для Excel 2003, 12.0 для 2007, 14.0 для 2010, 15.0 для 2013 и 16.0 для всех более новых версий, включая Microsoft 365. По ней выбирается формат сохранения: в Excel 2003 нет xlsx, а в новых версиях xls сохраняется только с явным кодом 56.
Public Function GetExcel() As Object
On Error Resume Next
Set GetExcel = CreateObject("Excel.Application")
If Err.Number <> 0 Then
Set GetExcel = Nothing
MsgBox "Для выгрузки нужен Microsoft Excel. Установите его " & _
"или сохраните отчёт в CSV.", vbExclamation
End If
End Function
' Код формата для SaveAs с учётом версии и расширения
Public Function SaveFormat(ByVal xl As Object, ByVal strPath As String) As Long
If Val(xl.Version) < 12 Then
SaveFormat = xlWorkbookNormal ' Excel 2003: только xls
ElseIf LCase$(Right$(strPath, 5)) = ".xlsx" Then
SaveFormat = xlOpenXMLWorkbook
Else
SaveFormat = xlExcel8
End If
End Function64-битный Office и провайдер ACE
Частый вопрос при переходе пользователей на 64-битный Office: будет ли работать 32-битная программа на VB6. Для автоматизации ответ положительный. Excel это COM-сервер в отдельном процессе, и обмен между 32-битным клиентом и 64-битным Excel обеспечивает сама Windows, код менять не нужно.
Иначе обстоит дело с компонентами, которые загружаются внутрь процесса программы. Если приложение читает файлы Excel не через автоматизацию, а через провайдер Microsoft.ACE.OLEDB.12.0, ему нужна 32-битная версия ACE. Поставить её рядом с 64-битным Office штатный установщик не даёт, и чтение файлов перестаёт работать. Для старого формата xls можно использовать провайдер Jet 4.0, который всегда 32-битный и есть в Windows, а для xlsx выгоднее перейти на автоматизацию или внешнюю утилиту.
Чеклист перед выпуском
Перед сборкой установщика для программы с Excel пройдите по списку:
- в релизной сборке нет ссылки на Excel Object Library, все переменные Excel объявлены как Object;
- константы Excel объявлены в модуле проекта, а не берутся из библиотеки;
- в пакете установки нет EXCEL.EXE, библиотек типов и других файлов Office;
- при отсутствии Excel программа показывает понятное сообщение и предлагает альтернативу, например CSV;
- формат сохранения выбирается по Application.Version;
- программа проверена хотя бы на самой старой и самой новой поддерживаемых версиях Office;
- после экспорта не остаётся процессов EXCEL.EXE, все обращения идут через явные переменные.
Как это делают сегодня
Вопросы и ответы
Почему VB6 ругается на Left$ после открытия проекта на другой машине?
Одна из ссылок проекта помечена как MISSING, обычно это библиотека Excel другой версии. Откройте Project, References, снимите отсутствующую ссылку и выберите имеющуюся или переведите код на позднее связывание.
Работает ли 32-битная VB6-программа с 64-битным Excel?
Да, если используется автоматизация через CreateObject. Excel работает в отдельном процессе, и разрядность не важна. Проблемы возникают только с внутрипроцессными компонентами вроде провайдера ACE OLEDB.
Насколько позднее связывание медленнее раннего?
Каждый вызов проходит через IDispatch и чуть дороже, но для типичной выгрузки разница незаметна. Гораздо больше влияет способ записи: массив в Range.Value быстрее поячеечного цикла в десятки раз при любом связывании.