Sunday, May 09, 2004

 

Windows Common Dialogs extensions

1. Rob Manderson
A class based on CFileDialog that provides easy image preview
http://www.codeproject.com/dialog/imagepreviewdialog.asp

2. Tim Gard
Creating an Open/SaveAs That Stores the MRUD
http://www.codeguru.com/Cpp/W-D/dislog/commondialogs/article.php/c5067/

3. Hans Dietrich
XFileDialog - Customizing CFileDialog
http://www.codeproject.com/dialog/xfiledialog.asp

4. Hans Dietrich
XDialogImport - How to share dialogs between projects
http://www.codeproject.com/dialog/xdialogimport.asp

5. dkotchan
Implementing a Read-Only 'File Open' or 'File Save' Common Dialog
http://www.codeproject.com/dialog/DavidKotchanFileDialog.asp

其中1和3分别把template和Hook两大技术掩饰的很清楚。行了,够用就可,反正MFC逐渐开始淡出啦。

6.
原文
http://msdn.microsoft.com/msdnmag/issues/03/09/CQA/default.aspx

编译/NothTibet
http://www.vckbase.com/document/viewdoc.asp?id=940

“ 那么,这个微软在“打开”文件对话框中未公开的秘密到底是什么呢?它就是CListCtrl::GetItemData 返回的列表项的数据,一个 DWORD 类型的值,它实际上是该列表项的 PIDL。这个 PIDL 是 Windows 外壳中的东西,它唯一标识一个外壳对象,象文件、文件夹、链接、磁盘驱动器或者是象“我的文档”这样的伪对象。对于一般普通文件,PIDL 是用宽字符表示的文件相对路径名。
一旦你了解了项目数据就是其PIDL,那么要获得它的路径名就很轻松。只不过要懂一点讨厌的外壳编程。首先,向对话框发送一个 CDM_GETFOLDERIDLIST 消息以便获得当前文件夹的 PIDL。它包括两个步骤:1、用空(NULL)缓冲发送一次 CDM_GETFOLDERIDLIST 获取路径长度,分配实际缓冲后再发送一次 CDM_GETFOLDERIDLIST;2、必须组合文件夹的 PIDL (路径名)和数据项的 PIDL(即相对路径名)来获取完整的 PIDL (全路径名)。为此,你必须获得文件夹的 IShellFolder 接口并用神奇的 SHGDN_FORPARSING 标志调用 IShellFolder::GetDisplayNameOf,以次获取全路径名,包括扩展名。”

这真的是挺有用的一篇翻译文章,解决了我设计中一直恼人的问题。我原来使用的方法实在太笨了,学习学习,关键是想到PIDL的这种思路。

7. 文件的预览的有趣实现

In the March 2004 issue of MSDN magazine, Paul DiLascia discusses one way to open the CFileDialog in "detail" view.

http://msdn.microsoft.com/msdnmag/issues/04/03/CQA/default.aspx

I'm certain that you could adapt the article easily so as to open the view in "thumbnail" view. DiLascia's solution involves a SendMessage of a WM_COMMAND message to the SHELLDLL_DefView window, with command ID for the "details" button on the control (which he found out was = 0x702c). So, I think you merely need to determine the ID for the "thumbnail" button, and do the same.

相关内容还可以参见
http://shell.franken.de/~sky/explorer-doc/structDesktopWindow.html

The solution involves the IFolderView class. Something like this:

IFolderView* pFolderView;

hr = _pShellView->QueryInterface(IID_IFolderView, (void**)&pFolderView);

if (SUCCEEDED(hr))
{
hr = pFolderView->SetCurrentViewMode(FVM_THUMBNAIL);
}

http://shell.franken.de/~sky/explorer-doc/ 的ROS Explorer Documentation的确是一份非常不错的档案。

目前我只需要图片预览,因此,我选用了
A class based on CFileDialog that provides easy image preview
By Rob Manderson
http://www.codeproject.com/dialog/imagepreviewdialog.asp

因为原文是Manage Code,使用隐含的Close方法来结束,可我这里不行,所以写了
[code]
virtual BOOL Close()
{
PostMessage(WM_CLOSE, 0, 0l);
return TRUE;
}
[/code]
来处理结束。然后就是countof改为sizeof。同时将图象改为按比例显示,否则太难看啦。

8.
修改已经打开的对话框的浏览路径

By THEMFS
From http://www.vccode.com/file_show.php?id=2771

最近做的一个小东西要控制另外一个程序的文件保存对话框,目的是让它只能保存到我指定的位置,:P,我要控制保存的数据。
我们知道用CFileDialog很容易设置对话框的初始浏览路径,只要在CFileDialog类的OPENFILENAME结构里用自己的目录改变lpstrInitialDir就可以了:
CFileDialog dlg;
dlg.m_ofn.lpstrInitialDir="c:WINDOWS";//这里设置对话框的浏览目录
dlg.DoModal();
可我要控制的是一个已经显示的对话框,这个时候再用m_ofn.lpstrInitialDir来设置就没有效果啦!我通过FindWindow找到了这个对话框的句柄,
CWnd * pWnd = FindWindow(NULL,"另存为");
if(pWnd)
{
....//这里该怎么改呢?
}
最开始想的方法就是用钩子截获对话框打开时的消息,在他显示之前改变他的墨认路径。呵呵,不想这么麻烦,等有时间再去试。后来从一个回帖里找到了一种比较方便的方法。
在实际操作中,如果我们在那个文件名编辑框中输入一个目录名,然后按确定按钮,那个对话框并没有被关闭,而是切换到那个新的目录。通过这个方法我们就可以改已经显示了对话框的浏览目录。具体步骤如下:
1、首先保存那个文件名编辑框的值
2、在那个编辑框中设置新的目录名
3、模拟鼠标单击“确定”按钮,这时候对话框切换到新的目录
4、恢复原来编辑框的值
这里还需要注意的一点就是第二步设置目录的时候要发送WM_SETTEXT消息,而不能直接用SetWindowText函数,因为它在跨进程使用的时候有问题,没有效果。
下面是部分关键代码:
CWnd * pWnd = FindWindow(NULL,"另存为");
CString sCtrlName;
CWnd *pedit;
CString filename;
TCHAR BUF[512];
if(pWnd)
{
CWnd *pwnd=pWnd->GetWindow(GW_CHILD);//枚举保存对话框的所有子控件
char *buf=new char[512];
while(pwnd!=NULL)
{
::GetClassName(pwnd->GetSafeHwnd(),buf,512);//得到枚举的控件是什么类型的,
CString str=buf;
str.TrimRight();
if(str=="Edit")//如果枚举到保存文件名的EDIT
{
pedit=pwnd;//保存EDIT句柄用来发消息
pwnd->SendMessage(WM_GETTEXT,sizeof(BUF)/sizeof(TCHAR),(LPARAM)(void*)BUF);
filename=BUF;//完成第一步保存原始的文件名
}
if(str=="Button")//枚举到按钮,有保存和取消按钮
{
pwnd->SendMessage(WM_GETTEXT,sizeof(BUF)/sizeof(TCHAR),(LPARAM)(void*)BUF);
//发消息得到按钮的标题
CString str=BUF;
str.TrimRight();
if(str=="保存(&S)")//如果是保存按钮
{
lstrcpy(BUF,(LPCTSTR)sPath);//sPath表示我们要设置的对话框浏览目录
//发消息设置文件名EDIT为我们的路径名,完成第二步
pedit->SendMessage(WM_SETTEXT,0,(LPARAM)BUF);
//发消息模拟点击保存按钮,完成第三步
::SendMessage(pwnd->GetSafeHwnd(),WM_LBUTTONDOWN,0,0);
::SendMessage(pwnd->GetSafeHwnd(),WM_LBUTTONUP,0,0);
}
}
}
pwnd = pwnd->GetNextWindow();
}
//发消息还原EDIT里的文件名,完工!
pedit->SendMessage(WM_SETTEXT,0,(LPARAM)filename);
delete buf;

果然搞怪!

9.

Use OpenFileDialog to open lots of files

By JiangChen
From http://blog.joycode.com/jiangsheng/archive/2004/11/22/39413.aspx

在我们的程序中有时候需要一次选择多个文件,例如InstallShield的安装程序向导中的一个步骤就是为feature选择文件;但是在选择的文件过多(超过30个)的时候,这样的选择没有效果。我不得不在InstallShield的文件打开对话框中切换到小图标模式,并且一次选择两列的文件(很久之前就不得不用Linked Directory来动态链接整个目录中的文件了,不知道新的InstallShield版本改进没有)。

这种情况是因为默认的缓冲区只有两百多个字符。如果缓冲区不够长,那么调用对话框可能会失败,CommDlgExtendedError()会返回FNERR_BUFFERTOOSMALL。为了解决这个问题,你可以处理CDN_SELCHANGE来在选择项数目变化的时候动态重新分配缓冲区。参见http://support.microsoft.com/kb/131462/
或者在创建对话框的时候把缓冲区设大一点:

[code]
#define MAX_FILENAMES 100000
LPTSTR m_pBigBuffer;//a large buffer which can hold all filenames.


CCManyFileDialogDlg::CCManyFileDialogDlg(BOOL bOpenFileDialog, DWORD dwFlags,
LPCTSTR lpszFilter, // = szCustomDefFilter
LPCTSTR lpszDefExt, // = szCustomDefExt
LPCTSTR lpszFileName, // = szCustomDefFileName
CWnd* pParentWnd) : // = NULL
CFileDialog(bOpenFileDialog, lpszDefExt, lpszFileName, dwFlags, lpszFilter, pParentWnd)
{
//{{AFX_DATA_INIT(CCManyFileDialogDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_pBigBuffer=new TCHAR[_MAX_PATH*MAX_FILENAMES];//默认栈没这么大,只能在堆上分配^_^bb

ZeroMemory(m_pBigBuffer,sizeof(TCHAR)*_MAX_PATH*MAX_FILENAMES);
m_ofn.lpstrFile = m_pBigBuffer;
m_ofn.nMaxFile = _MAX_PATH*MAX_FILENAMES;
}
[/code]

但是尽管提供了这么长的缓冲区(我是使用ANSI配置编译的,所以缓冲区有26M,足足可以容纳10万个文件名),但是结果还是没有获得全部文件名,这是因为GetOpenFileName限制了复制到缓冲区中的文件名的总长度
http://msdn.microsoft.com/library/en-us/winui/winui/windowsuserinterface/userinput/commondialogboxlibrary/
commondialogboxreference/commondialogboxfunctions/getopenfilename.asp

OK,在文档中我看到了解决方案之一是使用UNICODE版本的函数GetOpenFileNameW,但是在Windows 9x上似乎只能用ANSI版本的函数。难道我要编写Windows版本检查代码来调用不同版本的函数?

这是推荐的作法。但是程序员是很懒的,所以有时候我会使用一些非官方方法。在我以前的一篇BLOG(http://blog.joycode.com/jiangsheng/archive/2004/09/17/33756.aspx)中我提到了如何访问文件打开对话框的IShellBrowser接口。通过这个接口我们可以访问文件打开对话框的IShellView接口,进而获得选中的文件(为节省篇幅,ILIsFile、GetPIDLFolder、GetPIDLItem和WM_GETISHELLBROWSER的定义就不重复了):

[code]
class CCManyFileDialogDlg : public CFileDialog
{
.....
CStringList m_listFileNames; // list of actual items selected in listview
}
BOOL CCManyFileDialogDlg::OnInitDialog()
{
......
m_listFileNames.RemoveAll();
return TRUE; // return TRUE unless you set the focus to a control
}
BOOL CCManyFileDialogDlg::OnFileNameOK()
{
IShellBrowser* pSB=(IShellBrowser *)GetParent()->SendMessage(WM_GETISHELLBROWSER,0,0);
IShellView * pIShellView =NULL;
LPMALLOC pMalloc = NULL;
IDataObject* pIDataObject=NULL;
FORMATETC fmte;
STGMEDIUM stgmedium ;
ZeroMemory( (LPVOID)&fmte, sizeof(STGMEDIUM) );
ZeroMemory( (LPVOID)&fmte, sizeof(FORMATETC) );

fmte.tymed = TYMED_HGLOBAL;
fmte.lindex = -1;
fmte.dwAspect = DVASPECT_CONTENT;
fmte.cfFormat = RegisterClipboardFormatA(CFSTR_SHELLIDLIST);

LPITEMIDLIST pidlFull=NULL;
TCHAR szPath[_MAX_PATH];
do
{
HRESULT hr=pSB->QueryActiveShellView(&pIShellView);
if(FAILED(hr))break;
hr=::SHGetMalloc(&pMalloc); //Get pointer to shell alloc
if(FAILED(hr))break;
hr=pIShellView->GetItemObject(SVGIO_SELECTION ,IID_IDataObject,(LPVOID*)&pIDataObject);
if(FAILED(hr))break;
if(pIDataObject==NULL)break;
hr=pIDataObject->GetData(&fmte,&stgmedium);
if(FAILED(hr))break;
LPIDA pida = (LPIDA) GlobalLock(stgmedium.hGlobal);
if (pida)
{
LPCITEMIDLIST pidlFolder=GetPIDLFolder(pida);
for(UINT i=0;icidl;i++)
{
//filter folders
LPCITEMIDLIST pidl=GetPIDLItem(pida,i);
pidlFull=ILCombine(pidlFolder,pidl);
if(ILIsFile(pidlFull))
{
ZeroMemory(szPath,sizeof(TCHAR)*_MAX_PATH);
hr=SHGetPathFromIDList(pidlFull,szPath);
if(SUCCEEDED(hr))
{
m_listFileNames.AddTail(szPath);
}
}
pMalloc->Free(pidlFull);
pidlFull=NULL;
}
}
}
while(FALSE);
//Clean up
GlobalUnlock(stgmedium.hGlobal);
ReleaseStgMedium(&stgmedium);
if(pIDataObject)
pIDataObject->Release();
if(pIShellView)
pIShellView->Release();
if(pMalloc){
if(pidlFull)
pMalloc->Free(pidlFull);
pMalloc->Release();
}
return FALSE ;
}
[/code]

然后如下使用

[code]
CCManyFileDialogDlg dlg;
m_pMainWnd = &dlg;
int nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
TRACE(_T("Selected %d Files\r\n"),dlg.m_listFileNames.GetCount());
CStringList listFileNames;
POSITION pos=dlg.GetStartPosition();
while(pos)
{
CString strFileName=dlg.GetNextPathName(pos);
listFileNames.AddTail(strFileName);
}
TRACE(_T("GetOpenFileName returned %d Files\r\n"),listFileNames.GetCount());

}
[/code]

这后面一条我倒是一直没有注意,谢谢蒋兄提点。

10.

By JiangChen
From http://blog.joycode.com/jiangsheng/archive/2004/09/17/33756.aspx

在文件选择对话框和浏览器中都可以显示文件夹视图,但是有时需要对显示的方式进行控制,例如在文件选择对话框初始化时设置显示方式为详细资料视图或者缩略图视图,有的时候需要用程序来选择一些项目,例如在文件选择对话框中添加全选按钮,或者打开文件所在文件夹并且选中指定文件。

我在大约一年之前的一个Post(http://blog.joycode.com/jiangsheng/archive/2003/11/09/6152.aspx)中提及到这个问题,搞定了之后一直忘记公布答案了,今天在CSDN社区看到别人问的类似问题才想起自己已经解决了,所以现在拿出来分享……

关于如何设置文件夹视图的显示方式的问题,Paul DiLascia在他的MSDN杂志C++Q&A专栏中提供了另一个解决方案。参见
List View Mode, SetForegroundWindow, and Class Protection(http://msdn.microsoft.com/msdnmag/issues/04/03/CQA/)。PS:这家伙又用Spy++大法……

浏览器控件显示文件夹视图(例如本地目录或者FTP站点)时在其中双击目录,选中的目录会用新的资源管理器窗口打开的问题,是因为浏览器控件中的文件夹视图在打开文件夹的时候并不触发浏览器的NewWindow2事件,而是调用ShellExecuteEx,用DDE的方式和Shell通讯。微软知识库文章Q189634 WebApp.exe Enables User to Move WebBrowser Ctrl(http://support.microsoft.com?kbid=189634)描述了如何处理这样的DDE会话。呃……顺便说一下,这篇文章文不对题……

下面的给出访问文件打开/保存对话框和浏览器控件的文件夹视图的代码(MFC)

#ifndef WM_GETISHELLBROWSER
#define WM_GETISHELLBROWSER (WM_USER+7)//差不多一年过去了,这个消息还是a yet-to-be documented message
#endif

void CCustomFileDialog::OnInitDone()
{
CFileDialog::OnInitDone();
//Remember, when you customize the open file dialog,
//your CFileDialog is actually a child of the real dialog,
//which explains why you must use GetParent.
//(For details see article "Give Your Applications the Hot New Interface Look with Cool Menu Buttons"
//in the January 1998 issue of C++Q&A, MSJ.)

// WARNING! Although this is a non-intrusive customization,
// it does rely on unpublished (but easily obtainable)
// information. The Windows common file dialog box implementation
// may be subject to change in future versions of the
// operating systems, and may even be modified by updates of
// future Microsoft applications. This code could break in such
// a case. It is intended to be a demonstration of one way of
// extending the standard functionality of the common dialog boxes.
m_pSB=(IShellBrowser *)GetParent()->SendMessage(WM_GETISHELLBROWSER,0,0);

//To control the folder view of the webbrowse control (e.g. in CHTMLView),
//send this message to it and get its IShellBrowser interface.
//example:
//in CMyHTMLView::OnDocumentComplete()
// m_pSB=(IShellBrowser *)GetDlgItem(AFX_IDW_PANE_FIRST)->SendMessage(WM_GETISHELLBROWSER,0,0);
//if you create your webbrowser control manually as follows
//extern CWnd m_wndBrowser;
//m_wndBrowser.CreateControl(CLSID_WebBrowser, lpszWindowName,
// WS_VISIBLE | WS_CHILD, rectPosition, pwndParent, nChildID))
//then send message like this
//m_pSB=(IShellBrowser *)m_wndBrowser.SendMessage(WM_GETISHELLBROWSER,0,0);
}

通常获得IShellBrowser不足以满足我们的需求,我们要定制的特性通常和界面相关,例如控制文件夹视图选择一些项目。这时候可以调用IShellBrowser::QueryActiveShellView来获得IShellView接口进行选择操作(如果操作系统是WindowsXP,那么也可以从IShellView接口查询IFolderView接口来执行操作)。

为了偷懒,我的代码基于微软知识库文章Q195034 HOWTO: OfnKing Demonstrates CFileDialog Customization (http://support.microsoft.com/?kbid=195034)的代码,增加了一个全选按钮,在按钮的响应函数中编写了下面的代码选择文件夹视图中的全部文件。如果操作系统是WindowsXP,还同时切换到缩略图视图。

//If you don't have VS.NET, you need to download Platform SDK:
//http://www.microsoft.com/msdownload/platformsdk/sdkupdate/

#define GetPIDLFolder(pida) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[0])
#define GetPIDLItem(pida, i) (LPCITEMIDLIST)(((LPBYTE)pida)+(pida)->aoffset[i+1])
//a similar ILIsFolder function is included in the sample code of my article
//"Folder Thumbnail View Using Virtual List and Custom Draw"
//http://dev.csdn.net/article/22/22243.shtm

BOOL ILIsFile(LPCITEMIDLIST pidl)
{
BOOL bRet=FALSE;
LPCITEMIDLIST pidlChild=NULL;
IShellFolder* psf=NULL;
HRESULT hr = SHBindToParent(pidl, IID_IShellFolder, (LPVOID*)&psf, &pidlChild);
if (SUCCEEDED(hr) && psf)
{
SFGAOF rgfInOut=SFGAO_FOLDER|SFGAO_FILESYSTEM ;
hr=psf->GetAttributesOf(1,&pidlChild,&rgfInOut);
if (SUCCEEDED(hr))
{
//in file system, but is not a folder
if( (~rgfInOut&SFGAO_FOLDER) && (rgfInOut&SFGAO_FILESYSTEM) )
{
bRet=TRUE;
}
}
psf->Release();
}
return bRet;
}

void CCustomFileDialog::OnSelectAll()
{
if(m_pSB&&m_bMulti&&m_bExplorer)//Using IShellBrowser to Communicate with folder view
{
IShellView * pIShellView =NULL;
LPMALLOC pMalloc = NULL;
IDataObject* pIDataObject=NULL;
IFolderView* pFolderView=NULL;
FORMATETC fmte;
STGMEDIUM stgmedium ;
ZeroMemory( (LPVOID)&fmte, sizeof(STGMEDIUM) );
ZeroMemory( (LPVOID)&fmte, sizeof(FORMATETC) );

fmte.tymed = TYMED_HGLOBAL;
fmte.lindex = -1;
fmte.dwAspect = DVASPECT_CONTENT;
fmte.cfFormat = RegisterClipboardFormatA(CFSTR_SHELLIDLIST);

LPITEMIDLIST pidlFull=NULL;
do
{
HRESULT hr=m_pSB->QueryActiveShellView(&pIShellView);
if(FAILED(hr))break;
hr=pIShellView->QueryInterface(IID_IFolderView,(LPVOID*)&pFolderView);
if(pFolderView)//change view mode
{
pFolderView->SetCurrentViewMode(FVM_THUMBNAIL);
//pFolderView->SetCurrentViewMode(FVM_DETAILS);
}
hr=::SHGetMalloc(&pMalloc); //Get pointer to shell alloc
if(FAILED(hr))break;
hr=pIShellView->GetItemObject(SVGIO_ALLVIEW ,IID_IDataObject,(LPVOID*)&pIDataObject);
if(FAILED(hr))break;
if(pIDataObject==NULL)break;
hr=pIDataObject->GetData(&fmte,&stgmedium);
if(FAILED(hr))break;
LPIDA pida = (LPIDA) GlobalLock(stgmedium.hGlobal);
if (pida)
{
LPCITEMIDLIST pidlFolder=GetPIDLFolder(pida);
for(UINT i=0;icidl;i++)
{
//filter folders
LPCITEMIDLIST pidl=GetPIDLItem(pida,i);
pidlFull=ILCombine(pidlFolder,pidl);
if(ILIsFile(pidlFull))
{
hr=pIShellView->SelectItem(pidl,SVSI_SELECT);
if(FAILED(hr))
break;
}
pMalloc->Free(pidlFull);
pidlFull=NULL;
}
//Move focus to the folder view so that
//the selected items show properly
pIShellView->UIActivate(SVUIA_ACTIVATE_FOCUS);
OnSelectButton();
}
}
while(FALSE);
//Clean up
GlobalUnlock(stgmedium.hGlobal);
ReleaseStgMedium(&stgmedium);
if(pIDataObject)
pIDataObject->Release();
if(pIShellView)
pIShellView->Release();
if(pMalloc){
if(pidlFull)
pMalloc->Free(pidlFull);
pMalloc->Release();
}
if(pFolderView)
pFolderView->Release();
}
}

这世界上还是有人用C++的……



<< Home

This page is powered by Blogger. Isn't yours?