7.弹出打开、保存文件对话框

一、前言

很多应用都会有选择打开文件、或者选择保存文件的功能,这种情况下一般都是弹出一个对话框让用户自己选择。

这并不需要自己去写一个,因为windows系统已经自带了好看、易用的相关组件,可以直接在代码中使用。

就像下面这样:

在这里插入图片描述

二、原理

这涉及到了windows的com(Component Object Model)组件,这是windows较为底层的一项核心技术,目的是让在windows上的编程更加容易。

核心理念就是将某个功能写成一个模块,使用二进制数据作为接口,可以让许多不同的语言使用,并且可以动态加载、卸载。

理念很好,但事实上其使用起来却并不容易,甚至可以说很复杂。

在使用com组件前,你必须要先进行初始化操作:CoInitializeEx

HRESULT CoInitializeEx(
  [in, optional] LPVOID pvReserved,
  [in]           DWORD  dwCoInit
);

它有两个参数:

  • pvReserved:保留字段,填NULL
  • dwCoInit:线程的并发模型和初始化选项,可以点击这里查阅,一般是单线程的,填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;
}

唯一复杂点的就是这个类型参数填写,但也不是太复杂,直接按格式复杂粘贴写即可。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux