分类:
2008-10-13 16:47:01
再谈 Windows 2000 “打开”文件对话框
编译/
不久以前,VC知识库曾探讨过如何在MFC应用中打开(类似Outlook Express左边的Places
Bar)。最近又有许多人问及这方面的问题,也许我在那篇文章中讲的不是很清楚,所以这次在本文中重申这个问题。
Windows 2000有个新的“Open”对话框(图一),这个对话框的左边有一个Places Bar,要想让这个对话框出现,必须在::GetOpenFileName函数中传递OPENFILENAME结构,这个结构隐藏在MFC的CFileDialog类中。问题是这个结构最新的版本(即Windows
2000中的5.0及以后的版本)有附加的成员,增加它的尺寸。当你(或MFC)调用::GetOpenFileName之前,你必须初始化结构OPENFILENAME的第一个成员-lStructSize-它决定了OPENFILENAME结构的大小。如果你在Windows
2000(_WIN32_WINNT >= 0x0500)中编译,你会得到结构的最新尺寸;如果你在较早的Windows版本中编译,你却得到旧的结构尺寸。
图一 新的“打开”对话框
当你在Windows 95, Windows 98或Windows NT 4.0运行应用程序的时候,你必须使用旧的结构尺寸。而在Windows
2000中,你既可以使用旧的结构尺寸,也可以使用最新的结构尺寸。如果lStructSize是新的尺寸,Windows 2000打开新风格的对话框。如果lStructSize是旧的尺寸,Windows
2000还检查结构OPENFILENAME中其它的信息:即如果对话框有“钩子”(hook)功能标志(OFN_ENABLEHOOK)或模板标志(OFN_ENABLETEMPLATE),Windows
2000打开旧风格的对话框,否则打开新风格的对话框。这多少使人感觉有些混乱。无论你如何做,MFC42.DLL都被用旧的结构尺寸编译,并且MFC使用一个吊钩过程(OFN_ENABLEHOOK)将对话框连接到你的消息映射......。
天无绝人之路,一位名叫Michael Lemley哥儿们给了我一个问题的解决方案,其基本思路是:在MFC的CFileDialog中增加一个新成员-m_ofnEx,诱使MFC使用这个成员。为了保证代码能在所有的Windows版本中运行,lStructSize
结构成员必须根据实际运行的Windows版本被正确初始化。Michael的方法有两个地方值得商榷:一个是拷贝大量的MFC代码;一个是运行时检查操作系统版本。修改工作是在新类CFileDialogEx中进行的,
// OPENFILENAME 结构的 Windows 2000 版本 struct OPENFILENAMEEX : public OPENFILENAME { void * pvReserved; DWORD dwReserved; DWORD FlagsEx; }; // CFileDialog 类的 Windows 2000 版本 class CFileDialogEx : public CFileDialog { protected: OPENFILENAMEEX m_ofnEx; // 新增成员 };OPENFILENAME 的定义在commdlg.h文件中,而 OPENFILENAMEEX 是模仿 OPENFILENAME 的本地(局部)定义。OPENFILENAMEEX有三个新成员。为了使用这个新结构,CFileDialogEx 重写 DoModal。你必须从CFileDialog 拷贝整个函数到 CFileDialogEx,代码细节如下:
添加 m_ofnEx int CFileDialogEx::DoModal() { ...... // 前面是 MFC 代码 // 拷贝 m_ofn ==> m_ofnEx 并设置 memset(&m_ofnEx, 0, sizeof(m_ofnEx)); memcpy(&m_ofnEx, &m_ofn, sizeof(m_ofn)); if (IsWin2000()) m_ofnEx.lStructSize = sizeof(m_ofnEx); // 使用 m_ofnEx 运行 "打开"或"保存"对话框 int nResult; if (m_bOpenFileDialog) nResult = ::GetOpenFileName((OPENFILENAME*)&m_ofnEx); else nResult = ::GetSaveFileName((OPENFILENAME*)&m_ofnEx); // 回拷 m_ofnEx ==> m_ofn memcpy(&m_ofn, &m_ofnEx, sizeof(m_ofn)); m_ofn.lStructSize = sizeof(m_ofn); ...... }上述代码的基本思路是在调用 GetOpenFileName 或 GetSaveFileName 之前先将 m_ofn 拷贝到 m_ofnEx,调用完成后再拷回去。其中最重要的是其第一个结构成员----lStructSize 按照实际运行的操作系统版本被初始化。获取操作系统版本参见 IsWin2000 函数:
IsWin2000函数实现 //////////////////////////////////////////////////////////////// // 检测操作系统版本(Windows 2000 或以后)的函数 // BOOL IsWin2000 () { OSVERSIONINFOEX osvi; BOOL bOsVersionInfoEx; // 尝试调用 GetVersionEx 函数,使用 OSVERSIONINFOEX 结构, // 它被Windows 2000支持. // // 如果调用失败, 尝试使用 OSVERSIONINFO 结构. ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); if( !(bOsVersionInfoEx = GetVersionEx ((OSVERSIONINFO *) &osvi)) ) { // 如果 OSVERSIONINFOEX 不行, 就用 OSVERSIONINFO. osvi.dwOSVersionInfoSize = sizeof (OSVERSIONINFO); if (! GetVersionEx ( (OSVERSIONINFO *) &osvi) ) return FALSE; } switch (osvi.dwPlatformId) { case VER_PLATFORM_WIN32_NT: if ( osvi.dwMajorVersion >= 5 ) return TRUE; break; } return FALSE; }为了让代码正确运行,CFileDialogEx 还重写了另一个虚函数 OnNotify。当在文件对话框中发生某事件时,Windows发送一个 WM_NOTIFY 消息以及 OPENFILENAME 结构中更新的信息。因为该消息处理函数和其它的MFC代码都希望获悉存储在m_ofn中的信息,而不是m_ofnEx中的信息,OnNotify 截获通知并实现另一拷贝。
BOOL CFileDialogEx::OnNotify(...) { memcpy(&m_ofn, &m_ofnEx, sizeof(m_ofn)); m_ofn.lStructSize = sizeof(m_ofn); return CFileDialog::OnNotify( wParam, lParam, pResult); }下面是CFileDialogEx类最终的代码:
FileDialogEx 的头文件和实现文件 ///////////////////////////////////////////////////////////// // FileDialogEx.h // #pragma once // OPENFILENAME的 Windows 2000 版本. // 新版本多处三个成员. // 从commdlg.h文件中拷贝. struct OPENFILENAMEEX : public OPENFILENAME { void * pvReserved; DWORD dwReserved; DWORD FlagsEx; }; /////////////////////////////////////////////////////////////////////////// // CFileDialogEx: 封装 Windows-2000 风格的"打开"对话框. // class CFileDialogEx : public CFileDialog { DECLARE_DYNAMIC(CFileDialogEx) public: CFileDialogEx(BOOL bOpenFileDialog, // TRUE 为"打开", // FALSE 为 "另存为" LPCTSTR lpszDefExt = NULL, LPCTSTR lpszFileName = NULL, DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, LPCTSTR lpszFilter = NULL, CWnd* pParentWnd = NULL); // 重载 virtual int DoModal(); protected: OPENFILENAMEEX m_ofnEx; // 新的 OPENFILENAME Windows 2000 版本 virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult); // 处理不同通知消息的虚拟函数 virtual BOOL OnFileNameOK(); virtual void OnInitDone(); virtual void OnFileNameChange(); virtual void OnFolderChange(); virtual void OnTypeChange(); DECLARE_MESSAGE_MAP() //{{AFX_MSG(CFileDialogEx) //}}AFX_MSG }; ///////////////////////////////////////////////////////////// // FileDialogEx.cpp // #include "stdafx.h" #include一旦你用起来以后会发现另外的问题:那就是如何让MFC识别它?如果你深入到MFC中,会发现MFC在一个叫CDocManager::DoPromptFileName的函数中调用文件对话框。幸运的是这个函数是个虚函数,所以你可以重写它,但那需要一个从CDocManager派生一个新类CDocManagerEx,并将它装入你的应用程序。下面就是这个新的派生类:#include "FileDialogEx.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif static BOOL IsWin2000(); /////////////////////////////////////////////////////////////////////////// // CFileDialogEx IMPLEMENT_DYNAMIC(CFileDialogEx, CFileDialog) CFileDialogEx::CFileDialogEx(BOOL bOpenFileDialog, LPCTSTR lpszDefExt, LPCTSTR lpszFileName, DWORD dwFlags, LPCTSTR lpszFilter, CWnd* pParentWnd) : CFileDialog(bOpenFileDialog, lpszDefExt, lpszFileName, dwFlags, lpszFilter, pParentWnd) { } BEGIN_MESSAGE_MAP(CFileDialogEx, CFileDialog) //{{AFX_MSG_MAP(CFileDialogEx) //}}AFX_MSG_MAP END_MESSAGE_MAP() BOOL IsWin2000() { OSVERSIONINFOEX osvi; BOOL bOsVersionInfoEx; // 尝试调用 GetVersionEx 函数,使用 OSVERSIONINFOEX 结构, // 它被Windows 2000支持. // // 如果调用失败, 尝试使用 OSVERSIONINFO 结构. ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); if( !(bOsVersionInfoEx = GetVersionEx ((OSVERSIONINFO *) &osvi)) ) { // 如果 OSVERSIONINFOEX 不行, 就用 OSVERSIONINFO. osvi.dwOSVersionInfoSize = sizeof (OSVERSIONINFO); if (! GetVersionEx ( (OSVERSIONINFO *) &osvi) ) return FALSE; } switch (osvi.dwPlatformId) { case VER_PLATFORM_WIN32_NT: if ( osvi.dwMajorVersion >= 5 ) return TRUE; break; } return FALSE; } int CFileDialogEx::DoModal() { ASSERT_VALID(this); ASSERT(m_ofn.Flags & OFN_ENABLEHOOK); ASSERT(m_ofn.lpfnHook != NULL); // 仍然是个用户钩 // 文件缓冲初始化 ASSERT(AfxIsValidAddress(m_ofn.lpstrFile, m_ofn.nMaxFile)); DWORD nOffset = lstrlen(m_ofn.lpstrFile)+1; ASSERT(nOffset <= m_ofn.nMaxFile); memset(m_ofn.lpstrFile+nOffset, 0, (m_ofn.nMaxFile-nOffset)*sizeof(TCHAR)); // WINBUG: 这是一种"打开/保存"对话框的特殊情况, // 在它disable主窗口之前是需要处理的. // HWND hWndFocus = ::GetFocus(); BOOL bEnableParent = FALSE; m_ofn.hwndOwner = PreModal(); AfxUnhookWindowCreate(); if (m_ofn.hwndOwner != NULL && ::IsWindowEnabled(m_ofn.hwndOwner)) { bEnableParent = TRUE; ::EnableWindow(m_ofn.hwndOwner, FALSE); } _AFX_THREAD_STATE* pThreadState = AfxGetThreadState(); ASSERT(pThreadState->m_pAlternateWndInit == NULL); if (m_ofn.Flags & OFN_EXPLORER) pThreadState->m_pAlternateWndInit = this; else AfxHookWindowCreate(this); memset(&m_ofnEx, 0, sizeof(m_ofnEx)); memcpy(&m_ofnEx, &m_ofn, sizeof(m_ofn)); if (IsWin2000()) m_ofnEx.lStructSize = sizeof(m_ofnEx); int nResult; if (m_bOpenFileDialog) nResult = ::GetOpenFileName((OPENFILENAME*)&m_ofnEx); else nResult = ::GetSaveFileName((OPENFILENAME*)&m_ofnEx); memcpy(&m_ofn, &m_ofnEx, sizeof(m_ofn)); m_ofn.lStructSize = sizeof(m_ofn); if (nResult) ASSERT(pThreadState->m_pAlternateWndInit == NULL); pThreadState->m_pAlternateWndInit = NULL; // WINBUG: "打开/保存"对话框的特殊情况的第二部分. if (bEnableParent) ::EnableWindow(m_ofnEx.hwndOwner, TRUE); if (::IsWindow(hWndFocus)) ::SetFocus(hWndFocus); PostModal(); return nResult ? nResult : IDCANCEL; } BOOL CFileDialogEx::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult) { memcpy(&m_ofn, &m_ofnEx, sizeof(m_ofn)); m_ofn.lStructSize = sizeof(m_ofn); return CFileDialog::OnNotify( wParam, lParam, pResult); } //////////////////////////////////////////////////////////////////// // 下列函数只是用来说明他们获得了调用,实际上,类库(MFC)内部的对话框 // 过程是被挂钩的;如果你愿意的话,可以删除他们. // BOOL CFileDialogEx::OnFileNameOK() { TRACE(_T("CFileDialogEx::OnFileNameOK\n")); return CFileDialog::OnFileNameOK(); } void CFileDialogEx::OnInitDone() { TRACE(_T("CFileDialogEx::OnInitDone\n")); CFileDialog::OnInitDone(); } void CFileDialogEx::OnFileNameChange() { TRACE(_T("CFileDialogEx::OnFileNameChange\n")); CFileDialog::OnFileNameChange(); } void CFileDialogEx::OnFolderChange() { TRACE(_T("CFileDialogEx::OnFolderChange\n")); CFileDialog::OnFolderChange(); } void CFileDialogEx::OnTypeChange() { TRACE(_T("OnTypeChange(), index = %d\n"), m_ofn.nFilterIndex); CFileDialog::OnTypeChange(); }
DocMgrEx头文件和实现文件 //////////////////////////////////////////////////////////////// // DocMgrEx.h // #pragma once ////////////////// // CDocManagerEx: 扩展 CDocManager 类使用 CFileOpenEx. class CDocManagerEx : public CDocManager { public: CDocManagerEx(); ~CDocManagerEx(); // 为"打开/保存"对话框重载 virtual BOOL DoPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate); protected: // 创建"打开"文件对话框的新函数 virtual CFileDialog* OnCreateFileDialog(BOOL bOpenFileDialog); DECLARE_DYNAMIC(CDocManagerEx) }; //////////////////////////////////////////////////////////////// // DocMgrEx.cpp // // CDocManagerEx 为了Windows 2000中使用"打开"文件对话框, // 重写 CDocManager 中的一个函数 DoPromptFileName. // 使用这个类时, 在你添加任何文档模版之前,添加下列一行代码到应用 // 程序的实例化函数:InitInstance(). // m_pDocManager = new CDocManagerEx; // #include "stdafx.h" #include在程序中使用这个类时,你必须在应用的InitInstance函数中添加一行如下代码:#include "DocMgrEx.h" #include "FileDialogEx.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif AFX_STATIC void AFXAPI _AfxAppendFilterSuffix(CString& filter, OPENFILENAME& ofn, CDocTemplate* pTemplate, CString* pstrDefaultExt); IMPLEMENT_DYNAMIC(CDocManagerEx, CDocManagerEx) CDocManagerEx::CDocManagerEx() { } CDocManagerEx::~CDocManagerEx() { } //////////////////////////////////////////////////////////////////////// // 创建文件对话框的新函数,你可以重载它以创建一些不同类型的CFileDialog, // 缺省情况下创建 CFileDialogEx. // CFileDialog* CDocManagerEx::OnCreateFileDialog(BOOL bOpenFileDialog) { TRACE(_T("CDocManagerEx::OnCreateFileDialog\n")); return new CFileDialogEx(bOpenFileDialog); } ///////////////////////////////////////////////////////////////////////// // 这个函数是从MFC的 docmgr.cpp文件中考过来的,只对它做了稍微的修改以便 // 使用 CFileDialogEx. // BOOL CDocManagerEx::DoPromptFileName(CString& fileName, UINT nIDSTitle, DWORD lFlags, BOOL bOpenFileDialog, CDocTemplate* pTemplate) { // 修改日期 5-15-99 : 调用虚拟函数来创建对话框 CFileDialog* pDlg = OnCreateFileDialog(bOpenFileDialog); ASSERT(pDlg); CFileDialog& dlgFile = *pDlg; CString title; VERIFY(title.LoadString(nIDSTitle)); dlgFile.m_ofn.Flags |= lFlags; CString strFilter; CString strDefault; if (pTemplate != NULL) { ASSERT_VALID(pTemplate); _AfxAppendFilterSuffix(strFilter, dlgFile.m_ofn, pTemplate, &strDefault); } else { // 用于所有的文档模板 POSITION pos = m_templateList.GetHeadPosition(); BOOL bFirst = TRUE; while (pos != NULL) { CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetNext(pos); _AfxAppendFilterSuffix(strFilter, dlgFile.m_ofn, pTemplate, bFirst ? &strDefault : NULL); bFirst = FALSE; } } // 添加 "*.*" 对全部文件过滤 CString allFilter; VERIFY(allFilter.LoadString(AFX_IDS_ALLFILTER)); strFilter += allFilter; strFilter += (TCHAR)''\0''; // 下一个串 strFilter += _T("*.*"); strFilter += (TCHAR)''\0''; // 最后一个串 dlgFile.m_ofn.nMaxCustFilter++; dlgFile.m_ofn.lpstrFilter = strFilter; dlgFile.m_ofn.lpstrTitle = title; dlgFile.m_ofn.lpstrFile = fileName.GetBuffer(_MAX_PATH); int nResult = dlgFile.DoModal(); fileName.ReleaseBuffer(); // 修改日期 5-15-99 : 删除对话框 delete pDlg; return nResult == IDOK; } //////////////////////////////////////////////////////////////////////// // 这个函数是原封不动地从MFC的 docmgr.cpp文件中考过来的,因为它是静态函数, // 所以在这里是必须的. // AFX_STATIC void AFXAPI _AfxAppendFilterSuffix(CString& filter, OPENFILENAME& ofn, CDocTemplate* pTemplate, CString* pstrDefaultExt) { ASSERT_VALID(pTemplate); ASSERT_KINDOF(CDocTemplate, pTemplate); CString strFilterExt, strFilterName; if (pTemplate->GetDocString(strFilterExt, CDocTemplate::filterExt) && !strFilterExt.IsEmpty() && pTemplate->GetDocString(strFilterName, CDocTemplate::filterName) && !strFilterName.IsEmpty()) { // 添加一个基于文档模板的文件到过滤器 ASSERT(strFilterExt[0] == ''.''); if (pstrDefaultExt != NULL) { // 设置缺省扩展名 *pstrDefaultExt = ((LPCTSTR)strFilterExt) + 1; // skip the ''.'' ofn.lpstrDefExt = (LPTSTR)(LPCTSTR)(*pstrDefaultExt); ofn.nFilterIndex = ofn.nMaxCustFilter + 1; // 1 based number } // 添加过滤器 filter += strFilterName; ASSERT(!filter.IsEmpty()); // 必须有一个文件类型名 filter += (TCHAR)''\0''; // 下一个串 filter += (TCHAR)''*''; filter += strFilterExt; filter += (TCHAR)''\0''; // 下一个串 ofn.nMaxCustFilter++; } }
// 使用扩展的 CDocManager. // 在创建任何文档模板前添加! m_pDocManager = new CDocManagerEx;DocMgrEx.cpp 有一对从MFC拷贝过来的函数,用重新写的 DoPromptFileName 创建 CFileDialogEx,而不是不 CFileDialog。实际上,为了创建对话框,CDocManagerEx 调用一个新的虚函数----OnCreateFileDialog,从而解决了使用 CDocManager 创建对话框产生的问题。也就是说,没有能重写的虚函数来创建不同种类的对话框。真是聪明......