Tạo COM Add-In bằng Visual C++ (1 người xem)

Liên hệ QC

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 hoạt động
Tham gia
25/5/22
Bài viết
139
Được thích
126
Hiện nay để phát triển phần bổ trợ (add-in) cho Excel riêng và các ứng dụng Office khác nói chung, Microsoft cung cấp cho lập trình viên những công cụ sau đây:
  • VSTO: Được xây dựng trên nền tảng .NET Framework, cung cấp một bộ thư viện đồ sộ giúp lập trình viên có thể dễ dàng xây dựng một add-in bằng Visual Basic hoặc Visual C#. Nhược điểm: Do phải truy cập vào mô hình đối tượng của ứng dụng thông qua lớp biên dịch (translation) com interop, cho nên hiệu năng mang lại không cao so với những giải pháp khác. Ngoài ra, giải pháp này chỉ có thể chạy trên Windows.
  • Office Javascript API: Cho phép lập trình viên có thể viết add-in bằng Javascript hoặc Typescript chạy đa nền tảng, đa thiết bị, là giải pháp đang được Microsoft khuyến khích nhất hiện nay. Nhược điểm: Triển khai khá phức tạp, yêu cầu kiến thức chuyên sâu về lập trình web.
  • COM Add-In: Giải pháp cổ điển, trong đó yêu cầu lập trình viên viết một COM DLL có triển khai giao diện (interface) IDTExtensibility2 và IRibbonExtensibility để tương tác với ứng dụng chủ (host) và làm việc với giao diện ribbon. Có thể viết bằng bất cứ ngôn ngữ nào cho phép làm việc với COM, từ C/C++, VB6, Delphi cho đến ngôn ngữ nền .NET Framework (Visual Basic và Visual C#) thông qua com interop. Nhược điểm: Triển khai rất phức tạp, yêu cầu kiến thức sâu rộng về COM.
Bài viết này trình bày các bước viết một Excel COM Add-In bằng C++.
Tạo COM DLL:
ATL Project là một giải pháp gồm một bộ các lớp (class) tiêu bản (template) và macro do Microsoft phát triển giúp đơn giản hoá quá trình phát triển một COM DLL và ActiveX control. Để sử dụng, người dùng có thể sử dụng bản Visual Studio mới nhất hoặc những phiên bản khác có hỗ trợ ATL Project.

1751784602911.png

Lưu ý: Tính đến thời điểm hiện tại (6-7-2025) Visual Studio đang tồn tại một số lỗi (bug) nghiêm trọng khiến cho việc phát triển bằng ATL Project trở nên rất khó khăn, cho nên trong bài viết này sử dụng phiên bản Visual Studio 2019 để trình bày code.
Dưới đây là một trong những lỗi đã được Microsoft ghi nhận:
"Bad file path: 'project_name.idl'" error when creating ATL Simple Objects in an ATL Project created using VS2022 Version 17.13.7
Để tạo ATL Project, người dùng khởi chạy Visual Studio, sau đó từ màn hình chính của Visual Studio, chọn tiếp Create a new project và chọn ATL Project.

1751784623148.png

Tiến hành đặt tên cho dự án và thiết lập một vài tham số khác (nếu có). Trong bài viết này đặt tên dự án là ExcelAddInDemo.

1751784630098.png

Triển khai giao diện IDTExtensibility2 và IRibbonExtensibility:
Lúc này dự án đang trống rỗng, cho nên để add-in thực sự hoạt động được, người dùng cần triển khai giao diện IDTExtensibility2 và IRibbonExtensibility. IDTExtensibility2 gồm năm phương thức (method) OnConnection, OnDisconnection, OnStartupComplete, OnAddInsUpdate và OnBeginShutdown, giao diện này đóng vai trò định hình COM DLL trở thành add-in mà Excel có thể tương tác được. Còn IRibbonExtensibility chỉ có một phương thức duy nhất, GetCustomUI, đóng vai trò trình bày giao diện ribbon tuỳ chỉnh theo mong muốn của người dùng.
Để bắt đầu, trước tiên người dùng cần nhập (import) các thư viện cần thiết trong tập tin pch.h.

C++:
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.

#ifndef PCH_H
#define PCH_H

// add headers that you want to pre-compile here
#include "framework.h"
#import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4" raw_interfaces_only, raw_native_types, no_namespace, named_guids, auto_search
#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" auto_rename auto_search raw_interfaces_only rename_namespace("Office")
#import "libid:00020813-0000-0000-C000-000000000046" auto_rename auto_search rename("RGB", "ExcelRGB") rename("DocumentProperties", "ExcelDocumentProperties")
using namespace Office;
using namespace Excel;
#endif //PCH_H

Tiếp theo, tạo một ATL Simple Object bằng cách, từ cửa sổ Solution Explorer bên tay phải màn hình, nhấp chuột phải vào tên dự án (ở đây là ExcelAddInDemo), sau đó chọn Add – New Item. Cửa sổ Add New Item hiện ra, người dùng chọn tiếp mục ATL trong phần Visual C++ xổ ra và chọn ATL Simple Object, trong bài viết đặt tên là ThisAddIn.

1751784696862.png

Lưu ý thông tin ở ô ProgID, đây chính là căn cứ để đặt tên khoá registry giúp Excel nhận diện được add-in.

1751784706052.png

Một class mới được tạo ra, người dùng có thể triển khai hai giao diện quan trọng ở trên.

C++:
// ThisAddIn.h : Declaration of the CThisAddIn

#pragma once
#include "resource.h"       // main symbols



#include "ExcelAddInDemo_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

typedef IDispatchImpl <IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), 2, 5> RibbonImpl;
typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1, /* wMinor = */ 0> IDTImpl;
// CThisAddIn

class ATL_NO_VTABLE CThisAddIn :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CThisAddIn, &CLSID_ThisAddIn>,
    public IDispatchImpl<IThisAddIn, &IID_IThisAddIn, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public RibbonImpl,
    public IDTImpl
{
public:
    CThisAddIn()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CThisAddIn)
    COM_INTERFACE_ENTRY(IThisAddIn)
    COM_INTERFACE_ENTRY2(IDispatch, IThisAddIn)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(OnConnection)(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom);
    STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY** custom);
    STDMETHOD(OnAddInsUpdate)(SAFEARRAY** custom);
    STDMETHOD(OnStartupComplete)(SAFEARRAY** custom);
    STDMETHOD(OnBeginShutdown)(SAFEARRAY** custom);
    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR* RibbonXml);


};

OBJECT_ENTRY_AUTO(__uuidof(ThisAddIn), CThisAddIn)

C++:
// ThisAddIn.cpp : Implementation of CThisAddIn

#include "pch.h"
#include "ThisAddIn.h"


// CThisAddIn

STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    MessageBox(NULL, L"Add-in loaded", L"Add-in Event", MB_OK | MB_ICONINFORMATION);
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnDisconnection(ext_DisconnectMode RemoveMode, SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnAddInsUpdate(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnStartupComplete(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnBeginShutdown(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::GetCustomUI(BSTR RibbonID, BSTR* RibbonXml) {
    if (!RibbonXml) return E_POINTER;
    *RibbonXml = SysAllocString(LR"(<?xml version="1.0" encoding="UTF-8"?>
                                    <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoaded">
                                      <ribbon>
                                        <tabs>
                                          <tab id="customTab" label="Excel AddIn">
                                            <group id="customGroup" label="Group">
                                              <button id="customButton"
                                                      label="Click Me"
                                                      getImage="GetImage"
                                                      size="large"
                                                      onAction="ButtonClicked" />
                                            </group>
                                          </tab>
                                        </tabs>
                                      </ribbon>
                                    </customUI>)");
    return *RibbonXml ? S_OK : E_OUTOFMEMORY;
}

Đoạn code này nhìn chung khá đơn giản, trong phương thức OnConnection chứa một câu lệnh nhỏ có chức năng hiển thị một hộp thoại, còn phương thức GetCustomUI có tác dụng nạp bố cục (schema) của ribbon theo định dạng XML để Excel có thể trình bày. Để đơn giản, bài viết này chèn trực tiếp schema của ribbon vào code, còn khi viết code thực tế người dùng nên nạp schema vào DLL thông qua mục Resource Files, sau đó lần lượt sử dụng các hàm FindResource, LoadResource và LockResource để đọc nội dung và đưa vào phương thức GetCustomUI sẽ thuận tiện hơn.
Ngoài ra một tập tin MIDL cũng sẽ được tạo ra. Trong bài viết này không sử dụng tập tin này, tuy nhiên nó đóng vai trò quan trọng khi người dùng muốn định nghĩa các hàm gọi lại (callback function) từ các control trên ribbon.

C++:
// ExcelAddInDemo.idl : IDL source for ExcelAddInDemo
//

// This file will be processed by the MIDL tool to
// produce the type library (ExcelAddInDemo.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid(ded9f6b8-b802-4547-97fe-0dabdcd4d205),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IThisAddIn : IDispatch
{
};
[
    uuid(bd1b5ce2-9b75-49f5-b9a3-c9eb192b1be1),
    version(1.0),
]
library ExcelAddInDemoLib
{
    importlib("stdole2.tlb");
    [
        uuid(2762b776-5488-4564-8cbd-fb71834332ca)
    ]
    coclass ThisAddIn
    {
        [default] interface IThisAddIn;
    };
};

import "shobjidl.idl";

Tiếp theo, người dùng mở tập tin ThisAddIn.rgs trong mục Resource Files trong cửa sổ Solution Explorer và chèn vào đó đoạn mã dưới đây, lưu ý mã ProgID ExcelAddInDemo.ThisAddIn mà người dùng đã thiết lập khi tạo ATL Simple Object:
Mã:
HKCR
{
    ExcelAddInDemo.ThisAddIn.1 = s 'ThisAddIn class'
    {
        CLSID = s '{2762b776-5488-4564-8cbd-fb71834332ca}'
    }
    ExcelAddInDemo.ThisAddIn = s 'ThisAddIn class'
    {    
        CurVer = s 'ExcelAddInDemo.ThisAddIn.1'
    }
    NoRemove CLSID
    {
        ForceRemove {2762b776-5488-4564-8cbd-fb71834332ca} = s 'ThisAddIn class'
        {
            ProgID = s 'ExcelAddInDemo.ThisAddIn.1'
            VersionIndependentProgID = s 'ExcelAddInDemo.ThisAddIn'
            ForceRemove Programmable
            InprocServer32 = s '%MODULE%'
            {
                val ThreadingModel = s 'Apartment'
            }
            TypeLib = s '{bd1b5ce2-9b75-49f5-b9a3-c9eb192b1be1}'
            Version = s '1.0'
        }
    }
}

HKCU
{
    NoRemove Software
    {
        NoRemove Microsoft
        {
            NoRemove Office
            {
                NoRemove Excel
                {
                    NoRemove Addins
                    {
                        ExcelAddInDemo.ThisAddIn
                        {
                            val Description = s 'Sample Addin'
                            val FriendlyName = s 'Sample Addin'
                            val LoadBehavior = d 3
                        }
                    }
                }
            }
        }
    }
}
Cuối cùng, tiến hành biên dịch (build) ra DLL hoàn chỉnh, tuỳ thuộc vào phiên bản Excel cài đặt trên máy tính mà người dùng có thể biên dịch ra DLL phiên bản 32bit hoặc 64bit, sau đó cho Excel nạp add-in.
Kết quả:

1751785125655.png

Bố cục ribbon tuỳ chỉnh.

1751785149545.png
Người dùng có thể tìm thấy code đính kèm theo bài viết này.
 

File đính kèm

Lần chỉnh sửa cuối:
Tạo Ngăn Tác vụ (Task Pane):
Để tạo Task Pane, người dùng cần triển khai giao diện ICustomTaskPaneConsumer, kèm theo đó là tạo một ActiveX control đóng vai trò là cửa sổ (window) hiển thị cho Task Pane.
Trước tiên, người dùng hãy tạo ActiveX control bằng cách, từ cửa sổ Solution Explorer bên phải màn hình, nhấp chuột phải vào tên dự án (trong bài viết là ExcelAddInDemo), sau đó chọn Add - New Item.
Hộp thoại Add New Item hiện ra, người dùng chọn tiếp ATL - ATL Control trong mục Visual C++.
1751902512413.png
Tiến hành đặt tên, trong bài viết này để là MyCustomTaskPane. Người dùng có thể để trống ô ProgID, tuy nhiên người dùng nên nhập giá trị này để sau này có thể lấy làm tham số cho phương thức tạo Task Pane, không cần phải dùng hàm ProgIDFromCLSID để lấy thông tin ProgID của ActiveX control từ mã CLSID.
1751902646745.png
ATL Project sẽ tự động tạo ra một class mới tên là CMyCustomTaskPane, người dùng cần sửa một số thứ trong class này để đảm bảo Task Pane có thể sử dụng nó một cách thuận lợi.
Đầu tiên, sửa phương thức khởi tạo class (constructor) của class, cho phép biến ActiveX control thành dạng cửa sổ (window).
C++:
    CMyCustomTaskPane()
    {
        m_bWindowOnly = true;
    }
Tiếp theo, thêm thông điệp (message) cần xử lý riêng thông qua macro BEGIN_MSG_MAP, ở đây người dùng sẽ tạo một nút bấm (button) và một textbox (hay edit control) khi Task Pane hiện ra, cho nên người dùng sẽ cần xử lý thông điệp WM_CREATE thông qua macro MESSAGE_HANDLER, hàm gọi lại để xử lý thông điệp sẽ là OnCreate. Tiếp theo, xử lý thông điệp WM_COMMAND thông qua hàm gọi lại OnClick để thiết lập logic khi người dùng nhấp chuột vào nút bấm thì sẽ hiện ra hộp thoại.
C++:
// MyCustomTaskPane.h : Declaration of the CMyCustomTaskPane
#pragma once
#include "resource.h"       // main symbols
#include <atlctl.h>
#include "ExcelAddInDemo_i.h"

#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;



// CMyCustomTaskPane
class ATL_NO_VTABLE CMyCustomTaskPane :
    public CComObjectRootEx<CComSingleThreadModel>,
    public IDispatchImpl<IMyCustomTaskPane, &IID_IMyCustomTaskPane, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IOleControlImpl<CMyCustomTaskPane>,
    public IOleObjectImpl<CMyCustomTaskPane>,
    public IOleInPlaceActiveObjectImpl<CMyCustomTaskPane>,
    public IViewObjectExImpl<CMyCustomTaskPane>,
    public IOleInPlaceObjectWindowlessImpl<CMyCustomTaskPane>,
    public CComCoClass<CMyCustomTaskPane, &CLSID_MyCustomTaskPane>,
    public CComControl<CMyCustomTaskPane>
{
public:


    CMyCustomTaskPane()
    {
        m_bWindowOnly = true;
    }

DECLARE_OLEMISC_STATUS(OLEMISC_RECOMPOSEONRESIZE |
    OLEMISC_CANTLINKINSIDE |
    OLEMISC_INSIDEOUT |
    OLEMISC_ACTIVATEWHENVISIBLE |
    OLEMISC_SETCLIENTSITEFIRST
)

DECLARE_REGISTRY_RESOURCEID(IDR_MYCUSTOMTASKPANE)


BEGIN_COM_MAP(CMyCustomTaskPane)
    COM_INTERFACE_ENTRY(IMyCustomTaskPane)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IViewObjectEx)
    COM_INTERFACE_ENTRY(IViewObject2)
    COM_INTERFACE_ENTRY(IViewObject)
    COM_INTERFACE_ENTRY(IOleInPlaceObjectWindowless)
    COM_INTERFACE_ENTRY(IOleInPlaceObject)
    COM_INTERFACE_ENTRY2(IOleWindow, IOleInPlaceObjectWindowless)
    COM_INTERFACE_ENTRY(IOleInPlaceActiveObject)
    COM_INTERFACE_ENTRY(IOleControl)
    COM_INTERFACE_ENTRY(IOleObject)
END_COM_MAP()

BEGIN_PROP_MAP(CMyCustomTaskPane)
    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
    PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
    // Example entries
    // PROP_ENTRY_TYPE("Property Name", dispid, clsid, vtType)
    // PROP_PAGE(CLSID_StockColorPage)
END_PROP_MAP()


BEGIN_MSG_MAP(CMyCustomTaskPane)
    CHAIN_MSG_MAP(CComControl<CMyCustomTaskPane>)
    DEFAULT_REFLECTION_HANDLER()
    MESSAGE_HANDLER(WM_CREATE, OnCreate)
    MESSAGE_HANDLER(WM_COMMAND, OnClick)
END_MSG_MAP()
// Handler prototypes:
//  LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
//  LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);
//  LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);

// IViewObjectEx
    DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKGND | VIEWSTATUS_OPAQUE)

// IMyCustomTaskPane
public:
    LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnClick(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    HRESULT OnDraw(ATL_DRAWINFO& di)
    {
        RECT& rc = *(RECT*)di.prcBounds;
        // Set Clip region to the rectangle specified by di.prcBounds
        HRGN hRgnOld = nullptr;
        if (GetClipRgn(di.hdcDraw, hRgnOld) != 1)
            hRgnOld = nullptr;
        bool bSelectOldRgn = false;

        HRGN hRgnNew = CreateRectRgn(rc.left, rc.top, rc.right, rc.bottom);

        if (hRgnNew != nullptr)
        {
            bSelectOldRgn = (SelectClipRgn(di.hdcDraw, hRgnNew) != ERROR);
        }

        Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);
        SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE);
        LPCTSTR pszText = _T("MyCustomTaskPane");
#ifndef _WIN32_WCE
        TextOut(di.hdcDraw,
            (rc.left + rc.right) / 2,
            (rc.top + rc.bottom) / 2,
            pszText,
            lstrlen(pszText));
#else
        ExtTextOut(di.hdcDraw,
            (rc.left + rc.right) / 2,
            (rc.top + rc.bottom) / 2,
            ETO_OPAQUE,
            nullptr,
            pszText,
            ATL::lstrlen(pszText),
            nullptr);
#endif

        if (bSelectOldRgn)
            SelectClipRgn(di.hdcDraw, hRgnOld);

        DeleteObject(hRgnNew);

        return S_OK;
    }


    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }
};

OBJECT_ENTRY_AUTO(__uuidof(MyCustomTaskPane), CMyCustomTaskPane)
C++:
// MyCustomTaskPane.cpp : Implementation of CMyCustomTaskPane
#include "pch.h"
#include "MyCustomTaskPane.h"


// CMyCustomTaskPane

LRESULT CMyCustomTaskPane::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    HINSTANCE hInstance = (HINSTANCE)GetWindowLongPtr(GWLP_HINSTANCE);
    if (!hInstance) return -1;
    if (!CreateWindow(L"BUTTON", L"OK", WS_VISIBLE | WS_CHILD, 130, 150, 30, 30, m_hWnd, (HMENU)1, hInstance, NULL)) return -1;
    if (!CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"Hello World!", WS_CHILD | WS_VISIBLE | ES_LEFT | ES_UPPERCASE, 130, 200, 50, 50, m_hWnd, (HMENU)2, hInstance, NULL)) return -1;
    return 0;
}

LRESULT CMyCustomTaskPane::OnClick(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    switch (LOWORD(wParam)) {
    case 1:
    {
        MessageBox(L"You clicked button", L"Click Event", MB_OK | MB_ICONINFORMATION);
        break;
    }
    }
    return 0;
}
Như vậy là xong phần ActiveX control, tiếp theo người dùng cần triển khai giao diện ICustomTaskPaneConsumer. Để thuận tiện, người dùng nên triển khai ngay trong class ThisAddIn mà người dùng đã tạo trước đó.
C++:
// ThisAddIn.h : Declaration of the CThisAddIn

#pragma once
#include "resource.h"       // main symbols



#include "ExcelAddInDemo_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

typedef IDispatchImpl <IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), 2, 5> RibbonImpl;
typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1, /* wMinor = */ 0> IDTImpl;
typedef IDispatchImpl<ICustomTaskPaneConsumer, &__uuidof(ICustomTaskPaneConsumer), &__uuidof(__Office), 1, 0> CTPImpl;
// CThisAddIn

class ATL_NO_VTABLE CThisAddIn :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CThisAddIn, &CLSID_ThisAddIn>,
    public IDispatchImpl<IThisAddIn, &IID_IThisAddIn, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public RibbonImpl,
    public IDTImpl,
    public CTPImpl
{
public:
    CThisAddIn()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CThisAddIn)
    COM_INTERFACE_ENTRY(IThisAddIn)
    COM_INTERFACE_ENTRY2(IDispatch, IThisAddIn)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
    COM_INTERFACE_ENTRY(ICustomTaskPaneConsumer)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(OnConnection)(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom);
    STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY** custom);
    STDMETHOD(OnAddInsUpdate)(SAFEARRAY** custom);
    STDMETHOD(OnStartupComplete)(SAFEARRAY** custom);
    STDMETHOD(OnBeginShutdown)(SAFEARRAY** custom);
    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR* RibbonXml);
    STDMETHOD(CTPFactoryAvailable)(ICTPFactory* CTPFactoryInst);
private:
    ICTPFactoryPtr m_CTPFactoryInst;
    _CustomTaskPanePtr m_CTP;
};

OBJECT_ENTRY_AUTO(__uuidof(ThisAddIn), CThisAddIn)
C++:
// ThisAddIn.cpp : Implementation of CThisAddIn

#include "pch.h"
#include "ThisAddIn.h"


// CThisAddIn

STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnDisconnection(ext_DisconnectMode RemoveMode, SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnAddInsUpdate(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnStartupComplete(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnBeginShutdown(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::GetCustomUI(BSTR RibbonID, BSTR* RibbonXml) {
    if (!RibbonXml) return E_POINTER;
    *RibbonXml = SysAllocString(LR"(<?xml version="1.0" encoding="UTF-8"?>
                                    <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoaded">
                                      <ribbon>
                                        <tabs>
                                          <tab id="customTab" label="Excel AddIn">
                                            <group id="customGroup" label="Group">
                                              <button id="customButton"
                                                      label="Click Me"
                                                      getImage="GetImage"
                                                      size="large"
                                                      onAction="ButtonClicked" />
                                            </group>
                                          </tab>
                                        </tabs>
                                      </ribbon>
                                    </customUI>)");
    return *RibbonXml ? S_OK : E_OUTOFMEMORY;
}

STDMETHODIMP CThisAddIn::CTPFactoryAvailable(ICTPFactory* CTPFactoryInst) {
    if (!CTPFactoryInst) return E_POINTER;
    m_CTPFactoryInst = CTPFactoryInst;
    HRESULT hr = m_CTPFactoryInst->CreateCTP(L"ExcelAddInDemo.MyCustomTaskPane", L"My Custom Task Pane", vtMissing, &m_CTP);
    if (SUCCEEDED(hr)) {
        hr = m_CTP->put_Visible(VARIANT_TRUE);
        if (FAILED(hr)) return hr;
        hr = m_CTP->put_Width(600);
        if (FAILED(hr)) return hr;
    }
    return hr;
}

Kết quả:
1751905889664.png
 

File đính kèm

Lần chỉnh sửa cuối:
Xử lý sự kiện:
Trong VBA, để xử lý sự kiện của một đối tượng có hỗ trợ sự kiện (event), người dùng khai báo đối tượng kèm theo từ khoá WithEvents, sau đó chọn tên sự kiện trong phần xổ xuống bên phải để VBA tạo ra code tương ứng.

1752073107837.png
Để tạo ra được đoạn code trên, VBA đã phải thực hiện những công việc sau đây:
  1. Xác định dispinterface đóng vai trò là nguồn sự kiện (event source) cho đối tượng Application. Ở đây là AppEvents
  2. Tạo một class thông thường có triển khai dispinterface AppEvents, quan trọng nhất là phải ghi đè (override) phương thức Invoke của interface IDispatch, đồng thời nắm được mã dispid của sự kiện cần xử lý. Việc tự mình triển khai interface IDispatch là một việc rất khó, cho nên đa phần người ta sẽ chọn giải pháp triển khai class IDispatchImpl.
  3. Gọi phương thức QueryInterface của class ở bước 2 để lấy thông tin về interface IDispatch của class.
  4. Gọi phương thức QueryInterface của của đối tượng Application để truy vấn và sử dụng interface IConnectionPointContainer với riid là IID_IConnectionPointContainer.
  5. Tìm IConnectionPoint dựa vào IConnectionPointContainer của đối tượng Application đã xác định ở bước 3 thông qua phương thức FindConnectionPoint của IConnectionPointContainer với riid là __uuidof(AppEvents).
  6. Gọi phương thức Advise của IConnectionPoint với hai tham số là interface IDispatch của class ở bước 2, phương thức này trả về giá trị dwCookie để chứa thông tin cookie quản lý kết nối sự kiện.
  7. Gọi phương thức Unadvise của IConnectionPoint, truyền vào tham số dwCookie, sau khi đã sử dụng xong.
ATL Project cung cấp một số macro và class tiêu bản như IDispEventSimpleImpl và IDispEventImpl giúp đơn giản hoá các bước nêu trên. Người dùng chỉ cần tạo một class mới có triển khai một trong các class IDispEventSimpleImpl và IDispEventImpl, sau đó sử dụng macro BEGIN_SINK_ENTRY để định nghĩa sự kiện cần xử lý.
Giả sử người dùng muốn bắt sự kiện WorkbookNewSheet của đối tượng Application, trước hết cần phải nắm được mã dispid cùng với chữ ký (signature) của sự kiện, thuận tiện nhất là sử dụng trình oleview được cài đặt trong đường dẫn C:\Program Files (x86)\Windows Kits\phiên_bản_windows\bin\số_hiệu_phiên_bản\(x32 hoặc x64).
Dùng trình oleview để xem thông tin về dispinterface AppEvents của Excel, ở đây người dùng sẽ tìm thấy thông tin cần thiết. Sự kiện WorkbookNewSheet có mã dispid là 0x00000625 và chữ ký (signature) của sự kiện này như sau:
C++:
        [id(0x00000625), helpcontext(0x0007b117)]
        void WorkbookNewSheet(
                        [in] Workbook* Wb,
                        [in] IDispatch* Sh);
1752077577187.png
Sau khi đã thu thập đủ thông tin cần thiết, tiến hành viết code bắt sự kiện.
Class ApplicationEvents:
C++:
#pragma once
using namespace ATL;
class ApplicationEvents : IDispEventSimpleImpl<1, ApplicationEvents, &__uuidof(AppEvents)>
{
public:
    ApplicationEvents(_ApplicationPtr pApp);
    ~ApplicationEvents();
    BEGIN_SINK_MAP(ApplicationEvents)
        SINK_ENTRY_INFO(1, __uuidof(AppEvents), 0x00000625, OnWorkbookNewSheet, &workbookNewSheetInfo)
    END_SINK_MAP()
    STDMETHOD(OnWorkbookNewSheet)(Workbook* Wb, IDispatch* Sh);

private:
    static _ATL_FUNC_INFO workbookNewSheetInfo;
    _ApplicationPtr m_App;
};
C++:
#include "pch.h"
#include "ApplicationEvents.h"

ApplicationEvents::ApplicationEvents(_ApplicationPtr pApp) {
    m_App = pApp;
    DispEventAdvise(m_App);
}

ApplicationEvents::~ApplicationEvents() {
    DispEventUnadvise(m_App);
}

STDMETHODIMP ApplicationEvents::OnWorkbookNewSheet(Workbook* Wb, IDispatch* Sh) {
    _WorksheetPtr ws = Sh;
    CComBSTR bstrShName = NULL;
    HRESULT hr = ws->get_Name(&bstrShName);
    if (FAILED(hr)) return hr;
    MessageBox(NULL, bstrShName, L"WorkbookNewSheet Event", MB_OK | MB_ICONINFORMATION);
    return S_OK;
}

_ATL_FUNC_INFO ApplicationEvents::workbookNewSheetInfo = { CC_STDCALL, VT_EMPTY, 2, {VT_DISPATCH, VT_DISPATCH} };

Sửa nội dung của class ThisAddIn để triển khai class ApplicationEvents.
C++:
// ThisAddIn.h : Declaration of the CThisAddIn

#pragma once
#include "resource.h"       // main symbols
#include "ApplicationEvents.h"



#include "ExcelAddInDemo_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

typedef IDispatchImpl <IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), 2, 5> RibbonImpl;
typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1, /* wMinor = */ 0> IDTImpl;
// CThisAddIn

class ATL_NO_VTABLE CThisAddIn :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CThisAddIn, &CLSID_ThisAddIn>,
    public IDispatchImpl<IThisAddIn, &IID_IThisAddIn, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public RibbonImpl,
    public IDTImpl
{
public:
    CThisAddIn()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CThisAddIn)
    COM_INTERFACE_ENTRY(IThisAddIn)
    COM_INTERFACE_ENTRY2(IDispatch, IThisAddIn)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(OnConnection)(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom);
    STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY** custom);
    STDMETHOD(OnAddInsUpdate)(SAFEARRAY** custom);
    STDMETHOD(OnStartupComplete)(SAFEARRAY** custom);
    STDMETHOD(OnBeginShutdown)(SAFEARRAY** custom);
    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR* RibbonXml);

private:
    _ApplicationPtr m_App;
    ApplicationEvents* pEvents;
};

OBJECT_ENTRY_AUTO(__uuidof(ThisAddIn), CThisAddIn)
C++:
// ThisAddIn.cpp : Implementation of CThisAddIn

#include "pch.h"
#include "ThisAddIn.h"


// CThisAddIn

STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    m_App = Application;
    pEvents = new ApplicationEvents(m_App);
    if (!pEvents) return E_OUTOFMEMORY;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnDisconnection(ext_DisconnectMode RemoveMode, SAFEARRAY** custom) {
    delete pEvents;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnAddInsUpdate(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnStartupComplete(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnBeginShutdown(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::GetCustomUI(BSTR RibbonID, BSTR* RibbonXml) {
    if (!RibbonXml) return E_POINTER;
    *RibbonXml = SysAllocString(LR"(<?xml version="1.0" encoding="UTF-8"?>
                                    <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoaded">
                                      <ribbon>
                                        <tabs>
                                          <tab id="customTab" label="Excel AddIn">
                                            <group id="customGroup" label="Group">
                                              <button id="customButton"
                                                      label="Click Me"
                                                      getImage="GetImage"
                                                      size="large"
                                                      onAction="ButtonClicked" />
                                            </group>
                                          </tab>
                                        </tabs>
                                      </ribbon>
                                    </customUI>)");
    return *RibbonXml ? S_OK : E_OUTOFMEMORY;
}

Tiến hành chạy thử, khi nhấp chuột tạo bảng tính mới, Excel sẽ hiện thông báo là tên của bảng tính mới vừa tạo. Như vậy mục tiêu đã hoàn thành.

1752078016088.png

Để tìm hiểu thêm về cách bắt sự kiện của đối tượng COM trong ATL Project mà bài viết này sử dụng, vui lòng tham khảo bài viết:
Using IDispEventSimpleImpl
 

File đính kèm

Lần chỉnh sửa cuối:
Phản hồi hành động từ các control trên ribbon:
Ribbon là thành phần không thể thiếu khi viết COM add-in, nó đóng vai trò quan trọng trong việc bố trí các chức năng của add-in một cách trực quan, giúp người dùng có thể dễ dàng tương tác với add-in. Để phản hồi các thao tác của người dùng, ribbon sẽ gọi phương thức Invoke của class có triển khai interface IDispatch và IRibbonExtensibility, kèm theo đó là các tham số tuỳ thuộc vào chữ ký (signature) của hàm gọi lại (callback) tương ứng với hành động tương tác với ribbon hoặc control nằm trên ribbon.
Code trong bài viết này sử dụng bố cục (schema) ribbon được trình bày với định dạng XML như sau:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoaded">
  <ribbon>
    <tabs>
      <tab id="customTab" label="Excel AddIn">
        <group id="customGroup" label="Group">
          <button id="customButton"
                  label="Click Me"
                  getImage="GetImage"
                  size="large"
                  onAction="ButtonClicked" />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>
Trong đó,
  • onLoad được thực thi khi bố cục ribbon được nạp.
  • onAction và getImage của nút bấm với id là customButton, với onAction được thực thi khi người dùng nhấp chuột trái vào nút bấm, còn getImage được thực thi khi người dùng lần đầu nhấp chuột vào tab của ribbon tuỳ chỉnh, khi gọi phương thức Invalidate hoặc phương thức InvalidateControl của đối tượng IRibbonUI với tham số bstrControlID là mã id của nút bấm.
Dưới đây là chữ ký của ba hàm gọi lại nêu trên:
OnLoad.
C++:
STDMETHODIMP OnLoad(IDispatch* ribbon);
GetImage.
C++:
STDMETHODIMP GetImage(IDispatch* ribbonControl, IPictureDisp** image);
OnAction.
C++:
STDMETHODIMP OnAction(IDispatch* ribbonControl);
Như vậy, để có thể xử lý những hàm gọi lại trên, người dùng cần làm những việc sau đây:
1. Tạo một interface đóng vai trò định nghĩa các hàm gọi lại nêu trên, trong bài viết này đặt tên là IRibbonCallback. Người dùng cần thêm interface này vào tập tin .idl, sau đó thiết lập nó làm interface mặc định cho coclass ThisAddIn.
C++:
// ExcelAddInDemo.idl : IDL source for ExcelAddInDemo
//

// This file will be processed by the MIDL tool to
// produce the type library (ExcelAddInDemo.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid(ded9f6b8-b802-4547-97fe-0dabdcd4d205),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IThisAddIn : IDispatch
{
};
[
    object,
    uuid(6346E5DA-4D96-4C10-B793-88E697CF3491),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IRibbonCallback : IDispatch
{
    [id(40)] HRESULT OnRibbonLoaded([in]IDispatch* ribbon);
    [id(41)] HRESULT OnGetButtonImage([in]IDispatch* ribbonControl, [out, retval]IPictureDisp** image);
    [id(42)] HRESULT OnButtonClicked([in]IDispatch* ribbonControl);
};
[
    uuid(bd1b5ce2-9b75-49f5-b9a3-c9eb192b1be1),
    version(1.0),
]
library ExcelAddInDemoLib
{
    importlib("stdole2.tlb");
    [
        uuid(2762b776-5488-4564-8cbd-fb71834332ca)
    ]
    coclass ThisAddIn
    {
        [default] interface IRibbonCallback;
    };
};

import "shobjidl.idl";

Tiếp theo, chỉnh sửa nội dung của class CThisAddIn, trong đó triển khai (implement) interface IRibbonCallback vừa khai báo trong tập tin .idl. Lưu ý: Cần phải ghi đè (override) phương thức Invoke nhằm không bỏ sót bất kỳ hàm gọi lại nào.
C++:
// ThisAddIn.h : Declaration of the CThisAddIn

#pragma once
#include "Resource.h"       // main symbols
#include "ApplicationEvents.h"



#include "ExcelAddInDemo_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

typedef IDispatchImpl <IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), 2, 5> RibbonImpl;
typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1, /* wMinor = */ 0> IDTImpl;
typedef IDispatchImpl<IRibbonCallback, &__uuidof(IRibbonCallback)> RibbonCallbackImpl;
// CThisAddIn

class ATL_NO_VTABLE CThisAddIn :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CThisAddIn, &CLSID_ThisAddIn>,
    public IDispatchImpl<IThisAddIn, &IID_IThisAddIn, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public RibbonImpl,
    public IDTImpl,
    public RibbonCallbackImpl
{
public:
    CThisAddIn()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CThisAddIn)
    COM_INTERFACE_ENTRY(IThisAddIn)
    COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
    COM_INTERFACE_ENTRY(IRibbonCallback)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(OnConnection)(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom);
    STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY** custom);
    STDMETHOD(OnAddInsUpdate)(SAFEARRAY** custom);
    STDMETHOD(OnStartupComplete)(SAFEARRAY** custom);
    STDMETHOD(OnBeginShutdown)(SAFEARRAY** custom);
    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR* RibbonXml);
    STDMETHOD(OnButtonClicked)(IDispatch* ribbonControl);
    STDMETHOD(OnGetButtonImage)(IDispatch* ribbonControl, IPictureDisp** image);
    STDMETHOD(OnRibbonLoaded)(IDispatch* ribbon);
    STDMETHOD(Invoke)(DISPID dispIDMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr);

private:
    _ApplicationPtr m_App;
    ApplicationEvents* pEvents;
    IRibbonUIPtr m_ribbonUI;
};

OBJECT_ENTRY_AUTO(__uuidof(ThisAddIn), CThisAddIn)
Với hàm gọi lại OnGetButtonImage, hàm này có chức năng nạp ảnh bitmap được nhúng vào COM add-in thông qua mục Resource Files, sử dụng hàm trợ giúp BitMapToIPictureDisp giúp chuyển đổi ảnh bitmap thành định dạng IPictureDisp theo yêu cầu của nút bấm.
C++:
// ThisAddIn.cpp : Implementation of CThisAddIn

#include "pch.h"
#include "ThisAddIn.h"


// CThisAddIn

HRESULT BitMapToIPictureDisp(HBITMAP hBitmap, IPictureDisp** ppPicture);

STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    m_App = Application;
    pEvents = new ApplicationEvents(m_App);
    if (!pEvents) return E_OUTOFMEMORY;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnDisconnection(ext_DisconnectMode RemoveMode, SAFEARRAY** custom) {
    delete pEvents;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnAddInsUpdate(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnStartupComplete(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnBeginShutdown(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::GetCustomUI(BSTR RibbonID, BSTR* RibbonXml) {
    if (!RibbonXml) return E_POINTER;
    *RibbonXml = SysAllocString(LR"(<?xml version="1.0" encoding="UTF-8"?>
                                    <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoaded">
                                      <ribbon>
                                        <tabs>
                                          <tab id="customTab" label="Excel AddIn">
                                            <group id="customGroup" label="Group">
                                              <button id="customButton"
                                                      label="Click Me"
                                                      getImage="OnGetButtonImage"
                                                      size="large"
                                                      onAction="OnButtonClicked" />
                                            </group>
                                          </tab>
                                        </tabs>
                                      </ribbon>
                                    </customUI>)");
    return *RibbonXml ? S_OK : E_OUTOFMEMORY;
}

STDMETHODIMP CThisAddIn::Invoke(DISPID dispIDMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) {
    HRESULT hr = RibbonCallbackImpl::Invoke(dispIDMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
    if (hr == DISP_E_MEMBERNOTFOUND)
        hr = RibbonImpl::Invoke(dispIDMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
    return hr;
}

HRESULT BitMapToIPictureDisp(HBITMAP hBitmap, IPictureDisp** ppPicture) {
    if (!hBitmap || !ppPicture) return E_INVALIDARG;
    PICTDESC pd{};
    pd.bmp.hbitmap = hBitmap;
    pd.cbSizeofstruct = sizeof(pd);
    pd.picType = PICTYPE_BITMAP;
    IPictureDisp* pPicture = NULL;
    HRESULT hr = OleCreatePictureIndirect(&pd, IID_IPictureDisp, TRUE, (LPVOID*)&pPicture);
    if (FAILED(hr)) return hr;
    *ppPicture = pPicture;
    return *ppPicture ? S_OK : E_FAIL;
}

STDMETHODIMP CThisAddIn::OnGetButtonImage(IDispatch* ribbonControl, IPictureDisp** ppPicture) {
    if (!ppPicture) return E_POINTER;
    HBITMAP hBitmap = LoadBitmap(_AtlBaseModule.GetResourceInstance(), MAKEINTRESOURCE(IDB_BITMAP1));
    if (!hBitmap) return E_UNEXPECTED;
    HRESULT hr = BitMapToIPictureDisp(hBitmap, ppPicture);
    if (FAILED(hr)) return hr;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnButtonClicked(IDispatch* ribbonControl) {
    MessageBox(NULL, L"Button clicked", L"Button Click Event", MB_OK | MB_ICONINFORMATION);
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnRibbonLoaded(IDispatch* ribbon) {
    if (!ribbon) return E_POINTER;
    m_ribbonUI = ribbon;
    return S_OK;
}

Tiến hành biên dịch ra DLL, sau đó chạy thử trên Excel. Nút bấm đã hiển thị hỉnh ảnh như mong muốn.
1752162092465.png
Hộp thoại hiện ra khi nhấp chuột vào nút Click Me.
1752162167084.png
 

File đính kèm

Lần chỉnh sửa cuối:
Cảm ơn bác, bài viết rất chi tiết và tâm huyết!
 
Sử dụng thread:
Thread được sử dụng vì nhiều lý do khác nhau. Có người cần phân bổ công việc phức tạp cho các thread xử lý thay vì chỉ chạy trên thread chính duy nhất, giúp giảm đáng kể thời gian xử lý. Còn một số người khác thì mong giao diện người dùng (UI) không bị đơ, không bị tình trạng không phản hồi (not responding) bằng cách chuyển hết công việc xử lý sang thread phụ. Tuy nhiên thread không phải là "liều thuốc tiên", không phải cứ sử dụng nhiều thread thì hiệu năng của ứng dụng sẽ được cải thiện, điều này còn phụ thuộc vào cách tổ chức logic của ứng dụng, một ứng dụng tạo nhiều thread nhưng logic tổ chức kém sẽ cho hiệu năng tệ hơn so với ứng dụng đơn luồng nhưng được tổ chức một cách chặt chẽ và hợp lý. Ngoài ra đồng bộ giữa các thread (thread synchronization) là một vấn đề cần cân nhắc rất kỹ và rất phức tạp nhằm đảm bảo tài nguyên dùng chung (shared resource) giữa các thread luôn toàn vẹn và nhất quán.
Mô hình thread của Excel nói riêng và các ứng dụng Office nói chung là STA, hay Single-Threaded Apartment, tức là những đối tượng do một thread tạo ra thì chỉ thread đó mới có quyền truy cập những đối tượng đó, hay nói cách khác là chỉ có thread chính (hay UI thread) mới có quyền truy cập vào mô hình đối tượng của Excel hoặc Office. Như vậy có nghĩa là người dùng không thể cứ chỉ tạo một thread phụ, sau đó quẳng đối tượng Excel hoặc Office lên thread đó là vô tư chạy được, thay vào đó người dùng có hai lựa chọn sau đây:
  1. Đưa hết công việc nặng nhọc, xử lý mất nhiều thời gian lên thread phụ, sau khi có kết quả thì tạo một cơ chế thông báo cho thread chính biết và xử lý kết quả. Nhược điểm là code sẽ rối rắm phức tạp nếu cần tương tác nhiều với mô hình đối tượng của Excel hoặc Office.
  2. Sử dụng cơ chế chuyển đổi (marshal) con trỏ interface của Excel hoặc Office, sau đó tạo một thread mới (bắt buộc phải sử dụng hàm CreateThread), thiết lập mô hình thread cho thread đó là STA và truyền tham số là IStream vào thread đó. Từ thread phụ này chuyển đổi ngược (unmarshal) con trỏ interface Excel hoặc Office và có thể sử dụng một cách bình thường, tuy nhiên cần triển khai interface IMessageFilter để xử lý những phàn nàn từ Excel hoặc Office rằng hiện có thread khác (thường là thread chính hay UI thread, do người dùng tương tác với bảng tính hoặc những thành phần giao diện người dùng khác của Excel) đang truy cập vào mô hình đối tượng của nó.
Code trong bài viết này sử dụng cách tiếp cận số hai, trình tự các bước gồm có:
  1. Từ thread chính (hay UI thread), khởi tạo con trỏ IStream làm phương tiện chứa con trỏ Excel hoặc Office đã chuyển đổi con trỏ interface.
  2. Sử dụng hàm CoMarshalInterThreadInterfaceInStream để chuyển đổi con trỏ interface của Excel hoặc Office với phương tiện là IStream ở bước 1.
  3. Khởi tạo thread thông qua hàm CreateThread, truyền tham số là con trỏ IStream chứa interface Excel hoặc Office đã biến đổi ở bước 2. Trường hợp này không thể sử dụng thread pool do các thread trong thread pool khi được khởi tạo luôn là MTA (Multi-Threaded Apartment) không phù hợp với yêu cầu.
  4. Từ thread phụ tạo ở bước 3, dùng hàm CoGetInterfaceAndReleaseStream với tham số IStream để chuyển đổi ngược về con trỏ interface của Excel hoặc Office để thread phụ có thể sử dụng được, từ đây có thể thực hiện các thao tác giống như đang làm trên thread chính.
  5. Từ thread chính, sử dụng hàm MsgWaitForMultipleObjects nhằm đợi thread phụ chạy xong, ngoài ra còn có mục đích xử lý các thông điệp (message) bằng cách khởi tạo vòng lặp thông điệp (message loop) giúp cho giao diện người dùng của Excel luôn phản hồi.
  6. Thực hiện các công việc dọn dẹp sau khi quá trình xử lý hoàn tất.
Đoạn code dưới đây mô phỏng thread phụ thực hiện một công việc tính toán phức tạp và mất nhiều thời gian (Sleep(5000)). sau đó ghi kết quả ra ô A1. Code cũng có cơ chế đơn giản giúp kết thúc thread một cách lịch thiệp (gracefully terminating the thread) nhằm đề phòng trường hợp code đang chạy mà người dùng thoát Excel bất ngờ, giúp cho Excel không bị treo vô hạn hoặc sập (crash).
C++:
// ThisAddIn.h : Declaration of the CThisAddIn

#pragma once
#include "Resource.h"       // main symbols
#include "ApplicationEvents.h"



#include "ExcelAddInDemo_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

typedef IDispatchImpl <IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), 2, 5> RibbonImpl;
typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1, /* wMinor = */ 0> IDTImpl;
typedef IDispatchImpl<IRibbonCallback, &__uuidof(IRibbonCallback)> RibbonCallbackImpl;
// CThisAddIn

class ATL_NO_VTABLE CThisAddIn :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CThisAddIn, &CLSID_ThisAddIn>,
    public IDispatchImpl<IThisAddIn, &IID_IThisAddIn, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public RibbonImpl,
    public IDTImpl,
    public RibbonCallbackImpl,
    public IMessageFilter
{
public:
    CThisAddIn()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CThisAddIn)
    COM_INTERFACE_ENTRY(IThisAddIn)
    COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
    COM_INTERFACE_ENTRY(IRibbonExtensibility)
    COM_INTERFACE_ENTRY(IRibbonCallback)
    COM_INTERFACE_ENTRY(IMessageFilter)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(OnConnection)(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom);
    STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY** custom);
    STDMETHOD(OnAddInsUpdate)(SAFEARRAY** custom);
    STDMETHOD(OnStartupComplete)(SAFEARRAY** custom);
    STDMETHOD(OnBeginShutdown)(SAFEARRAY** custom);
    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR* RibbonXml);
    STDMETHOD(OnButtonClicked)(IDispatch* ribbonControl);
    STDMETHOD(OnGetButtonImage)(IDispatch* ribbonControl, IPictureDisp** image);
    STDMETHOD(OnRibbonLoaded)(IDispatch* ribbon);
    STDMETHOD(OnButtonGetEnabled)(IDispatch* ribbonControl, VARIANT_BOOL* enabled);
    STDMETHOD(Invoke)(DISPID dispIDMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr);
    //IMessageFilter implementation
    STDMETHODIMP_(DWORD) HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo);
    STDMETHODIMP_(DWORD) MessagePending (HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType);
    STDMETHODIMP_(DWORD) RetryRejectedCall (HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectedType);

private:
    _ApplicationPtr m_App;
    ApplicationEvents* pEvents;
    IRibbonUIPtr m_ribbonUI;
    VARIANT_BOOL m_bButtonEnabled = VARIANT_TRUE;
};

OBJECT_ENTRY_AUTO(__uuidof(ThisAddIn), CThisAddIn)

C++:
// ThisAddIn.cpp : Implementation of CThisAddIn

#include "pch.h"
#include "ThisAddIn.h"


// CThisAddIn
HRESULT BitMapToIPictureDisp(HBITMAP hBitmap, IPictureDisp** ppPicture);
long ThreadExitRequested = 0;

STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    m_App = Application;
    pEvents = new ApplicationEvents(m_App);
    if (!pEvents) return E_OUTOFMEMORY;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnDisconnection(ext_DisconnectMode RemoveMode, SAFEARRAY** custom) {
    InterlockedExchange(&ThreadExitRequested, 1);
    delete pEvents;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnAddInsUpdate(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnStartupComplete(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnBeginShutdown(SAFEARRAY** custom) {
    return S_OK;
}

STDMETHODIMP CThisAddIn::GetCustomUI(BSTR RibbonID, BSTR* RibbonXml) {
    if (!RibbonXml) return E_POINTER;
    *RibbonXml = SysAllocString(LR"(<?xml version="1.0" encoding="UTF-8"?>
                                    <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="OnRibbonLoaded">
                                      <ribbon>
                                        <tabs>
                                          <tab id="customTab" label="Excel AddIn">
                                            <group id="customGroup" label="Group">
                                              <button id="customButton"
                                                      label="Click Me"
                                                      getImage="OnGetButtonImage"
                                                      size="large"
                                                      getEnabled="OnButtonGetEnabled"
                                                      onAction="OnButtonClicked" />
                                            </group>
                                          </tab>
                                        </tabs>
                                      </ribbon>
                                    </customUI>)");
    return *RibbonXml ? S_OK : E_OUTOFMEMORY;
}

STDMETHODIMP CThisAddIn::Invoke(DISPID dispIDMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) {
    HRESULT hr = RibbonCallbackImpl::Invoke(dispIDMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
    if (hr == DISP_E_MEMBERNOTFOUND)
        hr = RibbonImpl::Invoke(dispIDMember, riid, lcid, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
    return hr;
}

DWORD STDMETHODCALLTYPE CThisAddIn::HandleInComingCall(DWORD dwCallType, HTASK htaskCaller, DWORD dwTickCount, LPINTERFACEINFO lpInterfaceInfo)
{
    return 1;
}

DWORD STDMETHODCALLTYPE CThisAddIn::MessagePending(HTASK htaskCallee, DWORD dwTickCount, DWORD dwPendingType)
{
    return 1;
}

DWORD STDMETHODCALLTYPE CThisAddIn::RetryRejectedCall(HTASK htaskCallee, DWORD dwTickCount, DWORD dwRejectedType)
{
    switch (dwRejectedType) {
    case SERVERCALL_RETRYLATER:
    {
        if (MessageBox(NULL, L"Excel is currently busy and/or unable to handle the call. Do you want to try again?", L"Error", MB_YESNO | MB_ICONQUESTION) == IDYES) return 1;
    }
    case SERVERCALL_REJECTED:
    {
        MessageBox(NULL, L"Excel rejected call to its object model", L"Call Rejected", MB_OK | MB_ICONEXCLAMATION);
        break;
    }
    }
    return -1;
}

HRESULT BitMapToIPictureDisp(HBITMAP hBitmap, IPictureDisp** ppPicture) {
    if (!hBitmap || !ppPicture) return E_INVALIDARG;
    PICTDESC pd{};
    pd.bmp.hbitmap = hBitmap;
    pd.cbSizeofstruct = sizeof(pd);
    pd.picType = PICTYPE_BITMAP;
    IPictureDisp* pPicture = NULL;
    HRESULT hr = OleCreatePictureIndirect(&pd, IID_IPictureDisp, TRUE, (LPVOID*)&pPicture);
    if (FAILED(hr)) return hr;
    *ppPicture = pPicture;
    return *ppPicture ? S_OK : E_FAIL;
}

STDMETHODIMP CThisAddIn::OnGetButtonImage(IDispatch* ribbonControl, IPictureDisp** ppPicture) {
    if (!ppPicture) return E_POINTER;
    CComPtr<IRibbonControl>pControl = NULL;
    HRESULT hr = ribbonControl->QueryInterface(__uuidof(IRibbonControl), (LPVOID*)&pControl);
    if (FAILED(hr)) return hr;
    CComBSTR bstrId = NULL;
    hr = pControl->get_Id(&bstrId);
    if (FAILED(hr)) return hr;
    if (wcscmp(bstrId, L"customButton") != 0) return S_FALSE;
    HBITMAP hBitmap = LoadBitmap(_AtlBaseModule.GetResourceInstance(), MAKEINTRESOURCE(IDB_BITMAP1));
    if (!hBitmap) return E_UNEXPECTED;
    hr = BitMapToIPictureDisp(hBitmap, ppPicture);
    if (FAILED(hr)) return hr;
    return S_OK;
}

DWORD WINAPI ThreadProc(LPVOID lpParam) {
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
    if (FAILED(hr)) return -1;
    CComObject<CThisAddIn>*pThisAddIn = NULL;
    hr = CComObject<CThisAddIn>::CreateInstance(&pThisAddIn);
    if (FAILED(hr)) return -1;
    pThisAddIn->AddRef();
    LPMESSAGEFILTER lpNewMf = NULL;
    hr = pThisAddIn->QueryInterface(__uuidof(IMessageFilter), (LPVOID*)&lpNewMf);
    if (FAILED(hr)) {
        pThisAddIn->Release();
        CoUninitialize();
        return -1;
    }
    pThisAddIn->Release();
    hr = CoRegisterMessageFilter(lpNewMf, NULL);
    IStream* pStream = (IStream*)lpParam;
    _ApplicationPtr pApp = NULL;
    hr = CoGetInterfaceAndReleaseStream(pStream, __uuidof(_Application), (LPVOID*)&pApp);
    if (FAILED(hr)) {
        hr = CoRegisterMessageFilter(NULL, NULL);
        lpNewMf->Release();
        CoUninitialize();
        return -1;
    }
    Sleep(5000);
    if (InterlockedCompareExchange(&ThreadExitRequested, 0, 1) == 1) {
        hr = CoRegisterMessageFilter(NULL, NULL);
        lpNewMf->Release();
        CoUninitialize();
        return 0;
    }
    try {
        _WorkbookPtr pWb = NULL;
        hr = pApp->get_ActiveWorkbook(&pWb);
        if (FAILED(hr)) {
            hr = CoRegisterMessageFilter(NULL, NULL);
            lpNewMf->Release();
            CoUninitialize();
            return -1;
        }
        IDispatchPtr pDispWs = NULL;
        hr = pWb->get_ActiveSheet(&pDispWs);
        if (FAILED(hr)) {
            hr = CoRegisterMessageFilter(NULL, NULL);
            lpNewMf->Release();
            CoUninitialize();
            return -1;
        }
        _WorksheetPtr pWs = pDispWs;
        RangePtr pRange = NULL;
        hr = pWs->get_Range(CComVariant("A1"), vtMissing, &pRange);
        if (FAILED(hr)) {
            hr = CoRegisterMessageFilter(NULL, NULL);
            lpNewMf->Release();
            CoUninitialize();
            return -1;
        }
        pRange->PutValue(vtMissing, CComVariant(L"Hello World!"));
    }
    catch (const _com_error&) {
        hr = CoRegisterMessageFilter(NULL, NULL);
        lpNewMf->Release();
        CoUninitialize();
        return -1;
    }
    hr = CoRegisterMessageFilter(NULL, NULL);
    lpNewMf->Release();
    CoUninitialize();
    return 0;
}

STDMETHODIMP CThisAddIn::OnButtonGetEnabled(IDispatch* ribbonControl, VARIANT_BOOL* enabled) {
    CComPtr<IRibbonControl>pControl = NULL;
    HRESULT hr = ribbonControl->QueryInterface(__uuidof(IRibbonControl), (LPVOID*)&pControl);
    if (FAILED(hr)) return hr;
    CComBSTR bstrId = NULL;
    hr = pControl->get_Id(&bstrId);
    if (FAILED(hr)) return hr;
    if (!wcscmp(bstrId, L"customButton")) *enabled = m_bButtonEnabled;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnButtonClicked(IDispatch* ribbonControl) {
    CComPtr<IRibbonControl>pControl = NULL;
    HRESULT hr = ribbonControl->QueryInterface(__uuidof(IRibbonControl), (LPVOID*)&pControl);
    if (FAILED(hr)) return hr;
    CComBSTR bstrId = NULL;
    hr = pControl->get_Id(&bstrId);
    if (FAILED(hr)) return hr;
    if (wcscmp(bstrId, L"customButton") != 0) return S_FALSE;
    IStream* pStream = SHCreateMemStream(NULL, 0);
    if (!pStream) return E_OUTOFMEMORY;
    hr = CoMarshalInterThreadInterfaceInStream(__uuidof(_Application), (IUnknown*)m_App, &pStream);
    if (FAILED(hr)) {
        pStream->Release();
        return hr;
    }
    HANDLE hThread = CreateThread(NULL, 0, ThreadProc, (LPVOID)pStream, 0, NULL);
    if (hThread == INVALID_HANDLE_VALUE) {
        pStream->Release();
        return E_FAIL;
    }
    BOOL bExit = FALSE, bAppQuit = FALSE;
    MSG msg{};
    while (!bExit) {
        switch (MsgWaitForMultipleObjects(1, &hThread, FALSE, INFINITE, QS_ALLINPUT)) {
            case WAIT_OBJECT_0:
            {
                bExit = TRUE;
                break;
            }
            case WAIT_OBJECT_0 + 1:
            {
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                    if (msg.message == WM_QUIT) {
                        InterlockedExchange(&ThreadExitRequested, 1);
                        bAppQuit = TRUE;
                    }
                }
                break;
            }
            default:
            {
                MessageBox(NULL, L"An error occurred", L"Error", MB_OK | MB_ICONEXCLAMATION);
                bExit = TRUE;
                break;
            }
        }
    }
    if (hThread) {
        if (!CloseHandle(hThread)) {
            return E_UNEXPECTED;
        }
    }
    if (FAILED(hr)) return hr;
    return S_OK;
}

STDMETHODIMP CThisAddIn::OnRibbonLoaded(IDispatch* ribbon) {
    if (!ribbon) return E_POINTER;
    m_ribbonUI = ribbon;
    return S_OK;
}

Tiến hành biên dịch ra DLL và chạy thử. Khi nhấp chuột vào nút bấm trên ribbon để chạy code, để ý thấy giao diện người dùng của Excel không bị đơ, phản hồi tốt các thao tác chuột và bàn phím.
1752316808494.png
Khi thread phụ đang truy cập vào mô hình đối tượng của Excel hoặc Office và người dùng đang thực hiện thao tác chỉnh sửa ô nào đó trên bảng tính thì thông báo lỗi sẽ xuất hiện, đại loại là Excel đang bận xử lý việc khác (cụ thể là thao tác chỉnh sửa ô trên bảng tính) và hỏi người dùng rằng có muốn thử lại không.
1752316925834.png
Để tìm hiểu thêm về hỗ trợ đa luồng trong Office, vui lòng tham khảo bài viết: Threading support in Office.
 

File đính kèm

Lần chỉnh sửa cuối:
Tham chiếu đến mô hình đối tượng của Excel:
Để làm việc với bảng tính, công thức, v.v., tất tần tật các thứ trong Excel, người dùng cần làm việc với đối tượng Application. Phương thức IDTExtensibility2::OnConnection chứa tham số Application chính là đối tượng Application dưới dạng con trỏ interface IDispatch.
Để làm việc với con trỏ interface _Application, người dùng có hai lựa chọn:
  1. Sử dụng luôn con trỏ interface IDispatch từ tham số Application (liên kết muộn, hay late binding), gọi phương thức Invoke và truyền vào các tham số cần thiết. Nhược điểm: Code dài loằng ngoằng, phức tạp và dễ lỗi nếu không viết cẩn thận.
  2. Gọi phương thức QueryInterface của Application để lấy thông tin interface _Application (liên kết sớm, hay early binding). Cách này giúp code gọn gàng hơn, hạn chế lỗi.
Với cách 1, người dùng có thể tham khảo bài viết này: How to automate Excel from C++ without using MFC or #import
Với cách 2, code sẽ như sau:
Dùng con trỏ thô (raw pointer), tuy nhiên cần gọi phương thức Release của con trỏ trước khi kết thúc hàm/phương thức nhằm tránh tình trạng rò rỉ bộ nhớ (memory leak).
C++:
STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    _Application* pApp = NULL;
    HRESULT hr = Application->QueryInterface(__uuidof(_Application), (LPVOID*)&pApp);
    if (FAILED(hr)) return hr;
    //Sử dụng con trỏ pApp
 
    //Nhớ gọi phương thức Release sau khi sử dụng xong
    pApp->Release();
    return S_OK;
}
Với rủi ro trình bày ở trên, thay vào đó người dùng nên sử dụng con trỏ thông minh (smart pointer), nó sẽ giúp quản lý vấn đề đếm tham chiếu (reference counting) một cách tự động mà không cần phải gọi phương thức Release.
Dùng lớp gói gọn CComPtr<T> của ATL:
C++:
STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    CComPtr<_Application>pApp = NULL;
    HRESULT hr = Application->QueryInterface(_uuidof(_Application), (LPVOID*)&pApp);
    if (FAILED(hr)) return hr;
    //Sử dụng con trỏ pApp

    //Không cần gọi phương thức Release, con trỏ thông minh sẽ tự quản lý việc đếm tham chiếu (reference counting)
    return S_OK;
}

Hoặc, sử dụng con trỏ thông minh do trình biên dịch của Visual C++ tự sinh ra khi người dùng sử dụng chỉ thị (directive) #import để nhập thư viện của Excel. Con trỏ thông minh này là lớp _com_ptr_t<T> và có hậu tố là Ptr. Ví dụ: _ApplicationPtr, RangePtr.
C++:
STDMETHODIMP CThisAddIn::OnConnection(IDispatch* Application, ext_ConnectMode ConnectMode, IDispatch* AddInInst, SAFEARRAY** custom) {
    if (!Application) return E_POINTER;
    _ApplicationPtr pApp = Application;
    //Sử dụng con trỏ pApp
 
    //Không cần gọi phương thức Release, con trỏ thông minh sẽ tự quản lý việc đếm tham chiếu (reference counting)
    return S_OK;
}
Để minh họa bằng một ví dụ cụ thể, bài viết này trình bày đoạn code, trong đó tạo một nút bấm (button) trên ribbon có chức năng hiển thị một cửa sổ theo kiểu (modal window), cửa sổ này chứa tiêu đề (static control), nút bấm (button) và bảng dữ liệu dưới dạng list view trình bày một số thông tin cơ bản về Excel. Code thiết kế một lớp (class) tên là CExcelWindow có triển khai lớp CWindowImpl của ATL giúp đơn giản hóa quá trình viết, tất nhiên là tất cả đều triển khai bằng thuần Win32 API.
C++:
#pragma once
#include <CommCtrl.h>
#pragma comment(lib, "ComCtl32.lib")
using namespace ATL;
class CExcelWindow : public CWindowImpl<CExcelWindow>
{
public:
    DECLARE_WND_CLASS(L"My Window")

    BEGIN_MSG_MAP(CExcelWindow)
        MESSAGE_HANDLER(WM_CREATE, OnCreate)
        MESSAGE_HANDLER(WM_PAINT, OnPaint)
        MESSAGE_HANDLER(WM_CTLCOLORSTATIC, OnCtlColorStatic)
        MESSAGE_HANDLER(WM_NOTIFY, OnNotify)
        MESSAGE_HANDLER(WM_COMMAND, OnCommand)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
    END_MSG_MAP()

    LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCtlColorStatic(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    BOOL Application(_ApplicationPtr pApp);

private:
    HINSTANCE hInstance;
    HFONT hFont;
    HWND hWndLabel, hWndListView, hWndButton;
    _ApplicationPtr m_App;
};

C++:
#include "pch.h"
#include "CExcelWindow.h"

LRESULT CExcelWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    if (!m_App) return -1;
    HDC hdc = GetDC();
    int nFontSize = -MulDiv(12, GetDeviceCaps(hdc, LOGPIXELSY), 72);
    ReleaseDC(hdc);
    HFONT hFont = CreateFont(nFontSize, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, L"Times New Roman");
    if (!hFont) return 0;
    hWndLabel = CreateWindow(WC_STATIC, L"Thông tin Excel", WS_VISIBLE | WS_CHILD, 8, 16, 88, 20, m_hWnd, (HMENU)1, hInstance, NULL);
    if (!hWndLabel) return 0;
    CWindow label(hWndLabel);
    label.SendMessage(WM_SETFONT, (WPARAM)hFont, 0);
    hWndButton = CreateWindow(WC_BUTTON, L"Đóng", WS_VISIBLE | WS_CHILD, 272, 352, 64, 24, m_hWnd, (HMENU)2, hInstance, NULL);
    if (!hWndButton) return 0;
    CWindow button(hWndButton);
    button.SendMessage(WM_SETFONT, (WPARAM)hFont, 0);
    INITCOMMONCONTROLSEX iccex{};
    iccex.dwICC = ICC_LISTVIEW_CLASSES;
    iccex.dwSize = sizeof(iccex);
    if (!InitCommonControlsEx(&iccex)) return 0;
    hWndListView = CreateWindow(WC_LISTVIEW, L"", WS_VISIBLE | WS_CHILD | LVS_REPORT | WS_BORDER | LVS_SINGLESEL, 8, 40, 328, 304, m_hWnd, (HMENU)3, hInstance, NULL);
    if (!hWndListView) return 0;
    LVCOLUMN lvc{};
    lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
    lvc.cx = 120;
    lvc.iSubItem = 0;
    lvc.pszText = L"Thuộc tính";
    lvc.fmt = LVCFMT_RIGHT;
    CWindow listView(hWndListView);
    listView.SendMessage(LVM_INSERTCOLUMN, 0, (LPARAM)&lvc);
    lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
    lvc.cx = 120;
    lvc.iSubItem = 1;
    lvc.pszText = L"Giá trị";
    lvc.fmt = LVCFMT_RIGHT;
    listView.SendMessage(LVM_INSERTCOLUMN, 1, (LPARAM)&lvc);
    CComBSTR bstrVersion = NULL;
    HRESULT hr = m_App->get_Version(LOCALE_USER_DEFAULT, &bstrVersion);
    if (FAILED(hr)) return -1;
    LVITEM lvItem{};
    lvItem.mask = LVIF_TEXT;
    lvItem.iSubItem = 0;
    lvItem.iItem = 0;
    lvItem.pszText = L"Phiên bản";
    lvItem.cchTextMax = 255;
    listView.SendMessage(LVM_INSERTITEM, 0, (LPARAM)&lvItem);

    lvItem.iSubItem = 1;
    lvItem.iItem = 0;
    lvItem.pszText = bstrVersion;
    listView.SendMessage(LVM_SETITEM, 0, (LPARAM)&lvItem);

    lvItem.iSubItem = 0;
    lvItem.iItem = 1;
    lvItem.pszText = L"Tên";
    listView.SendMessage(LVM_INSERTITEM, 1, (LPARAM)&lvItem);
 
    CComBSTR bstrAppName = NULL;
    hr = m_App->get_Name(&bstrAppName);
    if (FAILED(hr)) return -1;
    lvItem.iSubItem = 1;
    lvItem.iItem = 1;
    lvItem.pszText = bstrAppName;
    listView.SendMessage(LVM_SETITEM, 1, (LPARAM)&lvItem);
    return 0;
}

LRESULT CExcelWindow::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    PAINTSTRUCT ps{};
    HDC hdc = BeginPaint(&ps);
    FillRect(hdc, &ps.rcPaint, (HBRUSH)GetStockObject(WHITE_BRUSH));
    EndPaint(&ps);
    return 0;
}

LRESULT CExcelWindow::OnCtlColorStatic(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    HBRUSH hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH);
    return (LRESULT)hBrush;
}

LRESULT CExcelWindow::OnNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
 
    return 0;
}

LRESULT CExcelWindow::OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    switch (HIWORD(wParam)) {
        case BN_CLICKED:
        {
            switch (LOWORD(wParam)) {
                case 2:
                {
                    SendMessage(WM_CLOSE, 0, 0);
                    break;
                }
            }
            break;
        }
    }
    return 0;
}

LRESULT CExcelWindow::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    if (hFont) DeleteObject(hFont);
    m_hWnd = NULL;
    PostQuitMessage(0);
    return 0;
}

BOOL CExcelWindow::Application(_ApplicationPtr pApp) {
    m_App = pApp;
    return m_App ? TRUE : FALSE;
}
1752677730822.png
 

File đính kèm

Lần chỉnh sửa cuối:
Kiêm nhiệm chức năng của Automation Add-In:
Bên cạnh việc thực hiện các chức năng của COM add-in, người dùng cũng có thể triển khai chức năng của Automation add-in, thể loại add-in này cung cấp cho Excel những tính năng sau đây:
  • Tạo hàm tuỳ chỉnh (user-defined function) dùng cho bảng tính, tuy nhiên hiệu năng kém hơn so với XLL add-in truyền thống.
  • Tạo hàm RTD cho phép Excel hiển thị dữ liệu theo thời gian thực.
Automation add-in thực chất vẫn là COM DLL, cho nên người dùng cũng có thể triển khai coclass để VBA có thể khai báo và sử dụng được một cách dễ dàng.
Trước đây tôi từng trình bày một bài viết về chủ đề tương tự nhưng thực hiện bằng nền tảng .NET Framework, những độc giả nào quan tâm có thể đọc bài viết này: Tạo, sử dụng Automation Add-in trong Excel bằng Visual C#.
Quay trở lại chủ đề chính của bài viết này, để tạo Automation add-in, người dùng tạo một ATL Simple Object mới, trong bài viết này đặt tên là AutomationAddInDemo.

1752894838150.png

Ở đây người dùng sẽ viết một hàm khá đơn giản thôi, cộng hai số.
C++:
// AutomationAddInDemo.h : Declaration of the CAutomationAddInDemo

#pragma once
#include "resource.h"       // main symbols



#include "ExcelAddInDemo_i.h"



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;


// CAutomationAddInDemo

class ATL_NO_VTABLE CAutomationAddInDemo :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CAutomationAddInDemo, &CLSID_AutomationAddInDemo>,
    public IDispatchImpl<IAutomationAddInDemo, &IID_IAutomationAddInDemo, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
    CAutomationAddInDemo()
    {
    }

DECLARE_REGISTRY_RESOURCEID(107)


BEGIN_COM_MAP(CAutomationAddInDemo)
    COM_INTERFACE_ENTRY(IAutomationAddInDemo)
    COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(SumTwoNumbers)(double a, double b, double* pVal);


};

OBJECT_ENTRY_AUTO(__uuidof(AutomationAddInDemo), CAutomationAddInDemo)

C++:
// AutomationAddInDemo.cpp : Implementation of CAutomationAddInDemo

#include "pch.h"
#include "AutomationAddInDemo.h"


// CAutomationAddInDemo

STDMETHODIMP CAutomationAddInDemo::SumTwoNumbers(double a, double b, double* pVal) {
    if (!pVal) return E_POINTER;
    *pVal = a + b;
    return S_OK;
}

Cuối cùng, khai báo hàm trong tập tin IDL để Excel có thể nhận biết được.
C++:
[
    object,
    uuid(467efe7b-2628-4a8a-bcdc-acdc2a1f3031),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IAutomationAddInDemo : IDispatch
{
    HRESULT SumTwoNumbers([in]double a, [in]double b, [out, retval]double* pVal);
};

Mọi thứ đã xong, tiến hành biên dịch ra DLL. Để sử dụng, người dùng chọn thẻ Developer, chọn Excel Add-Ins. Hiện ra hộp thoại Add-Ins, người dùng chọn tiếp Automation và tìm đúng tên của coclass trong IDL, trong bài viết là AutomationAddIn class. Nhấn OK để đóng hộp thoại lại.
Gõ công thức "=SumTwoNumbers(1, 2)", kết quả trả về là 3.

1752890233550.png

Bên cạnh việc triển khai hàm tự tạo dùng cho bảng tính, người dùng cũng có thể triển khai hàm RTD bằng cách triển khai interface IRtdServer. Tiến hành chỉnh sửa class AutomationAddInDemo.

C++:
// AutomationAddInDemo.h : Declaration of the CAutomationAddInDemo

#pragma once
#include "resource.h"       // main symbols
#include <vector>
#include <string>
#include <strsafe.h>
#include "CTimer.h"



#include "ExcelAddInDemo_i.h"
struct Topic {
    long TopicId;
    std::wstring TopicString;
};



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;


// CAutomationAddInDemo
typedef IDispatchImpl<IRtdServer, &__uuidof(IRtdServer), &__uuidof(__Excel), 1, 0> IRtdServerImpl;
class ATL_NO_VTABLE CAutomationAddInDemo :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CAutomationAddInDemo, &CLSID_AutomationAddInDemo>,
    public IDispatchImpl<IAutomationAddInDemo, &IID_IAutomationAddInDemo, &LIBID_ExcelAddInDemoLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IRtdServerImpl
{
public:
    CAutomationAddInDemo()
    {
    }

DECLARE_REGISTRY_RESOURCEID(107)


BEGIN_COM_MAP(CAutomationAddInDemo)
    COM_INTERFACE_ENTRY(IAutomationAddInDemo)
    COM_INTERFACE_ENTRY2(IDispatch, IRtdServer)
    COM_INTERFACE_ENTRY(IRtdServer)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:
    STDMETHOD(Invoke)(DISPID dispIDMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr);
    STDMETHOD(SumTwoNumbers)(double a, double b, double* pVal);
    STDMETHOD(raw_ConnectData)(long TopicID, SAFEARRAY** Strings, VARIANT_BOOL* GetNewValues, VARIANT* pvarOut);
    STDMETHOD(raw_DisconnectData)(long TopicID);
    STDMETHOD(raw_Heartbeat)(long* pfRes);
    STDMETHOD(raw_RefreshData)(long* TopicCount, SAFEARRAY** parrayOut);
    STDMETHOD(raw_ServerStart)(IRTDUpdateEvent* CallbackObject, long* pfRes);
    STDMETHOD(raw_ServerTerminate)();

private:
    std::vector<Topic>m_topics{};
    CTimer m_timer{};
};

OBJECT_ENTRY_AUTO(__uuidof(AutomationAddInDemo), CAutomationAddInDemo)

C++:
#include "pch.h"
#include "AutomationAddInDemo.h"


// CAutomationAddInDemo

STDMETHODIMP CAutomationAddInDemo::SumTwoNumbers(double a, double b, double* pVal) {
    if (!pVal) return E_POINTER;
    *pVal = a + b;
    return S_OK;
}

STDMETHODIMP CAutomationAddInDemo::raw_ServerStart(IRTDUpdateEvent* CallbackObject, long* pfRes) {
    if (!CallbackObject) return E_POINTER;
    m_timer.SetCallback(CallbackObject);
    *pfRes = 1;
    return S_OK;
}

STDMETHODIMP CAutomationAddInDemo::raw_ServerTerminate() {
    m_timer.SetCallback(NULL);
    m_topics.clear();
    return S_OK;
}

static HRESULT GetCurrentSystemTime(VARIANT* pvarResult) {
    if (!pvarResult) return E_POINTER;
    SYSTEMTIME systemTime;
    GetLocalTime(&systemTime);
    wchar_t pszTime[20]{};
    HRESULT hr = StringCchPrintf(pszTime, 20, L"%i/%i/%i %i:%i:%i", systemTime.wYear, systemTime.wMonth, systemTime.wDay, systemTime.wHour, systemTime.wMinute, systemTime.wSecond);
    if FAILED(hr) return hr;
    pvarResult->vt = VT_BSTR;
    return (pvarResult->bstrVal = SysAllocString(pszTime)) ? S_OK : E_OUTOFMEMORY;
}

STDMETHODIMP CAutomationAddInDemo::raw_ConnectData(long TopicID, SAFEARRAY** Strings, VARIANT_BOOL* GetNewValues, VARIANT* pvarOut) {
    if (!Strings || !pvarOut || !GetNewValues) return E_POINTER;
    Topic topic{};
    topic.TopicId = TopicID;
    CComBSTR bstrTopicString = NULL;
    CComVariant varTopicString;
    long lIndex[1];
    lIndex[0] = 0;
    HRESULT hr = SafeArrayGetElement(*Strings, lIndex, &varTopicString);
    if (FAILED(hr)) return hr;
    bstrTopicString = SysAllocString(varTopicString.bstrVal);
    if (!bstrTopicString) return E_OUTOFMEMORY;
    topic.TopicString = std::wstring(bstrTopicString);
    m_topics.push_back(topic);
    hr = GetCurrentSystemTime(pvarOut);
    if (FAILED(hr)) return hr;
    if (!m_timer.Start()) return E_FAIL;
    return S_OK;
}

STDMETHODIMP CAutomationAddInDemo::raw_DisconnectData(long TopicID) {
    long nIndex = 0;
    for (const Topic& item : m_topics) {
        if (item.TopicId == TopicID) {
            m_topics.erase(m_topics.begin() + nIndex);
            break;
        }
        nIndex++;
    }
    if (m_topics.empty()) m_timer.Stop();
    return S_OK;
}

STDMETHODIMP CAutomationAddInDemo::raw_Heartbeat(long* pfRes) {
    *pfRes = 1;
    return S_OK;
}

STDMETHODIMP CAutomationAddInDemo::raw_RefreshData(long* TopicCount, SAFEARRAY** parrayOut) {
    if (!TopicCount || !parrayOut) return E_POINTER;
    CComVariant varValue;
    HRESULT hr = GetCurrentSystemTime(&varValue);
    if (FAILED(hr)) return hr;
    long nSize = (long)m_topics.size();
    SAFEARRAYBOUND sab[2]{};
    sab[0].cElements = 2;
    sab[0].lLbound = 0;
    sab[1].cElements = nSize;
    sab[1].lLbound = 0;
    SAFEARRAY* psa = SafeArrayCreate(VT_VARIANT, 2, sab);
    if (!psa) return E_OUTOFMEMORY;
    long nIndex[2]{};
    long nI = 0;
    for (const Topic& topic : m_topics) {
        nIndex[0] = 0;
        nIndex[1] = nI;
        hr = SafeArrayPutElement(psa, nIndex, (LPVOID) & (CComVariant(topic.TopicId)));
        if (FAILED(hr)) {
            SafeArrayDestroy(psa);
            return hr;
        }
        nIndex[0] = 1;
        hr = SafeArrayPutElement(psa, nIndex, (LPVOID)&varValue);
        if (FAILED(hr)) {
            SafeArrayDestroy(psa);
            return hr;
        }
        nI++;
    }
    *parrayOut = psa;
    *TopicCount = nSize;
    if (!m_timer.Start()) return E_FAIL;
    return S_OK;
}

Hàm RTD hoạt động bằng cách, cứ sau một khoảng thời gian nhất định nó sẽ gọi hàm IRtdServer::raw_RefreshData cập nhật dữ liệu mới và hiển thị ra bảng tính, cho nên người dùng cần tạo ra một cơ chế giống như một đồng hồ hẹn giờ, ở đây sử dụng hàm SetTimer để thiết lập đồng hồ hẹn giờ khoảng hai giây, tạo một cửa sổ ẩn và xử lý thông điệp WM_TIMER, sử dụng hàm KillTimer để dừng đồng hồ hẹn giờ. Mô hình thread của Excel là STA thread nên cũng có triển khai bơm thông điệp (message pump), vì thế người dùng có thể tận dụng luôn để có thể dễ dàng triển khai đồng hồ hẹn giờ.
Tạo một class mới tên là CTimer để triển khai đồng hồ hẹn giờ.

C++:
#pragma once
using namespace ATL;
class CTimer : public CWindowImpl<CTimer>
{
public:
    CTimer();
    ~CTimer();
    BEGIN_MSG_MAP(CTimer)
        MESSAGE_HANDLER(WM_TIMER, OnElapsed)
    END_MSG_MAP()

    LRESULT OnElapsed(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    BOOL Start();
    BOOL Stop();
    void SetCallback(IRTDUpdateEvent* pCallback);
private:
    UINT_PTR nTimerId;
    CComPtr<IRTDUpdateEvent> m_callback;
};

C++:
#include "pch.h"
#include "CTimer.h"

CTimer::CTimer() {
    if (!Create(NULL, NULL, NULL, WS_OVERLAPPEDWINDOW, 0, (HMENU)NULL, NULL)) return;
}

void CTimer::SetCallback(IRTDUpdateEvent* pCallback) {
    m_callback = pCallback;
}

CTimer::~CTimer() {
    DestroyWindow();
}

LRESULT CTimer::OnElapsed(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    Stop();
    m_callback->UpdateNotify();
    return 0;
}

BOOL CTimer::Start() {
    nTimerId = SetTimer(1, 2000, NULL);
    return nTimerId ? TRUE : FALSE;
}

BOOL CTimer::Stop() {
    return KillTimer(nTimerId);
}

Tiến hành biên dịch ra DLL. Sau khi đã tham chiếu đến Automation add-in như đã trình bày ở trên, từ bảng tính người dùng gõ công thức "=RTD("exceladdindemo.automationaddindemo", "", "1")". Hàm này chỉ đơn thuần trả về thời gian hiện tại của máy tính, tự động cập nhật giá trị mới sau mỗi hai giây. Người dùng có thể ứng dụng hàm này để hiển thị dữ liệu theo thời gian thực của chứng khoán, tiền số, v.v...

1752894267681.png
 

File đính kèm

Lần chỉnh sửa cuối:
Sử dụng thư viện ADO để truy vấn nhiều nguồn dữ liệu khác nhau:
Với những người đã quen làm việc với ADO bằng VBA chắc hẳn sẽ không gặp khó khăn khi chuyển sang lập trình bằng Visual C++ vì đã quen với cú pháp của thư viện này. Tuy vậy khi làm việc với ADO bằng VBA, ngôn ngữ này đã ngầm xử lý những công việc phức tạp phía sau hậu trường, từ việc khởi tạo và giải phóng con trỏ interface của ADO, cho đến xử lý những lỗi có thể phát sinh trong quá trình gọi các phương thức mà các interface của ADO cung cấp, từ đó người dùng có thể toàn tâm toàn ý tập trung vào triển khai ý tưởng của mình, còn khi làm việc bằng Visual C++, những công việc này người dùng đều phải tự xử lý sao cho chương trình chạy trơn tru, không xảy ra tình trạng rò rỉ bộ nhớ (memory leak) và tránh cho chương trình bị sập vì một lý do nào đó (vd: truy cập vào vùng nhớ đã giải phóng hoặc địa chỉ không hợp lệ). Để biết thêm thông tin chi tiết về những điều cần lưu ý khi lập trình ADO bằng Visual C++, người dùng có thể tham khảo bài viết này: Visual C++ ADO programming.
Ví dụ dưới đây trình bày một ribbon, trong đó chứa một nút bấm khi nhấn vào sẽ hiện ra một cửa sổ theo kiểu (modal window) gồm tiêu đề, hộp văn bản, list view và nút bấm. Cửa sổ này có chức năng mở một cơ sở dữ liệu Microsoft Access, dùng truy vấn SQL để lấy dữ liệu và hiển thị ra list view. Để ngắn gọn nhất có thể, code trong bài viết này chủ động không hiển thị thông báo lỗi dạng hộp thoại (message box), cho nên người dùng có thể bổ sung thêm nhằm giúp cho cửa sổ trở nên trực quan hơn.
Lưu ý: Do code trong bài viết này dùng phương thức Execute của con trỏ interface Connection để truy vấn và trả về Recordset cho nên nó chỉ hỗ trợ truy vấn SQL dạng SELECT.
C++:
#pragma once
#include <CommCtrl.h>
#include "DatabaseOperation.h"
#include <strsafe.h>
#include <Uxtheme.h>
#include <ShObjIdl.h>
#include <string>
#include "resource.h"
#pragma comment(lib, "UxTheme.lib")
#pragma comment(lib, "ComCtl32.lib")

using namespace ATL;
class CExcelWindow : public CWindowImpl<CExcelWindow>
{
public:
    DECLARE_WND_CLASS(L"My Window")

    BEGIN_MSG_MAP(CExcelWindow)
        MESSAGE_HANDLER(WM_CREATE, OnCreate)
        MESSAGE_HANDLER(WM_PAINT, OnPaint)
        MESSAGE_HANDLER(WM_CTLCOLORSTATIC, OnCtlColorStatic)
        MESSAGE_HANDLER(WM_COMMAND, OnCommand)
        MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
        MESSAGE_HANDLER(WM_NOTIFY, OnNotify)
    END_MSG_MAP()

    LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCtlColorStatic(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
    LRESULT OnNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);

private:
    HINSTANCE hInstance;
    DatabaseOperation dbo{};
    HWND hBtnExecuteQuery, hLv, hTitle, hOpenFileLabel, hOpenFileTxt, hBrowseFile, hSqlStatementLabel, hSqlStatementTxt;
    HFONT hFontTitle = NULL, hFontButtonLv = NULL;
    long lLvColCount;
    int lLastColumnClicked;
    bool bSortAsc = false;
};

C++:
#include "pch.h"
#include "CExcelWindow.h"

BOOL ToFontHeight(HWND hWnd, int nPointSize, int* nHeight);
BOOL OnDatabaseConnect(DatabaseOperation& Dbo, const BSTR bstrDatabaseFile);
BOOL AddListViewColumn(HWND hWnd, int nColunmIndex, const WCHAR* lpszText);
BOOL AddListViewColumnItem(HWND hWnd, int nColumnIndex, int nRowIndex, const WCHAR* lpszText);
HRESULT OnBrowseFileClicked(HWND hWndOwner, HWND hWnd, BSTR* bstrResult, DatabaseOperation& db);
HRESULT VariantToString(const VARIANT& varValue, BSTR* bstrResult);

BOOL ToFontHeight(HWND hWnd, int nPointSize, int* nHeight) {
    if (!hWnd) return FALSE;
    HDC hdc = GetDC(hWnd);
    *nHeight = -MulDiv(nPointSize, GetDeviceCaps(hdc, LOGPIXELSY), 72);
    ReleaseDC(hWnd, hdc);
    return TRUE;
}

BOOL OnDatabaseConnect(DatabaseOperation& Dbo, const BSTR bstrDatabaseFile) {
    BOOL bConnected = VARIANT_FALSE;
    HRESULT hr = Dbo.Connect(bstrDatabaseFile, &bConnected);
    if (FAILED(hr)) return FALSE;
    return bConnected;
}

BOOL AddListViewColumn(HWND hWnd, int nColunmIndex, const WCHAR* lpszText) {
    if (!hWnd || !lpszText) return FALSE;
    LVCOLUMN lvCol = { 0 };
    lvCol.mask = LVCF_SUBITEM | LVCF_TEXT | LVCF_WIDTH | LVCF_FMT;
    lvCol.iSubItem = nColunmIndex;
    lvCol.fmt = LVCFMT_RIGHT;
    lvCol.cx = 120;
    lvCol.pszText = const_cast<WCHAR*>(lpszText);
    if (SendMessage(hWnd, LVM_INSERTCOLUMN, (WPARAM)nColunmIndex, (LPARAM)&lvCol) == -1) return FALSE;
    return TRUE;
}

BOOL AddListViewColumnItem(HWND hWnd, int nColumnIndex, int nRowIndex, const WCHAR* lpszText) {
    if (!hWnd || !lpszText) return FALSE;
    LVITEM lvItem = { 0 };
    lvItem.mask = LVFIF_TEXT;
    lvItem.pszText = const_cast<WCHAR*>(lpszText);
    lvItem.iItem = nRowIndex;
    lvItem.iSubItem = nColumnIndex;
    if (!nColumnIndex) {
        if (SendMessage(hWnd, LVM_INSERTITEM, (WPARAM)nRowIndex, (LPARAM)&lvItem) == -1) return FALSE;
    }
    else {
        if (SendMessage(hWnd, LVM_SETITEM, (WPARAM)nRowIndex, (LPARAM)&lvItem) == -1) return FALSE;
    }
    return TRUE;
}

int CALLBACK CompareEx(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) {
    WCHAR pszBuffer1[255]{}, pszBuffer2[255]{};
    LVITEM lvItem1{}, lvItem2{};
    lvItem1.mask = LVIF_TEXT;
    lvItem1.cchTextMax = 255;
    lvItem1.iSubItem = (int)lParam1;
    lvItem1.pszText = pszBuffer1;
    lvItem2.mask = LVIF_TEXT;
    lvItem2.iSubItem = (int)lParam2;
    lvItem2.cchTextMax = 255;
    lvItem2.pszText = pszBuffer2;
    SortInfo* sortInfo = (SortInfo*)lParamSort;
    SendMessage(sortInfo->hLv, LVM_GETITEMTEXT, lParam1, (LPARAM)&lvItem1);
    SendMessage(sortInfo->hLv, LVM_GETITEMTEXT, lParam2, (LPARAM)&lvItem2);
    int res = wcscmp(pszBuffer1, pszBuffer2);
    return sortInfo->bAsc ? res >= 0 : res <= 0;
}

HRESULT OnBrowseFileClicked(HWND hWndOwner, HWND hWnd, BSTR* bstrResult, DatabaseOperation& db) {
    HRESULT hr = db.Close();
    if (FAILED(hr)) return hr;
    IFileOpenDialog* pFileOpenDialog = NULL;
    hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (LPVOID*)&pFileOpenDialog);
    if (FAILED(hr)) return hr;
    hr = pFileOpenDialog->SetTitle(L"Select a Microsoft Access database file to open");
    if (FAILED(hr)) {
        pFileOpenDialog->Release();
        return hr;
    }
    COMDLG_FILTERSPEC filters[1]{};
    filters[0].pszName = L"Microsoft Access database";
    filters[0].pszSpec = L"*.accdb";
    hr = pFileOpenDialog->SetFileTypes(1, filters);
    if (FAILED(hr)) {
        pFileOpenDialog->Release();
        return hr;
    }
    hr = pFileOpenDialog->Show(NULL);
    if (FAILED(hr)) {
        pFileOpenDialog->Release();
        return hr;
    }
    IShellItem* pItem = NULL;
    hr = pFileOpenDialog->GetResult(&pItem);
    if (FAILED(hr)) {
        pFileOpenDialog->Release();
        return hr;
    }
    WCHAR* pszFileName = NULL;
    hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFileName);
    if (FAILED(hr)) {
        pItem->Release();
        pFileOpenDialog->Release();
        return hr;
    }
    *bstrResult = SysAllocString(pszFileName);
    CoTaskMemFree(pszFileName);
    if (!*bstrResult) {
        pItem->Release();
        pFileOpenDialog->Release();
        return E_OUTOFMEMORY;
    }
    pItem->Release();
    pFileOpenDialog->Release();
    return S_OK;
}

LRESULT CExcelWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    hInstance = (HINSTANCE)GetWindowLongPtr(GWLP_HINSTANCE);
    int nTitleFontHeight = 0, nButtonLsFontHeight;
    if (!ToFontHeight(m_hWnd, 16, &nTitleFontHeight)) return -1;
    if (!ToFontHeight(m_hWnd, 11, &nButtonLsFontHeight)) return -1;
    hFontTitle = CreateFont(nTitleFontHeight, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, L"Times New Roman");
    if (!hFontTitle) return 0;
    hFontButtonLv = CreateFont(nButtonLsFontHeight, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, L"Times New Roman");
    if (!hFontButtonLv) {
        DeleteObject(hFontTitle);
        return 0;
    }
    hTitle = CreateWindow(WC_STATIC, L"MỞ CSDL MICROSOFT ACCESS", WS_VISIBLE | WS_CHILD | SS_CENTER, 167, 9, 320, 24, m_hWnd, (HMENU)0, hInstance, NULL);
    if (!hTitle) return 0;
    SendMessage(hTitle, WM_SETFONT, (WPARAM)hFontTitle, 0);
    hBtnExecuteQuery = CreateWindow(WC_BUTTON, L"Lấy dữ liệu/Làm mới", WS_VISIBLE | WS_CHILD, 229, 554, 177, 36, m_hWnd, (HMENU)1, hInstance, NULL);
    if (!hBtnExecuteQuery) return 0;
    CWindow btnExecuteQuery(hBtnExecuteQuery);
    btnExecuteQuery.EnableWindow(FALSE);
    SendMessage(hBtnExecuteQuery, WM_SETFONT, (WPARAM)hFontButtonLv, 0);
    INITCOMMONCONTROLSEX iccex{};
    iccex.dwICC = ICC_LISTVIEW_CLASSES;
    iccex.dwSize = sizeof(iccex);
    if (!InitCommonControlsEx(&iccex)) return 0;
    hLv = CreateWindow(WC_LISTVIEW, L"", WS_VISIBLE | WS_CHILD | WS_BORDER | LVS_REPORT | LVS_SINGLESEL | LVS_AUTOARRANGE, 12, 225, 600, 300, m_hWnd, (HMENU)2, hInstance, NULL);
    if (!hLv) {
        DeleteObject(hFontTitle);
        DeleteObject(hFontButtonLv);
        return 0;
    }
    SendMessage(hLv, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_FULLROWSELECT);
    //SendMessage(hLv, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_GRIDLINES);
    SetWindowTheme(hLv, L"Explorer", NULL);
    hOpenFileLabel = CreateWindow(WC_STATIC, L"Đường dẫn tập tin csdl:", WS_VISIBLE | WS_CHILD | SS_LEFT, 9, 84, 144, 17, m_hWnd, (HMENU)NULL, hInstance, NULL);
    if (!hOpenFileLabel) return 0;
    SendMessage(hOpenFileLabel, WM_SETFONT, (WPARAM)hFontButtonLv, 0);
    hOpenFileTxt = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"", WS_VISIBLE | WS_CHILD | ES_LEFT | ES_READONLY, 159, 76, 410, 25, m_hWnd, (HMENU)3, hInstance, NULL);
    if (!hOpenFileTxt) return 0;
    SendMessage(hOpenFileTxt, WM_SETFONT, (WPARAM)hFontButtonLv, 0);
    hBrowseFile = CreateWindow(WC_BUTTON, L"...", WS_VISIBLE | WS_CHILD, 580, 78, 32, 23, m_hWnd, (HMENU)4, hInstance, NULL);
    if (!hBrowseFile) return 0;
    SendMessage(hBrowseFile, WM_SETFONT, (WPARAM)hFontButtonLv, 0);
    hSqlStatementLabel = CreateWindow(WC_STATIC, L"Truy vấn SQL:", WS_VISIBLE | WS_CHILD | ES_LEFT, 12, 130, 95, 17, m_hWnd, (HMENU)NULL, hInstance, NULL);
    if (!hSqlStatementLabel) return 0;
    SendMessage(hSqlStatementLabel, WM_SETFONT, (WPARAM)hFontButtonLv, 0);
    hSqlStatementTxt = CreateWindowEx(WS_EX_CLIENTEDGE, WC_EDIT, L"", WS_VISIBLE | WS_CHILD | ES_LEFT | ES_MULTILINE, 159, 127, 410, 92, m_hWnd, (HMENU)5, hInstance, NULL);
    if (!hSqlStatementTxt) return 0;
    SendMessage(hSqlStatementTxt, WM_SETFONT, (WPARAM)hFontButtonLv, 0);
    return 0;
}

LRESULT CExcelWindow::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    PAINTSTRUCT ps{};
    HDC hdc = BeginPaint(&ps);
    FillRect(hdc, &ps.rcPaint, (HBRUSH)GetStockObject(WHITE_BRUSH));
    EndPaint(&ps);
    return 0;
}

LRESULT CExcelWindow::OnCtlColorStatic(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    HBRUSH hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH);
    return (LRESULT)hBrush;
}

LRESULT CExcelWindow::OnCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    CWindow openFileTxt(hOpenFileTxt);
    switch (HIWORD(wParam)) {
        case BN_CLICKED:
        {
            switch (LOWORD(wParam)) {
                case 4:
                {
                    CComBSTR bstrFileName = NULL;
                    HRESULT hr = OnBrowseFileClicked(m_hWnd, hBrowseFile, &bstrFileName, dbo);
                    if (FAILED(hr)) return 0;
                    openFileTxt.SetWindowText(bstrFileName);
                    break;
                }
                case 1:
                {
                    int iLen = openFileTxt.GetWindowTextLength();
                    if (!iLen) return 0;
                    WCHAR* pszFileName = (WCHAR*)CoTaskMemAlloc((iLen + 1) * sizeof(WCHAR));
                    if (!pszFileName) return 0;
                    openFileTxt.GetWindowText(pszFileName, iLen + 1);
                    BOOL bRet = OnDatabaseConnect(dbo, CComBSTR(pszFileName));
                    CoTaskMemFree(pszFileName);
                    if (!bRet) {
                        MessageBox(L"Failed to connect to database. Make sure the database file exists and is valid, and try again.", L"Database Operation", MB_OK | MB_ICONEXCLAMATION);
                    }
                    else {
                        CWindow sqlStatementTxt(hSqlStatementTxt);
                        int lLen = sqlStatementTxt.GetWindowTextLength();
                        if (!lLen) {
                            MessageBox(L"Please provide a valid SQL statement, and try again.", L"Error: Blank SQL Statement", MB_OK | MB_ICONEXCLAMATION);
                            return 0;
                        }
                        WCHAR* pszSqlStatement = (WCHAR*)CoTaskMemAlloc((lLen + 1) * sizeof(WCHAR));
                        if (!pszSqlStatement) {
                            MessageBox(L"Out of memory.", L"Critical Error", MB_OK | MB_ICONERROR);
                            return 0;
                        }
                        sqlStatementTxt.GetWindowText(pszSqlStatement, lLen + 1);
                        _RecordsetPtr pRs;
                        HRESULT hr = dbo.Query(CComBSTR(pszSqlStatement), pRs);
                        CoTaskMemFree(pszSqlStatement);
                        if (FAILED(hr)) return 0;
                        FieldsPtr pFields;
                        long lFieldCount = 0;
                        hr = pRs->get_Fields(&pFields);
                        if (FAILED(hr)) return 0;
                        hr = pFields->get_Count(&lFieldCount);
                        if (FAILED(hr)) return 0;
                        if (!lFieldCount) return 0;
                        SendMessage(hLv, LVM_DELETEALLITEMS, 0, 0);
                        if (lLvColCount) {
                            for (long i = lLvColCount; i >= 0; i--)
                                SendMessage(hLv, LVM_DELETECOLUMN, i, 0);
                        }
                        for (long i = 0; i < lFieldCount; i++) {
                            FieldPtr pField;
                            hr = pFields->get_Item(CComVariant(i), &pField);
                            if (FAILED(hr)) return 0;
                            CComBSTR bstrFieldName = NULL;
                            hr = pField->get_Name(&bstrFieldName);
                            if (FAILED(hr)) return hr;
                            AddListViewColumn(hLv, i, bstrFieldName);
                            lLvColCount = i;
                        }
                        VARIANT_BOOL bBOF = VARIANT_FALSE, bEOF = VARIANT_FALSE;
                        hr = pRs->get_BOF(&bBOF);
                        if (FAILED(hr)) return 0;
                        hr = pRs->get_EndOfFile(&bEOF);
                        if (FAILED(hr)) return 0;
                        int iRow = 0;
                        while (bBOF == VARIANT_FALSE && bEOF == VARIANT_FALSE) {
                            for (long i = 0; i < lFieldCount; i++) {
                                FieldPtr pField;
                                hr = pFields->get_Item(CComVariant(i), &pField);
                                if (FAILED(hr)) return 0;
                                CComVariant var{};
                                hr = pField->get_Value(&var);
                                if (FAILED(hr)) {
                                    return 0;
                                }
                                CComBSTR bstrVal = NULL;
                                hr = VariantToString(var, &bstrVal);
                                if (SUCCEEDED(hr)) {
                                    AddListViewColumnItem(hLv, i, iRow, bstrVal);
                                }
                            }
                            iRow++;
                            hr = pRs->get_BOF(&bBOF);
                            if (FAILED(hr)) return 0;
                            hr = pRs->get_EndOfFile(&bEOF);
                            if (FAILED(hr)) return 0;
                            hr = pRs->MoveNext();
                            if (FAILED(hr)) return 0;
                        }
                    }
                    break;
                }
            }
            break;
        }
        case EN_CHANGE:
        {
            switch (LOWORD(wParam))
            {
                case 5:
                {
                    CWindow txt((HWND)lParam), btn(hBtnExecuteQuery);
                    int lLen = txt.GetWindowTextLength();
                    if (!lLen) btn.EnableWindow(FALSE); else btn.EnableWindow(TRUE);
                    break;
                }
            }
            break;
        }
    }
    return 0;
}

LRESULT CExcelWindow::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled) {
    if (hFontTitle) DeleteObject(hFontTitle);
    if (hFontButtonLv) DeleteObject(hFontButtonLv);
    PostQuitMessage(0);
    return 0;
}

HRESULT VariantToString(const VARIANT& varValue, BSTR* bstrResult) {
    std::wstring pszCoercedValue{};
    if (!bstrResult) return E_POINTER;
    switch (varValue.vt) {
        case VT_BOOL:
        {
            pszCoercedValue = std::to_wstring(varValue.boolVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        case VT_DATE:
        {
            SYSTEMTIME systime{};
            VariantTimeToSystemTime(varValue.date, &systime);
            WCHAR pszTmp[30]{};
            HRESULT hr = StringCchPrintf(pszTmp, 30, L"%u/%u/%u %u:%u:%u", systime.wDay, systime.wMonth, systime.wYear, systime.wHour, systime.wMinute, systime.wSecond);
            if (FAILED(hr)) return hr;
            *bstrResult = SysAllocString(pszTmp);
            break;
        }
        case VT_BSTR:
        {
            *bstrResult = SysAllocString(varValue.bstrVal);
            break;
        }
        case VT_I2:
        {
            pszCoercedValue = std::to_wstring(varValue.iVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        case VT_I4:
        {
            pszCoercedValue = std::to_wstring(varValue.lVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        case VT_I8:
        {
            pszCoercedValue = std::to_wstring(varValue.llVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        case VT_I1:
        {
            pszCoercedValue = std::to_wstring(varValue.bVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        case VT_R4:
        {
            pszCoercedValue = std::to_wstring(varValue.fltVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        case VT_R8:
        {
            pszCoercedValue = std::to_wstring(varValue.dblVal);
            *bstrResult = SysAllocString(pszCoercedValue.c_str());
            break;
        }
        default: return E_UNEXPECTED;
    }
    return *bstrResult ? S_OK : E_OUTOFMEMORY;
}

LRESULT CExcelWindow::OnNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    LPNMHDR lpnmhdr = (LPNMHDR)lParam;
    if (lpnmhdr->code == LVN_COLUMNCLICK) {
        LPNMLISTVIEW lpnmLv = (LPNMLISTVIEW)lParam;
        HWND hHdr = (HWND)SendMessage(hLv, LVM_GETHEADER, 0, 0);
        if (!hHdr) return 0;
        HDITEM hdi, hdiLastColumn{};
        hdiLastColumn.mask = HDI_FORMAT;
        if (SendMessage(hHdr, HDM_GETITEM, lLastColumnClicked, (LPARAM)&hdi)) {
            hdi.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN);
            SendMessage(hHdr, HDM_SETITEM, lLastColumnClicked, (LPARAM)&hdi);
        }
        hdi.mask = HDI_FORMAT;
        if (SendMessage(hHdr, HDM_GETITEM, lpnmLv->iSubItem, (LPARAM)&hdi)) {
            SortInfo sortInfo{};
            sortInfo.hLv = hLv;
            sortInfo.bAsc = bSortAsc;
            sortInfo.iColumnIndex = lpnmLv->iSubItem;
            SendMessage(hLv, LVM_SORTITEMSEX, (WPARAM)&sortInfo, (LPARAM)CompareEx);
            hdi.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN);
            hdi.fmt |= sortInfo.bAsc ? HDF_SORTUP : HDF_SORTDOWN;
            SendMessage(hHdr, HDM_SETITEM, lpnmLv->iSubItem, (LPARAM)&hdi);
            lLastColumnClicked = lpnmLv->iSubItem;
        }
    }
    return 0;
}

1753272371072.png
 

File đính kèm

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

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

Back
Top Bottom