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

Outlook 插件集成 Skype

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (32投票s)

2012年11月18日

CPOL

20分钟阅读

viewsIcon

71370

downloadIcon

2490

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 语言编写,只使用操作系统提供的运行时,主要是因为:

  1. 插件的生命周期和流程由其宿主应用程序(Outlook)调用的已实现契约(在 COM 世界中,接口)决定,
  2. 该示例广泛使用 COM 和(尤其是)Win32 API,它们最适合用原生语言处理,
  3. (几乎)所有内容都在开发人员的控制之下,并且
  4. 简化了维护,消除了对复杂运行时依赖的需求。

该示例是一个 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 组件的常用步骤:实现 DllMainDllRegisterServerDllUnregisterServerDllGetClassObjectDllCanUnloadNow(除了 DllMain,其余四个函数来自 *OLNDesk.def* 定义文件)。

DllMain(位于 *Entry.c* 文件中)只是将调用分派给 DllMgr 对象。这是一个“事实上的”单例,就像其他对象(RegistryMgrMemMgr 等)一样——通常是那些具有返回文件作用域静态声明变量的 *xxx_GetObject* 函数的对象。DLL 管理器管理三件事

  1. 到达 DllMain 的 DLL_... 通知调用和 HINSTANCE(DLL 部分)
  2. 跟踪 COM 引用(插件 AddRef 的外部对象(ExtObjRef 成员),DLL 自身创建的 COM 对象(ObjRef 成员)和 IClassFactoryLockServer 调用放置的 COM 锁(LockRef 成员)。所有这些都用于四个导出函数之一,DllCanUnloadNow,以确定插件维护的活动对象数量。当数量降至 0 时,DllCanUnloadNow 返回 S_OK 给调用者。

     

  3. DLL 自身的初始化/去初始化(在这种情况下,什么也不做)。


其他四个函数在 *Exports.c* 文件中实现,同样只是将调用分派给 RegistryMgr 对象(与 DllMgr 类似),该对象处理注册(DllRegisterServerDllUnregisterServer)、对象创建(DllGetClassObject)和生命周期(DllCanUnloadNow)。虽然只有前两种方法实际处理注册表,但我将它们都保留在相同的结构中。

注册过程完成三件事:检查用户是否为管理员(调用 SecurityMgrIsUserAdmin),然后将 DLL 注册为

  1. 一个 COM 对象和
  2. 一个 Outlook 插件。

我不会在此过程上赘述——只是列举(繁琐的)步骤

  • 启动 VS 控制台并运行 *guidgen.exe* 以生成一个 CLSID(在源代码中:CLSID_OLNDesk);还要选择 ProgID 以在插件注册中指定它们 - 版本无关和当前版本(在源代码中:ProgID_OLNDeskVersionedProgID_OLNDesk
  • DllRegisterServer/DllUnregisterServer 中实现 COM 注册/注销,在 FRegistryMgr_RegisterServer_COM 中实现 Outlook 插件注册/注销。
  • 实现 DllGetClassObject 以创建我们的 IClassFactory 实现对象(见下文)并将其返回给调用者
  • 实现 DllCanUnloadNow 以调用前面讨论的 DllMgrGetDllRef 方法,如果没有保留任何活动引用,则返回 S_OK

此外,RegistryMgr 还实现了各种通用的注册表辅助函数(SetValueSzSetValueDwordDeleteKeyCloseKey),用于其他地方。

该插件实现了 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(在应用程序启动期间加载插件时调用)在我们的示例中什么也不做。

其他三个必须考虑许多事情才能正确实现。

首先,OnBeginShutdownOnDisconnection 在 Outlook 快速关闭期间不会被调用。这通常对那些只是在这些处理程序中释放 COM 对象的插件没有影响。我们的插件还有许多其他事情要做,所以我们必须

  1. 检测 Outlook 是否会为我们调用这些方法,并且
  2. 如果没有,则在某处手动调用它。
  1. 检测寻找两件事:如果 Outlook 快速关闭已启用(OutlookMgr ReadAddinFastShutdownBehavior - 请记住这在 Outlook 2010 中默认启用),以及如果插件在安装时在注册表中指定了 RequireShutdownNotification 设置为 0x0000001。这会覆盖默认的快速关闭行为,并告诉 Outlook“为我的插件调用 OnDisconnectionOnBeginShutdown 方法”。我认为这是为了支持由于这种新行为而遇到问题的旧版插件的标志,并且很可能会在未来版本中消失。
  2. 进行检测测试的地方(如果快速关闭为真 *并且* 插件不需要通知,则调用方法)是 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 中(并增加 DllMgrExtObjRef 成员,因为我们正在 AddRef 一个外部 COM 对象——请参阅上面 DllCanUnloadNow 的注意事项)。
  • 有了 Application 对象,我们现在通知三个事件接口:应用程序、资源管理器集合和检查器集合。所有这三个都使用了通常的 IConnectionPointContainer/IConnectionPoint/Advise。这些事件接口(XAppEventsXExplorersEventsXInspectorsEvents)以及应用程序自身的 COM 对象(在 OnConnection 中传入)以及 ExplorersInspectors 集合(插件的 IDispatch* 成员 ApplicationExplorersInspectors)都保存在插件对象中。所有事件接口都具有 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 的 InspectorsExplorersApplication 对象本身。除了显而易见的清理代码之外,这里唯一的非标准调用是 OutlookMgrCleanup——这会释放 OLEnumDesc 单链表结构列表。这个结构存储关于 Outlook 对象的类型库信息,并在 OutlookMgrFindEnumMemberByName 中填充——基本上是 *MSOUTL.OLB* 类型库的枚举器,它提取枚举常量。这在功能区元素内部需要(请参阅 *GroupSkypeStatus.c* - GroupSkypeStatus_Visible),通过查看 OlObjectClass 并与 ExplorerObjectClass(值 34)进行比较,以了解功能区元素是在检查器还是资源管理器内部。

第三步:功能区

现在是插件对象实现的第二个接口:IRibbonExtensibilityXAddiniRibbonX 成员)。该接口的定义(感谢 Jensen Harris)和 Ken Getz 关于功能区元素的详尽文档在这里非常有价值。功能区描述只是一个在 GetCustomUI 调用中提供给 Outlook 的 XML 文件——所有元素类型和处理程序都在这里描述。

首先是一个细节:QueryInterface 的实现也应该在对 IDispatch 执行 QI 时提供 IRibbonExtensibility

接下来是 IDispatch 的实现。这需要实现 GetIDsOfNames,因为我们将在功能区中提供许多功能区元素处理程序(OnLoadOnGetVisible 等),并且我们必须向调用者提供我们的 DISPID。这些将在我们实现它们的 Invoke 调用中使用。它们在 *IRibbonExtensibility.h* 文件中定义为常量,以及用于控件类型(选项卡、组、按钮和标签)的 RibbonElementType 枚举和用于控件大小(常规和大型)的 RibbonControlSize 枚举。

因此,由于所有内容都是动态的,GetIDsOfNames 的工作是将传入的 rgszNames 映射到适当的 DISPID;然后 Outlook 将使用提供的 DISPID 调用 Invoke。功能区原型和参数的非详尽列表可以在 *_Ribbon_Prototypes_.cpp* 文件中找到(未用于编译)。将根据 DISPID 提取适当的参数,然后调用插件方法(OnLoadGetControlPropertyAction2OnChange)。

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,它使用 ControlIDRibbonElements 中识别元素,并根据 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_SkypeControlAPIAttachOLNdesk_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) 解码器接口IWICBitmapDecoderIWICBitmapFrameDecodeIWICBitmapSource)——感谢 Marius Bancila用 C++ 输入时显示图像 示例。

最后

本文将各种细节留给用户自行发现。正如我之前所说,目的是更多地解释如何完成,包括可能的新手细节(例如关于 DllRegisterServer、如何手动制作 COM 等),而不是剖析源代码。我更倾向于解释为什么调用某个函数以及从哪里调用,而不是粘贴大段源代码并解释为什么调用 OleCreate。API 细节可以在 MSDN 上找到,本文本来很容易变得臃肿——无论如何,在某些地方可能已经过度了

以及个人注释(不幸的是,我处理的此类评论比我想要的要多……)。如果有人想说/评论/问以下任何一件事,请节省时间阅读答案

尊敬的先生或女士,您能用 VB 来实现这个吗? 不支持。
为什么是 C 而不是 <我偏爱的 .NET 语言> 我更喜欢 C。
我可以窃取您的代码并复制/粘贴来给我的海外雇主留下深刻印象吗?  也许吧。祝您指针好运。哦,对了,你们所有的钱都属于我们。
我用 Shim .NET Wizards 做这个更容易。 这是一个问题吗?
Skype 移除桌面 API 是白痴行为吗?  绝对是。
您认为 Skype 会推出同样强大的替代 API 吗?  不。遗憾的是,他们最终会为了云的更大福祉而掩埋一切。

历史   

2012/01/17 - 初次发布。

© . All rights reserved.