ATL7 和属性






4.88/5 (10投票s)
ATL7 和属性的描述和示例用法
VC++ 属性旨在简化编程。它们仅供编译器使用,并且实际上不会在编译阶段之后保留(IDL 属性除外,但即使是这些属性,也仅在链接时通过运行 midl 并构建 typelib 来添加)。编译器会检查属性并注入/生成相应的 C++ 代码。在使用属性时,最好使用 /Fx 编译器开关。它的作用是在处理完属性后生成合并代码的 .h/.cpp 文件。因此,您可以自由检查编译器生成的代码。您甚至可以将其复制到您的类中,然后注释掉某个特定属性,如果您决定不使用它。VC++ 通过以下几个方面来覆盖属性:
-
COM/IDL - 提供各种用于接口、COM 类、属性/方法和事件、脚本注册的属性;
-
OLEDB - 基于 OLEDB 消费者模板注入代码;
-
C++ 编译器 - 原生 C++ 事件、各种 IDL 文件相关功能;
-
ATL Server - 基于 ATL Server 类注入代码,用于 Web 服务和 Web 应用程序。
通过属性,我们可以只拥有包含所需所有内容的头文件(.h)和实现文件(.cpp),编译器和链接器会处理其余部分。VC++ 通过所谓的 属性提供程序(ATL 的在 Atlprov.dll
中)来完成属性处理。当编译器看到属性时,它会将信息传递给属性提供程序,属性提供程序随后会注入适当的代码。
下面是一个简单的 COM 服务器,如您所见,它仅在一个文件中实现!(我并不提倡单文件实现,这仅用于说明目的。我发现用快速紧凑的示例学习新东西更容易。)
顺便说一句:在使用属性时,最好使用 CTRL+F7 来仅编译单个文件(用于语法检查),而不是构建整个项目,每次都生成 .idl、运行 midl 并生成 .tlb。
// build as DLL and register with regsvr32 #define _ATL_ATTRIBUTES #define _ATL_APARTMENT_THREADED #include <atlbase.h> #include <atlcom.h> using namespace ATL; typedef LONG HRESULT; [module(type=DLL, name="Simple", uuid="67BF6350-E112-46fc-A6B6-00EFF8CBF1BB")]; [dual, uuid("A4042AEE-12C0-48d8-8CA4-80B8F957B7B3")] __interface ISimpleObject { [propget] HRESULT String([out, retval]BSTR* pVal); }; [coclass,uuid("E16E71E3-8217-4739-8AD7-2689581A75F0")] class ATL_NO_VTABLE SimpleObject : public ISimpleObject { public: HRESULT get_String(BSTR* pVal) { if(pVal == NULL) return E_POINTER; *pVal = CComBSTR("string").Detach(); return S_OK; } HRESULT FinalConstruct() { return S_OK; } void FinalRelease() { } };
不要忘记定义 _ATL_ATTRIBUTES
符号并包含 atlbase.h。此符号会引入 atlplus.h 文件以支持属性。如果不定义它,您将收到链接器错误,找不到入口点。另外,定义 module[(type=DLL|EXE|SERVICE)]
属性。在属性化 ATL/COM 编程中的起点是 module
属性。它有许多参数,其中一些是可选的。例如,type=EXE|DLL|SERVICE
、name="Type Library Name"
、uuid="{UUID of the type library}"
等。之后,您就可以开始定义接口和 COM 类了。如您所见,无需手动编写 .idl 或 .def 文件。module[()]
会处理它。如果您添加 /Fx
编译器开关并打开 *.mrg.cpp 文件,您可以看到编译器生成的代码。对于下一个示例,编译器自动生成了 WinMain
函数和一个派生自 CAtlExeModuleT
的类,因为它是一个 EXE 类型的应用程序。此外,它还声明了一个全局变量 _AtlModule
。因此,创建具有 COM 功能的 exe/dll 所需的就只有这些了。如果您愿意,可以派生自模块类并自定义默认行为。
[module(type=exe, name="Simple")] class CSomeClass { int WinMain(int nShowCmd) throw() { return __super::WinMain(nShowCmd); // same as return CAtlExeModuleT<CSomeClass>::WinMain(nShowCmd); } HRESULT RegisterClassObjects(DWORD dwClsContext, DWORD dwFlags) throw() { // example of modifying default behavior when CoRegisterClassObject is called dwFlags &= ~REGCLS_MULTIPLEUSE; dwFlags |= REGCLS_SINGLEUSE; return __super::RegisterClassObjects(dwClsContext, dwFlags); } // etc };
顺便说一句,如果您想在 VC++ .NET 编辑器中快速生成 UUID,只需开始键入 [uuid(
,之后就会为您生成并完成一个 GUID!
以下示例展示了如何使用属性公开 COM 事件。
// DLL Server project, register with regsvr32 [module(type=DLL, name="Simple", uuid="67BF6350-E112-46fc-A6B6-00EFF8CBF1BB")]; // use object attribute to inherit from IUnknown if needed [object, oleautomation, uuid("D59F99BE-5DFB-4C0C-B9A6-16853740E4AA")] __interface ISimpleObject { [id(1)] HRESULT RaiseEvent(); }; [dual, uuid("29D2345E-5E47-4F60-B3DC-46BDF29B98A9")] __interface _ISimpleEvent { [id(1)] HRESULT String(); }; [coclass, uuid("6B4A3FAC-6728-4058-8A1C-7352A27BFCCC"), event_source(com)] class ATL_NO_VTABLE SimpleObject : public ISimpleObject { public: __event __interface _ISimpleEvent; HRESULT RaiseEvent() { __raise String(); // or InterfaceName_EventName // _ISimpleEvent_String(); return S_OK; } };
同样,您可以使用 /Fx 编译器开关查看所有生成的代码。没有什么神秘之处。
使用属性接收事件如下:
#define _ATL_ATTRIBUTES #include <atlbase.h> #include <atlcom.h> using namespace ATL; #import "libid:67BF6350-E112-46fc-A6B6-00EFF8CBF1BB" \ auto_search no_implementation named_guids \ raw_interfaces_only raw_native_types no_namespace embedded_idl [module(name="EventReceiver")]; [emitidl(false)]; // don't need any of COM server files // generated (.idl, .h, *.c, .tlb) [event_receiver("com")] class CEventSink { public: HRESULT String() { ATLTRACE("\nGot Event\n"); return S_OK; } void Advise(IUnknown* pObj) { __hook(_ISimpleEvent::String, pObj, CEventSink::String); } void UnAdvise(IUnknown* pObj) { __unhook(_ISimpleEvent::String, pObj, CEventSink::String); } }; int main() { CComPtr<ISimpleObject> spObj; HRESULT hr = spObj.CoCreateInstance(CLSID_SimpleObject); CEventSink sink; sink.Advise(spObj); spObj->RaiseEvent(); sink.UnAdvise(spObj); }
您可以使用 #import
或服务器的头文件(包含所有属性)。如果您使用 #import
,请确保不要忘记 embedded_idl
,否则在 __hook
源事件接口时会出错。它的作用是在 .tlh 文件中生成属性,就像您为 COM 服务器所做的那样,这样客户端就可以让编译器快乐地读取它们。接下来要记住的是使用 [module(name)]
块,否则您也会遇到编译器/链接器错误。在某些情况下,可以使用 [emitidl(restricted)]
代替,但不能与 __hook
/__unhook
一起使用。它需要 module
块。如果您不需要生成 idl/tlb、*_i.c/*_p.c、.h、dlldata.c,例如在客户端应用程序中,您需要做的就是放置以下属性:[emitidl(false)]
。
OLEDB 相关属性也旨在简化客户端的数据库访问。最好的地方是它不需要 ATL 项目。创建一个简单的 C++ 控制台项目。然后选择“项目”|“添加类”|“ATL OLEDB 消费者”。例如,以下是一个使用 OLEDB 属性的简单示例。我在这里使用了一个免费提供的 MS Access 数据库:USDA Nutrient Database。
注意:确保数据库文件与 .exe 文件在同一目录中,或者从 VS.NET 运行时,确保它在项目目录中。否则,只需指定 .mdb 文件的完整路径。
[ db_source(L"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=sr13.mdb;\ Mode=ReadWrite|Share Deny None;Jet OLEDB:Engine Type=5;\ Jet OLEDB:Database Locking Mode=1;Jet OLEDB:Global Partial Bulk Ops=2;\ Jet OLEDB:Global Bulk Transactions=1;") ] class CDBSource{}; [ db_command(L"SELECT FOOD_DES.DESC, FOOD_DES.FAT_FACTOR, \ FOOD_DES.CHO_FACTOR, FOOD_DES.NDB_NO, \ FOOD_DES.SHRT_DESC, \ Abbrev.Water, \ Abbrev.Energy, \ Abbrev.Protein \ FROM FOOD_DES INNER JOIN ABBREV \ ON FOOD_DES.NDB_NO = Abbrev.[NDB No]") ] class CFOOD_DES { public: _CFOOD_DESAccessor() {} // if you need ctor/dtor remember the actual class ~_CFOOD_DESAccessor(){} // name is not CFOOD_DES! Again, /Fx switch! [ db_column(1, length=m_dwDESCLength) ] TCHAR m_DESC[201]; [ db_column(2) ] double m_FAT_FACTOR; [ db_column(3) ] double m_CHO_FACTOR; [ db_column(4, length=m_dwNDB_NOLength) ] TCHAR m_NDB_NO[6]; [ db_column(5, length=m_dwSHRT_DESCLength) ] TCHAR m_SHRT_DESC[61]; [ db_column(6) ] float m_Water; [ db_column(7) ] float m_Energy; [ db_column(8) ] float m_Protein; DBLENGTH m_dwDESCLength; DBLENGTH m_dwNDB_NOLength; DBLENGTH m_dwSHRT_DESCLength; void GetRowsetProperties(CDBPropSet* pPropSet) { pPropSet->AddProperty(DBPROP_CANFETCHBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_CANSCROLLBACKWARDS, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_IRowsetChange, true, DBPROPOPTIONS_OPTIONAL); pPropSet->AddProperty(DBPROP_UPDATABILITY, DBPROPVAL_UP_CHANGE | DBPROPVAL_UP_INSERT | DBPROPVAL_UP_DELETE); } };
使用方式与之前相同,实际上没有任何变化。
int main() { CoInitialize(NULL); { // don't forget to destroy COM objects before CoUninitialize is called CDBSource source; if(SUCCEEDED(source.OpenDataSource())) { CDBPropSet propset(DBPROPSET_ROWSET); CFOOD_DES food; food.GetRowsetProperties(&propset); if(SUCCEEDED(food.Open(source, NULL, propset))) { if(SUCCEEDED(food.MoveFirst())) { std::string sout; for(int i=0; i<10; ++i) { sout=""; sout.append(food.m_DESC, food.m_dwDESCLength); std::cout<<sout.c_str()<<std::endl; std::cout<<food.m_FAT_FACTOR<<std::endl; std::cout<<food.m_CHO_FACTOR<<std::endl; sout =""; sout.append(food.m_NDB_NO, food.m_dwNDB_NOLength); std::cout<<food.m_NDB_NO<<std::endl; sout =""; sout.append(food.m_SHRT_DESC, food.m_dwSHRT_DESCLength); std::cout<<food.m_SHRT_DESC<<std::endl; std::cout<<food.m_Water<<std::endl; std::cout<<food.m_Energy<<std::endl; std::cout<<food.m_Protein<<std::endl; std::cout<<"---------------"<<std::endl; if(food.MoveNext() == DB_S_ENDOFROWSET) break; } } } } CoUninitialize(); return 0; }
您还可以使用 db_param[]
属性使用参数化查询,或者自己构建查询。
food.Close(); WCHAR sQuery[] = L"SELECT FOOD_DES.DESC, FOOD_DES.FAT_FACTOR, \ FOOD_DES.CHO_FACTOR, FOOD_DES.NDB_NO, FOOD_DES.SHRT_DESC, \ Abbrev.Water, Abbrev.Energy, Abbrev.Protein \ FROM FOOD_DES INNER JOIN ABBREV ON \ FOOD_DES.NDB_NO = Abbrev.[NDB No] \ WHERE FOOD_DES.NDB_NO = '01011'"; if(SUCCEEDED(food.Open(source, sQuery))) { if(SUCCEEDED(food.MoveFirst())) { std::string sout; for(int i=0; i<10; ++i) { sout=""; sout.append(food.m_NDB_NO, food.m_dwNDB_NOLength); std::cout<<food.m_NDB_NO<<std::endl; // etc std::cout<<"---------------"<<std::endl; if(food.MoveNext() == DB_S_ENDOFROWSET) break; } } }
编辑/添加/删除与之前相同。
// updating // change current row fields. don't forget to update length field for strings. food.SetData(); food.Update(); food.MoveFirst(); //delete current row food.Delete(); food.Update(); food.MoveFirst(); //add new // the USDA database has constraints that have to be satisfied for Insert to work. sometable.MoveLast(); sometable.ClearRecordMemory(); // set field's length if needed // set other fields, etc then call: sometable.Insert();
使用属性还可以简化将 NT 性能计数器添加到代码中。我们有 [perfmon]
、[perf_object]
和 [perf_counter]
,它们分别对应 CPerfMon
、CPerfObject
和 DEFINE_COUNTER()
。创建一个简单的 Win32 DLL 项目。然后选择“项目”|“添加类”|“ATL 性能监视器对象管理器”,选择使用属性和 TODO 注释。您可以阅读关于如何添加计数器的注释。对于 DllRegisterServer
/DllUnregisterServer
的相应代码将为您添加。
// "perf.h" [ perf_object(namestring="Perf_Obj", helpstring="Sample Description", detail=PERF_DETAIL_NOVICE) ] class PerfSampleObject { public: [ perf_counter( namestring="perf1", helpstring="Some Description", countertype=PERF_COUNTER_RAWCOUNT, detail=PERF_DETAIL_NOVICE, default_counter = true) ] ULONG m_nCounter; }; [ perfmon(name="Perf_Mon", register=true) ] class PerfMon{};
注册性能计数器 DLL 后,将在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Perf_Mon\Performance 和 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\[langid] 下添加条目。要使用它,只需在主项目中包含定义性能计数器的头文件,例如 "perf.h":<
初始化和访问性能监视器对象 (MSDN)
#define _ATL_ATTRIBUTES #include <atlbase.h> #include <atlperf.h> #include "X:\path_to_dll\perf.h" PerfMon perf; int main(int argc, _TCHAR* argv[]) { HRESULT hr = perf.Initialize(); PerfSampleObject* obj=NULL; { CPerfLock lock(&perf); if(lock.GetStatus()== S_OK) perf.CreateInstanceByName(L"Perf_Obj", &obj); if(obj == NULL) return 0; } while(true) { InterlockedIncrement((LONG*)&obj->m_nCounter); Sleep(1000); } { CPerfLock lock(&perf); if(lock.GetStatus()== S_OK) perf.ReleaseInstance(obj); } perf.UnInitialize(); return 0; }
现在,当您运行应用程序时,可以打开系统监视器并监视您的性能对象及其计数器。当您运行应用程序时,性能对象将在“添加计数器”中可见。
本文未涵盖 ATL 服务器相关主题和属性。
您可以查阅 MSDN 上的一些教程,这些教程专门讨论了属性并展示了它们的用法。
属性教程 (MSDN)
使用文本编辑器创建 COM 服务器 (MSDN)
演练:使用属性创建 ActiveX 控件 (MSDN)
演练:使用 COM 属性开发 COM DLL (MSDN)
按用法分类的属性 (MSDN)
属性示例的字母顺序列表 (MSDN)
Visual C++ 中的事件处理 (MSDN)
使用数据库属性简化数据访问 (MSDN)
使用者向导生成的类 (MSDN)
手动定义性能对象 (MSDN)