VC++在MFC下呼叫EXCEL各種功能實現
阿新 • • 發佈:2019-02-12
一直以來就對EXCEL的各種功能很崇拜,後來經常使用VC,由於工作的需要,經常會遇到將文字檔案中的龐大資料提取到Excel中運算處理。這個工作量可謂是勞民傷財,但是又不可不做,於是使用最簡單的資料流(fscanf(), fprintf()之類)書寫文字格式的Excel檔案,其弱智程度我就不說了。。。
但是隨著資料量的增大,這種方法每次都要處理不相容問題,十分繁瑣。於是探索如何按照Excel的資料格式寫檔案,解決相容性問題。查詢了各種資料,MSDN也看了很多,最後終於成功了。哈哈!!!
好吧,現在就來說說這個鬼東西如何操作。首先,我對VC++高深的架構,介面,資料型別瞭解的並不多,這與絕大多數人是相同的。隨意很多東西都是隻管拿來用,只要不出問題,就不去想他。閒話少說,現在就開始。
1。首先按照常規的方式建立對話方塊的MFC應用程式,然後新增類,也就是我們需要進行資料處理的類。由於只涉及到字元轉換和數字提取,我建的是常規類,沒有基類。
2。既然要使用EXCEL的各種功能,那麼就必須包含EXCEL的類檔案(excel.h, excel.cpp)。晚上有大把教程告訴我們如何從excel的OBJ檔案或者EXE檔案提取原始檔。這裡不再敘述,反正我很早以前提取過office 2003的,就一直在用。
3。如果我們看Excel的標頭檔案(excel.h)會發現裡面有大量的資料型別是我們不常使用的。因此如果要直接套用這些函式,就必須做資料型別轉換,我這裡使用了com庫的介面,至於原理什麼的不清楚。
4。首先我們需要特別說明一個數據型別,這一個資料型別在Excel操作中反覆用到,就是 VARIANT 型別。這是一個結構體,裡面包含了大量的資料型別,其定義如下:
struct tagVARIANT {
union {
struct __tagVARIANT {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
ULONGLONG ullVal; /* VT_UI8 */
LONGLONG llVal; /* VT_I8 */
LONG lVal; /* VT_I4 */
BYTE bVal; /* VT_UI1 */
SHORT iVal; /* VT_I2 */
FLOAT fltVal; /* VT_R4 */
DOUBLE dblVal; /* VT_R8 */
VARIANT_BOOL boolVal; /* VT_BOOL */
_VARIANT_BOOL bool; /* (obsolete) */
SCODE scode; /* VT_ERROR */
CY cyVal; /* VT_CY */
DATE date; /* VT_DATE */
BSTR bstrVal; /* VT_BSTR */
IUnknown * punkVal; /* VT_UNKNOWN */
IDispatch * pdispVal; /* VT_DISPATCH */
SAFEARRAY * parray; /* VT_ARRAY */
BYTE * pbVal; /* VT_BYREF|VT_UI1 */
SHORT * piVal; /* VT_BYREF|VT_I2 */
LONG * plVal; /* VT_BYREF|VT_I4 */
LONGLONG * pllVal; /* VT_BYREF|VT_I8 */
FLOAT * pfltVal; /* VT_BYREF|VT_R4 */
DOUBLE * pdblVal; /* VT_BYREF|VT_R8 */
VARIANT_BOOL *pboolVal; /* VT_BYREF|VT_BOOL */
_VARIANT_BOOL *pbool; /* (obsolete) */
SCODE * pscode; /* VT_BYREF|VT_ERROR */
CY * pcyVal; /* VT_BYREF|VT_CY */
DATE * pdate; /* VT_BYREF|VT_DATE */
BSTR * pbstrVal; /* VT_BYREF|VT_BSTR */
IUnknown ** ppunkVal; /* VT_BYREF|VT_UNKNOWN */
IDispatch ** ppdispVal; /* VT_BYREF|VT_DISPATCH */
SAFEARRAY ** pparray; /* VT_BYREF|VT_ARRAY */
VARIANT * pvarVal; /* VT_BYREF|VT_VARIANT */
PVOID byref; /* Generic ByRef */
CHAR cVal; /* VT_I1 */
USHORT uiVal; /* VT_UI2 */
ULONG ulVal; /* VT_UI4 */
INT intVal; /* VT_INT */
UINT uintVal; /* VT_UINT */
DECIMAL * pdecVal; /* VT_BYREF|VT_DECIMAL */
CHAR * pcVal; /* VT_BYREF|VT_I1 */
USHORT * puiVal; /* VT_BYREF|VT_UI2 */
ULONG * pulVal; /* VT_BYREF|VT_UI4 */
ULONGLONG * pullVal; /* VT_BYREF|VT_UI8 */
INT * pintVal; /* VT_BYREF|VT_INT */
UINT * puintVal; /* VT_BYREF|VT_UINT */
struct __tagBRECORD {
PVOID pvRecord;
IRecordInfo * pRecInfo;
} __VARIANT_NAME_4; /* VT_RECORD */
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
};
VARIANT資料結構包含兩個域(如果不考慮保留的域)。vt域描述了第二個域的資料型別。為了使多種型別能夠在第二個域中出現,我們定義了一個聯合結構。所以,第二個域的名稱隨著vt域中輸入值的不同而改變。用於指定vt域值情況的常量在聯合的定義中以每一行的註釋形式給出。
使用VARIANT和VARIANTARG資料結構要分兩步完全。舉一個例子,讓我們考慮如下程式碼:
long lValue = 999;
VARIANT vParam;
vParam.vt = VT_I4;
vParam.lVal = lValue;
在第一行中指定資料型別。常量VT_I4表明在第二個域中將出現一個long型的資料。根據型別VARIANT的定義,可以得知,當一個long型資料存入VARIANT型別時,其第二個域使用的名稱是lVal。
5。瞭解了VARIANT資料型別,我們還需要了解一個函式,我實在微軟的官網查到這個函式的,網上講解的不是很全面,我大概說下,基本上是照貓畫虎,原理什麼的都不懂的。這個函式就是AutoWrap()函式。可能很多人都在網上查到過這個函式,但是其工作原理不是很清楚,我覺得沒必要全懂,只要理解兩句就足夠了。
HRESULT AutoWrap(int autoType, VARIANT *pvResult, IDispatch *pDisp, LPOLESTR ptName, int cArgs...)
{
// Begin variable-argument list...
va_list marker;
va_start(marker, cArgs);
if(!pDisp)
{
MessageBox(NULL, _T("NULL IDispatch passed to AutoWrap()"), _T("Error"), 0x10010);
_exit(0);
}
// Variables used...
DISPPARAMS dp = { NULL, NULL, 0, 0 };
DISPID dispidNamed = DISPID_PROPERTYPUT;
DISPID dispID;
HRESULT hr;
char buf[200];
char szName[200];
// Convert down to ANSI
WideCharToMultiByte(CP_ACP, 0, ptName, -1, szName, 256, NULL, NULL);
// Get DISPID for name passed...
hr = pDisp->GetIDsOfNames(IID_NULL, &ptName, 1, LOCALE_USER_DEFAULT, &dispID);
if(FAILED(hr))
{
sprintf(buf, "IDispatch::GetIDsOfNames(\"%s\") failed w/err 0x%08lx", szName, hr);
MessageBox(NULL, buf, L"AutoWrap()", 0x10010);
_exit(0);
return hr;
}
// Allocate memory for arguments...
VARIANT *pArgs = new VARIANT[cArgs+1];
// Extract arguments...
for(int i=0; i<cArgs; i++)
{
pArgs[i] = va_arg(marker, VARIANT);
}
// Build DISPPARAMS
dp.cArgs = cArgs;
dp.rgvarg = pArgs;
// Handle special-case for property-puts!
if(autoType & DISPATCH_PROPERTYPUT)
{
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &dispidNamed;
}
// Make the call!
hr = pDisp->Invoke(dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT, autoType, &dp, pvResult, NULL, NULL);
if(FAILED(hr))
{
sprintf(buf, "IDispatch::Invoke(\"%s\"=%08lx) failed w/err 0x%08lx", szName, dispID, hr);
MessageBox(NULL, buf, "AutoWrap()", 0x10010);
_exit(0);
return hr;
}
// End variable-argument section...
va_end(marker);
delete [] pArgs;
return hr;
}
其實這個函式的作用,就是將特定字串轉換成Excel命令,然後呼叫Invoke函式對相應資料進行處理。詳細說來:
(1)。int autoType:這裡只能有4個值,表示Invoke如何處理相關資料。在OLEAUTO.h檔案中定義如下:
/* Flags for IDispatch::Invoke */
#define DISPATCH_METHOD 0x1
#define DISPATCH_PROPERTYGET 0x2
#define DISPATCH_PROPERTYPUT 0x4
#define DISPATCH_PROPERTYPUTREF 0x8
這裡面我用了前面三個,主要的是中間的兩個。
(2)。VARIANT *pvResult:Invoke處理完資料後的返回指標,指向處理結果,後面我們看到就是子函式的返回值
(3)。 IDispatch *pDisp:一個指標,用於呼叫DISPID方法,其實就是隻呼叫個兩個方法,即在特定層次下,將特定字串轉換成命令值,然後執行命令
(4)。LPOLESTR ptName:就是一直在說的特定字串
(5)。int cArgs...:命令引數個數,由於是模板函式,所以後面可跟更多的引數。
好的,我們把引數都解釋了一下,那麼函式中有兩個特殊語句我們再說一下:
(1)。hr = pDisp->GetIDsOfNames(IID_NULL, &ptName, 1, LOCALE_USER_DEFAULT, &dispID)
這個函式在微軟官網上面有,但是我相信很多人沒看懂。如果我們把後面的一個語句放到一塊,就比較輕鬆了:
hr = pDisp->Invoke(dispID, IID_NULL, LOCALE_SYSTEM_DEFAULT, autoType, &dp, pvResult, NULL, NULL);
我們可以開啟我們的工程中的excel.cpp檔案,隨便找一個函式:
LPDISPATCH CalloutFormat::GetParent()
{
LPDISPATCH result;
InvokeHelper(0x1, DISPATCH_PROPERTYGET, VT_DISPATCH, (void*)&result, NULL);
return result;
}
這裡面的InvokeHelper()可以看做與Invoke()相同。所以第一個引數是一個十六進位制的數,每個函式這個十六進位制的數都不同,所以簡單來說,GetIDsOfNames函式就是將我們的特殊字串轉換成命令值。我們後面可知,這個特殊字串實際上只能是Excel相關的函式名或者部分函式名(這個比較難理解,可以先放下,後面會舉例)。
所以GetIDsOfNames最後只得到了一個int型別的值dispID,用以Invoke()函式的執行。
(2)。hr = pDisp->Invoke(disIpD, IID_NULL, LOCALE_SYSTEM_DEFAULT, autoType, &dp, pvResult, NULL, NULL);
這個函式就解釋一句話:根據定義的執行型別(autotype),特定的引數(dp),執行特定的命令值(disIpD),得到執行結果(pvResult)和執行狀態(hr )。如果hr不等於0,就說明執行失敗。
其實這個語句跟函式的意思是相同的,只是方式改變了而已。
所以這個函式:
HRESULT AutoWrap(int autoType, VARIANT *pvResult, IDispatch *pDisp, LPOLESTR ptName, int cArgs...)
的意思就是,我們可以直接通過直接寫一個函式名稱(或者部分函式名稱),新增相關引數(實參)後,就可以實現通常語法的函式功能,比如如下程式段:
例 1:
//select LineNum x RowNum range
IDispatch *pXlRange;
{
VARIANT parm;
parm.vt = VT_BSTR;
parm.bstrVal = ::SysAllocString(bstr_Range_Str);
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlSheet, L"Range", 1, parm);
VariantClear(&parm);
pXlRange = result.pdispVal;
}
這個語句段是呼叫pXlSheet.GetRange(Range)這個函式裡面的Invokehelper(),實際我們可以理解為就是在呼叫pXlSheet.GetRange(Range)函式。
可能有人會問:裡面的字串是"Range",為什麼函式本體是GetRange()哪?
我們看第一個引數:DISPATCH_PROPERTYGET,這表示我們需要Get相關的命令值(輸出),所以就是GetRange()了
如果是DISPATCH_PROPERTYPUT, 就表示此時命令值作為輸入要去改變部分引數,所以就是SetRange()了
如果是DISPATCH_METHOD,則在寫字串時,需要寫函式的完整名稱。
我們只要一個完整的函式包括形參和返回值,那在AutoWrap()中實參怎麼傳遞給形參哪?
這個就是int cArgs...的妙處,cArgs是一個int型別,表示需要傳遞的實參的數目,而cArgs後面的引數就都是要傳遞給函式的實參了。
我們可以看看Excel.h檔案,裡面的函式傳遞的引數各不相同,有的沒有引數,有的有十幾個引數。那我們要使用一個函式解決這所有的問題,就要使用如此的函式模板了。.如上面的例1。
特別需要說明的一點:如果你需要傳遞多個引數,引數的順序是反向的:SetItem(parm1, parm2, parm3, parm4)。則在AutoWrap(。。。。。。,L"Item", 4, parm4, parm3, parm2, parm1)。
6。如果以上內容都理解了(我說的可能有不對的地方,不過你們看了下面的例子應該就會明白),我們就開始正式些程式了:
(1)在我們剛才建好的工程中新增檔案:excel.h,excel.cpp
(2)在我們新建的那個類DatalogConvertor的cpp檔案中新增:
#include <ole2.h> //需要呼叫OLE方法
#include <comutil.h> //需要呼叫com介面
#pragma once
#pragma comment(lib, "comsupp.lib ") //呼叫Com庫
新增 HRESULT CDatalogConvertor::AutoWrap(int autoType, VARIANT *pvResult, IDispatch *pDisp, LPOLESTR ptName, int cArgs...)函式,就把上面的程式碼黏貼進去即可。
(3)在你的excel操作函式中開始啟動Excel程序,Excel採用層次化程式設計,如果我們要在Excel的一個sheet上寫資料,就要先開啟程序,建立工作薄,在工作薄中新增一個工作頁,啟用此工作頁(Sheet),之後才可以進行寫入操作。
//initial COM lib
CoInitialize(NULL);
CLSID clsid;
HRESULT hr = CLSIDFromProgID(L"Excel.Application", &clsid);
if(FAILED(hr))
{
::MessageBox(NULL, "CLSIDFromProgID() function error\nEXCEL Not Be Installed!", "error", 0x10010);
//::MessageBox(NULL, "CLSIDFromProgID() function error!", "error", 0x10010);
return -1;
}
// creat instance
IDispatch *pXlApp;
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (void **)&pXlApp);
if(FAILED(hr))
{
::MessageBox(NULL, "Pls check whether setuped EXCEL!", "error", 0x10010);
return -2;
}
// Application.Visible is ture
VARIANT IsVisible;
IsVisible.vt = VT_I4;
IsVisible.lVal = 0; //0=not visible, 1=visible
AutoWrap(DISPATCH_PROPERTYPUT, NULL, pXlApp, L"Visible", 1, IsVisible);
//get WorkBooks
IDispatch *pXlBooks;
{
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlApp, L"Workbooks", 0);
pXlBooks = result.pdispVal;
}
//create new Workbook using Workbook.Add() method
IDispatch *pXlBook;
{
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlBooks, L"Add", 0);
pXlBook = result.pdispVal;
}
//get Worksheet object from Application.ActiveSheet attribute
IDispatch *pXlSheet;
{
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlApp, L"ActiveSheet", 0);
pXlSheet = result.pdispVal;
}
到這裡我們順利啟動了Excel並且建立了工作薄,得到了當前的工作頁。我們要寫資料進去,就有兩種方法:一個是一個單元一個單元的寫(Cell),還有一個就是選定一個範圍(Range),然後一個特定格式的陣列進行新增。我這裡面選取的是Range。
//fill excel file function
int CDatalogConvertor::FillExcel(IDispatch *pXlSheet, int LineStart, int LineNum, int RowStart, int RowNum, CString Data[], int flag)
{
int i, j;
int data_flag;
//***create a LineNum x RowNum arrary to fill excel format***//
VARIANT arr;
WCHAR szTmp[128];
arr.vt = VT_ARRAY | VT_VARIANT;
SAFEARRAYBOUND sab[2];
sab[0].lLbound = 1; sab[0].cElements = LineNum;
sab[1].lLbound = 1; sab[1].cElements = RowNum;
arr.parray = SafeArrayCreate(VT_VARIANT, 2, sab);
//***Convert string to BSTR and fill the data into the array***//
BSTR bstrData[128][64]={0};
for(i=0;i<LineNum;i++)
{
for(j=0;j<RowNum;j++)
{
//Convert string to BSTR
bstrData[i][j]=_com_util::ConvertStringToBSTR(Data[i+j]);
VARIANT tmp;
tmp.vt = VT_BSTR;
wsprintfW(szTmp,bstrData[i][j],i,j);
tmp.bstrVal = SysAllocString(szTmp);
//fill the data into the array
long indices[]={i+1,j+1};
SafeArrayPutElement(arr.parray,indices,(void *)&tmp);
}
}
//Math Line Number and Row Number for excel range and array
char Row_Start = (char) (RowStart+64); //convert 1 to A
char Row_Stop = Row_Start+RowNum-1;
CString Range_Str, LineStart_Str, LineStop_Str;
BSTR bstr_Range_Str;
LineStart_Str.Format("%d", LineStart);
LineStop_Str.Format("%d", LineStart+LineNum-1);
Range_Str = _T(Row_Start)+LineStart_Str+_T(":")+_T(Row_Stop)+LineStop_Str;
bstr_Range_Str = _com_util::ConvertStringToBSTR(Range_Str);
//select LineNum x RowNum range
IDispatch *pXlRange;
{
VARIANT parm;
parm.vt = VT_BSTR;
parm.bstrVal = ::SysAllocString(bstr_Range_Str);
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlSheet, L"Range", 1, parm);
VariantClear(&parm);
pXlRange = result.pdispVal;
}
//fill the Range by our array
AutoWrap(DISPATCH_PROPERTYPUT, NULL, pXlRange, L"Value", 1, arr);
pXlRange->Release();
return 0;
}
這個函式裡面最後的AutoWrap(DISPATCH_PROPERTYPUT, NULL, pXlRange, L"Value", 1, arr);
就是將陣列填寫進相關的Range中。
(4)寫完了資料後就要儲存檔案退出Excel程序:
//***************************************************** //save Excel file and quit excel.exe //***************************************************** //save excel file through Worksheet.SaveAs(),
ignore all parameter expect filemname. VARIANT filename; filename.vt = VT_BSTR; filename.bstrVal = SysAllocString(_com_util::ConvertStringToBSTR(FileSaveName)); AutoWrap(DISPATCH_METHOD, NULL, pXlSheet, L"SaveAs", 1, filename); SysFreeString(filename.bstrVal);
// exit EXCEL app through Application.Quit() AutoWrap(DISPATCH_METHOD, NULL, pXlApp, L"Quit", 0); //release all parameter //pXlRange->Release(); pXlSheet->Release(); pXlBook->Release(); pXlBooks->Release(); pXlApp->Release(); //VariantClear(&arr); //*********************************************************
//close files //********************************************************* //close COM lib CoUninitialize();
(5)如果我們除了寫資料意外還想做點其他的操作,如改變字型格式,顏色,填充顏色,設定寬度,甚至更高階的功能,不用著急,AutoWrap()的功能弄清楚之後就很容易:
CString cstr_Line, cstr_Range;
BSTR bstr_Range;
cstr_Line.Format("%d",data_line);
cstr_Range = _T("A")+cstr_Line+_T(":")+_T("A")+cstr_Line;
bstr_Range = _com_util::ConvertStringToBSTR(cstr_Range);
//select LineNum x RowNum range 得到Range
IDispatch *pXlRange;
{
VARIANT parm;
parm.vt = VT_BSTR;
parm.bstrVal = ::SysAllocString(bstr_Range);
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlSheet, L"Range", 1, parm);
VariantClear(&parm);
pXlRange = result.pdispVal;
}
//Set Column Width object form Range.SetColumnWidth() attribute
//設定此部分Range的列寬度
{
VARIANT parm;
parm.vt = VT_I4;
parm.lVal = 40.0;
AutoWrap(DISPATCH_PROPERTYPUT, NULL, pXlRange, L"ColumnWidth", 1, parm);
}
//get Font object from Range.GetFont() attribute
//得到相應Range內預設的字型屬性
IDispatch *pXlFonts;
{
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlRange, L"Font", 0);
pXlFonts = result.pdispVal;
}
//Set Font object from Range.SetColor() attribute
//設定字型顏色
IDispatch *pXlFont;
{
VARIANT result;
VariantInit(&result);
VARIANT parm;
parm.vt = VT_I4;
parm.lVal = RGB(255,0,0); //red color
AutoWrap(DISPATCH_PROPERTYPUT, &result, pXlFonts, L"Color", 1,parm);
pXlFont = result.pdispVal;
}
//get Interior object from Range.GetInterior() attribute
//得到相應Range內的框體預設屬性
IDispatch *pXlInterior;
{
VARIANT result;
VariantInit(&result);
AutoWrap(DISPATCH_PROPERTYGET, &result, pXlRange, L"Interior", 0);
pXlInterior = result.pdispVal;
}
//Set Back Color object from Interior.SetColor() attribute
//設定框體背景顏色
IDispatch *pXlBackColor;
{
VARIANT result;
VariantInit(&result);
VARIANT parm;
parm.vt = VT_I4;
parm.lVal = RGB(140,227,190); //blue back color
AutoWrap(DISPATCH_PROPERTYPUT, &result, pXlInterior, L"Color", 1,parm);
pXlBackColor = result.pdispVal;
}
所以只要想新增什麼功能,在Excel.h檔案中找到相關的函式,根據函式名和引數型別,定製AutoWrap()函式,程式碼會變得很清晰整潔。同時據微軟官網說,這也會是程式碼執行效率提高很多(不知是真是假。。)