65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (42投票s)

2006年10月6日

7分钟阅读

viewsIcon

375034

downloadIcon

20431

演示如何使用 WTL 和 C++ 在 Visual Studio .NET 2003 中访问 Excel 电子表格。

WTL 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++ 的火焰。他们会需要我们。他们将永远需要我们。

历史

首次修订。

© . All rights reserved.