通过 C++ 访问 Excel 电子表格






4.78/5 (42投票s)
2006年10月6日
7分钟阅读

375034

20431
演示如何使用 WTL 和 C++ 在 Visual Studio .NET 2003 中访问 Excel 电子表格。
引言
此代码演示了如何在 Visual Studio .NET 2003 (v7.1) 中使用 C++、COM 和 ATL 访问 Excel 电子表格。
背景
我是一名老派的 C++ 程序员,我的大部分工作都围绕着使用 C++。虽然我不反对使用新的方法论,如 C#(事实上,我也为其他咨询项目使用过),但我始终喜欢回归 C++。
首先,我最初需要访问 Excel 的原因。
我的公司正在为 Windows 2000 和 XP 开发一款产品,需要将其字符串进行翻译。上面的人决定,我们将交付给翻译人员的文件将是 Excel 文件,每个项目一个。一切都很好。
但我很懒,不想手动维护一个充斥着翻译字符串的电子表格。我更愿意编写一个程序,提取字符串(它们位于 .xml 文件中)并将其存入 Excel 电子表格。嗯,第一部分很容易。只需使用方便的 IXMLDOMDocument
COM 接口即可。小菜一碟。
然后我开始探索如何通过 C++ 以编程方式访问 Excel。
哦……我的……天哪!关于这个主题的文档缺乏的不仅仅是稀少。简直是荒谬地不存在。只有少数几篇关于如何使用 MFC 来实现此目的的文章(除非你喜欢在几分钟内生成十亿个小模块,同时 MFC 导入向导愉快地生成 Excel 许多接口的 IDispatch
代码包装器)。但我早已放弃了 MFC,转而崇拜 ATL 和 WTL 的圣坛,并且再也没有回头。
经过数小时的搜索,我终于找到了一篇链接,其中提到了各种 Excel 版本各种接口的存储位置。由于我安装了 Office 2003,所以我选择了那个版本。(顺便说一句,它存储在实际的 *Excel.EXE* 二进制文件中)。
“太棒了!”我对自己说。只需在 *stdafx.h* 中添加一个 #import 行,我就完成了。
#import "C:\\Program Files\\Microsoft Office\\OFFICE11\\EXCEL.EXE"
是啊,没错。唉,我多么失望啊。你收到了编译器和/或链接器的缺失、重复、三重、谷歌搜索式的错误消息、警告、咒骂和普遍的咆哮。是时候卷起袖子,准备好与包含文件和 #import 指令搏斗了。
我注意到的第一件事是,许多缺失的类都有“mso”前缀。嗯……猜猜这是否意味着 Microsoft Office?果然,似乎所有 Office 产品都需要相同的 *MSO.DLL* 来执行其许多工作。所以将其添加进去。
#import "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE11\\MSO.DLL" #import "C:\\Program Files\\Microsoft Office\\OFFICE11\\EXCEL.EXE"
好吧,这很有帮助。但它仍然在抱怨“定义”参数不足。似乎 Microsoft Office 中的许多属性、方法名称等与标准的 C/C++ #define
声明发生了冲突。幸运的是,Microsoft 的 Visual Studio 传道者预见了这一点,并提供了一种解决这些冲突的机制,即 #import 指令的“rename”选项。另外,Office 似乎与 Visual Basic 紧密合作,因为你也需要一个 #import 来支持它。
一切就绪后,我终于找到了将 Excel 的 COM 接口包含到 C++ 程序中的神奇炼金术公式。
#import "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE11\\MSO.DLL" \ rename( "RGB", "MSORGB" ) using namespace Office; #import "C:\\Program Files\\Common Files\\Microsoft Shared\\VBA\\VBA6\\VBE6EXT.OLB" using namespace VBIDE; #import "C:\\Program Files\\Microsoft Office\\OFFICE11\\EXCEL.EXE" \ rename( "DialogBox", "ExcelDialogBox" ) \ rename( "RGB", "ExcelRGB" ) \ rename( "CopyFile", "ExcelCopyFile" ) \ rename( "ReplaceText", "ExcelReplaceText" )
完成了!
我尝试了一些花哨的操作,包括“using namespace Excel;”,但编译器开始发出咆哮声,并在地上刨了起来,我决定趁着还有优势时退出。我可以输入几个“Excel::”前缀来保持社交。
使用代码
该应用程序是一个简单的 WTL 基于对话框的小应用程序。对话框包含一个以报表模式显示的列表视图控件。它已编译为 Unicode,因此我无需担心混乱的 MBCS 字符串。所有与 Excel 交互的代码都位于用户按下“Load...”按钮时调用的 OnLoad()
方法中。
此方法的第一件事是清除列表控件中的项目。
m_list.DeleteAllItems( ); while ( m_list.DeleteColumn( 0 ) );
然后它使用 WTL 类 CFileDialog
提示用户选择 Excel 文件。顺便说一下,如果您不熟悉 WTL,它是一个非常精简的包装器(有一些值得注意的例外),封装了 Win32 API。例如。CFileDialog
接受指向一系列以 null 结尾的字符串的指针,这些字符串定义了帮助用户进行文件选择过程的过滤器。在动态定义这些字符串有点麻烦,所以我写了一个简单的类,它接受一个带有垂直条 (|) 的单个字符串,这些垂直条是 null 终止符的占位符。它有一个 LPCTSTR()
运算符方法,该方法返回一个指向字符串缓冲区的指针,其中垂直条被替换为其 null 终止符。
我为您呈现 AFileFilter
类。
class AFileFilter { public: AFileFilter( LPCTSTR pszFilter ) : m_strFilter( pszFilter ), m_pszFilter( NULL ) { m_pszFilter = m_strFilter.GetBuffer( 0 ); LPTSTR psz = m_pszFilter; while ( *psz ) { LPTSTR pszNext = ::CharNext( psz ); if ( *psz == _T('|') ) *psz = _T('\0'); psz = pszNext; } return; } virtual ~AFileFilter( ) { m_strFilter.ReleaseBuffer( ); return; } public: operator LPCTSTR( ) const { return ( m_pszFilter ); } protected: CString m_strFilter; LPTSTR m_pszFilter; };
现在进入方法的关键部分。首先,我们需要一个指向 Excel 应用程序的指针。如果您在调试器中运行此代码,您会注意到在跟踪此代码时会有几秒钟的延迟,因为 Excel 是由 COM 加载的。当我第一次调试程序时,我认为这是一个好迹象:我实际上在调用 Excel!(您是不是也像我一样,在第一次运行程序时,总是会逐行调试新代码?)
Excel::_ApplicationPtr pApplication; if ( FAILED( pApplication.CreateInstance( _T("Excel.Application") ) ) ) { Errorf( _T("Failed to initialize Excel::_Application!") ); return; }
Errorf()
方法只是允许我为用户快速格式化带有可选参数的消息。
接下来,我们必须在 Excel 中打开 *.xls* 文件。这通过 Workbooks 属性及其 Open()
方法完成。
_variant_t varOption( (long) DISP_E_PARAMNOTFOUND, VT_ERROR );
Excel::_WorkbookPtr pBook;
pBook =
pApplication->Workbooks->Open( dlgFile.m_szFileName,
varOption, varOption, varOption, varOption,
varOption, varOption, varOption, varOption,
varOption, varOption, varOption, varOption );
如果您省略了其中一个 varOption
参数,它将不起作用。唉,我真希望我知道它们都做什么。
接下来,我获取工作簿中第一个工作表的指针。自然,第一个工作表的索引是 1,而不是 0。(记住:Office 与 Visual Basic 合作。BitTorrent 上 11mps 的 MPEG!)
Excel::_WorksheetPtr pSheet = pBook->Sheets->Item[ 1 ];
呼!终于,我们可以开始筛选电子表格数据了。据我所知,这一切都是通过 Range
对象完成的。现在,Range
对象,如果在 C# 或 Visual Basic 等脚本语言中使用,它是一个大胆而美妙的东西,具有有趣的接口和强大的方法。在 C++ 中,这就像在宙斯摆弄他的埃癸斯时与他争论一样。
我发现使用它的基本方法是过度提供范围。然后我通过扫描空单元格作为列结束或行结束标记来使用一个技巧。我确信在 C++ 中有更多优雅的使用此对象的方法,我将其作为练习留给学生。
我做的第一件事就是获取第一行,我假设它是分隔所有列名的标题行。
Excel::RangePtr pRange = pSheet->GetRange( _bstr_t( _T("A1") ), _bstr_t( _T("Z1" ) ) );
代码通过 Range
对象的 Item 运算符扫描此行。我采取了一个两步过程,首先将单元格内容存入 _variant_t
,然后存入 _bstr_t
,因为我很懒,不想检查 _variant_t
的类型。
_variant_t vItem = pRange->Item[ 1 ][ iColumn ];
_bstr_t bstrText( vItem );
当 bstrText
为空时,就该停止添加列了。
然后我用疯狂来跟随疯狂,并创建了一个 Range
对象的令人印象深刻的实例。
pRange = pSheet->GetRange( _bstr_t( _T("A2") ), _bstr_t( _T("Z16384" ) ) );
我使用简单的嵌套循环来创建列表视图控件中的行并设置列数据。
最后,我使用 VARIANT_FALSE
关闭工作簿,以防止任何意外更改意外进入 *.xls* 文件。
pBook->Close( VARIANT_FALSE );
最后,我退出 Excel 应用程序。这是一个重要的步骤,因为如果您不这样做,Excel 将会留在内存中,更糟糕的是,它会锁定您上面打开的文件。呸!
pApplication->Quit( );
就是这样!玩得开心!
关注点
哦,是的。如果你想修改电子表格中的数据怎么办?这其实很简单。只需将 Item
属性用作左操作数即可。
_bstr_t bstrText( _T("Some text!") ); _variant_t vItem( bstrText ); pRange->Item[ 5 ][ 1 ] = vItem;
继续燃烧 C++ 的火焰。他们会需要我们。他们将永远需要我们。
历史
首次修订。