一、前言
很多应用都会有选择打开文件、或者选择保存文件的功能,这种情况下一般都是弹出一个对话框让用户自己选择。
这并不需要自己去写一个,因为windows系统已经自带了好看、易用的相关组件,可以直接在代码中使用。
就像下面这样:
二、原理
这涉及到了windows的com
(Component Object Model)组件,这是windows较为底层的一项核心技术,目的是让在windows上的编程更加容易。
核心理念就是将某个功能写成一个模块,使用二进制数据作为接口,可以让许多不同的语言使用,并且可以动态加载、卸载。
理念很好,但事实上其使用起来却并不容易,甚至可以说很复杂。
在使用com组件前,你必须要先进行初始化操作:CoInitializeEx
HRESULT CoInitializeEx(
[in, optional] LPVOID pvReserved,
[in] DWORD dwCoInit
);
它有两个参数:
pvReserved
:保留字段,填NULLdwCoInit
:线程的并发模型和初始化选项,可以点击这里查阅,一般是单线程的,填COINIT_APARTMENTTHREADED
即可,如果多线程使用,那就用COINIT_MULTITHREADED
它的返回值为一个HRESULT
类型,只要等于S_OK
这个宏就说明成功了,如果为S_FALSE
,就代表当前程序已经初始化过了,但程序也不会出错。
所以为了保险起见,你是可以多次调用这个函数的,只要记得在每次调用初始化之后,还要反初始化一次即可。
所以总的来说就可以写成下面这样:
HRESULT ret=CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // 初始化 COM 环境
if (ret != S_OK && ret != S_FALSE) {
return -1; //初始化失败。
}
甚至可以不用管这个返回值,因为出错的概率…应该是极低的,至少我至今没遇到过初始化失败的情况。
初始化完成后,你就可以正式使用com组件了,但在使用完成后,还需要反初始化,用于释放资源:
CoUninitialize(); // 反初始化 COM 环境
有了com组件的环境之后,我们就可以开始获取我们想要的组件了,这可以通过函数CoCreateInstance来完成:
HRESULT CoCreateInstance(
[in] REFCLSID rclsid,
[in] LPUNKNOWN pUnkOuter,
[in] DWORD dwClsContext,
[in] REFIID riid,
[out] LPVOID *ppv
);
这个函数就稍显复杂了:
rclsid
:要创建的 COM 类的 CLSID(Class Identifier)。CLSID 是一个唯一标识符,用于标识一个特定的 COM 类型,我们意思通过这个值来决定我们想要获得的com组件。pUnkOuter
:一个指向外部未知对象(Outer Unknown)的指针,用于实现对象的聚合(aggregation)。通常设置为NULL
即可。dwClsContext
:指定对象创建的上下文。可以是以下之一,一般是进程内创建:CLSCTX_INPROC_SERVER
:在进程内的 COM 服务器中创建实例。CLSCTX_LOCAL_SERVER
:在本地机器上的 COM 服务器中创建实例。CLSCTX_REMOTE_SERVER
:在远程机器上的 COM 服务器中创建实例。
riid
:要获取的接口的 IID(Interface Identifier),组件就像一个对象,它可以实现很多接口,所以这个参数就是询问我们想要这个对象上的哪个接口ppv
:用于接收指向对象接口指针的指针,我们也是通过这个参数获取到我们想要的文件对话框对象。
是不是看着有点抽象!确实很抽象…所以还是直接来看看代码怎么写吧!
比如我们想要一个打开文件的对话框com组件,它的接口名称为:IFileOpenDialog
然后在官方文档中我们可以找到对应的clsid
,也就是这个函数的第一个参数:
然后在vs中写出用这个类型声明一个指针:
注意:它所在的头文件为shlObj.h
。
然后重点来了,我们可以右键这个类型,然后速览定义、或者转到定义:
然后我们就可以在这里拿到对应的IID,也就是函数的倒数第二个参数:
有了前面的基础,我们就可以填写函数了:
IFileOpenDialog* pFileOpenDialog=NULL;
ret = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER,
IID_IFileOpenDialog, (void**)&pFileOpenDialog);
if (ret != S_OK) return -1; //创建失败
注意其结果是通过最后一个参数返回的,原理就是将指针传进去,函数为这个指针赋值。
函数的返回值仍然是HRESULT
类型,所以直接和S_OK
比较即可,更多内容可以参考官方文档。
成功获取了这个接口类指针,剩下的就稍微简单一点了,那就是调用这个结构相应的函数即可。
但其实仍然不简单。
因为这个接口继承的另外一个接口:IFileDialog
它本身只有两个方法,一个是获取用户选择的结果,另一个是获取用户选择的项,很明显我们需要使用到第一个函数来获取用户选择的结果。
但选择对话框怎么弹出呢?如何设置过滤器呢?如何设置文件单选还是多选呢?
这就得去看它的父类有哪些函数了:IFileDialog
首先就能看到它又是一个子类接口,所以如果这个类也没有我们想要的函数,我们就得继续向上看。
可以看到父类有一个和子类很像的函数,这个函数用于单选文件的获取,上面子类那个函数用于多选文件获取。
还有这三个函数,除了第二个可以使用,用来设置默认打开的目录外,其它两个一般用于保存文件对话框的,用于设置默认文件扩展名、默认文件名
没错,保存文件对话框也是继承的这个接口。
通过下面这个函数来设置类型:
还有设置过滤文件的,明确可以选择哪种类型的文件:
设置相关选项,比如允许用于用户选择多个文件,以及设置对话框的标题:
其中的实用函数有很多,这里不再一一列举,有兴趣的可以自己都试一试,这里只演示使用比较关键的一些函数。
注意!你发现看完之后都还没有弹出对话框窗口的函数对吧!所以我们还得向上找:IModalWindow
这不就有了吗?它唯一的一个函数就是显示对话框的函数。
自此我们就完成了所有相关函数的查询。
然后顺着前文我们已经获取到了接口指针:IFileOpenDialog
第一步,来设置对话框,允许用户一次选择多个文件。
FILEOPENDIALOGOPTIONS options;
hr = pFileOpenDialog->GetOptions(&options);
if (hr==S_OK)
{
options |= FOS_ALLOWMULTISELECT;
pFileOpenDialog->SetOptions(options);
}
这里出现了一个结构体FILEOPENDIALOGOPTIONS ,可以在官方GetOptions
或者SetOptions
函数介绍中看到,不再赘述。
逻辑就是先获取到原来的标志,然后通过或运算,添加新的、可多选文件的标志,然后重新设置回去。
除此之外它还有很多其它选项,有兴趣的可以自己去研究研究。
然后第二步,设置过滤器,比如只能选择文本文件:txt结尾的,或者图片文件:png、jpg结尾的等等。
COMDLG_FILTERSPEC types[] = {
{L"文本",L"*.txt"},
{L"图片",L"*.png;*.jpg"}
};
pFileOpenDialog->SetFileTypes(2, types);
这可以用函数SetFileTypes
实现,第一个参数为我们要设置的个数,第二个参数为具体类型的数组,数组中的个数要和保持和前一个参数一致。
注意COMDLG_FILTERSPEC
结构的填写,第一个字段为类型名称,第二个字段为匹配的模式。
一般用*
代表任意字符,后面的.txt
为后缀,所以组合起来就是任意以.txt
结尾的文件名。
如果你想表达多个,那就在不同模式之间用;
作为分隔符,就如同上面的图片。
完成了上面的步骤之后,我们就可以弹出这个对话框了:
pFileOpenDialog->Show(NULL);
它唯一一个参数就是设置它的父窗口,但可以为NULL。
如果其返回值为S_OK,就说明按了确认选择,如果不是,可能就是用户按了取消键、或者出现了其它错误,这可以查官方文档:Show
假设用于已经成功选择了文件,并按下了确认,那么下面我们就需要取出用户选择的文件名。
这就需要用到前面提到的GetResults
函数了:
IShellItemArray* pItemArray;
ret = pFileOpenDialog->GetResults(&pItemArray);
成功之后,它会返回一个IShellItemArray类型的数组对象,这都可以在官方文档函数介绍中一步一步查询到,不再过多解释:
我们主要用到这两个函数,一个获取用户选择的个数,另一个来遍历具体的文件路径。
获取文件个数
DWORD count;
ret = pItemArray->GetCount(&count);
遍历所有项:
vector<wstring> result;
for (int i = 0; i < count; i++) {
IShellItem* pItem; //具体的项
ret=pItemArray->GetItemAt(i, &pItem);
if (ret != S_OK) continue;
PWSTR pszFilePath;
ret = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); //获取文件路径
if (ret == S_OK) {
result.push_back(pszFilePath); //保存结果
CoTaskMemFree(pszFilePath); //释放内存
}
pItem->Release(); //释放资源
}
注意GetItemAt
函数的结果又是一个接口IShellItem
可以通过这个函数来获取路径,它又有两个参数。
第一个参数为指示如何返回这个路径,一般就返回系统绝对路径就可以了,如果你有其它要求,就选择其它参数。
第二个参数为保存路径的缓存区,这是函数内部自己分配的,所以你无需分配,只需要声明一个指针即可。
完成之后,将其保存,然后还需要用CoTaskMemFree
函数对它进行释放,这个函数哪来的?就是查询官方文档GetDisplayName看到的:
同时注意,所有使用完的com接口,都需要调用其中的release
函数进行释放,所有接口都有这个函数。
最后我们还需要释放前面的接口:
//释放资源
pItemArray->Release();
pFileOpenDialog->Release();
以上就是使用一个com组件的全过程,略显繁琐,而且这只是打开文件对话框的,但保存对话框其实大致也是一样的,所以不再赘述了。
三、代码封装
由于com组件使用起来太过于复杂麻烦,所以我将其封装了一下,便于使用。
打开文件对话框:
#include<Windows.h>
#include<ShlObj.h>
/**
* @brief 弹出文件打开对话框选择文件
* @param types 想要显示的类型数组
* @param n 类型的个数
* @return 返回获取到的结果
*/
static std::vector<std::wstring> FileOpenDialog(COMDLG_FILTERSPEC* types, int n) {
HRESULT ret = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // 初始化 COM 环境
if (ret != S_OK && ret != S_FALSE) {
throw "failed to init com"; //初始化失败。
}
//创建实例
IFileOpenDialog* pFileOpenDialog = NULL;
ret = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER,
IID_IFileOpenDialog, (void**)&pFileOpenDialog);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
throw "failed to create IFileOpenDialog"; //创建失败
}
//设置可多选
FILEOPENDIALOGOPTIONS options;
ret = pFileOpenDialog->GetOptions(&options);
if (ret == S_OK)
{
options |= FOS_ALLOWMULTISELECT;
pFileOpenDialog->SetOptions(options);
}
//设置类型
ret = pFileOpenDialog->SetFileTypes(n, types);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
throw "failed to set types"; //设置类型失败
}
std::vector<std::wstring> result; //用于保存用户选择的结果
ret = pFileOpenDialog->Show(NULL); //显示对话框
if (ret != S_OK) { //没有按确认键、或者出现错误,直接返回空
CoUninitialize(); // 反初始化 COM 环境
return result;
}
//获取数据
IShellItemArray* pItemArray;
ret = pFileOpenDialog->GetResults(&pItemArray);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
return result;
}
//选择的个数
DWORD count;
ret = pItemArray->GetCount(&count);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
return result;
}
//获取结果
for (int i = 0; i < count; i++) {
IShellItem* pItem; //具体的项
ret = pItemArray->GetItemAt(i, &pItem);
if (ret != S_OK) continue;
PWSTR pszFilePath;
ret = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); //获取文件路径
if (ret == S_OK) {
result.push_back(pszFilePath); //保存结果
CoTaskMemFree(pszFilePath); //释放内存
}
pItem->Release(); //释放资源
}
//释放资源
pItemArray->Release();
pFileOpenDialog->Release();
CoUninitialize(); // 反初始化 COM 环境
return result;
}
保存文件对话框:
#include<Windows.h>
#include<ShlObj.h>
std::wstring FileSaveDialog(COMDLG_FILTERSPEC* types, int n) {
HRESULT ret = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // 初始化 COM 环境
if (ret != S_OK && ret != S_FALSE) {
throw "failed to init com"; //初始化失败。
}
//创建保存文件对话框实例
IFileSaveDialog* pFileSaveDialog = NULL;
ret = CoCreateInstance(CLSID_FileSaveDialog, NULL, CLSCTX_ALL, IID_IFileSaveDialog, (void**)&pFileSaveDialog);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
throw "failed to create IFileSaveDialog"; //创建失败
}
//设置类型
ret = pFileSaveDialog->SetFileTypes(n, types);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
throw "failed to set types"; //设置类型失败
}
std::wstring result; //保存结果
ret = pFileSaveDialog->Show(NULL); //显示对话框
if (ret != S_OK) { //没有按确认键、或者出现错误,直接返回空
CoUninitialize(); // 反初始化 COM 环境
return result;
}
//获取数据
IShellItem* pItem;
ret = pFileSaveDialog->GetResult(&pItem);
if (ret != S_OK) {
CoUninitialize(); // 反初始化 COM 环境
return result;
}
PWSTR pszFilePath;
ret = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); //获取文件路径
if (ret == S_OK) {
result=pszFilePath; //保存结果
CoTaskMemFree(pszFilePath); //释放内存
}
pItem->Release(); //释放资源
pFileSaveDialog->Release();
CoUninitialize(); // 反初始化 COM 环境
return result;
}
四、使用示例
使用起来很简单:
int main()
{
COMDLG_FILTERSPEC types[] = {
{L"文件",L"*.txt"}
};
auto ret = FileOpenDialog(types, 1);
for (auto& i : ret) {
wcout << i << endl;
}
auto ret1 = FileSaveDialog(types, 1);
wcout << ret1 << endl;
}
唯一复杂点的就是这个类型参数填写,但也不是太复杂,直接按格式复杂粘贴写即可。