Outlook 插件集成 Skype
Outlook 插件集成 Skype 即时通讯:Skype 事件、Outlook Skype 功能区等。
编辑 3:遗憾的是,聊天功能仍在消失:Skype 调整桌面 API 计划:聊天功能仍在消失,通话录音和设备兼容性暂时保留。
编辑 2:似乎(终于!)在没有替代方案的情况下移除桌面 API 并不是一个好主意,所以 - 至少是暂时性地 - 微软/Skype 撤回了这一决定。所以,“直到我们确定替代方案或停止当前解决方案”(引用:Noah Edelstein)。更多详情请参见此处。
编辑:Skype/微软 移除了桌面 API,并用 Skype URI 取代了它(是啊,没错)。恕我直言,这是一个 *非常* 糟糕的决定,因为现在除了发起一些通话之外,没有其他方式可以与 Skype 通信。用他们的话说,这是为了“继续改善用户体验”,这只是公司废话的另一个名称。
注意:一旦我了解到 Skype API 状态更新的新进展,我将尝试在上方的“编辑 n”部分发布。
简介
我们每天都在使用各种工具;我们使用多种即时通讯应用程序(对我而言是 Skype 和 Yahoo!)进行交流;我们每天使用我们最喜欢的客户端(我的是 Outlook)处理大量电子邮件,并且我们还在处理其他大量数据以进行跟踪活动、搜索等等。
但是,尽管数据量急剧增加,这些程序之间的互操作性却很低。我知道,我知道,每个人每天都在试图向我们推销一切都在云端。但是剪贴板和 Alt-Tab 仍然是日常工作的主要工具。我希望能够直接从 JIRA 收到的电子邮件中将任务设置为已完成或分配任务。市场上可能有这样的插件(Skype 肯定有),但没有源代码。
本文的主要目的是将 Skype 功能直接嵌入到 Outlook 中。这不是一个完整的 Skype 客户端嵌入(它只演示了一些获取/设置属性和事件),但它可以很好地演示 Skype 桌面 API 与 Outlook 插件的结合。
还有另外两个目的:
- 描述如何用纯 C 语言创建 COM 组件(通用步骤)
- 演示如何在没有向导的情况下实现 Outlook 插件
对于不耐烦的读者:它长什么样
注意:并非所有控件都有效;其中一些仅用于演示目的。读者将在代码中发现哪些仅用于显示。
背景
熟悉 Skype 桌面 API 将有助于理解该插件如何与 Skype 交互。
简而言之,外部应用程序与 Skype 通信有三个阶段
- 发现 Skype 并建立连接 (
SkypeControlAPIDiscover
,SkypeControlAPIAttach
) - Skype 中的用户确认以允许外部应用程序交互
- 基于
WM_COPYDATA
的数据交换,遵循 Skype 开发者文档中描述的协议。
需要了解 Win32、COM、Outlook 对象模型和 C 编程才能更好地理解本文。
使用代码
该示例是用 C 语言编写的单个 DLL。虽然我确信在当今世界这可能听起来很疯狂,但我仍然出于一些(个人)原因偏爱使用 C 语言编写,只使用操作系统提供的运行时,主要是因为:
- 插件的生命周期和流程由其宿主应用程序(Outlook)调用的已实现契约(在 COM 世界中,接口)决定,
- 该示例广泛使用 COM 和(尤其是)Win32 API,它们最适合用原生语言处理,
- (几乎)所有内容都在开发人员的控制之下,并且
- 简化了维护,消除了对复杂运行时依赖的需求。
该示例是一个 Visual Studio 2010 解决方案,其中包含一个 COM DLL。有 Win32(和 x64,适用于使用 Office 2010 64 位版本的人)配置与 /MT 链接,因此 DLL 可以简单地复制并使用 *regsvr32* 注册(目前没有安装程序)。该示例仅在 Office 2010 上进行了测试(Explorer 用户界面仅为功能区,没有 Office 2007 工具栏)。
我还将逐步解释如何通过添加所需功能来构建项目(不仅是深入研究代码),也适用于那些只使用向导创建 COM 和/或插件,或者只是没有手动完成整个过程的读者。
项目文件按用途分组,通常只实现一件事(例如 DLL 调度、类工厂、功能区元素功能等)。功能通常打包在一个结构中(下文称为**对象**——不是 C++ 意义上的,而是指一个自包含的实体),它有一个第一个成员,一个 `lpVtbl`(类似于 COM 接口的 C 实现),然后是“私有”成员函数和数据成员(如来自 *Allocator.h* 的以下示例):
typedef struct Allocator Allocator;
typedef struct AllocatorVtbl {
PVOID (NDAPI *Alloc)(
Allocator *pAllocator,
ULONG cb
);
... other function members of AllocatorVtbl
} AllocatorVtbl;
struct Allocator {
const AllocatorVtbl *lpVtbl;
BOOL (NDAPI *Initialize)(
Allocator *pAllocator,
HANDLE heap
);
BOOL (NDAPI *Terminate)(
Allocator *pAllocator
);
HANDLE Heap;
#ifdef _DEBUG
_CrtMemState MemState;
#endif
};
源代码文件根据其用途分为类别和子类别。
- 插件:事件 (Outlook)、功能区(分层排列)、Skype I/O(与 Skype 的通用交互),以及插件 COM 对象的实现(*Addin.c Factory.c RibbonElements.c*)。
- 入口:
DllMain
和 *.def* 导出。 - Outlook:
IDispatch
定义和几个 Outlook 对象的 IID,以及一个 *OutlookUtils.c* 文件,它处理 Outlook 插件快速关闭行为、版本、IDispatch
助手、事件通知和类型库助手。 - 运行时:COM 工具、DLL、导出、内存、资源、注册表、Shell 助手、字符串工具、线程;
- Skype:通信和实用工具;
- 支持:调试、安全、字符串和变体助手。
第一步:COM DLL Outlook 插件
创建 DLL 项目并设置配置后,执行创建 DLL COM 组件的常用步骤:实现 DllMain
、DllRegisterServer
、DllUnregisterServer
、DllGetClassObject
和 DllCanUnloadNow
(除了 DllMain
,其余四个函数来自 *OLNDesk.def* 定义文件)。
DllMain
(位于 *Entry.c* 文件中)只是将调用分派给 DllMgr
对象。这是一个“事实上的”单例,就像其他对象(RegistryMgr
、MemMgr
等)一样——通常是那些具有返回文件作用域静态声明变量的 *xxx_GetObject* 函数的对象。DLL 管理器管理三件事
- 到达
DllMain
的 DLL_... 通知调用和HINSTANCE
(DLL 部分) - 跟踪 COM 引用(插件
AddRef
的外部对象(ExtObjRef
成员),DLL 自身创建的 COM 对象(ObjRef
成员)和IClassFactory
的LockServer
调用放置的 COM 锁(LockRef
成员)。所有这些都用于四个导出函数之一,DllCanUnloadNow
,以确定插件维护的活动对象数量。当数量降至 0 时,DllCanUnloadNow
返回S_OK
给调用者。 - DLL 自身的初始化/去初始化(在这种情况下,什么也不做)。
其他四个函数在 *Exports.c* 文件中实现,同样只是将调用分派给 RegistryMgr
对象(与 DllMgr
类似),该对象处理注册(DllRegisterServer
、DllUnregisterServer
)、对象创建(DllGetClassObject
)和生命周期(DllCanUnloadNow
)。虽然只有前两种方法实际处理注册表,但我将它们都保留在相同的结构中。
注册过程完成三件事:检查用户是否为管理员(调用 SecurityMgr
的 IsUserAdmin
),然后将 DLL 注册为
- 一个 COM 对象和
- 一个 Outlook 插件。
我不会在此过程上赘述——只是列举(繁琐的)步骤
- 启动 VS 控制台并运行 *guidgen.exe* 以生成一个 CLSID(在源代码中:
CLSID_OLNDesk
);还要选择 ProgID 以在插件注册中指定它们 - 版本无关和当前版本(在源代码中:ProgID_OLNDesk
和VersionedProgID_OLNDesk
) - 在
DllRegisterServer
/DllUnregisterServer
中实现 COM 注册/注销,在FRegistryMgr_RegisterServer_COM
中实现 Outlook 插件注册/注销。 - 实现
DllGetClassObject
以创建我们的IClassFactory
实现对象(见下文)并将其返回给调用者 - 实现
DllCanUnloadNow
以调用前面讨论的DllMgr
的GetDllRef
方法,如果没有保留任何活动引用,则返回S_OK
。
此外,RegistryMgr
还实现了各种通用的注册表辅助函数(SetValueSz
、SetValueDword
、DeleteKey
、CloseKey
),用于其他地方。
该插件实现了 IClassFactory
接口(在 *Factory.c* 中)。实现是标准的:唯一需要提及的是 DllMgr
对象维护的对象引用(在 AddRef
/Release
调用时增加/减少 ObjRef
,并在 LockServer
中增加 LockRef
)。工厂接口通过 XClassFactory_Create
函数创建,而插件对象(XAddin
)在 CreateInstance
内部创建。
第二步:插件
插件对象是最重要的,它以多种方式运作
- 实现 Outlook 插件的主要功能,主要是
_IDTExtensibility2
接口(COM 插件接口)和IRibbonExtensibility
接口(用于公开 Outlook 用户界面) - 建议 Outlook 对象实现事件接口并实现这些接口
- 实现 Skype 消息和事件处理程序,维护 Skype 运行时信息,并运行一个线程用于与 Skype 进行 I/O 消息交换。
首先,_IDTExtensibility2
接口(* _IDTExtensibility.h* 和 * _IDTExtensibility.c*)。此接口有五个方法(除了从 IDispatch
继承的方法),其中三个代表我们插件对象在 Outlook 中的生命周期。我认为 99% 的可怕的 Outlook 关闭问题都源于此接口的错误实现(希望我做得对)。因此,OnAddInsUpdate
(通知插件 Outlook 插件集合有更新时)和 OnStartupComplete
(在应用程序启动期间加载插件时调用)在我们的示例中什么也不做。
其他三个必须考虑许多事情才能正确实现。
首先,OnBeginShutdown
和 OnDisconnection
在 Outlook 快速关闭期间不会被调用。这通常对那些只是在这些处理程序中释放 COM 对象的插件没有影响。我们的插件还有许多其他事情要做,所以我们必须
- 检测 Outlook 是否会为我们调用这些方法,并且
- 如果没有,则在某处手动调用它。
- 检测寻找两件事:如果 Outlook 快速关闭已启用(
OutlookMgr
ReadAddinFastShutdownBehavior
- 请记住这在 Outlook 2010 中默认启用),以及如果插件在安装时在注册表中指定了RequireShutdownNotification
设置为 0x0000001。这会覆盖默认的快速关闭行为,并告诉 Outlook“为我的插件调用OnDisconnection
和OnBeginShutdown
方法”。我认为这是为了支持由于这种新行为而遇到问题的旧版插件的标志,并且很可能会在未来版本中消失。 - 进行检测测试的地方(如果快速关闭为真 *并且* 插件不需要通知,则调用方法)是 Outlook 应用程序事件的
OnQuit
方法(这在启动期间会被通知 - 见下文)。
OnConnection
方法是插件准备自己的地方。连接过程中执行的步骤是
- 检测上面讨论的 Outlook 版本和快速关闭行为。
- 清理前一个插件执行留下的临时文件(如果有)(这指的是用于显示 Skype 用户图片的临时图像 - 见下文)。
- 然后我们将插件
IUnknown
接口封送到一个IStream
中。这是因为稍后我们需要从另一个线程访问插件(即 Skype I/O 线程),这是 Outlook 插件崩溃/挂起的第二个常见原因:从不是创建它的线程直接访问插件 COM 接口。Outlook 对象(包括插件)通常是 STA——也就是说,我们可以在它们的公寓线程中访问它们。这就是为什么我们应该将这些调用封送到线程 0(所有 OOM 所在的位置)。 - 下一步是读取 Skype 信息(应用程序路径和可执行文件图标,使用 *SkypeUtils.c* 中的
Skype_GetAppInfo
助手)。我们需要 Skype 路径和图标来启动 Skype 并在我们的功能区中显示其图标。 - 然后我们创建并启动 Skype I/O 线程(*SkypeComm.c*)。这是一个消息循环线程,它创建一个类名为
L"OLNDesk.OutlookAddin:Windows:SkypeWatch"
(kSkypeWatchWndClassName
常量)的不可见窗口,其唯一目的是与 Skype 交换 Windows 消息(请参阅上面 Skype 桌面 API 链接,了解外部应用程序与 Skype 之间的消息交换协议描述)。Thread
对象的当前实现假设线程对象有一个主窗口(ThreadHWND
),并向该窗口发送WM_CLOSE
消息,该消息将终止消息循环并返回到线程函数,然后结束。(线程Start
创建线程结束事件以在终止时等待,然后调用_beginthreadex
运行SThreadProc
线程函数;该函数运行ThreadProc
,当消息循环结束时结束,最后将ThreadEndEvent
发送回我们的插件。源代码对此相当自解释)。 - 下一步是将传入的 Outlook 的
Application
对象存储到pAddin->Application
中(并增加DllMgr
的ExtObjRef
成员,因为我们正在AddRef
一个外部 COM 对象——请参阅上面DllCanUnloadNow
的注意事项)。 - 有了
Application
对象,我们现在通知三个事件接口:应用程序、资源管理器集合和检查器集合。所有这三个都使用了通常的IConnectionPointContainer
/IConnectionPoint
/Advise
。这些事件接口(XAppEvents
、XExplorersEvents
和XInspectorsEvents
)以及应用程序自身的 COM 对象(在OnConnection
中传入)以及Explorers
和Inspectors
集合(插件的IDispatch*
成员Application
、Explorers
和Inspectors
)都保存在插件对象中。所有事件接口都具有IDispatch
实现,并依赖于Invoke
调用来查找传入的dispidMember
并直接调用已实现的方法。这比实现整个接口更简单,因为我们可以在Invoke
中只选择我们感兴趣的内容——例如,只有当Quit
事件到达时,AppEvents
才有趣。(那些与感兴趣的事件对应的 DISPID 也可以在运行时通过类型库调用“获取”,但是,更简单的方法是在 OleView 工具中打开 *MSOUTL.OLB*,保存为 *.IDL* 文件,然后手动查找相应的 DISPID。本文采用了这种方法,并且相应的 DISPID 在每个事件接口头文件中定义,例如#define DISPID_APPEVENTS_QUIT (0x0000f007)
)。
对于另外两个方法,它们的主要工作是关闭/反转 OnConnection
所做的事情。OnBeginShutdown
取消对检查器、资源管理器和应用程序对象的事件接口的通知,而 OnDisconnection
释放已封送的插件,清理内部插件结构,停止 Skype I/O 线程并等待其完成,最后释放 Outlook 的 Inspectors
、Explorers
和 Application
对象本身。除了显而易见的清理代码之外,这里唯一的非标准调用是 OutlookMgr
的 Cleanup
——这会释放 OLEnumDesc
单链表结构列表。这个结构存储关于 Outlook 对象的类型库信息,并在 OutlookMgr
的 FindEnumMemberByName
中填充——基本上是 *MSOUTL.OLB* 类型库的枚举器,它提取枚举常量。这在功能区元素内部需要(请参阅 *GroupSkypeStatus.c* - GroupSkypeStatus_Visible
),通过查看 OlObjectClass
并与 ExplorerObjectClass
(值 34)进行比较,以了解功能区元素是在检查器还是资源管理器内部。
第三步:功能区
现在是插件对象实现的第二个接口:IRibbonExtensibility
(XAddin
的 iRibbonX
成员)。该接口的定义(感谢 Jensen Harris)和 Ken Getz 关于功能区元素的详尽文档在这里非常有价值。功能区描述只是一个在 GetCustomUI
调用中提供给 Outlook 的 XML 文件——所有元素类型和处理程序都在这里描述。
首先是一个细节:QueryInterface
的实现也应该在对 IDispatch
执行 QI 时提供 IRibbonExtensibility
。
接下来是 IDispatch
的实现。这需要实现 GetIDsOfNames
,因为我们将在功能区中提供许多功能区元素处理程序(OnLoad
、OnGetVisible
等),并且我们必须向调用者提供我们的 DISPID。这些将在我们实现它们的 Invoke
调用中使用。它们在 *IRibbonExtensibility.h* 文件中定义为常量,以及用于控件类型(选项卡、组、按钮和标签)的 RibbonElementType
枚举和用于控件大小(常规和大型)的 RibbonControlSize
枚举。
因此,由于所有内容都是动态的,GetIDsOfNames
的工作是将传入的 rgszNames
映射到适当的 DISPID
;然后 Outlook 将使用提供的 DISPID
调用 Invoke
。功能区原型和参数的非详尽列表可以在 *_Ribbon_Prototypes_.cpp* 文件中找到(未用于编译)。将根据 DISPID
提取适当的参数,然后调用插件方法(OnLoad
、GetControlProperty
、Action2
和 OnChange
)。
GetCustomUI
通过提供功能区 XML 完成实现。这里使用了 *RibbonElements.c* 的实现——主要是一个处理程序集合,它维护插件的 RibbonElements
单链表。所有元素都维护控件 ID、功能区 ID、父功能区 ID、函数处理程序(并非所有,只在功能区元素定义中指定的内容),最后是上下文(这是指向插件对象本身的弱指针)。
功能区元素在单个文件中实现(在项目中分组在 *Addin/Ribbon* 下)。
我们总结整个功能区实现逻辑(以 GroupSkypeLoggedOnUser
功能区组元素为例):
RibbonElements_Add(
&pAddin->RibbonElements,
RibbonID,
L"*",
L"OLNDesk.OutlookAddin.GroupSkypeLoggedOnUser",
&GroupSkypeLoggedOnUser_Visible,
&GroupSkypeLoggedOnUser_Label,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
(PVOID)pAddin
);
并返回定义该元素的适当 XML 部分
// Logged on user
L" <group " CRLF
L" id=\"OLNDesk.OutlookAddin.GroupSkypeLoggedOnUser\" " CRLF
L" getLabel=\"OnGetLabel\" " CRLF
L" getVisible=\"OnGetVisible\" " CRLF
L" > " CRLF
......
L" </group> " CRLF
static
HRESULT
__stdcall
FRibbonExtensibility_GetIDsOfNames(
IRibbonExtensibility *This,
REFIID riid,
LPOLESTR *rgszNames,
UINT cNames,
LCID lcid,
DISPID *rgDispId
) {
/*DTrace0(TRACE_LEVEL_DEBUG,
L"Addin/IRibbonExtensibility->GetIDsOfNames"
);*/
if(rgDispId != NULL) {
UINT c;
for(c = 0; c < cNames; c++) {
rgDispId[c] = DISPID_UNKNOWN;
// ribbon methods
......
else if(wcscmp(rgszNames[c], L"OnGetVisible") == 0) {
rgDispId[c] = DISPID_RIBBONCALLBACK_ONGETVISIBLE;
}
else if(wcscmp(rgszNames[c], L"OnGetLabel") == 0) {
rgDispId[c] = DISPID_RIBBONCALLBACK_ONGETLABEL;
}
.......
}
}
return S_OK;
UNREFERENCED_PARAMETER(riid);
UNREFERENCED_PARAMETER(lcid);
UNREFERENCED_PARAMETER(This);
}
static
HRESULT
__stdcall
FRibbonExtensibility_Invoke(
IRibbonExtensibility *This,
DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS *pDispParams,
VARIANT *pVarResult,
EXCEPINFO *pExcepInfo,
UINT *puArgErr
) {
......
pAddin = IFace_GetStruct(XAddin,
iRibbonX,
This
);
......
switch(dispIdMember) {
case DISPID_RIBBONCALLBACK_ONGETVISIBLE:
case DISPID_RIBBONCALLBACK_ONGETLABEL:
......
if(pDispParams != NULL
&& pDispParams->cArgs == 1
&& V_VT(&pDispParams->rgvarg[0]) == VT_DISPATCH
&& pVarResult != NULL
) {
return pAddin->GetControlProperty(pAddin,
V_DISPATCH(&pDispParams->rgvarg[0]),
dispIdMember,
pVarResult
);
}
break;
default:
break;
}
return E_NOTIMPL;
}
GetCustomUI
将元素添加到功能区元素列表中GetIDsOfNames
将处理程序名称映射到DISPID
Invoke
使用 DISPID 和功能区处理程序原型进行插件调用- 最后,插件(在这种情况下)实现了
GetControlProperty
,它使用ControlID
在RibbonElements
中识别元素,并根据 DISPID(类似于 *Invoke* 的“父”调用)调用在创建时定义的RibbonElement
处理程序——在这些示例中是 *Element->Visible* 和 *Element->Label* ——并设置要返回给Invoke
调用者的输出值。
当值改变/需要改变并且功能区 UI 需要更新时,会使用一些支持函数(主要用于使功能区控件或整个功能区失效)。这些是 另一个接口 IRibbonUI
的成员,在 *IRibbonUI.h* 中定义。它在 OnLoad
功能区处理程序中传递(并缓存到插件对象中),用于使 UI 失效/刷新。
第四步:Skype I/O(以及插件对应的元素)
一些 Skype 内部定义收集在 *SkypeDefs.h* 中。这些定义在 Skype 桌面 API 网页中有所说明,它们只是各种 Skype 常量的枚举类型(并在下面的实现中使用)。
插件维护一个内部结构(SkypeInfo
),其中包含一些 Skype 属性,以及在需要进行 Skype I/O 时调用的处理程序(在 *Addin.h* 文件中的“// Skype handlers”之后)。与 Skype 的通信通过向 Skype API 窗口发送 WM_COPYDATA
消息来完成——我们将在下面讨论。结构成员在收到来自 Skype 的消息时更新(这会更改相应的成员结构,例如当收到来自 Skype 的 USERSTATUS
通知消息时,UserStatus
会更改)。
SkypeWatch.c 包含 Skype 发现、建立连接和消息交换的实现。这在 Skype I/O 线程隐藏窗口过程 SkypeWatchWndProc
中完成。该窗口过程处理以下消息:
功能区将处于 *未运行* 状态。用户可以点击 *启动* 按钮来启动 Skype。
Skype 正在运行并等待用户登录。插件状态现在变为 *连接中*。
用户登录后,Skype 将请求用户确认。插件将保持 *连接中* 状态,直到用户允许外部应用程序与 Skype 交互。
最后,如果用户在 Skype 中点击 *允许访问*,这将允许 Outlook(插件)与 Skype 交互,监视窗口过程将收到新的 API 状态,该状态将从 SKYPECONTROLAPI_ATTACH_PENDING_AUTHORIZATION
转换为 SKYPECONTROLAPI_ATTACH_SUCCESS
。
WM_CREATE
。这里创建连接计时器并注册 Skype 消息(L"SkypeControlAPIDiscover"
和L"SkypeControlAPIAttach"
);还注册了两个孪生消息(OLNdesk_msg_SkypeControlAPIAttach
和OLNdesk_WM_COPYDATA
),以便在收到 Skype 消息时进行PostMessage
(基本上是用于不阻塞 Skype 的 send-to-post 转换器)。WM_TIMER
。在这里,插件的IsSkypeRunning
被调用(它会创建一个工具帮助快照并查找 *Skype.exe*)。根据是否找到 Skype,设置适当的 API 状态(在 *SkypeDefs.h* 文件中的SKYPE_API_STATUS
枚举中定义),并使用SendMessageTimeoutW
广播msg_SkypeControlAPIDiscover
。如果 Skype 可用,它将使用msg_SkypeControlAPIAttach
向我们在发现消息中提供的窗口回复一个SKYPE_API_STATUS
值(除了未定义的值)。如果收到的附加 API 状态是SKYPECONTROLAPI_ATTACH_SUCCESS
,那么在WPARAM
中,我们得到 Skype API 窗口,从现在开始我们将通过它交换消息。msg_SkypeControlAPIAttach
。当通信建立后,从 Skype 收到;将使用孪生消息OLNdesk_msg_SkypeControlAPIAttach
发布到我们的窗口。OLNdesk_msg_SkypeControlAPIAttach
。这里是封送的插件对象发挥作用的地方——该消息解封送插件对象,并使用DISPID_ADDIN_SETAPISTATUS
(在 *Addin.h* 中定义)调用其上的Invoke
。这个 DISPID(以及DISPID_ADDIN_PROCESSSKYPEMESSAGE
)在插件的IDispatch
实现(FAddin_Invoke
)中处理。第一个调用SetSkypeApiStatus
——它更新SkypeInfo.ApiStatus
(和SzApiStatus
),使功能区状态元素失效,并执行对各种 Skype 属性的首次读取,以填充SkypeInfo
成员(例如余额、货币、头像、全名等)。
后者处理通过 Skype 监视窗口收到的 Skype 消息:从 Skype 收到的 WM_COPYDATA
通过 OLNdesk_WM_COPYDATA
发布到监视窗口(分配并复制收到的 COPYDATASTRUCT
到一个类似的 COPYDATASTRUCT_CTX
结构),该消息反过来再次检索插件,准备一个包含 COPYDATASTRUCT_CTX
内容的 SAFEARRAY
,并使用 DISPID_ADDIN_PROCESSSKYPEMESSAGE
调用插件上的 Invoke
。最后,FXAddin_ProcessSkypeMessage
从解包的 SAFEARRAY
中检索 Skype 发送的 UTF-8 字符串,解释消息,然后解析字符串,并分派命令。
命令大多是“GET <SOMEHING>”,Skype 会回复“<SOMETHING> <VALUE>”。
**GET AVATAR** 命令可能是这里最有趣的。我们创建一个临时 JPG 文件路径并发送 **GET AVATAR 1 <filename>**,Skype 回复 **AVATAR 1 <filename>**,ProcessSkypeMessage
将路径存储在 SkypeInfo.Avatar
变量中并使 SkypeLoggedOnUser
元素失效。功能区元素的失效将调用 OnGetImage
功能区处理程序,最终分派到 btnSkypeLoggedOnUser_Image
调用,图像将通过 BitmapFromJPGFile
从 JPEG 文件中提取到 IPicture
对象中。此函数演示了 CreateStreamOnHGlobal
(将 JPG 文件内容转换为 IStream
)和 OleLoadPicture
以获取 IPicture
的用法。
资源加载
RcUtils.c 中的另一个函数是 BitmapFromPNGResource
。它用于从 PNG 文件获取 HBITMAP
,并用于加载 Skype UI 资源(需要 Skype 开发者帐户才能使用它们),例如状态按钮图像(在线、离开等)。此函数从类型为“PNG”的资源中加载 PNG,然后使用 Windows 图像组件 (WIC) 解码器接口(IWICBitmapDecoder
、IWICBitmapFrameDecode
、IWICBitmapSource
)——感谢 Marius Bancila 的 用 C++ 输入时显示图像 示例。
最后
本文将各种细节留给用户自行发现。正如我之前所说,目的是更多地解释如何完成,包括可能的新手细节(例如关于 DllRegisterServer
、如何手动制作 COM 等),而不是剖析源代码。我更倾向于解释为什么调用某个函数以及从哪里调用,而不是粘贴大段源代码并解释为什么调用 OleCreate
。API 细节可以在 MSDN 上找到,本文本来很容易变得臃肿——无论如何,在某些地方可能已经过度了 。
以及个人注释(不幸的是,我处理的此类评论比我想要的要多……)。如果有人想说/评论/问以下任何一件事,请节省时间阅读答案
尊敬的先生或女士,您能用 VB 来实现这个吗? | 不支持。 |
为什么是 C 而不是 <我偏爱的 .NET 语言>? | 我更喜欢 C。 |
我可以窃取您的代码并复制/粘贴来给我的海外雇主留下深刻印象吗? | 也许吧。祝您指针好运。哦,对了,你们所有的钱都属于我们。 |
我用 Shim .NET Wizards 做这个更容易。 | 这是一个问题吗? |
Skype 移除桌面 API 是白痴行为吗? | 绝对是。 |
您认为 Skype 会推出同样强大的替代 API 吗? | 不。遗憾的是,他们最终会为了云的更大福祉而掩埋一切。 |
历史
2012/01/17 - 初次发布。