Подобные вопросы часто можно встретить в конференциях Fidonet, посвящённых программированию на Visual C++. Как правило, после некоторого обсуждения, фидошная общественность приходит к мнению, что лучшее решение - использование директивы #import.
В данной статье я попытаюсь объяснить то, как работает эта директива и привести несколько примеров её использования. Надеюсь, после этого вы тоже найдёте её полезной.
Директива #import введена в Visual C++, начиная с версии 5.0. Её основное назначение облегчить подключение и использование интерфейсов COM, описание которых реализовано в библиотеках типов.
Полное описание директивы приведено в MSDN в одной единственной статье, которую можно найти по указателю, введя ключевое слово #import или по содержанию:
MSDN Library Visual C++ Documentation Using Visual C++ Visual C++ Programmer's Guide Preprocessor Reference The Preprocessor Preprocessor Directives The #import Directive
Библиотека типов представляет собой файл или компонент внутри другого файла, который содержит информацию о типе и свойствах COM объектов. Эти объекты представляют собой, как правило, объекты OLE автоматизации. Программисты, которые пишут на Visual Basic'е, используют такие объекты, зачастую сами того не замечая. Это связано с тем, что поддержка OLE автоматизации вляется неотъемлемой частью VB и при этом создаётся иллюзия того, что эти объекты также являются частью VB.
Добиться такого же эффекта при работе на C++ невозможно (да и нужно ли?), но можно упростить себе жизнь, используя классы представляющие обёртки (wrappers) интерфейса IDispatch. Таких классов в библиотеках VC имеется несколько.
Последний способ доступа к объектам OLE Automation является наиболее предпочтительным, так как предоставляет достаточно полный и довольно удобный набор классов.
Рассмотрим пример.
Создадим IDL-файл, описывающий библиотеку типов. Наш пример будет содержать
описание одного перечисляемого типа SamplType и описание одного объекта
ISamplObject, который в свою очередь будет содержать одно свойство
Prop и один метод Method.
Sampl.idl:
// Sampl.idl : IDL source for Sampl.dll // This file will be processed by the MIDL tool to // produce the type library (Sampl.tlb) and marshalling code. import "oaidl.idl"; import "ocidl.idl"; [ uuid(37A3AD11-F9CC-11D3-8D3C-0000E8D9FD76), version(1.0), helpstring("Sampl 1.0 Type Library") ] library SAMPLLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); typedef enum { SamplType1 = 1, SamplType2 = 2 } SamplType; [ object, uuid(37A3AD1D-F9CC-11D3-8D3C-0000E8D9FD76), dual, helpstring("ISamplObject Interface"), pointer_default(unique) ] interface ISamplObject : IDispatch { [propget, id(1)] HRESULT Prop([out, retval] SamplType *pVal); [propput, id(1)] HRESULT Prop([in] SamplType newVal); [id(2)] HRESULT Method([in] VARIANT Var,[in] BSTR Str,[out, retval] ISamplObject** Obj); }; [ uuid(37A3AD1E-F9CC-11D3-8D3C-0000E8D9FD76), helpstring("SamplObject Class") ] coclass SamplObject { [default] interface ISamplObject; }; };
После подключения соответствующей библиотеки типов с помощью директивы #import будут созданы два файла, которые генерируются в выходном каталоге проекта. Это файл sampl.tlh, содержащий описание классов, и файл sampl.tli, который содержит реализацию членнов классов. Эти файлы будут включены в проект автоматически. Ниже приведено содержимое этих файлов.
Sampl.tlh:
// Created by Microsoft (R) C/C++ Compiler Version 12.00.8472.0 (53af584f). // // sampl.tlh // // C++ source equivalent of Win32 type library Debug\sampl.dll // compiler-generated file created 03/14/00 at 20:43:40 - DO NOT EDIT! #pragma once #pragma pack(push, 8) #include <comdef.h> namespace SAMPLLib { // Forward references and typedefs struct __declspec(uuid("37a3ad1d-f9cc-11d3-8d3c-0000e8d9fd76")) /* dual interface */ ISamplObject; struct /* coclass */ SamplObject; // Smart pointer typedef declarations _COM_SMARTPTR_TYPEDEF(ISamplObject, __uuidof(ISamplObject)); // Type library items enum SamplType { SamplType1 = 1, SamplType2 = 2 }; struct __declspec(uuid("37a3ad1d-f9cc-11d3-8d3c-0000e8d9fd76")) ISamplObject : IDispatch { // Property data __declspec(property(get=GetProp,put=PutProp)) enum SamplType Prop; // Wrapper methods for error-handling enum SamplType GetProp ( ); void PutProp (enum SamplType pVal ); ISamplObjectPtr Method (const _variant_t & Var,_bstr_t Str ); // Raw methods provided by interface virtual HRESULT __stdcall get_Prop (enum SamplType * pVal) = 0 ; virtual HRESULT __stdcall put_Prop (enum SamplType pVal) = 0 ; virtual HRESULT __stdcall raw_Method (VARIANT Var,BSTR Str,struct ISamplObject** Obj) = 0 ; }; struct __declspec(uuid("37a3ad1e-f9cc-11d3-8d3c-0000e8d9fd76")) SamplObject; #include "debug\sampl.tli" } // namespace SAMPLLib #pragma pack(pop)
Sampl.tli:
// Created by Microsoft (R) C/C++ Compiler Version 12.00.8472.0 (53af584f). // // sampl.tli // // Wrapper implementations for Win32 type library Debug\sampl.dll // compiler-generated file created 03/14/00 at 20:43:40 - DO NOT EDIT! #pragma once // interface ISamplObject wrapper method implementations inline enum SamplType ISamplObject::GetProp ( ) { enum SamplType _result; HRESULT _hr = get_Prop(&_result); if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); return _result; } inline void ISamplObject::PutProp ( enum SamplType pVal ) { HRESULT _hr = put_Prop(pVal); if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); } inline ISamplObjectPtr ISamplObject::Method ( const _variant_t & Var, _bstr_t Str ) { struct ISamplObject * _result; HRESULT _hr = raw_Method(Var, Str, &_result); if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); return ISamplObjectPtr(_result, false); }
Первое на что следует обратить внимание - это на строчку файла sampl.tlh:
namespace SAMPLLib {
Это означает, что компилятор помещает описание классов в отдельное пространство имён, соответствующее имени библиотеки типов. Это является необходимым при использовании нескольких библиотек типов с одинаковыми именами классов, такими, например, как IDocument. При желании, имя пространства имён можно изменить или запретить его генерацию совсем:
#import "sampl.dll" rename_namespace("NewNameSAMPLLib") #import "sampl.dll" no_namespace
Теперь рассмотрим объявление метода Method:
ISamplObjectPtr Method (const _variant_t & Var,_bstr_t Str);
Здесь мы видим использование компилятором классов поддержки COM. К таким классам относятся следующие.
Нам осталось уточнить природу класса ISamplObjectPtr. Мы уже говорили о классе _com_ptr_t. Он используется для реализации smart-указателей на интерфейсы COM. Мы будем часто использовать этот класс, но не будем делать этого напрямую. Директива #import самостоятельно генерирует определение smart-указателей. В нашем примере это сделано следующим образом.
// Smart pointer typedef declarations _COM_SMARTPTR_TYPEDEF(ISamplObject,__uuidof(ISamplObject));
Это объявление эквивалентно следующему:
typedef _com_ptr_t<ISamplObject,&__uuidof(ISamplObject)> ISamplObjectPtr
Использование smart-указателей позволяет не думать о счётчиках ссылок на
объекты COM, т.к. методы AddRef и Release интерфейса
IUnknown вызываютс автоматически в перегруженных операторах класса
_com_ptr_t.
Помимо прочих этот класс имеет следующий перегруженный оператор.
Interface* operator->() const throw(_com_error);где Interface - тип интерфейса, в нашем случае - это ISamplObject. Таким образом мы сможем обращаться к свойствам и методам нашего COM объекта. Вот как будет выглядеть пример использования директивы #import для нашего примера (красным цветом выделены места использования перегруженного оператора).
#import "sampl.dll" void SamplFunc () { SAMPLLib::ISamplObjectPtr obj; obj.CreateInstance(L"SAMPLLib.SamplObject"); SAMPLLib::ISamplObjectPtr obj2 = obj->Method(1l,L"12345"); obj->Prop = SAMPLLib::SamplType2; obj2->Prop = obj->Prop; }
Как видно из примера создавать объекты COM с использованием классов, сгенерированных директивой #import, достаточно просто. Во-первых, необходимо объявить smart-указатель на тип создаваемого объекта. После этого для создания экземпляра нужно вызвать метод CreateInstance класса _com_ptr_t, как показано в следующих примерах:
SAMPLLib::ISamplObjectPtr obj; obj.CreateInstance(L"SAMPLLib.SamplObject"); или obj.CreateInstance(__uuidof(SamplObject));
Можно упростить этот процесс, передавая идентификатор класса в конструктор указателя:
SAMPLLib::ISamplObjectPtr obj(L"SAMPLLib.SamplObject"); или SAMPLLib::ISamplObjectPtr obj(__uuidof(SamplObject));
Прежде чем перейти к примерам, нам необходимо рассмотреть обработку исключительных ситуаций. Как говорилось ранее, директива #import использует для генерации исключительных ситуаций класс _com_error. Этот класс инкапсулирует генерируемые значения HRESULT, а также поддерживает работу с интерфейсом IErrorInfo для получения более подробной информации об ошибке. Внесём соответствующие изменения в наш пример:
#import "sampl.dll" void SamplFunc () { try { using namespace SAMPLLib; ISamplObjectPtr obj(L"SAMPLLib.SamplObject"); ISamplObjectPtr obj2 = obj->Metod(1l,L"12345"); obj->Prop = SAMPLLib::SamplType2; obj2->Prop = obj->Prop; } catch (_com_error& er) { printf("_com_error:\n" "Error : %08lX\n" "ErrorMessage: %s\n" "Description : %s\n" "Source : %s\n", er.Error(), (LPCTSTR)_bstr_t(er.ErrorMessage()), (LPCTSTR)_bstr_t(er.Description()), (LPCTSTR)_bstr_t(er.Source())); } }
При изучении файла sampl.tli хорошо видно как директива #import генерирует исключения. Это происходит всегда при выполнении следующего условия:
if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
Этот способ, безусловно, является универсальным, но могут возникнуть некоторые неудобства. Например, метод MoveNext объекта Recordset ADO возвращает код, который не является ошибкой, а лишь индицирует о достижении конца набора записей. Тем не менее, мы получим исключение. В подобных случаях придётся использовать либо вложенные операторы try {} catch, либо корректировать wrapper, внося обработку исключений непосредственно в тело сгенерированных процедур. В последнем случае, правда, придется подключать файлы *.tlh уже обычным способом, через #include. Но делать это никто не запрещает.
Наконец, настало время рассмотреть несколько практических примеров. Я приведу четыре примера работы с MS Word, MS Excel, ADO DB и ActiveX Control. Первые три примера будут обычными консольными программами, в последнем примере я покажу, как можно заменить класс COleDispatchDriver сгенерированный MFC Class Wizard'ом на классы полученные директивой #import.
Для первых двух примеров нам понадобиться файл следующего содержания:
// Office.h #define Uses_MSO2000 #ifdef Uses_MSO2000 // for MS Office 2000 #import "C:\Program Files\Microsoft Office\Office\MSO9.DLL" #import "C:\Program Files\Common Files\Microsoft Shared\VBA\VBA6\VBE6EXT.OLB" #import "C:\Program Files\Microsoft Office\Office\MSWORD9.OLB" \ rename("ExitWindows","_ExitWindows") #import "C:\Program Files\Microsoft Office\Office\EXCEL9.OLB" \ rename("DialogBox","_DialogBox") \ rename("RGB","_RGB") \ exclude("IFont","IPicture") #import "C:\Program Files\Common Files\Microsoft Shared\DAO\DAO360.DLL" \ rename("EOF","EndOfFile") rename("BOF","BegOfFile") #import "C:\Program Files\Microsoft Office\Office\MSACC9.OLB" #else // for MS Office 97 #import "C:\Program Files\Microsoft Office\Office\MSO97.DLL" #import "C:\Program Files\Common Files\Microsoft Shared\VBA\VBEEXT1.OLB" #import "C:\Program Files\Microsoft Office\Office\MSWORD8.OLB" \ rename("ExitWindows","_ExitWindows") #import "C:\Program Files\Microsoft Office\Office\EXCEL8.OLB" \ rename("DialogBox","_DialogBox") \ rename("RGB","_RGB") \ exclude("IFont","IPicture") #import "C:\Program Files\Common Files\Microsoft Shared\DAO\DAO350.DLL" \ rename("EOF","EndOfFile") rename("BOF","BegOfFile") #import "C:\Program Files\Microsoft Office\Office\MSACC8.OLB" #endif
Этот файл содержит подключение библиотек типов MS Word, MS Excel и MS Access. По умолчанию подключаются библиотеки для MS Office 2000, если на вашем компьютере установлен MS Office 97, то следует закомментировать строчку
#define Uses_MSO2000
Если MS Office установлен в каталог отличный от "C:\Program Files\Microsoft Office\Office\", то пути к библиотекам также следует подкорректировать. Обратите внимание на атрибут rename, его необходимо использовать, когда возникают конфликты имён свойств и методов библиотеки типов с препроцессором. Например, функция ExitWindows объявлена в файле winuser.h как макрос:
#define ExitWindows(dwReserved,Code) ExitWindowsEx(EWX_LOGOFF,0xFFFFFFFF)
В результате, там, где препроцессор встретит имя ExitWindows, он будет пытаться подставлять определение макроса. Этого можно избежать при использовании атрибута rename, заменив такое имя на любое другое.
// console.cpp : Defines the entry point for the console application. #include "stdafx.h" #include <stdio.h> #include "Office.h" void main() { ::CoInitialize(NULL); try { using namespace Word; _ApplicationPtr word(L"Word.Application"); word->Visible = true; word->Activate(); // создаём новый документ _DocumentPtr wdoc1 = word->Documents->Add(); // пишем пару слов RangePtr range = wdoc1->Content; range->LanguageID = wdRussian; range->InsertAfter("Пара слов"); // сохраняем как HTML wdoc1->SaveAs(&_variant_t("C:\\MyDoc\\test.htm"), &_variant_t(long(wdFormatHTML))); // иногда придется прибегать к явному преобразованию типов, // т.к. оператор преобразования char* в VARIANT* не определён // открывает документ test.doc _DocumentPtr wdoc2 = word->Documents->Open(&_variant_t("C:\\MyDoc\\test.doc")); // вызываем макрос word->Run("Macro1"); } catch (_com_error& er) { char buf[1024]; sprintf(buf,"_com_error:\n" "Error : %08lX\n" "ErrorMessage: %s\n" "Description : %s\n" "Source : %s\n", er.Error(), (LPCTSTR)_bstr_t(er.ErrorMessage()), (LPCTSTR)_bstr_t(er.Description()), (LPCTSTR)_bstr_t(er.Source())); CharToOem(buf,buf); // только для косольных приложений printf(buf); } ::CoUninitialize(); }
// console.cpp : Defines the entry point for the console application. #include "stdafx.h" #include <stdio.h> #include "Office.h" void main() { ::CoInitialize(NULL); try { using namespace Excel; _ApplicationPtr excel("Excel.Application"); excel->Visible[0] = true; // создаём новую книгу _WorkbookPtr book = excel->Workbooks->Add(); // получаем первый лист (в VBA нумерация с единицы) _WorksheetPtr sheet = book->Worksheets->Item[1L]; // Аналогичная конструкция на VBA выглядит так: // book.Worksheets[1] // В библиотеке типов Item объявляется как метод или // свойство по умолчанию (id[0]), поэтому в VB его // можно опускать. На C++ такое, естественно, не пройдёт. // заполняем ячейки sheet->Range["B2"]->FormulaR1C1 = "Строка 1"; sheet->Range["C2"]->FormulaR1C1 = 12345L; sheet->Range["B3"]->FormulaR1C1 = "Строка 2"; sheet->Range["C3"]->FormulaR1C1 = 54321L; // заполняем и активизируем итоговую строку sheet->Range["B4"]->FormulaR1C1 = "Итого:"; sheet->Range["C4"]->FormulaR1C1 = "=SUM(R[-2]C:R[-1]C)"; sheet->Range["C4"]->Activate(); // типа делаем красиво :o) sheet->Range["A4:D4"]->Font->ColorIndex = 27L; sheet->Range["A4:D4"]->Interior->ColorIndex = 5L; // Постфикс L говорит, что константа является числом типа long. // Вы всегда должны приводить числа к типу long или short при // преобразованию их к _variant_t, т.к. преобразование типа int // к _variant_t не реализовано. Это вызвано не желанием // разработчиков компилятора усложнить нам жизнь, а спецификой // самого типа int. } catch (_com_error& er) { char buf[1024]; sprintf(buf,"_com_error:\n" "Error : %08lX\n" "ErrorMessage: %s\n" "Description : %s\n" "Source : %s\n", er.Error(), (LPCTSTR)_bstr_t(er.ErrorMessage()), (LPCTSTR)_bstr_t(er.Description()), (LPCTSTR)_bstr_t(er.Source())); CharToOem(buf,buf); // только для косольных приложений printf(buf); } ::CoUninitialize(); }
// console.cpp : Defines the entry point for the console application. #include "stdafx.h" #include <stdio.h> #import "C:\Program Files\Common Files\System\ado\msado20.tlb" \ rename("EOF","ADOEOF") rename("BOF","ADOBOF") // оператор rename необходим, т.к. EOF определён как макрос // в файле stdio.h using namespace ADODB; void main() { ::CoInitialize(NULL); try { // открываем соединение с БД _ConnectionPtr con("ADODB.Connection"); con->Open(L"Provider=Microsoft.Jet.OLEDB.3.51;" L"Data Source=Elections.mdb","","",0); // открываем таблицу _RecordsetPtr rset("ADODB.Recordset"); rset->Open(L"ElectTbl",(IDispatch*)con, adOpenDynamic,adLockOptimistic,adCmdTable); FieldsPtr flds = rset->Fields; // добавляем rset->AddNew(); flds->Item[L"Фамилия"] ->Value = L"Пупкин"; flds->Item[L"Имя"] ->Value = L"Василий"; flds->Item[L"Отчество"] ->Value = L"Карлович"; flds->Item[L"Голосовал ли"] ->Value = false; flds->Item[L"За кого проголосовал"]->Value = L"Против всех"; rset->Update(); // подменяем flds->Item[L"Голосовал ли"] ->Value = true; flds->Item[L"За кого проголосовал"]->Value = L"За наших"; rset->Update(); // просмотр rset->MoveFirst(); while (!rset->ADOEOF) { char buf[1024]; sprintf(buf,"%s %s %s: %s - %s\n", (LPCTSTR)_bstr_t(flds->Item[L"Фамилия"]->Value), (LPCTSTR)_bstr_t(flds->Item[L"Имя"]->Value), (LPCTSTR)_bstr_t(flds->Item[L"Отчество"]->Value), (bool)flds->Item[L"Голосовал ли"]->Value? "Да": "Нет", (LPCTSTR)_bstr_t(flds->Item[L"За кого проголосовал"]->Value)); CharToOem(buf,buf); printf(buf); rset->MoveNext(); } } catch (_com_error& er) { char buf[1024]; sprintf(buf,"_com_error:\n" "Error : %08lX\n" "ErrorMessage: %s\n" "Description : %s\n" "Source : %s\n", er.Error(), (LPCTSTR)_bstr_t(er.ErrorMessage()), (LPCTSTR)_bstr_t(er.Description()), (LPCTSTR)_bstr_t(er.Source())); CharToOem(buf,buf); // только для косольных приложений printf(buf); } ::CoUninitialize(); }
Для этого примера нам понадобится любое оконное приложение.
ActiveX Control'ы вставляются в диалог обычно через
Components and Controls Gallery:
Меню-Project-Add_To_Project-Components_and_Controls-Registered_ActiveX_Controls.
Нам в качестве примера вполне подойдёт Microsoft FlexGrid Control. Нажмите кнопку Insert для добавления его в проект, в появившемся окне Confirm Classes оставьте галочку только возле элемента CMSFlexGrid и смело жмите OK. В результате будут сформированы два файла msflexgrid.h и msflexgrid.cpp, большую часть содержимого которых нам придётся удалить. После всех изменений эти файлы будут иметь следующий вид:
msflexgrid.h
// msflexgrid.h #ifndef __MSFLEXGRID_H__ #define __MSFLEXGRID_H__ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #pragma warning(disable:4146) #import <MSFLXGRD.OCX> class CMSFlexGrid : public CWnd { protected: DECLARE_DYNCREATE(CMSFlexGrid) public: MSFlexGridLib::IMSFlexGridPtr I; // доступ к интерфейсу void PreSubclassWindow (); // инициализация I }; //{{AFX_INSERT_LOCATION}} #endif
msflexgrid.cpp
// msflexgrid.cpp #include "stdafx.h" #include "msflexgrid.h" IMPLEMENT_DYNCREATE(CMSFlexGrid, CWnd) void CMSFlexGrid::PreSubclassWindow () { CWnd::PreSubclassWindow(); MSFlexGridLib::IMSFlexGrid *pInterface = NULL; if (SUCCEEDED(GetControlUnknown()->QueryInterface(I.GetIID(), (void**)&pInterface))) { ASSERT(pInterface != NULL); I.Attach(pInterface); } }
Теперь вставим элемент в любой диалог, например CAboutDlg. В диалог добавим переменную связанную с классом CMSFlexGrid и метод OnInitDialog, текст которого приведён ниже. При вызове диалога в наш FlexGrid будут добавлены два элемента:
BOOL CAboutDlg::OnInitDialog() { CDialog::OnInitDialog(); m_grid.I->AddItem("12345"); m_grid.I->AddItem("54321"); return TRUE; }
В заключении, позволю себе высказать ещё несколько замечаний.
book->Worksheets->Item[1L]->Range["B2"]->FormulaR1C1 = "Строка 1"; book->Worksheets->Item[1L]->Range["C2"]->FormulaR1C1 = 12345L;Но в данном случае вы получите неоправданное замедление из-за лишнего межзадачного взаимодействия, а в случае DCOM - сетевого взаимодействия. Лучше написать так:
_WorksheetPtr sheet = book->Worksheets->Item[1L]; sheet->Range["B2"]->FormulaR1C1 = "Строка 1"; sheet->Range["C2"]->FormulaR1C1 = 12345;
inline _bstr_t IAdodc::GetConnectionString () { BSTR _result; HRESULT _hr = _com_dispatch_propget(this,0x01,VT_BSTR,&_result); // HRESULT _hr = get_ConnectionString(&_result); if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); return _bstr_t(_result, false); }
Удачи в бою.