65.9K
CodeProject 正在变化。 阅读更多。
Home

Excel 自定义函数编写指南:第三部分,C++ RTD 插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (6投票s)

2011年8月25日

CPOL

8分钟阅读

viewsIcon

54650

在上一篇文章中,我们使用自动化插件在 Excel 中开发了一个自定义函数。它相当简单,花费的时间也不长,但性能并不是它的强项,而且无法在 Excel 中为用户正确记录文档。RTD 是一个自动化插件

引言

上一篇文章中,我们使用自动化插件在 Excel 中开发了一个自定义函数。它相当简单,花费的时间也不长,但性能并不是它的强项,而且无法在 Excel 中为用户正确记录文档。RTD 是一个自动化插件,它实现了 IRtdServer 接口,因此创建它与实现其他接口类似。RTD 插件与其他选项不同之处在于它允许异步调用,当函数需要调用数据库或 Web 服务时,这是一个巨大的优势。因为 RTD 是异步的,所以用户可以在函数计算时继续在 Excel 中工作。RTD 的缺点是它们不会出现在函数向导中,并且用户被迫使用笨拙的语法。

异步 XLL(未来帖子主题)与 RTD 类似,都可以将数据拉入 Excel,但 RTD 插件的不同之处在于它们也可以将数据推入 Excel。因此,如果 RTD 服务器提供股票价格并且股票价值更新,RTD 就可以告诉 Excel 有一个新值并更新 Excel。XLL 无法做到这一点,用户将被迫重新计算公式才能获得更新的值。

网上有几篇关于创建 RTD 服务器的文章。我认为大多数都过于复杂,因此在这篇文章中,我希望向您展示一种更直接的方法,该方法利用了 Visual Studio IDE 的优势,并且只需要最少的代码更改(更多点击,更少代码)。

入门

第一步是创建一个 C++ 项目(非托管)。RTD 服务器只是自动化插件的一种,因此本文的第一部分将与上一篇文章相同。从菜单中选择“文件 -> 新建 -> 项目”。然后从模板列表中选择“其他语言 -> Visual C++ -> ATL”。

 

图 1:创建新项目。

NewProject

图 2:应用程序设置页面。

ApplicationSettings

实现 IRtdServer 接口

现在我们已经创建了一个项目,我们可以创建一个 ATL 简单对象,其中包含我们的 RTD 服务器实现。切换到*类视图*,右键单击“RTDExample”项目,然后从菜单中选择“添加 -> 类…” 。在 ATL 下选择“ATL 简单对象”并单击“添加”。在向导中,我们只需要指定类的名称,并且我们不需要聚合。在此示例中,我使用的是“SimpleRTDServer”。

图 3:添加 ATL 简单对象,第一页。

AddClass1

图 4:在“短名称”框中输入名称。

AddClass2

 

图 6:“选项”页面,只需单击“否”以进行聚合。其余均为默认值。

AddClass4

现在我们有了新类,我们需要它来实现 IRTDServer 接口。右键单击类视图中的 CSimpleRTDServer,然后选择“添加 -> 实现接口”。选择“文件”,然后使用文件浏览器定位 Excel 可执行文件。然后从左侧列表找到 IRtdServer,然后单击“>”按钮。

图 7:使用实现接口向导实现 IRtdServer。

ImplementInterfaceWizard

使其编译

现在尝试构建项目,它会抱怨各种东西被重新定义。现在是时候进行一些编辑了。错误可能包含类似以下的语句:

错误 18 error C2011: 'Font' : 'struct' type redefinition excel.tlh 4732 1 RTDExample

双击错误以转到 excel.tlh 文件。由于我们不需要此处的所有定义,因此最好只复制我们需要的并删除 excel.tlh 的导入语句。我们需要的第一件事是 LIBID_Excel 的定义。您可以搜索它,或者直接使用下面的代码。

extern "C" const GUID __declspec(selectany) LIBID_Excel =
    {0x00020813,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};

接下来我们需要 IRtdServer 本身的定义,这应该不难找到,或者如果您觉得懒惰,可以直接从图 8 复制。

 

图 8:IRtdServer 定义。

struct __declspec(uuid("ec0e6191-db51-11d3-8f3e-00c04f3651b8"))
IRtdServer : IDispatch
{
    //
    // Raw methods provided by interface
    //

      virtual HRESULT __stdcall ServerStart (
        /*[in]*/ struct IRTDUpdateEvent * CallbackObject,
        /*[out,retval]*/ long * pfRes ) = 0;
      virtual HRESULT __stdcall ConnectData (
        /*[in]*/ long TopicID,
        /*[in]*/ SAFEARRAY * * Strings,
        /*[in,out]*/ VARIANT_BOOL * GetNewValues,
        /*[out,retval]*/ VARIANT * pvarOut ) = 0;
      virtual HRESULT __stdcall RefreshData (
        /*[in,out]*/ long * TopicCount,
        /*[out,retval]*/ SAFEARRAY * * parrayOut ) = 0;
      virtual HRESULT __stdcall DisconnectData (
        /*[in]*/ long TopicID ) = 0;
      virtual HRESULT __stdcall Heartbeat (
        /*[out,retval]*/ long * pfRes ) = 0;
      virtual HRESULT __stdcall ServerTerminate ( ) = 0;
};

尽管此时可以编译,但我们还需要 IRTDUpdateEvent 定义。它提供了我们将用于通知 Excel 结果已准备好的回调方法。没有它,插件将无法返回任何值(通常认为这很重要)。定义可以在 excel.tlh 或图 9 中找到。

图 9:IRTDUpdateEvent 定义。

struct __declspec(uuid("a43788c1-d91b-11d3-8f39-00c04f3651b8"))
IRTDUpdateEvent : IDispatch
{
    //
    // Raw methods provided by interface
    //

      virtual HRESULT __stdcall UpdateNotify ( ) = 0;
      virtual HRESULT __stdcall get_HeartbeatInterval (
        /*[out,retval]*/ long * plRetVal ) = 0;
      virtual HRESULT __stdcall put_HeartbeatInterval (
        /*[in]*/ long plRetVal ) = 0;
      virtual HRESULT __stdcall Disconnect ( ) = 0;
};

 

 

打开“stdafx.h”,注释掉或删除导入行,然后粘贴 excel.tlh 文件中的定义。代码应如下所示:

//#import "C:\Program Files (x86)\Microsoft Office\OFFICE11\EXCEL.EXE" raw_interfaces_only, raw_native_types, no_namespace, named_guids, auto_search


extern "C" const GUID __declspec(selectany) LIBID_Excel =
    {0x00020813,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};

struct __declspec(uuid("ec0e6191-db51-11d3-8f3e-00c04f3651b8"))
IRtdServer : IDispatch
{
    //
    // Raw methods provided by interface
    //

      virtual HRESULT __stdcall ServerStart (
        /*[in]*/ struct IRTDUpdateEvent * CallbackObject,
        /*[out,retval]*/ long * pfRes ) = 0;
      virtual HRESULT __stdcall ConnectData (
        /*[in]*/ long TopicID,
        /*[in]*/ SAFEARRAY * * Strings,
        /*[in,out]*/ VARIANT_BOOL * GetNewValues,
        /*[out,retval]*/ VARIANT * pvarOut ) = 0;
      virtual HRESULT __stdcall RefreshData (
        /*[in,out]*/ long * TopicCount,
        /*[out,retval]*/ SAFEARRAY * * parrayOut ) = 0;
      virtual HRESULT __stdcall DisconnectData (
        /*[in]*/ long TopicID ) = 0;
      virtual HRESULT __stdcall Heartbeat (
        /*[out,retval]*/ long * pfRes ) = 0;
      virtual HRESULT __stdcall ServerTerminate ( ) = 0;
};

现在,当您尝试构建时,仍然会有一个错误

Error	1	error C2259: 'ATL::CComObject' : cannot instantiate abstract class	
c:\program files (x86)\microsoft visual studio 8\vc\atlmfc\include\atlcom.h	186

一旦 SimpleRTDServer 有一些代码,这个错误就会自行消失。

处理请求

我们已经有了 RTD 服务器,但它还没有那么有趣。让我们让它做点什么。出于演示目的,它实际上不会做任何有用的事情,但我们会让它稍微延迟一下以模拟网络活动,然后计算一些东西并返回结果。RTD 服务器中的事件流程如下:

  1. Excel 在公式中遇到 =RTD() 函数。
  2. Excel 调用 ConnectData() 并向 RTD 服务器提供分配给 RTD 函数的主题 ID 和参数。主题 ID 用于将结果与对 RTD 函数的调用进行匹配。
  3. 然后,RTD 服务器可以自由地执行一些工作。您可以排队请求,或创建一个线程来处理请求等。
  4. 一旦服务器有了结果,它就会调用 UpdateNotify() 来通知 Excel 结果已准备好。
  5. 当 Excel 准备好时,它会调用 RefreshData() 来从 RTD 服务器请求结果。然后,RTD 服务器以 SAFEARRAY 的形式将已准备好的结果(包含主题 ID 及其值)提供给 Excel。
  6. Excel 将值导入工作表。
  7. 当删除公式并且 Excel 不再需要某个主题时,它会通过调用 DisconnectData() 并提供不再需要的主题的 ID 来通知 RTD。

让我们来实现 IRtdServer 接口的方法。我们将首先删除 SimpleRTDServer.h 中的默认实现,该实现现在只是返回 E_NOTIMPL。将 SimpleRTDServer.h 替换为图 10 中的代码。

图 10:SimpleRTDServer.h。

// SimpleRTDServer.h : Declaration of the CSimpleRTDServer

#pragma once
#include "resource.h"       // main symbols
#include <map>
#include <string>
#include <boost>

#include "RTDExample.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;
using namespace boost;
using namespace std;

// CSimpleRTDServer

class ATL_NO_VTABLE CSimpleRTDServer :
	public CComObjectRootEx<ccomsinglethreadmodel>,
	public CComCoClass<csimplertdserver,>,
	public IDispatchImpl<isimplertdserver, *wmajor="*/" *wminor="*/">,
	public IDispatchImpl<irtdserver, wmajor="*/" wminor="*/">
{
public:
	CSimpleRTDServer()
	{
	}

	DECLARE_REGISTRY_RESOURCEID(IDR_SIMPLERTDSERVER)

	DECLARE_NOT_AGGREGATABLE(CSimpleRTDServer)

	BEGIN_COM_MAP(CSimpleRTDServer)
		COM_INTERFACE_ENTRY(ISimpleRTDServer)
		COM_INTERFACE_ENTRY2(IDispatch, IRtdServer)
		COM_INTERFACE_ENTRY(IRtdServer)
	END_COM_MAP()



	DECLARE_PROTECT_FINAL_CONSTRUCT()

	HRESULT FinalConstruct()
	{
		return S_OK;
	}

	void FinalRelease()
	{
	}

public:


	// IRtdServer Methods
public:
	STDMETHODIMP ServerStart(IRTDUpdateEvent * CallbackObject, long * pfRes);
	STDMETHODIMP ConnectData(long TopicID, SAFEARRAY * * Strings, VARIANT_BOOL * GetNewValues, VARIANT * pvarOut);
	STDMETHODIMP RefreshData(long * TopicCount, SAFEARRAY * * parrayOut);
	STDMETHODIMP DisconnectData(long TopicID);
	STDMETHODIMP Heartbeat(long * pfRes);
	STDMETHODIMP ServerTerminate();

private:
	std::list<std::wstring> StringsAsList(SAFEARRAY * * Strings);
	IRTDUpdateEvent * m_callBackObj;
	std::map<long,>> m_results;
	std::list<long> m_new_results;
	boost::thread m_backgroundThread;
};
class CWorkerTask
{
private:
	long m_topicID;
	IRTDUpdateEvent * m_callBackObj;
	std::list<long> * m_pNewResults;
	std::list<std::wstring> m_args;
public:
	CWorkerTask(long topicID, IRTDUpdateEvent * callBackObj, std::list<long> * newResults, std::list<std::wstring> args);
	double operator()();
};
OBJECT_ENTRY_AUTO(__uuidof(SimpleRTDServer), CSimpleRTDServer)

现在切换到 SimpleRTDServer.cpp(图 11)。这里的代码使用 boost 库(https://boost.ac.cn)来处理多线程。如果您更喜欢使用其他东西,替换 boost 应该不难。其工作原理是:在 ConnectData 中,我们创建一个 CWorker 对象,该对象知道参数是什么,并将其传递给 packaged_task。packaged_task 连接到一个 shared_promise,后者保存线程计算的值。当线程完成时,它调用 UpdateNotify(),通知 Excel 从 RTD 请求值。然后返回将在 m_results 映射中复制到 shared_promise 的计算值。它在返回值之前调用 UpdateNotify(),因此 RefreshData 可能在结果准备好之前被调用。在 RefreshData 中,我们在获取值之前检查/等待值准备好,从而避免了这个问题。

图 11:SimpleRTDServer.cpp。

// SimpleRTDServer.cpp : Implementation of CSimpleRTDServer

#include "stdafx.h"
#include "SimpleRTDServer.h"
#include <boost>
#include <boost>
#include <string>
#include <list>

using namespace std;
using namespace boost;

// CSimpleRTDServer

// Called during loading of the dll. CallbackObject is what we use to notify Excel that
// we're done calculating some values and we're ready to refresh the data in the spreadsheet.
HRESULT CSimpleRTDServer::ServerStart(IRTDUpdateEvent * CallbackObject, long * pfRes /* <= 0 means failure */)
{
	if(CallbackObject == NULL || pfRes == NULL)
	{
		return E_POINTER;
	}
	m_callBackObj = CallbackObject;
	*pfRes = 1;
	return S_OK;
}
// Whenever a new topic is needed Excel will call this. The GetNewValues parameter tells Excel to use
// the previous value until the call to RefreshData or display the default while waiting.
HRESULT CSimpleRTDServer::ConnectData(long TopicID, SAFEARRAY * * Strings, VARIANT_BOOL * GetNewValues, VARIANT * pvarOut)
{
	CWorkerTask worker(TopicID, m_callBackObj, &m_new_results, StringsAsList(Strings));
	boost::packaged_task<double> pt(worker);
	
	
	m_results[TopicID] = boost::move(pt.get_future());
	boost::thread task(boost::move(pt)); // start thread.
	task.detach();
	return S_OK;
}

// After we call UpdateNotify, Excel calls this function to request the values
// of the topics that have been updated.
HRESULT CSimpleRTDServer::RefreshData(long * TopicCount, SAFEARRAY * * parrayOut)
{
	HRESULT hr = S_OK;
	if(TopicCount == NULL || parrayOut == NULL || (*parrayOut != NULL))
	{
		hr = E_POINTER;
	}
	else
	{
		*TopicCount = m_new_results.size();
		SAFEARRAYBOUND bounds[2];
		VARIANT value;
		long index[2];

		// Create a safe array		
		bounds[0].cElements = 2;
		bounds[0].lLbound = 0;
		bounds[1].cElements = *TopicCount;
		bounds[1].lLbound = 0;
		*parrayOut = SafeArrayCreate(VT_VARIANT, 2, bounds);
		int i = 0;
		for(list<long>::const_iterator itor = m_new_results.begin(); itor != m_new_results.end(); ++itor)
		{
			index[0] = 0;
			index[1] = i;
			
			VariantInit(&value);
			value.vt = VT_I4;
			value.lVal = *itor; // Topic ID
			SafeArrayPutElement(*parrayOut, index, &value);

			index[0] = 1;
			VariantInit(&value);
			value.vt = VT_R8;
			if(!m_results[*itor].is_ready())
				m_results[*itor].wait();
			value.dblVal = m_results[*itor].get(); // Result
			SafeArrayPutElement(*parrayOut, index, &value);			
		}
		m_new_results.clear();
		hr = S_OK;
	}
	return hr;
}
// Excel tells us that it doesn't need a topic by calling this.
// We remove the TopicID from the results. In a real add-in you'd
// probably want to check if the thread is still running and stop it.
HRESULT CSimpleRTDServer::DisconnectData(long TopicID)
{
	m_results.erase(TopicID);
	return S_OK;
}
// Excel calls this to determine if we're still alive.
// If pfRes is non-negative then we're still good.
HRESULT CSimpleRTDServer::Heartbeat(long * pfRes)
{
	HRESULT hr = S_OK;
	if(pfRes == NULL)
		hr = E_POINTER;
	else
		*pfRes = 1;
	return hr;
}
// Before Excel unloads the dll it calls this.
HRESULT CSimpleRTDServer::ServerTerminate()
{
	return S_OK;
}
// Converts the parameters supplied to ConnectData "Strings" into a list of wstrings 
// to make it easier to work with.
std::list<std::wstring> CSimpleRTDServer::StringsAsList(SAFEARRAY * * Strings)
{
	std::list<std::wstring> result;
	LONG lbound, ubound;
	SafeArrayGetLBound(*Strings,1,&lbound);
	SafeArrayGetUBound(*Strings,1,&ubound);
	
	VARIANT* pvar;
	SafeArrayAccessData(*Strings, (void HUGEP**) &pvar);
	for(long i = lbound; i <= ubound; i++)
	{
		BSTR bs = pvar[i].bstrVal;
		
		result.push_back(std::wstring(bs,SysStringLen(bs)));
	}
	SafeArrayUnaccessData(*Strings);

	return result;
}

// CWorkerTask

// This is what we'll be doing in the thread. Typically you might want to query a service of somekind here.
double CWorkerTask::operator()()
{
	this_thread::sleep(boost::posix_time::seconds(5));
	double dPerimeter = 0.0;
	for(std::list<std::wstring>::const_iterator itor = m_args.begin(); itor != m_args.end(); ++itor)
	{
		dPerimeter += boost::lexical_cast<double>(*itor);
	}
	m_pNewResults->push_front(m_topicID);
	m_callBackObj->UpdateNotify();
	
	return dPerimeter;
}
CWorkerTask::CWorkerTask(long topicID, IRTDUpdateEvent *callBackObj,std::list<long> * newResults, std::list<std::wstring> args)
{
	m_callBackObj = callBackObj;
	m_pNewResults = newResults;
	m_topicID = topicID;
	m_args = args;
}

注册 RTD 服务器

在使用它之前,必须注册 RTD 服务器。当您构建项目时,Visual Studio 应该会自动为您注册 DLL。万一您收到错误,请转到命令行(以管理员身份运行),将目录更改到项目的 debug 文件夹,然后手动注册(图 13)。

图 13:注册 RTD 服务器。

regsvr32 RTDExample.dll

设置调试

要调试项目,请在解决方案资源管理器中右键单击项目,然后单击“属性”。找到调试页面,并将其更改为如图 14 所示(将 excel.exe 添加为命令)。

图 14:调试设置。

image

使用 RTD 服务器

要使用 RTD 服务器,请构建项目并进行调试。在 Excel 中,输入如图 12/13 所示的 RTD 公式。起初它会显示 0,但几秒钟后将被结果替换。Add-In 示例当前计算多项式的周长(换句话说,等同于 SUM 函数)。

图 15:在单元格中输入此公式进行测试 RTD。

=RTD("rtdexample.simplertdserver.1","",1.5,2.5,3.5)

图 16:RTD 服务器运行中。

image

结论

虽然它不是很令人兴奋,但 RTD 的真正目的是执行耗时的工作(网络、某种 I/O)或处理自行变化的内容(股票、温度)并需要将数据推送到 Excel。UpdateNotify 可以在任何时候从 RTD 和任何线程调用。它不一定是 ConnectData 启动的线程,而可能是一个轮询外部资源的线程(例如,从 Web 服务获取数据)或由外部事件触发(实时市场数据馈送或某些传感器)。

正如您所见,调用 RTD 服务器非常笨拙,谁会记住如何调用您的函数,如果没有任何提示说明有多少参数或每个参数是什么。我们可以通过将 RTD 与 XLL 插件结合来解决这些问题。XLL 提供更好的接口,而 RTD 则完成所有实际工作。我们将在以后的帖子中探讨该解决方案。

下载

Visual Studio 2005 示例解决方案

RTD 插件解决方案下载

© . All rights reserved.