C++Builder OLE自动化Excel:从基础封装到大数据报表实战
1. 项目概述:为什么选择C++Builder与OLE来驱动Excel?
如果你是一位长期使用C++Builder进行工业控制、数据采集或企业级应用开发的工程师,那么处理Excel报表的需求几乎无处不在。无论是将设备运行日志导出成周报,还是将数据库查询结果整理成可交互的分析图表,Excel都是绕不开的一环。手动操作?那太原始了。用Python或C#?它们固然强大,但你可能面临一个现实:你的整个核心业务系统、硬件驱动、复杂的实时逻辑都是用C++Builder构建的,引入另一种语言意味着额外的学习成本、部署复杂度和潜在的跨语言调用开销。这时候,直接在你的C++Builder工程里,用原生的OLE技术来操控Excel,就成了一条最直接、最“原汤化原食”的技术路径。
OLE,即对象链接与嵌入,是微软在Windows平台上推出的一套组件对象模型技术。简单理解,它允许一个应用程序(比如你的C++Builder程序)去调用和控制另一个应用程序(比如Excel)的对象,就像在本地直接操作一样。通过OLE,你可以让Excel在后台默默打开,读取单元格数据、设置公式、调整格式、生成图表,最后保存关闭,全程无需用户手动干预。这不仅仅是“导出个CSV文件”那么简单,而是实现了对Excel这个复杂应用的深度、自动化控制。
我之所以花时间整理这份指南,是因为在实际项目中,我发现网上很多关于C++Builder OLE操作Excel的资料要么过于零碎,只讲一个打开操作;要么版本陈旧,还在用早已淘汰的Variant方法;要么就是缺乏关键的异常处理和性能优化细节,照着做很容易掉坑里。这份指南将从一个完整的、可复用的类封装出发,带你从环境配置、基础操作,一路深入到图表生成、大数据量处理和实战避坑,目标是让你看完就能在自己的项目里用起来。
2. 环境准备与核心对象模型解析
2.1 开发环境与组件配置
首先,确保你的开发环境就绪。我使用的是Embarcadero RAD Studio 10.4 Sydney(C++Builder版本),理论上从较旧的XE系列到最新的11版本都适用。关键不在于IDE版本,而在于对COM(组件对象模型)的支持。
在C++Builder中操作OLE,核心是正确导入Excel的类型库。这是很多新手的第一步绊脚石。不要手动去声明那些复杂的接口,让IDE帮你完成。
导入Excel类型库:在C++Builder中,点击菜单
Component->Import Component->Import a Type Library。在弹出的列表中,找到Microsoft Excel 16.0 Object Library(版本号可能因你安装的Office版本而异,如15.0对应2013,16.0对应2016/2019/365)。选中它,点击Install。这会在你的系统上生成一个Excel_2K_SRVR.h和Excel_2K_SRVR.cpp文件(文件名可能略有不同),其中包含了所有Excel对象(如_Application,_Workbook,_Worksheet,Range等)的C++包装类。在工程中包含头文件:在你需要使用Excel操作的单元文件(.cpp)中,包含生成的头文件:
#include "Excel_2K_SRVR.h"同时,由于我们使用OLE,还需要初始化COM库。通常在主窗体的构造函数或程序入口处进行:
// 在Form1的构造函数中 __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { // 初始化COM库,使用多线程公寓模型以提高性能 CoInitializeEx(NULL, COINIT_MULTITHREADED); } // 在Form1的析构函数中 __fastcall TForm1::~TForm1() { // 释放COM库 CoUninitialize(); }
注意:
CoInitializeEx的参数COINIT_MULTITHREADED表示使用多线程公寓。如果你的操作涉及多线程访问同一个Excel实例,这个设置很重要。对于简单的单线程GUI应用,使用CoInitialize(NULL)也可以。
2.2 Excel对象模型核心概念
在动手写代码前,必须理解Excel的OLE对象模型。它是一个层次化的结构,理解这个结构是写出正确代码的关键。
- Application(应用程序):这是根对象,代表整个Excel程序。你可以通过它创建新的实例,或者连接到已有的Excel进程。
- Workbooks(工作簿集合):
Application包含一个Workbooks集合,代表所有打开的工作簿文件(.xlsx, .xls等)。 - Workbook(工作簿):一个具体的Excel文件。通过
Workbooks的Add(新建)或Open(打开)方法获得。 - Worksheets(工作表集合):一个
Workbook包含多个Worksheets,就是文件底部的那些Sheet标签。 - Worksheet(工作表):具体的一个Sheet,如“Sheet1”。我们大部分的数据操作发生在这里。
- Range(区域):这是最常用、最核心的对象。它可以是一个单元格(如“A1”),一行,一列,或者一个矩形区域(如“A1:D10”)。几乎所有的数据读写、格式设置都是针对
Range对象进行的。
它们的关系可以简单理解为:Application->Workbooks->Workbook->Worksheets->Worksheet->Range。你的代码通常需要沿着这条链,一步步获取到你想要操作的那个对象。
3. 基础操作封装:一个可复用的ExcelOLE类
直接在每个按钮事件里写一堆OLE调用代码是难以维护的。更好的做法是封装一个工具类。下面我将展示一个简化但功能完整的TExcelOLEHelper类的核心部分。
3.1 类的声明与基本生命周期管理
// ExcelOLEHelper.h #ifndef ExcelOLEHelperH #define ExcelOLEHelperH #include <System.Classes.hpp> #include <Vcl.Controls.hpp> #include <Vcl.StdCtrls.hpp> #include "Excel_2K_SRVR.h" // 导入的类型库头文件 class TExcelOLEHelper { private: Excel::_ApplicationPtr m_excelApp; // Excel应用程序指针 Excel::_WorkbookPtr m_workbook; // 工作簿指针 Excel::_WorksheetPtr m_worksheet; // 活动工作表指针 bool m_isVisible; // 控制Excel是否可见 // 内部方法:处理OLE错误 void HandleOLEError(const _com_error& e); public: TExcelOLEHelper(bool visible = false); ~TExcelOLEHelper(); // 连接与创建 bool ConnectToRunningExcel(); // 连接到已运行的Excel bool CreateNewExcelInstance(); // 创建新的Excel实例 // 工作簿与工作表操作 bool OpenWorkbook(const String& filePath); bool CreateNewWorkbook(); bool SetActiveWorksheet(int index); // 按索引 bool SetActiveWorksheet(const String& sheetName); // 按名称 bool AddWorksheet(const String& sheetName); // 数据读写(核心) Variant ReadCell(int row, int col); // 读取单个单元格 bool WriteCell(int row, int col, const Variant& value); // 写入单个单元格 bool WriteRange(int startRow, int startCol, int endRow, int endCol, const Variant& values); // 写入区域 // 格式设置 bool SetCellFont(int row, int col, const String& fontName, int size, bool isBold = false); bool SetRangeNumberFormat(int startRow, int startCol, int endRow, int endCol, const String& format); // 文件操作 bool SaveAs(const String& filePath); bool Save(); void CloseWorkbook(bool saveChanges = true); void QuitExcel(); // 属性 __property bool Visible = {read=m_isVisible, write=SetVisible}; void SetVisible(bool visible); }; #endif3.2 核心方法的实现与详解
让我们看看几个最关键方法的实现,并解释其中的细节。
// ExcelOLEHelper.cpp #include "ExcelOLEHelper.h" #include <comdef.h> // 用于 _com_error #include <utilcls.h> // 用于 StringToWideString TExcelOLEHelper::TExcelOLEHelper(bool visible) : m_isVisible(visible), m_excelApp(nullptr), m_workbook(nullptr), m_worksheet(nullptr) { // 构造函数里不创建实例,延迟到需要时 } TExcelOLEHelper::~TExcelOLEHelper() { // 析构时确保资源释放 if (m_workbook != nullptr) { CloseWorkbook(false); } if (m_excelApp != nullptr) { QuitExcel(); } } bool TExcelOLEHelper::CreateNewExcelInstance() { try { // 创建Excel.Application对象的实例 m_excelApp.CreateInstance(__uuidof(Excel::Application)); m_excelApp->Visible = m_isVisible ? VARIANT_TRUE : VARIANT_FALSE; m_excelApp->DisplayAlerts = VARIANT_FALSE; // 不显示警告对话框,如覆盖保存 return true; } catch (_com_error& e) { HandleOLEError(e); return false; } } bool TExcelOLEHelper::OpenWorkbook(const String& filePath) { if (m_excelApp == nullptr) { if (!CreateNewExcelInstance()) return false; } try { // Workbooks->Open 方法需要完整的文件路径 m_workbook = m_excelApp->Workbooks->Open(WideString(filePath).c_bstr()); // 默认激活第一个工作表 m_worksheet = m_workbook->Worksheets->Item[1]; return true; } catch (_com_error& e) { HandleOLEError(e); return false; } } bool TExcelOLEHelper::WriteCell(int row, int col, const Variant& value) { if (m_worksheet == nullptr) return false; try { // 获取指定单元格的Range对象 Excel::RangePtr range = m_worksheet->Cells->Item[row][col]; // 将值写入该单元格 range->Value2 = value; return true; } catch (_com_error& e) { HandleOLEError(e); return false; } } bool TExcelOLEHelper::WriteRange(int startRow, int startCol, int endRow, int endCol, const Variant& values) { if (m_worksheet == nullptr) return false; try { // 构造类似 "A1:D10" 的地址字符串 String address = String().sprintf(L"%c%d:%c%d", L'A' + startCol - 1, startRow, L'A' + endCol - 1, endRow); // 获取区域对象 Excel::RangePtr range = m_worksheet->Range[WideString(address).c_bstr()]; // 一次性写入一个二维Variant数组,效率远高于循环写单个单元格 range->Value2 = values; return true; } catch (_com_error& e) { HandleOLEError(e); return false; } } void TExcelOLEHelper::HandleOLEError(const _com_error& e) { // 将OLE错误信息转换为可读字符串 String errorMsg = L"OLE Error: "; errorMsg += e.ErrorMessage(); // 在实际项目中,这里可以记录日志、弹出消息框等 OutputDebugString(errorMsg.c_str()); }实操心得:
WriteRange方法中的批量写入是性能关键。如果你需要写入几千甚至几万行数据,绝对不要用循环调用WriteCell,那会慢得让你怀疑人生。正确做法是将你的数据组织成一个二维的Variant数组,然后一次性赋值给Range->Value2。这个性能差异可以达到几个数量级。
4. 实战应用:从数据导出到图表生成
有了基础类,我们来看看如何解决几个常见的、从热搜词里也能看到的实际问题。
4.1 场景一:将数据库查询结果导出为格式化的报表
假设你从数据库(如通过TFDQuery)查询到了一批设备运行数据,包含时间、编号、温度、压力等字段,需要导出到Excel并做简单格式化。
void __fastcall TMainForm::BtnExportToExcelClick(TObject *Sender) { TExcelOLEHelper excelHelper(false); // 后台运行,不可见 if (!excelHelper.CreateNewWorkbook()) { ShowMessage(L"创建Excel失败!"); return; } // 1. 设置工作表名称 excelHelper.AddWorksheet(L"设备运行报告"); excelHelper.SetActiveWorksheet(L"设备运行报告"); // 2. 写入表头并设置格式 String headers[] = {L"时间戳", L"设备编号", L"温度(℃)", L"压力(MPa)", L"状态"}; for (int i = 0; i < 5; ++i) { excelHelper.WriteCell(1, i + 1, headers[i]); excelHelper.SetCellFont(1, i + 1, L"微软雅黑", 12, true); } // 3. 从数据库循环读取并写入数据 FDQuery1->Open(L"SELECT log_time, device_id, temperature, pressure, status FROM device_log ORDER BY log_time"); int currentRow = 2; while (!FDQuery1->Eof) { excelHelper.WriteCell(currentRow, 1, FDQuery1->FieldByName(L"log_time")->AsDateTime); excelHelper.WriteCell(currentRow, 2, FDQuery1->FieldByName(L"device_id")->AsString); excelHelper.WriteCell(currentRow, 3, FDQuery1->FieldByName(L"temperature")->AsFloat); excelHelper.WriteCell(currentRow, 4, FDQuery1->FieldByName(L"pressure")->AsFloat); excelHelper.WriteCell(currentRow, 5, FDQuery1->FieldByName(L"status")->AsString); // 根据状态设置单元格背景色(简单示例) if (FDQuery1->FieldByName(L"status")->AsString == L"报警") { // 这里需要调用另一个设置背景色的方法(略,需扩展ExcelOLEHelper类) } FDQuery1->Next(); currentRow++; } FDQuery1->Close(); // 4. 设置数字格式(例如,压力列保留两位小数) excelHelper.SetRangeNumberFormat(2, 4, currentRow - 1, 4, L"0.00"); // 5. 自动调整列宽(通过OLE调用Excel的AutoFit) // 需要获取表头所在的范围,然后调用AutoFit // excelHelper.AutoFitColumn(1, 1, 1, 5); // 假设我们实现了这个方法 // 6. 保存文件 String savePath = ExtractFilePath(Application->ExeName) + L"设备运行报告_" + FormatDateTime(L"yyyymmdd_hhnnss", Now()) + L".xlsx"; if (excelHelper.SaveAs(savePath)) { ShowMessage(L"报表已成功导出至: " + savePath); } else { ShowMessage(L"保存文件失败!"); } // 析构函数会自动关闭工作簿和Excel }4.2 场景二:实现复杂的数据查找与条件筛选
热搜词里提到了“若同一编号对应多个数据,需通过条件判断筛选”。这在OLE中可以通过Excel的AutoFilter(自动筛选)功能模拟实现,或者更高效地,在C++端处理好数据再写入。
方法A:利用Excel的自动筛选功能(适用于数据已写入Excel后)
bool TExcelOLEHelper::ApplyAutoFilter(int row, int col, const String& criteria) { if (m_worksheet == nullptr) return false; try { // 假设表头在第一行,数据从第二行开始 String rangeAddress = String().sprintf(L"A1:E%d", row); // 假设有5列 Excel::RangePtr range = m_worksheet->Range[WideString(rangeAddress).c_bstr()]; // 启用自动筛选 range->AutoFilter(col, WideString(criteria).c_bstr(), Excel::xlAnd, EmptyParam, true); return true; } catch (_com_error& e) { HandleOLEError(e); return false; } } // 调用:excelHelper.ApplyAutoFilter(lastDataRow, 2, L"DEV-1001"); // 筛选设备编号为DEV-1001的行方法B:在C++端预处理数据(推荐,更可控)如果你的数据源在数据库或内存中,在写入Excel前就完成筛选和聚合,通常是更好的选择。你可以用TFDQuery的SQL语句直接完成复杂筛选(如GROUP BY,HAVING),或者用C++标准库算法在内存中处理,然后将结果集一次性写入Excel。这样避免了依赖Excel的交互功能,更适合自动化报告生成。
4.3 场景三:创建图表与数据可视化
通过OLE创建图表稍微复杂,但步骤是清晰的。核心是获取数据区域,然后创建ChartObject并设置其属性。
bool TExcelOLEHelper::CreateChart(int dataStartRow, int dataStartCol, int dataEndRow, int dataEndCol, int chartRow, int chartCol, int chartHeight, int chartWidth) { if (m_worksheet == nullptr) return false; try { // 1. 获取数据区域 String dataRangeStr = String().sprintf(L"%c%d:%c%d", L'A' + dataStartCol - 1, dataStartRow, L'A' + dataEndCol - 1, dataEndRow); Excel::RangePtr dataRange = m_worksheet->Range[WideString(dataRangeStr).c_bstr()]; // 2. 在工作表上添加一个图表对象 Excel::ChartObjectsPtr chartObjects = m_worksheet->ChartObjects(); Excel::ChartObjectPtr chartObject = chartObjects->Add(chartCol * 10, chartRow * 10, chartWidth, chartHeight); // 位置和大小(单位是点) // 3. 获取图表对象并设置数据源和类型 Excel::_ChartPtr chart = chartObject->Chart; chart->SetSourceData(dataRange, Excel::xlColumns); // xlColumns 表示系列产生在列 chart->ChartType = Excel::xlColumnClustered; // 簇状柱形图 // 4. 设置图表标题等属性 chart->HasTitle = VARIANT_TRUE; chart->ChartTitle->Text = L"设备温度压力趋势图"; // 5. 可以进一步设置坐标轴、图例等 chart->Axes(Excel::xlCategory, Excel::xlPrimary)->HasTitle = VARIANT_TRUE; chart->Axes(Excel::xlCategory, Excel::xlPrimary)->AxisTitle->Text = L"时间序列"; return true; } catch (_com_error& e) { HandleOLEError(e); return false; } }5. 性能优化与大数据量处理实战
当处理成千上万行数据时,性能成为首要问题。以下是几个关键优化点:
关闭屏幕更新:在批量操作前关闭Excel的屏幕刷新,操作完成后再打开。这是提升速度最有效的方法之一。
m_excelApp->ScreenUpdating = VARIANT_FALSE; // 关闭 // ... 执行大量数据写入或格式操作 ... m_excelApp->ScreenUpdating = VARIANT_TRUE; // 开启禁用计算与事件:同样,在批量操作期间,可以暂停Excel的公式计算和事件触发。
m_excelApp->Calculation = Excel::xlCalculationManual; // 手动计算 m_excelApp->EnableEvents = VARIANT_FALSE; // 禁用事件 // ... 执行操作 ... m_excelApp->Calculation = Excel::xlCalculationAutomatic; m_excelApp->EnableEvents = VARIANT_TRUE;批量写入数据:如前所述,使用二维
Variant数组通过Range->Value2一次性写入,而不是循环写单个单元格。// 假设有1000行,5列数据 int rowCount = 1000; int colCount = 5; // 创建一个二维Variant数组 Variant varData; varData = VarArrayCreate(OPENARRAY(int, (0, rowCount-1, 0, colCount-1)), varVariant); // 填充varData(这里用随机数示例) for (int i = 0; i < rowCount; ++i) { for (int j = 0; j < colCount; ++j) { varData.PutElement(Variant(i * j), i, j); // 实际应填充你的业务数据 } } // 一次性写入A1:E1000区域 String rangeAddr = String().sprintf(L"A1:%c%d", L'A' + colCount - 1, rowCount); Excel::RangePtr targetRange = m_worksheet->Range[WideString(rangeAddr).c_bstr()]; targetRange->Value2 = varData;合理释放COM对象:虽然智能指针(如
_ApplicationPtr)能自动管理引用计数,但在长时间、大循环中,显式地将局部Range等对象设置为nullptr有助于及时释放资源。
6. 常见问题、异常处理与调试技巧
即使按照指南操作,在实际开发中你仍可能遇到各种问题。这里记录了一些典型的“坑”和解决方法。
6.1 典型问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
编译错误:Excel::_Application未声明 | 未正确导入类型库或未包含生成的头文件。 | 检查#include “Excel_2K_SRVR.h”语句,并确认该文件存在于工程搜索路径中。 |
运行时错误:CoCreateInstance failed或Class not registered | 1. 目标机器未安装Office或Excel。 2. 安装了WPS等第三方办公软件,未安装Microsoft Excel。 3. Office版本(32/64位)与你的程序位数不匹配。 | 1. 确保部署环境安装了完整版Microsoft Excel。 2. 程序若为32位,需安装32位Office;64位程序对应64位Office。可通过修改项目配置调整程序位数。 |
操作过程中Excel进程未关闭,积累多个EXCEL.EXE | 未正确调用Quit()方法,或异常导致未执行到Quit()。 | 1. 确保在try-catch的finally块或类的析构函数中调用QuitExcel()。2. 检查所有异常分支都释放了对象。 |
| 写入大量数据时程序卡死或极慢 | 1. 未关闭屏幕更新和自动计算。 2. 使用循环单单元格写入。 | 1. 在批量操作前设置ScreenUpdating = false和Calculation = xlManual。2. 改用 Range->Value2批量写入二维数组。 |
| 保存文件时弹出“是否覆盖”对话框 | DisplayAlerts属性未设置为false。 | 在初始化Application对象后,立即设置m_excelApp->DisplayAlerts = VARIANT_FALSE;。 |
读取的单元格值总是Null或错误 | 1. 单元格本身为空或包含公式未计算。 2. 使用了 Value属性而非Value2。 | 1. 确保操作前计算模式为自动,或手动触发Calculate()。2. 对于纯数据(文本、数字),优先使用 Value2属性,它不处理货币和日期格式,更稳定。 |
| 操作特定功能(如图表)时出现参数错误 | OLE方法参数类型或数量不匹配。Excel对象模型版本差异。 | 1. 使用IDE的代码提示功能查看方法签名。 2. 对于可选参数,使用 vtMissing(在C++Builder中常用EmptyParam或Variant()表示)。3. 查阅对应版本Excel的VBA对象模型文档作为参考。 |
6.2 调试与错误处理增强
基本的try-catch只能捕获严重错误。为了更好调试,可以增强错误处理函数:
void TExcelOLEHelper::HandleOLEError(const _com_error& e) { String errorMsg; errorMsg.sprintf(L"OLE Error (0x%08lx): %s\n", e.Error(), e.ErrorMessage()); // 尝试获取更详细的错误描述 IErrorInfo* pErrorInfo = NULL; if (GetErrorInfo(0, &pErrorInfo) == S_OK) { BSTR bstrDescription; if (pErrorInfo->GetDescription(&bstrDescription) == S_OK) { errorMsg += L"\nDescription: "; errorMsg += bstrDescription; SysFreeString(bstrDescription); } pErrorInfo->Release(); } // 记录到文件或调试输出 OutputDebugString(errorMsg.c_str()); // 在实际应用中,可以考虑将错误信息记录到日志文件 TStringList* log = new TStringList(); if (FileExists(L”app.log”)) log->LoadFromFile(L”app.log”); log->Add(DateTimeToStr(Now()) + L”: ” + errorMsg); log->SaveToFile(L”app.log”); delete log; }6.3 关于“找不到OLE DB驱动”等关联问题的说明
热搜词中出现了“找不到产品 microsoft ole db driver for sql server 的安装包”和“无法创建链接服务器的 ole db 访问接口”。这些问题通常与通过OLE DB或ADO连接数据库有关,而不是直接操作Excel OLE。虽然技术栈不同,但根源相似:都是COM组件注册或缺失的问题。
如果你的C++Builder程序同时需要连接数据库和操作Excel,请确保:
- 数据库连接(如使用
TADOConnection)所需的OLE DB驱动(如MSOLEDBSQL或旧版的SQLNCLI)已在目标机器上正确安装并注册。 - 操作Excel所需的Office Primary Interop Assemblies (PIA) 或直接通过OLE Automation,依赖于Office本身的安装和注册。
- 两者是独立的问题。数据库连接失败,不会影响Excel OLE自动化(只要Excel本身安装正确),反之亦然。排查时应分开处理。
7. 进阶话题:线程安全与资源管理
在GUI程序中,长时间操作Excel可能导致界面卡死。一个自然的想法是将耗时的Excel操作放到后台线程中。但这里有一个重要警告:Excel的OLE Automation对象绝大多数情况下不是线程安全的。这意味着你不能在非主线程中直接创建或操作在另一个线程中创建的Excel对象。
安全的做法是:
- 主线程创建,主线程使用:将所有Excel OLE调用限制在主线程(主VCL线程)中。
- 使用
TThread::Synchronize或TThread::Queue:如果必须在后台线程中触发Excel操作,将实际调用OLE的代码块通过Synchronize方法抛给主线程执行。// 在后台线程中 void __fastcall TExcelWorkerThread::Execute() { // ... 准备数据 ... TThread::Synchronize(NULL, [this]() { // 这段Lambda将在主线程中执行 m_excelHelper.WriteRange(…); // 安全的OLE调用 }); // ... 后续处理 ... } - 使用独立的Excel实例:如果逻辑允许,且操作完全独立,可以在后台线程中创建一个全新的、独立的Excel Application实例,并确保该实例的所有操作都在这个后台线程内完成,不与主线程的实例混用。但这需要在线程内单独初始化COM (
CoInitializeEx),并且管理起来更复杂。
关于资源管理,务必记住:谁创建,谁释放。确保Application.Quit()被调用,并且所有COM智能指针在适当的作用域外被析构。将Excel操作封装在类的析构函数或try...catch...finally块中是良好的实践,可以避免因异常导致Excel进程残留。
最后,我个人在实际大型项目中的体会是,对于非常复杂的、动态的报表生成,C++Builder OLE直接操作Excel虽然直接,但当逻辑极其复杂时,维护成本会上升。另一种架构选型是:使用C++Builder生成结构化的数据(如JSON、XML),然后调用一个轻量级的模板引擎(或者甚至是一个预先配置好的Excel模板文件,通过简单的占位符替换)来生成最终报表。但对于大多数需要深度控制Excel格式、公式、图表的自动化场景,掌握本文所述的OLE技术,仍然是C++Builder开发者手中最强大、最直接的武器。