Excel自定义函数

自定义函数

假定你想要为Excel编写自定义函数,但是这个自定义函数用vba写起来比较麻烦,还有一种做法就是编写一个dll。我们准备用熟悉的C/C++来编写dll,此时我们打开VS,写下了一个自定义函数Foo,头文件定义如下

#ifndef TEST_H
#define TEST_H

#pragma once

#include <Windows.h>

#ifdef TESTDLL_EXPORT
# define TESTDLL_API __declspec(dllexport)
#else
# define TESTDLL_API __declspec(dllimport)
#endif

#ifdef __cplusplus
extern "C" {
#endif

	TESTDLL_API int WINAPI Foo();

#ifdef __cplusplus
}
#endif

#endif // !TEST_H

这里大部分都是模板代码,需要注意的是Foo的声明。Foo的calling convetion是WINAPI,也就是__stdcall,vba中自定义c函数必须是WINAPI

我们在再来看vba中如何声明该函数

Declare Function Foo Lib "libpath" () As Long

这里有3点需要注意

  1. Foo有返回值,所以声明为Function
  2. libpath为dll的路径,可以是绝对路径,也可以是相对路径,这到后面再介绍
  3. 返回值为Long不是Integer

C/C++导出函数名称

我们知道,c/c++编译器通常会对函数名进行修饰,其中两个原因是因为函数可以重载,还有为了区分不同的调用约定。这就导致dll中的导出函数名称和我们编写的函数名字不一样。为了能够让VBA找到导出函数,我们一般可以在声明VBA函数时指定Alias

假设c中有如下函数

void WINAPI Foo(int i);

对于32位和64位平台,导出函数名称不一样。[1]

32位

导出函数名字位_Foo@4,所以函数声明为

Declare Sub Foo Lib "libpath" Alias "_Foo@4" (ByVal i As Long)

64位

Declare PtrSafe Foo Lib "libpath" (ByVal i As Long)

注意,这里我们使用了PtrSafe关键字,因为在64位Office下面,函数声明需要PtrSafe关键字。

数据类型

下表给出VBAC/C++中常见的数据类型的对应关系

VBAC/C++说明
BooleanVARIANT_BOOLvba中的True=-1,False=0
VARIANT_BOOL实际为short的typedef
Integershort
Longint
Doubledouble
StringBSTRBSTR是OLE的数据类型,是一个带长度前缀的字符串
VariantVARIANT
ArraySAFEARRAY

String

vba中的String是Unicode string,对于常见的Win32 API,通常接受LPCWSTRLPCSTR两种字符指针,分别是utf-16和ANSI的字符串。

String -> LPCWSTR、const wchar_t*或BSTR

这种比较简单,我们使用StrPtr函数,直接获取String的指针即可,因为String本质上是一个BSTR

Declare Sub Foo Lib "libpath" (ByVal str As Long)
void WINAPI Foo(BSTR);
// void WINAPI Foo(LPCWSTR);
// void WINAPI Foo(const wchar_t*);

注意,vba的文档中找不到StrPtr

String -> LPCSTR 或 const char*

String是utf-16编码的,如果我们声明vba中的函数接受String,且c中的函数接受const char*,那么vba就会根据系统当前的locale,将utf-16转换为ANSI字符串。显然这个过程可能是有损的,通常不建议这么做。还有中方法就是再vba中将字符串转换为utf-8数组后,再传递给c函数。

Declare Sub Foo Lib "libpath" (ByVal str As String)
void WINAPI Foo(const char*);
// void WINAPI Foo(LPCSTR);

ARRAY

要传递vba的数组到c/c++函数中,可以使用以下两种方法[2]

指向第一个元素的指针

Declare Sub Foo Lib "libpath" (ByRef anArray As Long, ByVal size As Long)

Dim anArray() As Long
Foo anArray(0), UBound(anArray) + 1
void WINAPI Foo(const int* anArray, int size);

SAFEARRAY指针

Delcare Sub Foo Lib "libpath" (ByRef anArray() As Long)

Dim anArray() Long
Foo anArray
void WINAPI Foo(SAFEARRAY** ppArray);

vba函数命名

vba函数的名字不能同Excel中已经存在的名字相冲突,比如单元格的名字。所以F2这样的名字是非法,在调用时你会得到#REF!错误。

Dll路径

Declare时我们需要指定dll的路径,显然我们不想要硬编码路径。一种方法是我们修改PATH变量,加入dll所在的目录。这个动作可以在打开插件的工作簿时完成,代码如下

Private Declare PtrSafe Function SetEnvironmentVariableW Lib "Kernel32" (ByVal name As LongPtr, ByVal value As LongPtr) As Long

Private Sub Workbook_Open()
	Dim newPath As String
	newPath = "path to dll" & Environ("PATH")
	' TODO Warn if changing PATH fails
	Debug.Print SetEnvironmentVariableW(StrPtr("PATH"), StrPtr(newPath))
End Sub

References

[1] Decorated names
[2] How To Pass Arrays Between Visual Basic and C

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值