1. 程式人生 > >VC++在MFC下呼叫EXCEL各種功能實現

VC++在MFC下呼叫EXCEL各種功能實現

一直以來就對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()函式,程式碼會變得很清晰整潔。同時據微軟官網說,這也會是程式碼執行效率提高很多(不知是真是假。。)