Thử lập trình XLL add-in bằng C/C++ (1 người xem)

Người dùng đang xem chủ đề này

Tôi tuân thủ nội quy khi đăng bài

nguyendang95

Thành viên thường trực
Tham gia
25/5/22
Bài viết
241
Được thích
196
Có lẽ rất nhiều người dùng đã quen với các dạng add-in mà Excel hỗ trợ, những dạng phổ biến hiện nay (không tính những add-in chỉ dùng cho các bản Office cũ hơn bản 2007) bao gồm .xlam và .xll, trong đó .xlam được viết bằng VBA, còn .xll được viết bằng C/C++ và một số ngôn ngữ lập trình khác như C#, Delphi, v.v., bất cứ ngôn ngữ lập trình nào hỗ trợ khả năng xuất ra DLL tiêu chuẩn (Standard DLL). Với .xlam, người dùng có thể tương tác sâu rộng với Excel cũng như có thể viết hàm tùy chỉnh (hoặc hàm do người dùng tự định nghĩa, user-defined function) để sử dụng trong bảng tính, trong khi .xll chỉ hỗ trợ khả năng tương tác hạn chế với Excel thông qua Excel C API , ngoài ra loại add-in này còn cho phép người dùng tạo ra sheet macro và dialog (những tính năng này đều đã lỗi thời và đã bị macro VBA thay thế từ rất lâu), bên cạnh đó .xll cũng cho phép người dùng tạo ra các hàm tùy chỉnh để dùng trong bảng tính và hiệu năng mà nó mang lại vượt trội so với .xlam của VBA.
Để lập trình một .xll add-in bằng C/C++, người dùng cần đáp ứng những tiêu chí sau đây:
  • Hiểu biết cơ bản về C/C++.
  • Đã cài đặt Excel Software Development Kit (tải xuống ở đây).
Để viết mã C/C++, người dùng có thể chọn môi trường lập trình tích hợp (IDE) bất kỳ, ở đây tôi chọn Visual Studio bởi vì đã quen với giao diện của nó cũng như làm việc nhiều với Visual C++.
Đầu tiên, người dùng mở Visual Studio, chọn Create New Project, sau đó chọn tiếp Dynamic-Link Library (DLL) và lưu lại dự án mới.
1740587660860.png
Tiếp theo, tiến hành cài đặt Excel Software Development Kit, đường dẫn mặc định là C:\2013 Office System Developer Resources, sau đó sao chép những tệp sau đây vào thư mục của dự án vừa mới tạo ở trên:
1. Excel2013XLLSDK\INCLUDE:
  • XLCALL.H
2. Excel2013XLLSDK\LIB\x64 (với Excel bản 64bit), hoặc Excel2013XLLSDK\LIB (với Excel bản 32bit):
  • XLCALL32.LIB
3. Excel2013XLLSDK\SAMPLES\FRAMEWRK:
  • FRAMEWRK.C
  • FRAMEWRK.H
  • MemoryManager.cpp
  • MemoryManager.h
  • MemoryPool.cpp
  • MemoryPool.h
4. Excel2013XLLSDK\SRC:
  • XLCALL.CPP
1740588008767.png
Tiến hành viết code, ở đây viết một hàm tên là PrintHello với một tham số duy nhất kiểu chuỗi, nó chỉ có tác dụng in ra dòng chữ "Hello, <tham_số>". Lưu ý, do từ phiên bản Excel 2007 trở đi đều có tính năng Multithreaded Recalculation và Multithreaded Calculation cho phép Excel tận dụng số nhân xử lý của CPU để thực hiện việc tính toán, cho nên tiện lợi nhất là người dùng nên sử dụng Thread-local Storage API nhằm tránh những rắc rối như race condition (điều kiện tranh giành) do tính toán đa luồng gây ra khiến cho kết quả tính toán bị sai lệch.
C++:
#include "XLCALL.H"
#include "FRAMEWRK.H"
#include "stdlib.h"
#include "string"
#include "Windows.h"

DWORD tlsIndex; //Khai báo một biến DWORD để lưu trữ chỉ số của Thread-local Storage mà các thread có thể sử dụng để lưu trữ dữ liệu cục bộ cho mỗi thread

//Khai báo struct để lưu trữ dữ liệu cục bộ cho mỗi thread
struct TlsData {
    XLOPER12 data;
};

//Hàm này trả về struct từ Thread-local Storage mà thread hiện tại đang sử dụng
TlsData* GetTlsData() {
    if (!tlsIndex) return NULL;
    TlsData* tlsData = reinterpret_cast<TlsData*>(TlsGetValue(tlsIndex));
    return tlsData;
}

//Khởi tạo Thread-local Storage
BOOL InitializeTls(DWORD DllMainCallReason) {
    switch (DllMainCallReason) {
    case DLL_PROCESS_ATTACH:
        if ((tlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
            return FALSE;
    case DLL_PROCESS_DETACH:
        if (tlsIndex) {
            TlsFree(tlsIndex);
            break;
        }
    }
    return TRUE;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    //Khởi tạo Thread-local Storage
    return InitializeTls(ul_reason_for_call);
}

//Hàm này chuyển chuỗi ký tự wide char sang định dạng mà Excel yêu cầu
wchar_t *CreateCountedUnicodeWcharStringFromNullTerminatedUnicodeWCharString(const wchar_t* text) {
    size_t len; //Khai báo biến chứa độ dài của chuỗi cần chuyển đổi
    if (!text || !(len = wcslen(text))) return NULL; //Nếu chuỗi rỗng thì trả về NULL
    if (len > 32767u) len = 32767u; //Nếu độ dài chuỗi lớn hơn 32767u ký tự thì cắt gọt chuỗi
    wchar_t *wstr = new wchar_t[(len + 2)]; //Cấp phát bộ nhớ động cho chuỗi kết quả, số lượng byte cần cấp phát là độ dài chuỗi cần chuyển đổi cộng thêm 2 ký tự (để dành ký tự cuối cùng làm ký tự kết thúc chuỗi) * 2 byte
    if (!wstr) return NULL; //Nếu không cấp phát được bộ nhớ thì trả về NULL
    memcpy(wstr + 1, text, (len + 1) * sizeof(wchar_t)); //Sao chép chuỗi cần chuyển đổi, bắt đầu từ ký tự thứ hai, vào chuỗi kết quả
    wstr[0] = len; //Gán độ dài chuỗi vào vị trí đầu tiên của chuỗi kết quả
    return wstr; //Trả về chuỗi kết quả
}

extern "C" __declspec(dllexport) LPXLOPER12 WINAPI PrintHello(wchar_t* name) {
    //static LPXLOPER12 result; Không an toàn thread vì biến tĩnh này có thể bị ghi đè bởi các thread khác
    TlsData* tlsData = GetTlsData(); //Thay vào đó, sử dụng Thread-local Storage API để lưu trữ dữ liệu cục bộ cho mỗi thread
    if (!tlsData) return NULL;
    LPXLOPER12 result = &(tlsData->data); //Khai báo biến con trỏ của kiểu XLOPER12 để lưu trữ kết quả trả về
    result->xltype = xltypeStr | xlbitDLLFree; //Thiết lập kiểu trả về của hàm là chuỗi, đồng thời thông báo cho Excel biết rằng Excel sẽ giải phóng bộ nhớ cho chuỗi này do nó được cấp phát động
    wchar_t wstrInput[100] = L"Hello, "; //Khai báo một mảng ký tự wide char để lưu trữ chuỗi kết quả
    wcscat_s(wstrInput, name); //Nối chuỗi name vào chuỗi wstrInput
    wchar_t* pWstrOutput = CreateCountedUnicodeWcharStringFromNullTerminatedUnicodeWCharString(wstrInput); //Chuyển chuỗi wstrInput sang định dạng mà Excel yêu cầu
    result->val.str = pWstrOutput; //Gán địa chỉ của chuỗi kết quả vào biến con trỏ result
    return result; //Trả về con trỏ result để Excel có thể sử dụng kết quả
}
//Excel gọi hàm này khi nạp add-in
extern "C" __declspec(dllexport) int WINAPI xlAutoOpen(void)
{
    XLOPER12 xDLL;
    //Xác định tên của DLL
    if (Excel12f(xlGetName, &xDLL, 0) == xlretSuccess) {
        Excel12f(xlfRegister, 0, 11, (LPXLOPER12)&xDLL, //Đăng ký tên của DLL
            (LPXLOPER12)TempStr12(L"PrintHello"), //Tên hàm
            (LPXLOPER12)TempStr12(L"UC%"), //Kiểu tham số, ký tự đầu tiên đại diện cho kiểu dữ liệu trả về của hàm, các ký tự tiếp theo là kiểu dữ liệu của các tham số
            (LPXLOPER12)TempStr12(L"PrintHello"), //Tên hàm trong trình Function Wizard
            (LPXLOPER12)TempStr12(L"name"), //Mô tả tham số trong trình Function Wizard
            (LPXLOPER12)TempStr12(L"1"), //Kiểu macro
            (LPXLOPER12)TempStr12(L"myOwnCppFunctions"), //Hạng mục
            (LPXLOPER12)TempStr12(L""), //Tổ hợp phím tắt
            (LPXLOPER12)TempStr12(L""), //Tên tập tin Help
            (LPXLOPER12)TempStr12(L"Print 'Hello <user>'"), //Mô tả hàm khi được chọn trong trình Function Wizard
            (LPXLOPER12)TempStr12(L"")); //Mô tả tham số khi được chọn trong trình Function Wizard (tối đa lên đến 255 tham số)
        //Giải phóng biến xDLL
        Excel12f(xlFree, 0, 1, (LPXLOPER12)&xDLL);
    }
    return 1;
}

//Excel gọi hàm này khi người dùng tắt Add-in hoặc thoát khỏi Excel
extern "C" __declspec(dllexport) int WINAPI xlAutoClose(void) {
    return 1;
}

//Excel gọi hàm này khi kiểu XLOPER12 trả về có chứa cờ bit xlbitDLLFree hoặc xlbitXLFree để dọn dẹp bộ nhớ
extern "C" __declspec(dllexport) void WINAPI xlAutoFree12(LPXLOPER12 p_oper) {
    //Nếu kiểu dữ liệu trả về là chuỗi
    if (p_oper->xltype & xltypeStr) {
        delete p_oper->val.str; //Giải phóng bộ nhớ đã cấp phát động cho chuỗi
    }
}

Tiến hành biên dịch ra DLL, sau đó đổi phần mở rộng thành .xll và đưa vào Excel.
1740589448063.png
Kiểm tra add-in đã được Excel nạp vào.
1740589490521.png
Viết thử hàm.
1740589526459.png
Để biết thêm thông tin chi tiết về những tính năng mà .xll add-in hỗ trợ, tham khảo bài viết Programming with the C API in Excel
 
Lần chỉnh sửa cuối:
Cảm ơn bạn về bài viết này, bạn có thể giúp mình làm 1 video về việc tạo add-in vào excel từ C++ không, cảm ơn bạn.
 
Cảm ơn bạn về bài viết này, bạn có thể giúp mình làm 1 video về việc tạo add-in vào excel từ C++ không, cảm ơn bạn.
XLL add-in thực chất vẫn là Standard DLL, chỉ là DLL đó cần hỗ trợ một số hàm bắt buộc để Excel gọi và nhận diện add-in.
Nếu bạn muốn lập trình XLL add-in bằng C/C++ thì có thể tham khảo cuốn sách này xem sao.
1754736528882.png
 
Bạn có thể gửi giúp mình file visual studio C++ theo bài post bên trên được không ah, mình có làm theo nhưng không được chạy được code. Cảm ơn bạn
nguyentruongtho1711@gmail.com
 
Lần chỉnh sửa cuối:
Hàm bất đồng bộ (Asynchronous User-Defined Functions)
Trong một số trường hợp, hàm có thể mất nhiều thời gian hơn để xử lý và trả kết quả (vd: Đợi thông tin từ internet hoặc khối lượng công việc tính toán lớn mất nhiều thời gian), cho nên khi hàm chạy đồng bộ, Excel phải chờ hàm trả về kết quả xong thì mới cho phép người dùng tương tác với giao diện người dùng (UI) của nó.
Để giải quyết vấn đề này, Excel giới thiệu mô hình hàm bất đồng bộ, trong đó trình tự các bước thực thi như sau:
  • Đăng ký hàm là hàm bất đồng bộ, hàm này bắt buộc phải trả về kiểu void và phải có tham số chứa kiểu XLOPER hoặc XLOPER12 để Excel sử dụng làm điều khiển (handle) quản lý hàm bất đồng bộ. Tham khảo thêm: xlfRegister (Form 1)
  • Trong hàm, tiến hành khởi tạo thread để xử lý các công việc chính của hàm (mục đích chính của hàm bất đồng bộ), chuẩn bị các dữ liệu cần thiết cho việc tính toán (lưu ý: Các giá trị khi truyền vào thread bắt buộc phải dưới dạng sao chép sâu (deep copy) và được lưu trữ trên vùng nhớ heap bởi vì nếu chỉ lưu trữ trên vùng nhớ stack, khi hàm chính kết thúc thì dữ liệu trên vùng nhớ stack sẽ bị xóa và thu hồi, dẫn đến thread của hàm tham chiếu đến vùng nhớ không hợp lệ và khiến cho Excel bị sập (crash)).
  • Từ thread của hàm, tiến hành tính toán như bình thường, sau khi có kết quả thì dùng hàm gọi lại (callback) xlAsyncReturn để trả kết quả cho Excel.
C++:
void AsyncFuncCallback(LPXLOPER12 lpHandle) {
    //Giả lập công việc tính toán mất nhiều thời gian
    Sleep(3000);
    //Chuẩn bị kết quả để trả về cho Excel
    XLOPER12 result = {};
    result.xltype = xltypeNum;
    result.val.num = 10;
    //Trả kết quả về cho Excel
    Excel12f(xlAsyncReturn, NULL, 2, lpHandle, &result);
}

DWORD WINAPI AsyncFuncCallbackProc(LPVOID lpParam) {
    //Kiểm tra điều kiện đầu vào
    if (!lpParam) return 1;
    LPXLOPER12 pHandle = (LPXLOPER12)lpParam;
    //Gọi hàm tính toán
    AsyncFuncCallback(pHandle);
    return 0;
}

extern "C" __declspec(dllexport) void WINAPI AsyncFunc(LPXLOPER12 lpHandle) {
    //Kiểm tra điều kiện đầu vào
    if (!lpHandle) return;
    //Tạo thread để tính toán
    HANDLE hThread = CreateThread(NULL, 0, AsyncFuncCallbackProc, lpHandle, 0, NULL);
    if (!hThread) return;
}

extern "C" __declspec(dllexport) int WINAPI xlAutoOpen()
{
    XLOPER12 xlDllName;
    //Xác định tên của DLL
    if (Excel12f(xlGetName, &xlDllName, 0) == xlretSuccess) {
        //Đăng ký hàm với Excel
        Excel12f(xlfRegister, NULL, 10, (LPXLOPER12)&xlDllName, //Đăng ký tên của DLL
            (LPXLOPER12)TempStr12(L"AsyncFunc"), //Tên hàm
            (LPXLOPER12)TempStr12(L">X"), //Kiểu tham số, ký tự đầu tiên đại diện cho kiểu dữ liệu trả về của hàm, các ký tự tiếp theo là kiểu dữ liệu của các tham số
            (LPXLOPER12)TempStr12(L"AsyncFunc"), //Tên hàm trong trình Function Wizard
            (LPXLOPER12)TempStr12(L""), //Mô tả tham số trong trình Function Wizard
            (LPXLOPER12)TempStr12(L"1"), //Kiểu macro
            (LPXLOPER12)TempStr12(L"Asynchronous Functions"), //Hạng mục
            (LPXLOPER12)TempStr12(L""), //Tổ hợp phím tắt
            (LPXLOPER12)TempStr12(L""), //Tên tập tin Help
            (LPXLOPER12)TempStr12(L""));
        //Giải phóng bộ nhớ
        Excel12f(xlFree, NULL, 1, (LPXLOPER12)&xlDllName);
    }
    return 1;
}

Khi hàm chạy trên bảng tính, điều khiển được trả về ngay cho Excel giúp cho giao diện người dùng không bị đơ, trên ô bảng tính sẽ hiện lỗi #CALC! tức là hàm đang bận tính toán.

1758287064865.png

Hàm sau khi chạy xong sẽ trả kết quả về cho Excel.

1758287137042.png
 
Mới viết xong XLL tham gia chơi với cho Vui bản 64 bít

1782809934579.png
 

File đính kèm

Bà tám với ChatGPT kết luận như sau

1782870793970.png

Thực tế Mạnh tự định nghĩa như sau

XLL menu Ribbon.png

Ms không hổ trợ là lý thuyết còn thực tế thì tư duy logis tí là xong
1782872617839.png
 
Lần chỉnh sửa cuối:
Bài viết này chỉ quăng mỗi mã lên đây thì hơi nhàm chán, nếu có thêm người quan tâm phân tích những khía cạnh kỹ thuật liên quan thì sẽ hay hơn.
P/s: Đang nói bài #1.
 

Xin chào thế giới​

Xem xét chữ ký của một hàm sau đây, với mục đích là trả về chuỗi “Hello, World” khi người dùng gõ hàm này vào bảng tính trên Excel:

C++:
extern "C" __declspec(dllexport) wchar_t* WINAPI PrintHelloWorld();

Để Excel có thể nhận diện được hàm này, trước tiên cần phải đăng ký hàm với Excel đã. Để đăng ký hàm với Excel, người dùng cần gọi hàm xlfRegister, thông tin chi tiết về hàm này người dùng có thể tìm thấy ở đây: xlfRegister (Form 1) | Microsoft Learn

1784205096282.png
1784205104139.png

Chú ý đến đối số pxTypeText, tham số cho Excel biết hàm được đăng ký có bao nhiêu tham số, kiểu giá trị trả về và kiểu giá trị của các tham số là gì, ở đây Excel yêu cầu hai loại chuỗi:
  • Null-terminated Unicode wide-character string, tức là chuỗi wchar_t thông thường kết thúc bằng ký tự rỗng (null).
  • Counted Unicode wide-character string, tức là chuỗi wchar_t giống như trên, nhưng khác biệt ở chỗ chuỗi có chứa thông tin về độ dài chuỗi, thông tin này được lưu trữ ở phần tử đầu tiên trong mảng chuỗi.
Ở đây hàm PrintHelloWorld chọn trả về chuỗi wchar_t thông thường, cho nên ký tự đại diện sẽ là C%. Với việc hàm này không có đối số nên đối số pxTypeText cuối cùng sẽ là “C%”.

Như vậy nội dung của hàm PrintHelloWorld có thể được viết như sau:

C++:
extern "C" __declspec(dllexport) wchar_t* WINAPI PrintHelloWorld() {
    const wchar_t szInput[] = L"Hello World";
    size_t len = wcslen(szInput);
    wchar_t* pwszResult = new wchar_t[len + 1];
    if (!pwszResult) return NULL;
    errno_t err = wcscpy_s(pwszResult, len + 1, szInput);
    if (err) {
        delete[] pwszResult;
        return NULL;
    }
    return pwszResult;
}

Thường thì người ta sẽ muốn đăng ký hàm ngay khi Excel nạp XLL, cho nên hàm xlAutoOpen sẽ là nơi lý tưởng nhất để làm việc này bởi vì Excel sẽ gọi hàm này khi vừa mới nạp XLL.

Sau đây là mã đăng ký hàm:

C++:
extern "C" __declspec(dllexport) int WINAPI xlAutoOpen() {
    XLOPER12 xlDllName;
    int iResult = Excel12(xlGetName, &xlDllName, 0);
    if (iResult == xlretFailed) return 0;
    iResult = Excel12(xlfRegister, NULL, 11,
        &xlDllName,
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L"C%"),
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L""),
        TempStr12(L"1"),
        TempStr12(L"UDF"),
        TempStr12(L""),
        TempStr12(L""),
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L"")
    );
    Excel12(xlFree, NULL, 1, &xlDllName);
    return iResult == xlretSuccess ? 1 : 0;
}

Hàm khi được sử dụng trên bảng tính:

1784205174863.png

Lưu ý: Do hàm trả trực tiếp con trỏ wchar_t* về cho Excel, cho nên sẽ xảy ra tình trạng rò rỉ bộ nhớ sau mỗi lần gọi. Excel sau khi sử dụng xong thì sẽ không giải phóng bộ nhớ mà con trỏ này trỏ đến là vì nó không thể nào biết được vùng nhớ được cấp phát bằng phương thức nào (malloc, new, GlobalAlloc, LocalAlloc, HeapAlloc, v.v...) để giải phóng bằng phương thức tương ứng. Cho nên thường thì người ta sẽ trả về giá trị cho Excel thông qua cấu trúc (struct) XLOPER12 và sử dụng hàm gọi lại xlAutoFree12 để giải phóng vùng nhớ mà cấu trúc này chứa con trỏ trỏ đến.
 

File đính kèm

Lần chỉnh sửa cuối:

Khắc phục vấn đề còn tồn đọng: Rò rỉ bộ nhớ​

Như đã đề cập ở bài trước, hàm PrintHelloWorld trả con trỏ wchar_t* về cho Excel, cho nên tình trạng rò rỉ bộ nhớ sẽ xảy ra mỗi khi Excel gọi đến hàm này. Để khắc phục tình trạng này, người dùng nên sử dụng cấu trúc (struct) XLOPER12 để gửi gắm giá trị cần trả về cho Excel, cấu trúc này gồm nhiều hợp (union) của các cấu trúc khác nhau.

C++:
/*
** XLOPER12 structure
**
** Excel 12's fundamental data type: can hold data
** of any type. Use "U" as the argument type in the
** REGISTER function.
**/

typedef struct xloper12
{
    union
    {
        double num;                           /* xltypeNum */
        XCHAR *str;                           /* xltypeStr */
        BOOL xbool;                           /* xltypeBool */
        int err;                           /* xltypeErr */
        int w;
        struct
        {
            WORD count;                       /* always = 1 */
            XLREF12 ref;
        } sref;                        /* xltypeSRef */
        struct
        {
            XLMREF12 *lpmref;
            IDSHEET idSheet;
        } mref;                        /* xltypeRef */
        struct
        {
            struct xloper12 *lparray;
            RW rows;
            COL columns;
        } array;                    /* xltypeMulti */
        struct
        {
            union
            {
                int level;            /* xlflowRestart */
                int tbctrl;            /* xlflowPause */
                IDSHEET idSheet;        /* xlflowGoto */
            } valflow;
            RW rw;                           /* xlflowGoto */
            COL col;                       /* xlflowGoto */
            BYTE xlflow;
        } flow;                        /* xltypeFlow */
        struct
        {
            union
            {
                BYTE *lpbData;            /* data passed to XL */
                HANDLE hdata;            /* data returned from XL */
            } h;
            long cbData;
        } bigdata;                    /* xltypeBigData */
    } val;
    DWORD xltype;
} XLOPER12, *LPXLOPER12;

Như vậy để truyền con trỏ wchar_t* cho Excel thông qua XLOPER12, người dùng cần bật cờ bit xltypeStr cho trường xltype để thông báo cho Excel biết hàm muốn trả về giá trị kiểu chuỗi, rồi sau đó chỉ định con trỏ wchar_t* trong cấu trúc val.str, ví dụ:

C++:
static XLOPER12 xlRet;
xlRet.xltype = xltypeStr;
xlRet.val.str = pwszVal;

Có một vấn đề thế này, bởi vì Excel đòi hỏi chuỗi được truyền thông qua XLOPER12 phải là chuỗi có chứa thông tin về độ dài chuỗi, cho nên người dùng không thể nào sử dụng chuỗi wchar_t thông thường được. Thay vào đó, người dùng sẽ phải bố trí cấu trúc của chuỗi đúng như những gì mà Excel yêu cầu: Phần tử đầu tiên trong mảng chứa thông tin về độ dài chuỗi, mà phần tử này có kích thước của kiểu wchar_t là 2 byte, cho nên giá trị dương tối đa mà nó có thể biểu diễn là 32768, phần còn lại của mảng là chuỗi như thường dùng. Do phải tốn một phần tử trong mảng để chứa thông tin về độ dài chuỗi, cho nên 32768 – 1 = 32767 độ dài chuỗi tối đa mà Excel có thể tiếp nhận.

Như vậy có thể viết hàm chuyển đổi chuỗi theo yêu cầu Excel như sau:

C++:
#define MAX_XL12_STR_LEN 32767u

wchar_t* WideCharToXl12Str(const wchar_t* lpwszInput) {
    size_t len;
    if (!lpwszInput || !(len = wcslen(lpwszInput))) return NULL;
    if (len > MAX_XL12_STR_LEN) len = MAX_XL12_STR_LEN;
    wchar_t* pwszResult = new wchar_t[len + 2];
    if (!pwszResult) return NULL;
    pwszResult[0] = (wchar_t)len;
    errno_t err = wcscpy_s(pwszResult + 1, len + 1, lpwszInput);
    if (err) {
        delete[] pwszResult;
        return NULL;
    }
    return pwszResult;
}

Sửa lại hàm PrintHelloWorld như sau:

C++:
extern "C" __declspec(dllexport) LPXLOPER12 WINAPI PrintHelloWorld() {
    const wchar_t szInput[] = L"Hello World";
    static XLOPER12 xlRet;
    xlRet.xltype = xltypeStr | xlbitDLLFree;
    xlRet.val.str = WideCharToXl12Str(szInput);
    if (!xlRet.val.str) return NULL;
    return &xlRet;
}

Sửa lại phần đăng ký hàm với Excel:

C++:
extern "C" __declspec(dllexport) int WINAPI xlAutoOpen() {
    XLOPER12 xlDllName;
    int iResult = Excel12(xlGetName, &xlDllName, 0);
    if (iResult == xlretFailed) return 0;
    iResult = Excel12(xlfRegister, NULL, 11,
        &xlDllName,
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L"Q"),
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L""),
        TempStr12(L"1"),
        TempStr12(L"UDF"),
        TempStr12(L""),
        TempStr12(L""),
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L"")
    );
    Excel12(xlFree, NULL, 1, &xlDllName);
    return iResult == xlretSuccess ? 1 : 0;
}

Quan trọng: Để giải quyết vấn đề rò rỉ bộ nhớ, Excel cung cấp một hàm gọi lại tên là xlAutoFree12, hàm này có một tham số là con trỏ kiểu LPXLOPER12, đây chính là con trỏ mà người dùng đã trả về từ hàm (vd: PrintHelloWorld chẳng hạn). Hàm xlAutoFree12 được gọi khi và chỉ khi người dùng bật cờ bit xlbitDLLFree hoặc xlbitXLFree. Cờ bit xlbitDLLFree được sử dụng khi người dùng cấp phát và sử dụng vùng nhớ theo cách thông thường (như trong hàm PrintHelloWorld) chẳng hạn, còn cờ bit xlbitXLFree được sử dụng khi vùng nhớ đó được tạo ra nhờ kết quả của hàm Excel12 (vd: Trả về từ hàm xlGetName chẳng hạn).

Nhờ hàm gọi lại xlAutoFree12 mà người dùng sẽ có cơ hội giải phóng bộ nhớ sau khi hàm được thực hiện xong, loại bỏ hoàn toàn vấn đề rò rỉ bộ nhớ.

C++:
extern "C" __declspec(dllexport) int WINAPI xlAutoFree12(LPXLOPER12 pOper) {
    if (!pOper) return 0;
    if (pOper->xltype & (xltypeStr | xlbitDLLFree) || pOper->val.str) {
        delete[] pOper->val.str;
    }
    return 0;
}

Kiểm tra xem Excel có gọi lại hàm xlAutoFree12 không sau khi thực hiện xong hàm PrintHelloWorld. Kết quả đúng như ý muốn.

1784257739328.png

Tạm thời cứ đơn giản như trên đã, sau này muốn tận dụng tính năng Multithreaded Recalculation của Excel, tức là Excel sẽ sử dụng nhiều thread khác nhau để tính toán lại hàm khi có yêu cầu cần tính toán lại trên bảng tính, thì hàm PrintHelloWorld ở trên bắt buộc phải đảm bảo an toàn thread, tuy nhiên khai báo static XLOPER12 thì không được rồi, cho nên sẽ cần phải làm thêm một số việc khác.
 
Lần chỉnh sửa cuối:

Khắc phục vấn đề còn tồn đọng: Rò rỉ bộ nhớ​

Như đã đề cập ở bài trước, hàm PrintHelloWorld trả con trỏ wchar_t* về cho Excel, cho nên tình trạng rò rỉ bộ nhớ sẽ xảy ra mỗi khi Excel gọi đến hàm này. Để khắc phục tình trạng này, người dùng nên sử dụng cấu trúc (struct) XLOPER12 để gửi gắm giá trị cần trả về cho Excel, cấu trúc này gồm nhiều hợp (union) của các cấu trúc khác nhau.

C++:
/*
** XLOPER12 structure
**
** Excel 12's fundamental data type: can hold data
** of any type. Use "U" as the argument type in the
** REGISTER function.
**/

typedef struct xloper12
{
    union
    {
        double num;                           /* xltypeNum */
        XCHAR *str;                           /* xltypeStr */
        BOOL xbool;                           /* xltypeBool */
        int err;                           /* xltypeErr */
        int w;
        struct
        {
            WORD count;                       /* always = 1 */
            XLREF12 ref;
        } sref;                        /* xltypeSRef */
        struct
        {
            XLMREF12 *lpmref;
            IDSHEET idSheet;
        } mref;                        /* xltypeRef */
        struct
        {
            struct xloper12 *lparray;
            RW rows;
            COL columns;
        } array;                    /* xltypeMulti */
        struct
        {
            union
            {
                int level;            /* xlflowRestart */
                int tbctrl;            /* xlflowPause */
                IDSHEET idSheet;        /* xlflowGoto */
            } valflow;
            RW rw;                           /* xlflowGoto */
            COL col;                       /* xlflowGoto */
            BYTE xlflow;
        } flow;                        /* xltypeFlow */
        struct
        {
            union
            {
                BYTE *lpbData;            /* data passed to XL */
                HANDLE hdata;            /* data returned from XL */
            } h;
            long cbData;
        } bigdata;                    /* xltypeBigData */
    } val;
    DWORD xltype;
} XLOPER12, *LPXLOPER12;

Như vậy để truyền con trỏ wchar_t* cho Excel thông qua XLOPER12, người dùng cần bật cờ bit xltypeStr cho trường xltype để thông báo cho Excel biết hàm muốn trả về giá trị kiểu chuỗi, rồi sau đó chỉ định con trỏ wchar_t* trong cấu trúc val.str, ví dụ:

C++:
static XLOPER12 xlRet;
xlRet.xltype = xltypeStr;
xlRet.val.str = pwszVal;

Có một vấn đề thế này, bởi vì Excel đòi hỏi chuỗi được truyền thông qua XLOPER12 phải là chuỗi có chứa thông tin về độ dài chuỗi, cho nên người dùng không thể nào sử dụng chuỗi wchar_t thông thường được. Thay vào đó, người dùng sẽ phải bố trí cấu trúc của chuỗi đúng như những gì mà Excel yêu cầu: Phần tử đầu tiên trong mảng chứa thông tin về độ dài chuỗi, mà phần tử này có kích thước của kiểu wchar_t là 2 byte, cho nên giá trị dương tối đa mà nó có thể biểu diễn là 32768, phần còn lại của mảng là chuỗi như thường dùng. Do phải tốn một phần tử trong mảng để chứa thông tin về độ dài chuỗi, cho nên 32768 – 1 = 32767 độ dài chuỗi tối đa mà Excel có thể tiếp nhận.

Như vậy có thể viết hàm chuyển đổi chuỗi theo yêu cầu Excel như sau:

C++:
#define MAX_XL12_STR_LEN 32767u

wchar_t* WideCharToXl12Str(const wchar_t* lpwszInput) {
    size_t len;
    if (!lpwszInput || !(len = wcslen(lpwszInput))) return NULL;
    if (len > MAX_XL12_STR_LEN) len = MAX_XL12_STR_LEN;
    wchar_t* pwszResult = new wchar_t[len + 2];
    if (!pwszResult) return NULL;
    pwszResult[0] = (wchar_t)len;
    errno_t err = wcscpy_s(pwszResult + 1, len + 1, lpwszInput);
    if (err) {
        delete[] pwszResult;
        return NULL;
    }
    return pwszResult;
}

Sửa lại hàm PrintHelloWorld như sau:

C++:
extern "C" __declspec(dllexport) LPXLOPER12 WINAPI PrintHelloWorld() {
    const wchar_t szInput[] = L"Hello World";
    static XLOPER12 xlRet;
    xlRet.xltype = xltypeStr | xlbitDLLFree;
    xlRet.val.str = WideCharToXl12Str(szInput);
    if (!xlRet.val.str) return NULL;
    return &xlRet;
}

Sửa lại phần đăng ký hàm với Excel:

C++:
extern "C" __declspec(dllexport) int WINAPI xlAutoOpen() {
    XLOPER12 xlDllName;
    int iResult = Excel12(xlGetName, &xlDllName, 0);
    if (iResult == xlretFailed) return 0;
    iResult = Excel12(xlfRegister, NULL, 11,
        &xlDllName,
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L"Q"),
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L""),
        TempStr12(L"1"),
        TempStr12(L"UDF"),
        TempStr12(L""),
        TempStr12(L""),
        TempStr12(L"PrintHelloWorld"),
        TempStr12(L"")
    );
    Excel12(xlFree, NULL, 1, &xlDllName);
    return iResult == xlretSuccess ? 1 : 0;
}

Quan trọng: Để giải quyết vấn đề rò rỉ bộ nhớ, Excel cung cấp một hàm gọi lại tên là xlAutoFree12, hàm này có một tham số là con trỏ kiểu LPXLOPER12, đây chính là con trỏ mà người dùng đã trả về từ hàm (vd: PrintHelloWorld chẳng hạn). Hàm xlAutoFree12 được gọi khi và chỉ khi người dùng bật cờ bit xlbitDLLFree hoặc xlbitXLFree. Cờ bit xlbitDLLFree được sử dụng khi người dùng cấp phát và sử dụng vùng nhớ theo cách thông thường (như trong hàm PrintHelloWorld) chẳng hạn, còn cờ bit xlbitXLFree được sử dụng khi vùng nhớ đó được tạo ra nhờ kết quả của hàm Excel12 (vd: Trả về từ hàm xlGetName chẳng hạn).

Nhờ hàm gọi lại xlAutoFree12 mà người dùng sẽ có cơ hội giải phóng bộ nhớ sau khi hàm được thực hiện xong, loại bỏ hoàn toàn vấn đề rò rỉ bộ nhớ.

C++:
extern "C" __declspec(dllexport) int WINAPI xlAutoFree12(LPXLOPER12 pOper) {
    if (!pOper) return 0;
    if (pOper->xltype & (xltypeStr | xlbitDLLFree) || pOper->val.str) {
        delete[] pOper->val.str;
    }
    return 0;
}

Kiểm tra xem Excel có gọi lại hàm xlAutoFree12 không sau khi thực hiện xong hàm PrintHelloWorld. Kết quả đúng như ý muốn.

View attachment 312075

Tạm thời cứ đơn giản như trên đã, sau này muốn tận dụng tính năng Multithreaded Recalculation của Excel, tức là Excel sẽ sử dụng nhiều thread khác nhau để tính toán lại hàm khi có yêu cầu cần tính toán lại trên bảng tính, thì hàm PrintHelloWorld ở trên bắt buộc phải đảm bảo an toàn thread, tuy nhiên khai báo static XLOPER12 thì không được rồi, cho nên sẽ cần phải làm thêm một số việc khác.
mới viết có một hàm PrintHelloWorld thì dòm cái hàm xlAutoOpen đã dài cả mét rồi vậy nếu có 50 hàm với các tham số động thì điều gì xẩy ra

nó sẽ lên cấp số nhân hay số cộng vậy tạm tính dài lên 50 mét quá ... xong quản lý kiểu gì ???!!!

đừng buồn từ đó tư duy chút có phương pháp viết thôi ... không lẽ cứ mãi PrintHelloWorld hay Chào thế giới
 
mới viết có một hàm PrintHelloWorld thì dòm cái hàm xlAutoOpen đã dài cả mét rồi vậy nếu có 50 hàm với các tham số động thì điều gì xẩy ra

nó sẽ lên cấp số nhân hay số cộng vậy tạm tính dài lên 50 mét quá ... xong quản lý kiểu gì ???!!!

đừng buồn từ đó tư duy chút có phương pháp viết thôi ... không lẽ cứ mãi PrintHelloWorld hay Chào thế giới
Buồn cái gì, demo ngắn gọn thì viết thế cho người mới tìm hiểu mới dễ lĩnh ngộ được, chứ nếu muốn ngắn gọn thì viết thêm vài hàm gói gọn tham số là xong, có gì khó.
Chứ như bên ông dùng Delphi có sẵn thư viện người ta viễn sẵn cho thì nói làm gì.
Bắt đầu tìm hiểu cái mới thì phải biết viết Hello World, chứ tay mơ đòi nhảy vào phải viết được cái này phải viết được cái kia thì có mà tới mùa quít à.
 
Buồn cái gì, demo ngắn gọn thì viết thế cho người mới tìm hiểu mới dễ lĩnh ngộ được, chứ nếu muốn ngắn gọn thì viết thêm vài hàm gói gọn tham số là xong, có gì khó.
Chứ như bên ông dùng Delphi có sẵn thư viện người ta viễn sẵn cho thì nói làm gì.
Bắt đầu tìm hiểu cái mới thì phải biết viết Hello World, chứ tay mơ đòi nhảy vào phải viết được cái này phải viết được cái kia thì có mà tới mùa quít à.
Delphi viết cho Excel nói chung XLL hay TaskPane làm gì có hàm nào "Delphi có sẵn thư viện người ta viễn sẵn" mà mọi cái định định nghĩa lấy mà dùng

nếu nói CSDL như FireDAC họ viết cho SQLite với các hàm bao bọc wrapper thì còn nghe được ... quên chuyện ăn sẵn đi
 
Delphi viết cho Excel nói chung XLL hay TaskPane làm gì có hàm nào "Delphi có sẵn thư viện người ta viễn sẵn" mà mọi cái định định nghĩa lấy mà dùng

nếu nói CSDL như FireDAC họ viết cho SQLite với các hàm bao bọc wrapper thì còn nghe được ... quên chuyện ăn sẵn đi
Rồi ông có dám khẳng định cái XLL ông viết có thông qua một lớp thư viện gói gọn nào không. Excel vốn dĩ ban đầu viết bằng C, nếu không nhờ viết lại mã dựa trên những tệp header .h mà Microsoft cung cấp, nếu không nhờ Delphi ngầm thực hiện việc chuyển đổi kiểu dữ liệu qua lại giữa Delphi với C thì có mà chạy được đấy.
Hình như tôi thấy ông hơi lạc đề rồi, bài viết này tập trung vào những khía cạnh viết XLL bằng C++ chứ không phải để khoe xem viết bằng Delphi hay C++ thì ngắn hơn, đẳng cấp hơn.
 
Rồi ông có dám khẳng định cái XLL ông viết có thông qua một lớp thư viện gói gọn nào không. Excel vốn dĩ ban đầu viết bằng C, nếu không nhờ viết lại mã dựa trên những tệp header .h mà Microsoft cung cấp, nếu không nhờ Delphi ngầm thực hiện việc chuyển đổi kiểu dữ liệu qua lại giữa Delphi với C thì có mà chạy được đấy.
Hình như tôi thấy ông hơi lạc đề rồi, bài viết này tập trung vào những khía cạnh viết XLL bằng C++ chứ không phải để khoe xem viết bằng Delphi hay C++ thì ngắn hơn, đẳng cấp hơn.
Không lạc đâu còn ai viết trên bất cứ ngôn ngữ nào cũng dựa vào header.h là c++ của Ms xong XLL ... còn chuyện ăn sãn chỉ mấy chú tái sử dụng ví dụ

như : Excel-DNA hay ADD-IN EXPRESS™ hay bên thứ ba nào đó xong viết XLL thì mới gọi là ăn sẵn

Rảnh tôi sẽ viết c++ builder cũng thế thôi ... Tôi dừng ở đây không bàn luận nữa vì không cần thiết
 
Không lạc đâu còn ai viết trên bất cứ ngôn ngữ nào cũng dựa vào header.h là c++ của Ms xong XLL ... còn chuyện ăn sãn chỉ mấy chú tái sử dụng ví dụ

như : Excel-DNA hay ADD-IN EXPRESS™ hay bên thứ ba nào đó xong viết XLL thì mới gọi là ăn sẵn

Rảnh tôi sẽ viết c++ builder cũng thế thôi ... Tôi dừng ở đây không bàn luận nữa vì không cần thiết
Gửi ông mã viết bằng CodeBlocks sử dụng trình biên dịch GCC để tham khảo.

1784298002836.png

Nói chung là cú pháp thì vẫn thế thôi, chỉ là khi làm việc bằng IDE khác nhau và trình biên dịch khác nhau thì sẽ có những cách làm việc khác nhau.
 

File đính kèm

Bài viết mới nhất

Back
Top Bottom