`
20386053
  • 浏览: 427850 次
文章分类
社区版块
存档分类
最新评论

一个分析“文件夹”选择框实现方法的过程

 
阅读更多

在软件开发中,我们如果存在“导入导出”的场景时,难免会用到“文件夹”选择框。之前一直没有太关注过这个的实现过程。最近在工作中遇到了一些问题,我做了一些研究。在此记录下研究的过程。(转载请指明出于breaksoftware的csdn博客)

首先,我们发现我们的文件选择框,只能显示出本地文件夹,而不能显示设备虚拟出来的文件。比如


这样的设备,就不会在我们的文件选择框中出现。


我们看下我们代码中的设置

BROWSEINFOA bi;   
bi.hwndOwner      = hWnd;
bi.pidlRoot       = NULL;  
bi.pszDisplayName = NULL;   
bi.lpszTitle      = "请选择下载位置";   
bi.ulFlags        = BIF_RETURNONLYFSDIRS | BIF_USENEWUI;  
bi.lpfn           = BrowseCallbackProc;   
g_defaultfolder = WinTools::GetSystemPath(CSIDL_DESKTOP);
bi.lParam         = (LPARAM)(g_defaultfolder.c_str());
bi.iImage         = 0; 

当时我的第一直觉就是我们的ulFlags设置的不对,然后我翻阅了MSDN,去掉了BIF_RETURNONLYFSDIRS就好了。

但是问题接踵而至


当我们选择了这个设备下的文件夹后,我们并不能获取我们选择的文件夹路径。经调试发现是我们之后调用的获取文件夹路径的函数SHGetPathFromIDList返回失败。
那我们就让选择框对这类文件进行过滤。当时我还是认为是不是我们哪个ulflags没有设置。可是试了几个感觉可能的flags,还是不行。

后来,我寻找到一个该功能完善的软件A,它的展现是正确的。


最后我决定不再闭门造车,而是分析该软件A这块功能的具体实现。

首先我们要确认A软件使用的哪个函数打开文件选择框的。众所周知,我们使用的SHBrowseForFolderA属于SH类函数,即shell32.dll中的导出函数。SH类函数基本都是辅助类型函数,其是在windows原生API基础上做了一层封装。所以我们先要确定A软件使用的是不是SHBrowseForFolder函数。我们使用Windbg附加到A进程上


其次,使用bp shell32!SHBrowseForFolderA 和bp shell32!SHBrowseForFolderW下函数断点。一般来说,Windows平台的API都有的A版和一个W版(有特殊的函数只有一个版本),所以我们在分析时,往往给A版和W版都下断点。
最后运行挂起的A软件,点击“打开文件夹”。Windbg果然断住了,这个证明A软件使用的是SHBrowseForFolderW。


这样我们确定了软件A是使用的SHBrowseForFolderW,那么我们开始分析,看看它是如何个这个函数的。这儿涉及一个稍微有点复杂的过程,因为A软件很多地方是用.net写的。我调回到调用SHBrowseForFolderW的地方,仍然难以直观看到起参数的传递。我就改成分析SHBrowseForFolderW的实现,来查看其参数。我使用IDA,对Shell32.dll中的SHBrowseForFolderW进行逆向。以下列出其重要的代码


直到此时,我仍然认为我们的问题是出在flags设置不对上。所以我仍然只是关注了ulflags这个参数。我们看到我们可以在mov eax , [esi+10h]处看到ulflags的值。
回到windbg,使用u 7648dfae 7648dfff得到内存中的汇编代码和地址(7648dfae是中断下来后,得知SHBrowseForFolderW的入口地址)


我们在7648dff5处下断点(bp 7648dff5)。
中断后我们用r eax指令查看eax的值


我们终于拿到A软件的ulflags的设置,本来以为大功告成。于是在代码中将ulflags的值设置为0x40对应的宏。可是悲剧的是,问题依旧。看来并不是我们ulflags设置的不对。我们回到BROWSEINFO的参数说明。

typedef struct _browseinfo {
  HWND              hwndOwner;
  PCIDLIST_ABSOLUTE pidlRoot;
  LPTSTR            pszDisplayName;
  LPCTSTR           lpszTitle;
  UINT              ulFlags;
  BFFCALLBACK       lpfn;
  LPARAM            lParam;
  int               iImage;
} BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO;
这些参数,除了ulflags对窗口的行为可能产生影响外,只能是lpfn了。我看了下我们的lpfn传递的是NULL, 而A软件是否传了值呢?
我们将断点下在7648dffb,看看A软件是否传了值。

A软件传递了值!
那如何验证是否就是这个回调函数导致了我们之间的差异?
MSDN说明lpfn可以为NULL,那么我使用r eax=0来修改此处的eax,然后待7648dffb处指令执行完毕,就可以修改SHBrowseForFolderW内部使用了该回调地址的地方了。
修改好后,我们继续执行A软件,并选择之前出现“确定”按钮不可用的文件夹,可以看到这个时候的“确定”按钮可用了。


于是原因找到了,此时我们只要关注该回调 如何实现便可以实现和A软件的功能。

那么这个回调如何实现呢?我们看个网上很普及的例子

int CALLBACK BrowseCallbackProcSetting(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)  
{  
         if  ( BFFM_INITIALIZED == uMsg ) {  
                   ::SendMessage(hwnd,BFFM_SETSELECTION,TRUE,lpData);  
         }  
         else if ( BFFM_SELCHANGED == uMsg )
         {
                   char pszPath[MAX_PATH] = {0};
                   LPITEMIDLIST pidl = (LPITEMIDLIST)(lParam);
                   if ( NULL == pidl ) {
                            return 0;
                   }
                   if (SHGetPathFromIDListA(pidl, pszPath)) {     
                            ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 1 );
                   }
                   else {
                            ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 0 );
                   }
         }
         else if ( uMsg == BFFM_VALIDATEFAILED ){
                   return 1;
         }

         return 0; 
}
这段代码中,我们主要用的是BFFM_SELCHANGED == uMsg这段。
这段的主要思想是:用户点击的那个文件夹,我们可以获取pidl,但是如果之后我们不能获取pidl对应的文件夹路径,我们的逻辑还是有问题。所有,在用户点击了一个文件夹后,我们在会立即检查该文件夹的pidl是否可以拿到。如果可以拿到,那么我们就让选择框的OK按钮置成可用,否则不可用。这种思想是预防于未来,我觉的还是很赞的。
但是这段代码还是不健壮的。在win32位机子上,我们发现了一个特殊的场景:就是pidl可以获得文件夹路径,但是该文件夹不可访问。导致我们设置后,无法打开这个文件夹,导致之后要将文件保存到该目录下失败。这个是个非常严重的问题。其实这个问题还是很常见的,我们永远无法预测神奇的用户诡异的行为:比如他把A目录设置为只读,然后通过我们程序去选择这个目录,导致我们无法成功在该文件夹下新建文件——因为该文件夹只读。那么这个时候,我们需要做到:在用户选择时,判断该文件夹我们是否可以写入,如果可以写入,则OK按钮置为可用,否则置为不可用。
所以要将
if (SHGetPathFromIDListA(pidl, pszPath)) {      
    ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 1 );
}
else {
    ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 0 );
}
改成
if (SHGetPathFromIDListA(pidl, pszPath)) {     
    HANDLE hFile = CreateFileA(pszPath, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL  );
    if ( INVALID_HANDLE_VALUE == h )  {
                   ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 0 );
    }
    else {
        CloseHandle(hFile);
        hFile = NULL;
        ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 1 );
    }
}
else {
    ::SendMessage(hwnd, BFFM_ENABLEOK, 0, 0 );
}
假如你认为一切已经大功告成,那就错了。后来我们又发现,“新建文件夹”按钮无法和“确定”按钮同步。

我目前还没找到一个优雅的控制“新建文件夹”按钮的方法,只能通过枚举子窗口,同时在子窗口中寻找“(”和“)”来识别和控制“新建文件夹”按钮。于是整套完成的流程是

int CALLBACK BrowseCallbackProcSetting(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)  
{  
	if  ( BFFM_INITIALIZED == uMsg ) {  
		::SendMessage(hwnd,BFFM_SETSELECTION,TRUE,lpData);  
	}  
	else if ( BFFM_SELCHANGED == uMsg )
	{
        HWND hNewFloderButton = NULL; 
        HWND hChild = GetWindow(hwnd, GW_CHILD);
        int nMaxCount = 64;
        while ( hChild && nMaxCount > 0 ) {
            // 控制循环次数,以免死循环,保守性编程
            nMaxCount--;
            WCHAR wszBuffer[MAX_PATH] = {0};
            int nCount = GetClassName( hChild, wszBuffer, MAX_PATH );
            std::wstring wstrClassName( wszBuffer, nCount); 
            if ( 0 == wstrClassName.compare(L"Button") ) {
                memset(wszBuffer, 0, MAX_PATH);
                nCount = GetWindowText(hChild, wszBuffer, MAX_PATH);
                std::wstring wstrText(wszBuffer, nCount);
                // 不同操作系统上,显示不一样,比如Win7 64bit是(&M)
                if ( -1 != wstrText.find(L"(")
                    && -1 != wstrText.find(L")") ) {
                    // 新建文件夹按钮
                    hNewFloderButton = hChild;
                    break;
                }
            }
            hChild = GetNextWindow(hChild, GW_HWNDNEXT);
        }

		char pszPath[MAX_PATH] = {0};
		LPITEMIDLIST pidl = (LPITEMIDLIST)(lParam);
		if ( NULL == pidl ) {
			return 0;
		}
		if (SHGetPathFromIDListA(pidl, pszPath)) {     
			HANDLE hFile = CreateFileA(pszPath, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL  );
			if ( INVALID_HANDLE_VALUE == hFile )  {
                if ( NULL != hNewFloderButton ) {
                    ::EnableWindow(hNewFloderButton, FALSE);
                }
                
				::SendMessage(hwnd, BFFM_ENABLEOK, 0, 0 );
			}
			else {
				CloseHandle(hFile);
				hFile = NULL;

                if ( NULL != hNewFloderButton ) {
                    ::EnableWindow(hNewFloderButton, TRUE);
                }
				
				::SendMessage(hwnd, BFFM_ENABLEOK, 0, 1 );
			}
		}
		else {
            if ( NULL != hNewFloderButton ) {
                ::EnableWindow(hNewFloderButton, FALSE);
            }
			
			::SendMessage(hwnd, BFFM_ENABLEOK, 0, 0 );
		}
	}
	else if ( uMsg == BFFM_VALIDATEFAILED ){
		return 1;
	}

	return 0; 
}

std::string ExportFodler()
{	
	char pszPath[MAX_PATH] = {0};  
	BROWSEINFOA bi;   
	bi.hwndOwner      = GetHwnd();  
	bi.pidlRoot       = NULL;  
	bi.pszDisplayName = NULL;   
	bi.lpszTitle      = "请选择下载位置";   
	bi.ulFlags        = BIF_NEWDIALOGSTYLE;
	bi.lpfn           = BrowseCallbackProcSetting;
	bi.lParam         = 0;  
	bi.iImage         = 0;   

	LPITEMIDLIST pidl = SHBrowseForFolderA(&bi);  
	if ( NULL == pidl )  {
	      return "";
	}

	if ( SHGetPathFromIDListA( pidl, pszPath ) )  {	
                strcat(pszPath,"\\");
		return std::string(pszPath);
	}
	return "";	
}

分享到:
评论

相关推荐

    优影文件整理工具_下载店数据整理

    通常音乐文件都是按歌手分文件夹,每一位歌手一个文件夹,这样的整理一目了然,是一种良好的整理习惯,然而歌手有上千位,就需要上千上文件夹,如果一个一个手工方式来划分将是一缓慢的过程。此程序可以方便的自动将...

    本科毕业设计开题报告(学生电子作业提交与分析系统的设计与实现V2)

    通过Internet/Intranet来实现网上作业提交,是现代教育技术的一个具体实现,具有很重要的现实意义。通过网络的了解,已经有不少高等学校已经推出了自己的作业上传系统。但是由于实现技术有限,系统的功能不完善,...

    OPhone平台2D游戏引擎实现——物理引擎

     首先分析一下我们在Ophone平台上的Box2dDemo需要实现什么功能,首先我们将整个屏幕构建成一个盒子,然后再盒子中设置各种障碍,当我们触摸屏幕上任意位置时,就释放一个当前选择的物体,然后该物体将受到重力等...

    QAC工具介绍和使用说明(供一种可量化措施的代码度量值属性:33基于功能 32基于文件和4个项目级别)

    7、为文件夹选择一个个性,可能会使用默认设置为起始点,可以在QAC中选择Configuration菜单 8、点击OK就是建立了工程,包含源文件工程和子文件夹 9、保存文件,外部扩展名为.prj 注意:也可以在已有的项目上自动生成...

    联考成绩统计分析系统 v4.0.zip

    联考成绩统计分析系统软件是一款专门为成绩统计分析设计的专业的成绩统计分析软件,主要功能是为多校联考统计、分析分数提供方便,包括可以通过软件自动计算每个同学总分、片内名次、校内名次、班级名次、可以分别以...

    java源码包---java 源码 大量 实例

     Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码截图如上所示。 Java 数字签名、数字证书生成源码 2个目标文件 摘要:JAVA源码,...

    asp.net知识库

    .NET关于string转换的一个小Bug Regular Expressions 完整的在.net后台执行javascript脚本集合 ASP.NET 中的正则表达式 常用的匹配正则表达式和实例 经典正则表达式 delegate vs. event 我是谁?[C#] 表达式计算引擎...

    365DiskDate(磁盘空间分析专家) v3.0.6.zip

    365DiskDate是一款功能齐全的磁盘工具,软件通过扫描用户所选的磁盘或文件夹,然后以图表方式显示用户硬盘或指定文件夹的磁盘使用情况,同时显示每个文件、文件夹的大小及占父文件夹和根文件夹的百分比,并且用简单...

    java源码包2

     Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码截图如上所示。 Java 数字签名、数字证书生成源码 2个目标文件 摘要:JAVA源码...

    java源码包3

     Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码截图如上所示。 Java 数字签名、数字证书生成源码 2个目标文件 摘要:JAVA源码...

    java源码包4

     Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码截图如上所示。 Java 数字签名、数字证书生成源码 2个目标文件 摘要:JAVA源码...

    JAVA上百实例源码以及开源项目源代码

     Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码截图如上所示。 Java 数字签名、数字证书生成源码 2个目标文件 摘要:JAVA源码,...

    成百上千个Java 源码DEMO 4(1-4是独立压缩包)

    Java zip压缩包查看程序源码 1个目标文件 摘要:Java源码,文件操作,压缩包查看 Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码...

    vc++ 开发实例源码包

    电子钟的实现,自绘Button、Static的实现,其中自定了一个辅助主题风格类。 CctryLog(web拦截网页帐号密码) 实现了一个控件去获得IHTMLDocument2接口,然后读取内容,匹配用户名与密码等。 CFile64_src 操作大...

    成百上千个Java 源码DEMO 3(1-4是独立压缩包)

    Java zip压缩包查看程序源码 1个目标文件 摘要:Java源码,文件操作,压缩包查看 Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码...

    Visual+C#+2008程序设计经典案例设计与实现.rar

    案例7 利用ListBox控件实现数据源字段的选择 案例8 利用LislNiew控件实现图标的管理 案例9 利用TreeView控件浏览图像 案例10 利用ProgressBar控件实现进度控制 案例11 利用Timel控件显示当前时间 本章小结 第10章 ...

    JAVA上百实例源码以及开源项目

     Java zip压缩包查看程序,应用弹出文件选择框,选择ZIP格式的压缩文件,可以像Winrar软件一样查看压缩文件内部的文件及文件夹,源码截图如上所示。 Java 数字签名、数字证书生成源码 2个目标文件 摘要:JAVA源码,...

    Visual C# 2008程序设计经典案例设计与实现

    案例7 利用ListBox控件实现数据源字段的选择 案例8 利用ListView控件实现图标的管理 案例9 利用TreeView控件浏览图像 案例10 利用ProgressBar控件实现进度控制 案例11 利用Timer控件显示当前时间 章小结 第10章...

    文件服务器迁移多种方案.doc

    现在客户决定购买新的服务器替换之前旧的服务器,面临 着一个问题: 1, 如何使得这些共享文件夹由旧服务器move到新服务器上时,共享权限及NTFS权限保持不变 案例分析: 客户准备使用新服务器替换旧的2003域成员文件服务...

Global site tag (gtag.js) - Google Analytics