Windows Shell提取媒体信息分类: 2.1 VC++/MFC2010-04-15 22:50 242人阅读 评论(0) 收藏 举报这个Project有三个有趣而可以参考的地方:使用COM接口操作Windows Shell,并提取多媒体文件的标签信息编写Dll,并提供对DLL中的类显示调用的支持最小化编译时的依赖,即正确地使用#include、理清C/CPP文件和H文件的关系为了照顾这个Project研究的逻辑思考过程,将这三点按上述顺序排列,虽然我觉得后面的更好玩一点。Moreover, the term Project here refers to its meaning in Visual Studio, rather than the meaning in Engineering of zhuangbility - -||最后我们将把这个Shell的API按提取多媒体文件标签的这个需要打一个包,形成一个新的库文件以供其他使用。1. Shell操作Windows Shell顾名思义就是Windows系统的外衣,能看到的日常操作都由Shell负责,而很多Shell提供的功能都作为系统API放到了DLL文件里可供调用(shell32.dll)。因为这次要做的是提取多媒体文件的标签信息,并且对这个信息的要求不高,即不需要提取很全面,例如mp3文件只需ID3v1标签即可满足我们的要 求。而我们可以看到,Windows Explorer已经把这些信息提取出来了。类似的,对图片和视频文件它也能提供标签信息。需要注意的是,Windows Shell仅仅提供mp3 的ID3v1标签提取,对ID3v2不予支持。即如果媒体只有ID3v2标签,此处读出来的就是空字符串。 好了,确定了范围和方向之后,就是如何使用COM接口调用Shell组件读取信息这一步了。这里有几个概念,Shell即是外壳,Shell的基础是桌面,桌面之下衍生出很多子文件夹,以及系统的“网络”、“控制面板”、“C:/”等文件 夹,这些文件夹里又有很多层子文件夹。因此,我们想要获得一首歌的标签信息,需要首先获得桌面文件夹的对象,然后找到对应的目录,然后找到那个目录中的对 应文件,然后才能提取文件的信息。这里需要用到几个接口和结构体:IShellFolder接口,用来定位某个文件夹,并对其下的文件和文件夹进行操作。IShellFolder2接口,从IShellFolder接口继承而来,提供了一些新的功能。ItemIDList, 每个文件夹或者文件都维护自己的ItemIDList,里面记录了它们的所有属性,比如文件名、类型、大小、修改时间。也就是说每个文件逻辑上都对应一个 二维表,表有一个ID列,有一个值列,每行的记录用链表实现,Windows提供了ItemIDList这样的一个结构。 EnumIDList,一个文件夹下所有的对象(文件和文件夹),形成了一个有序链表。对这个链表进行遍历即可找到所有的文件。链表的每个节点就是上面的ItemIDList可以以这样的树状结构来看上述概念: 每个实际的文件夹对应一个IShellFolder,每个IShellFolder可以获得一个EnumIDList,遍历每个EnumIDList可以获得每个ItemIDList,每个ItemIDList就已经与文件一一对应。上面已经提到,所有文件夹的父文件夹是桌面,于是先获得桌面的IShellFolder2接口对象。这里使用SHGetDesktopFolder()函数获得了桌面的IShellFolder接口对象,然后通过COM的QueryInterface()方法实例化了IShellFolder2接口对象。为什么?首先我们肯定需要一个对应的IShellFolder2接口来提取信息,这个接口是否可以留到调用它的增强功能之前再实例化我没有确认,不过既然它继承了IShellFolder并提供了更多的功能,我就打算从最开始就实例化它。为什么要用IShellFolder来实例化这个IShellFolder2?QueryInterface()函数按照COM原理是从IUnknown 继承来的,因此理论上只要任何一个COM对象都可以通过QueryInterface( IID_IShellFolder2, (void**) &psf2Desktop );来实例化IShellFolder2。使用IShellFolder来担任此工作也是因为SHGetDesktopFolder()使用较方便。另外,SHGetDesktopFolder获得的psfDesktop一定是与桌面绑定的,而此时我们实例化的psf2Desktop是否已经与桌面相关了我没有确认。接下来的工作就是定位到文件上,我们需要获得文件的ItemIDList。此时我们获得了指定文件的ItemIDList,既然属性都在里面,那就可以开始提取了。这样就获得了音乐文件的标题,存入了m_sTitle成员变量里。GetDetailsOf()函数中的数字即是ID号,至于当前文件夹支持多少ID号,可以给第一个参数以NULL,然后使用循环打印m_sTitle就能知道当前ID对应什么信息。即:另外,使用GetDetail***()函数可以不用使用ID号,但我做了XP到Win7的迁移后发现GetDetail***()好像也没有能跨 越平台障碍,所以索性还是用GetDetailsOf()了。注意上面提取标题时的::CoInitialize( NULL );这表示初始化COM对象。没有这一句,所有的文件夹都只能提取出前几个ID对应的文件名、类型、修改时间、大小等基本信息,无法提取出标题、专辑等特 别的信息。一个文件能提取出什么样的信息与所使用的IShellFolder2有关。此外注意GetDetailsOf()的平台差异,WinXP上提取出来的东西比较贫乏,Vista和Win7能提取的标签就很丰富,但是与 WinXP相同的部分在ID编号上有变化。所以这个方法需要对XP和Vista做两套平台的库文件,并需要在运行时检查系统的版本号,动态载入不同的库文 件。2. Dll调用Dll(即dynamic link library)在编译后至少会有a.dll和a.lib两个文件。这样导入DLL就有三种方式:使用lib直接链接;使用lib并启用delay load;使用dll动态导入。粗略地说,lib中记录了dll的函数入口,编译自己程序时链接器里加入lib即可在运行时使用dll内的函数。这样的程序在启动时就会载入 dll,如果目标机器上不存在,那么就会给出“应用程序不能运行,需要重新安装”之类的提示。而delay load是VC6之后较新的版本提供的功能,即将dll的载入延迟到需要调用它的函数的时候。如果目标机器没有dll,那程序依然能够启动,但是要执行函 数的时候会发生不友好的异常错误。而使用dll动态导入,就是在代码里载入dll的导出函数,程序可以在需要时载入它,一些实现不同语言、添加插件等功能 就可以使用这种方式来实现。下面主要说第三种方式。使用C语言即可调用系统API来动态导入dll。首先LoadLibrary()载入Dll返回句柄,GetProcAddress()使用句柄返 回函数指针,FreeLibrary()使用句柄释放dll。这三个DLL套装的详细用法和示例可以查阅MSDN。但是,它们只能导出函数,而在C++里 需要导出一个类时,就得用其他办法了。首先,按照DLL的一贯做法,导出函数和导出类都要有__declspec(dllexport),在导入的地方声明这些函数时,相对地要有__declspec(dllimport)。因此我们使用了这样的一个宏定义:然后就可以使用如下的方式声明导出类:使用如下方式声明导出函数(extern "C"的作用见本节最后):在这个头文件对应的CPP实现文件里首先加上:然后按照原有方式实现CAudioInfo类,按照如下方式实现导出的函数:按照原本方法,在头文件里添加对应的指针:这样,通过导出函数,我们就能获得对应的类的指针,这样既可实现导出类。并且此时这个头文件我们就可以用到需要调用dll的地方了。在调用DLL的cpp里,如下:上述代码段中,通过函数指针pfnAudio来执行函数的调用。仅仅这样,把上述思想应用到实际时,编译依然会报错。还缺什么呢?DLL文件作为一个独立的Project可以正常地Build,但是调用DLL的 文件却无法链接成功。在链接时无法找到对CAudioInfo类的成员函数,这里我做了一个测试,一个类成员函数仅做类内声明,在类外却并不实现它的话, 这个cpp编译是正常的,但如果这个成员函数被调用了,linker就会提示找不到。这说明类成员函数仅仅声明是可以通过编译的,但是调用时链接器无法找 到它。反观上面的调用DLL的cpp,我们也是仅仅把头文件包含进来,这不是一样的效果吗?那么,怎么才能让成员函数在外面被调用?这里又有很多种办法,我采取了其一,其他的方法可以参考最后的参考资料。参考《C++ Primer》第四版,15.2.4,类内的虚函数编译后会有一个VTable表,因此加了virtual关键字的非纯虚函数,在编译时一定会被要求有实 现,链接时可以通过VTable里的指针来找到对应函数。所以,将所有要导出的成员函数,包括析构函数,都加上virtual关键字(因为delete操作会调用析构函数),之后就可以正常编译了。3. 最小化编译依赖这需要我们理顺Project里各个.h和.cpp文件之间的关系。比如我们建立这样两个类,放到四个文件里:A.hA.cppB.hB.cpp#pragma once #include "b.h"class A { public: A(void); ~A(void); B* m_b; };#include "StdAfx.h" #include "A.h"A::A(void) { }A::~A(void) { }#pragma once #include "a.h"class B { public: B(void); ~B(void); A* m_a; };#include "StdAfx.h" #include "B.h"B::B(void) { }B::~B(void) { }很简单的两个类,每个类内有一个指向对方类一个对象的指针。也许这两个类的设计有点问题,但也确有这种可能——比如数据库两个表是一对一的关系,而 我们使用C++来对这两个表进行面向对象的抽象,那可能就会形成这种类的设计思路。按照以前的想法,很正常啊,A类里要有一个B类的指针,那就在开始把 b.h包含进来,B类要有个A类指针,那就也把a.h包含进来吧。编译——6个错误。再看一遍源码,哪有语法错误啊,这让人怎么改?于是我们需要明白.h和.cpp文件的意义,参考《Exceptional C++》的Item 26到Item 30。首先,.h文件是头文件,header文件,头文件是干什么的?包含用的,头文件不会参与编译,只有在.cpp里用.h时,.h里内容才有意 义。#include "A.h"意义是原封不动地把a.h文件的内容在这一行完全展开。既然编译器只会去编译.cpp文件,并且在cpp中将.h文件展开,那我们自己展开来看 看?以a.cpp为例,A.h要展开,又遇到了b.h要展开,好吧继续展开,b.h又要展开a.h?因为有#pragma once的预编译指令,于是展开工作到此结束。最后,在a.cpp完全展开之后,”b.h”留在展开的内容的最上面,好了,b.h文件内容是什么呢,有个A* m_a,A是什么?A不是个类吗,不是包含过了吗?很遗憾,在最后展开的文件里,A的内容在下面呢,因为#pragma once作祟,最后需要的a.h没有展开,那就去掉a.h的#pragma once呢?那A就会是一个重复定义的类,同样收到一堆错误。诶?重复定义?是不是可以有办法解决了?定义是完全写出类或者全局函数的内容,声明则是通知编译器这个东西类型和名字是什么。也就是说,把 declaration放到前面,把implementation放到后面,不就结了?前面指的当然就是类的头文件里,后面指的就是CPP文件。于是修改 代码如下:A.hA.cppB.hB.cpp#pragma once class B;class A { public: A(void); ~A(void); B* m_b; };#include "StdAfx.h"#include "b.h" #include "A.h"A::A(void) { }A::~A(void) { }#pragma once class A;class B { public: B(void); ~B(void); A* m_a; };#include "StdAfx.h"#include "a.h" #include "B.h"B::B(void) { }B::~B(void) { }在.h文件中,只留下最简单的声明,在cpp文件中如果用到了再包含要使用的东西。这样即成功编译。其实在上例中,就算去掉.cpp文件中对对方类的包含也能通过,因为没有对m_a,m_b成员进行操作。在这个提取文件信息的项目中,我自己的机器是Win7+VS2008,但是工作的机器是XP+VC6。对IShellFolder2的操作是在 Windows SDK里才有的,VC6出的比较早,最后的更新是到Windows 2003的一个SDK。Windows SDK也是后来更名的,之前叫做Platform SDK。机房机器装的VC6没有办法使用一些Shell相关的函数和接口,也没有shlwapi.h和shlwapi.lib等文件了。于是我采用了这个减少编译依赖的方法去做。首先,因为按照第二点的思路制作的DLL文件仍然需要在调用它的Project里包含DLL的.h文件, 这是库文件的必然。但是VC6没法认这个有一些Shell接口成员声明的.h文件。按照《Code Complete》(代码大全)第二版一书6.2节关于隐藏类实现达成良好封装的叙述,将所有有关Shell操作的接口形成一个单独的实现类 CMediaImp,将CMediaImp的声明放到这个类里,将此类的成员放到该类的实现文件中。这样在.h文件里就没有了Shell的内容,但cpp 在编译时能正常找到Shell的操作。此时将这个库编译成DLL,并随库提供DLL的.h头文件,交给使用该库的程序员,他在工作的机器环境VC6上就能正常编译使用这个库了。反之,如 果不这么做的话,DLL是正常了,但是该程序员在引用了随库的头文件时依然会遇到编译无法通过,缺少Shell接口相关声明的问题。4. 小结至此,项目结束。附上一些较好的参考材料: DLL导出类,显示链接到DLL中的类一步一步教你DLL,第四部分,DLL动态导入DLL很简单,第一部分 ,第二部分 ,第三部分 ,第四部分Shell操作,在应用程序中集成外壳的上下文菜单《Exceptional C++》, Item 26 ~ Item 30 本文转载自:http://hi.baidu.com/ecluytj/blog/item/de28cdbfbb2e4d0318d81f4d.html 11-08 04:52