1.原因
前段時間需要使用OpenFileDialog選擇檔案夾,google上有c#的辦法,比較簡單,只要設定Filter=亂七八糟的符號,讓所有檔案都顯示不出來就可以。但是這樣總是有點不舒服。讓我想起過去在MFC模式下建立的VC的OpenFileDialog定製,需要使用到資源檔(因為系統函數中要求你提供你的模板ID). c#也可以實現,但是必須內建res檔案,這點非常麻煩,可以看這裡:http://blog.csdn.net/norsd/article/details/8840761, 所以考慮產生一個c++/cli/vc.net 作為語言的.net類庫
2.原理
原理非常簡單,還是和過去一樣:
::GetOpenFileName(&stOFN)
就是這個函數,囉嗦一下GetOpenFileName其實是一個宏,分別根據環境被定義為GetOpenFileNameA和GetOpenFileNameW
然後stOFN 是一個結構類型為:OPENFILENAME
其中為了定製,我們必須設定:OPENFILENAME::lpTemplateName = ID_DIALOG 這裡很奇怪,MSDN要求的是一個string,但是我們必須傳一個數字(資源號),具體原因我過去看過,一本黑皮書叫:MFC技術內部(http://book.douban.com/subject/1000127/) 裡面有寫過一句。
OPENFILENAME::lpfnHook 這個其實就是這個Dialog的MessageProc,其中對於一個訊息返回true代表外部定製處理,false為系統預設處理。
3.標頭檔:
#pragma once#include <windows.h>#include <Commdlg.h>#include <Commctrl.h>#include <vector>#pragma comment(lib,"Comdlg32.lib")#pragma comment(lib,"user32.lib")#pragma comment(lib,"Comctl32.lib")#include <vcclr.h>#include "resource.h"using namespace std;using namespace System;using namespace System::Windows::Forms;using namespace System::Collections::Generic;using namespace System::IO;using namespace System::Text;using namespace System::Runtime::InteropServices;namespace norlib{namespace Controls{public delegate UINT_PTR OFNHOOKPROCOLDSTYLE(HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam);public ref class OpenFileDialogEx:CommonDialog{public:OpenFileDialogEx(String^ arg_InitPath){InitPath = arg_InitPath;OFNHOOKPROCOLDSTYLE^ fp = gcnew OFNHOOKPROCOLDSTYLE(this,&norlib::Controls::OpenFileDialogEx::OFNHookProcOldStyle);_gchOFNHookProcOldStyle = GCHandle::Alloc(fp);_inrOFNHookProcOldStyle = Marshal::GetFunctionPointerForDelegate(fp);}~OpenFileDialogEx(){_gchOFNHookProcOldStyle.Free();}public://外部傳入的一個字串property String^ InitPath;property String^ InitFolderPath;property String^ Title;property String^ FileName;property array<String^>^ FileNames;property bool ShowReadOnly;property bool AcceptFiles;property bool MultiSelect;property String^ FolderName;//For instance:"c:\\MyDir1\\MyDir2"public:virtual void Reset() override{FolderName = nullptr;Title = nullptr;AcceptFiles = true;} virtual bool RunDialog(IntPtr hwndOwner) override { #pragma region 分析arg_strInitPathInitFolderPath = InitPath;FileName = L"";if (IO::File::Exists(InitPath)){InitFolderPath = IO::Path::GetDirectoryName(InitPath);if (AcceptFiles){FileName = IO::Path::GetFileName(InitPath);}}#pragma endregionpin_ptr<const wchar_t> pinTitle = PtrToStringChars(Title);pin_ptr<const wchar_t> pinInitFolderPath = PtrToStringChars(InitFolderPath);pin_ptr<const wchar_t> pinFileName = PtrToStringChars(FileName);TCHAR chsFileName[FILEMAXLEN];::memset(chsFileName,0,sizeof(chsFileName));::wcscpy(chsFileName,pinFileName);OPENFILENAME stOFN = {0}; stOFN.lStructSize = sizeof(OPENFILENAME);stOFN.hwndOwner = (HWND)hwndOwner.ToInt64();stOFN.nMaxFile = FILEMAXLEN;stOFN.lpstrFile = (PWSTR)&chsFileName;stOFN.lpstrInitialDir = pinInitFolderPath;if (!AcceptFiles){String^ str = String::Format("Folders\0*.{0}-{1}\0\0", Guid::NewGuid().ToString("N"), Guid::NewGuid().ToString("N"));pin_ptr<const wchar_t> pcwStr = PtrToStringChars(str);stOFN.lpstrFilter = pcwStr;}else{stOFN.lpstrFilter = NULL;}stOFN.nMaxCustFilter = 0;stOFN.nFilterIndex = 0;stOFN.nMaxFile = FILEMAXLEN;stOFN.nMaxFileTitle = 0;stOFN.lpstrTitle = pinTitle ;stOFN.lpfnHook = (LPOFNHOOKPROC)_inrOFNHookProcOldStyle.ToPointer();stOFN.lpTemplateName = (PCWSTR)IDD_CustomOpenDialog;stOFN.hInstance = (HINSTANCE)(Marshal::GetHINSTANCE( this->GetType()->Module).ToInt64());stOFN.Flags =OFN_DONTADDTORECENT |OFN_ENABLEHOOK |OFN_ENABLESIZING |OFN_NOTESTFILECREATE |OFN_EXPLORER |OFN_FILEMUSTEXIST |OFN_PATHMUSTEXIST |OFN_NODEREFERENCELINKS |OFN_ENABLETEMPLATE |(MultiSelect?OFN_ALLOWMULTISELECT:0)|(ShowReadOnly?0:OFN_HIDEREADONLY);::GetOpenFileName(&stOFN);int extErrpr = ::CommDlgExtendedError();if (extErrpr != 0){String^ strErr = String::Format(L"建立OpenFileName對話方塊失敗\r\n錯誤:{0}",extErrpr);System::Windows::Forms::MessageBox::Show(strErr); }FileName = nullptr;FileNames = nullptr;if( _bResult ){PWSTR pw1st = chsFileName;FileName = gcnew String(pw1st);///MultiSelect傳回值是///1.檔案夾路徑 d:/test/dir////2.檔案名稱1 Test1.txt///3.檔案名稱2 Test2.txtif( MultiSelect ){vector<PWSTR> vtStr;PWSTR pwFileName = pw1st;int nIndex = wcslen(pwFileName)+1;int nMaxIndex = FILEMAXLEN;while( nIndex<nMaxIndex ){pwFileName = chsFileName+nIndex;if(pwFileName[0]==NULL)break;vtStr.push_back(pwFileName);nIndex += wcslen(pwFileName)+1;}int nCount = vtStr.size();String^ strFolder = gcnew String(pw1st) + "\\";FileNames = gcnew array<String^>(nCount);vector<PWSTR>::iterator p;// 指向容器的首個元素p = vtStr.begin();nIndex = 0;for( ; p!= vtStr.end(); p++ ){FileNames[nIndex++]= strFolder + (gcnew String(*p));}FileName = FileNames->Length>0?FileNames[0]:FileName;}}return _bResult; } protected: virtual IntPtr HookProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lparam) override {throw "Impo!";}UINT_PTR OFNHookProcOldStyle(HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam); private: void InitDialog(HWND hWnd); void _ResizeCustomeControl();//設定自訂按鈕位置void _OnClickSelect( HWND arg_hWnd, UINT arg_uMessage, WPARAM arg_WParam, LPARAM arg_LParam ); int ProcessNotifyMessage(HWND hWnd, OFNOTIFY& notifyData);///工具函數private:String^ _GetDlgItemText_T(UINT arg_uControlId){HWND hItem = _GetDlgItem_T(arg_uControlId);return _GetWindowText_T(hItem);}String^ _GetWindowText_T(HWND hWnd){TCHAR chsText[2048]={0};::GetWindowText(hWnd,chsText,sizeof(chsText)/sizeof(TCHAR));return gcnew String(chsText);}void _EnableControl_T( UINT arg_uControlId, BOOL arg_bEnable){HWND hFilterCombo = _GetDlgItem_T( arg_uControlId);::EnableWindow( hFilterCombo, arg_bEnable );}void _HideControl_T( UINT arg_uControlId ){SendMessage(_hDlg, CDM_HIDECONTROL, arg_uControlId, 0);}void _SetControlText_T( UINT arg_uControlId, LPCWSTR arg_pcwText ){HWND hControl = _GetDlgItem_T(arg_uControlId);::SetWindowText(hControl,arg_pcwText);}WINDOWPLACEMENT _GetPlacement_T( UINT arg_uControlId ){WINDOWPLACEMENT stPlacement;HWND hControl = _GetDlgItem_T(arg_uControlId);::GetWindowPlacement(hControl,&stPlacement);return stPlacement;}void _SetPlacement_T( UINT arg_uControlId, WINDOWPLACEMENT& arg_stPlacement ){HWND hControl = _GetDlgItem_T(arg_uControlId);SetWindowPlacement(hControl,&arg_stPlacement);}HWND _GetDlgItem_T(UINT arg_uControlId){HWND hwnd = GetDlgItem(_hDlg,arg_uControlId);if( hwnd == NULL ){return GetDlgItem(_hThis,arg_uControlId);}return hwnd;}UINT_PTR _GetFont_T( UINT arg_uControlId){HWND hWnd = _GetDlgItem_T( arg_uControlId );return SendMessage(hWnd,WM_GETFONT,0,0);}void _SetFont_T(UINT arg_uControlId, UINT_PTR arg_hFont){HWND hWnd = _GetDlgItem_T( arg_uControlId );SendMessage(hWnd, WM_SETFONT, arg_hFont, 0);}String^ _GetFolderPath_T(){TCHAR chsText[2048];CommDlg_OpenSave_GetFolderPath(_hDlg,chsText,sizeof(chsText)/sizeof(TCHAR));return gcnew String(chsText);}private:///有兩層結構見InitDialogHWND _hThis;HWND _hDlg;GCHandle _gchOFNHookProcOldStyle;IntPtr _inrOFNHookProcOldStyle;bool _bResult;static const int FILEMAXLEN=2048;};}}
實現檔案:
// This is the main DLL file.#include "stdafx.h"#include "Controls.OpenFileDialogEx.h"void norlib::Controls::OpenFileDialogEx::_OnClickSelect( HWND arg_hWnd, UINT arg_uMessage, WPARAM arg_WParam, LPARAM arg_LParam ){if( AcceptFiles ){SendMessage(arg_hWnd, arg_uMessage, arg_WParam, arg_LParam);}else{//處理FolderString^ strFolderPath = _GetFolderPath_T();//絕對路徑if (IO::Path::IsPathRooted(strFolderPath)){if (Directory::Exists(strFolderPath)){FolderName = strFolderPath;_bResult = true;::SendMessage( _hDlg, WM_CLOSE, 0, 0);}}////相對路徑//else if (!String::IsNullOrEmpty(m_currentFolder) && strFileNameCombo != "")//{//var combined = System::IO::Path::Combine(m_currentFolder, currentText);//if (Directory.Exists(combined))//{////the contents of the text box are a relative path, that points to a ////an existing directory. We interpret the users intent to mean that they wanted////to select the existing path.//m_useCurrentDir = true;//m_currentFolder = combined;//hParent.SendMessage(InteropUtil.WM_CLOSE, 0, 0);//break;//}//}////The user has not selected an existing folder.////So we translate a click of our "Select" button into the OK button and forward the request to the////open file dialog.//hParent.SendMessage//(//InteropUtil.WM_COMMAND,//(InteropUtil.BN_CLICKED << 16) | InteropUtil.IDOK,//unchecked((uint)hParent.GetDlgItem(InteropUtil.IDOK))//);}}int norlib::Controls::OpenFileDialogEx::ProcessNotifyMessage( HWND hWnd, OFNOTIFY& notifyData ){switch (notifyData.hdr.code){case CDN_FOLDERCHANGE:{//String^ newFolder = GetTextFromCommonDialog( ::GetParent(hWnd), CDM_GETFOLDERPATH);//if (m_currentFolder != nullptr && newFolder != nullptr && newFolder->PathContains(m_currentFolder))//{//m_suppressSelectionChange = true;//}//m_currentFolder = newFolder;//var fileNameCombo = hWnd.GetParent().AssumeNonZero().GetDlgItem(InteropUtil.ID_FileNameCombo).AssumeNonZero();//if (m_hasDirChangeFired)//{//fileNameCombo.SetWindowTextW("");//}//m_hasDirChangeFired = true;break;}case CDN_FILEOK:{if (!AcceptFiles){return 1;}break;}case CDN_INITDONE:{HWND hParent = ::GetParent(hWnd);HWND hFile = ::GetDlgItem(hParent, ID_FileNameTextCombo);::SetFocus(hFile);break;}}return 0;}void norlib::Controls::OpenFileDialogEx::_ResizeCustomeControl(){WINDOWPLACEMENT locCancel = _GetPlacement_T(IDCANCEL);WINDOWPLACEMENT locSelect = _GetPlacement_T(ID_SELECT);locSelect.rcNormalPosition.right = _GetPlacement_T(ID_FileNameTextCombo).rcNormalPosition.right;_SetPlacement_T(ID_CUSTOM_CANCEL,locCancel );RECT& rcCancel = locCancel.rcNormalPosition;RECT& rc = locSelect.rcNormalPosition;rc = rcCancel;rc.right = rc.left-10;rc.left = rc.right-(rcCancel.right-rcCancel.left);_SetPlacement_T(ID_SELECT,locSelect);HWND hSelectBtn = _GetDlgItem_T(ID_SELECT);HWND hCacelBtn = _GetDlgItem_T(ID_CUSTOM_CANCEL);InvalidateRect(hSelectBtn,NULL,TRUE);InvalidateRect(hCacelBtn,NULL,TRUE);}void norlib::Controls::OpenFileDialogEx::InitDialog( HWND hWnd ){_hDlg = ::GetParent(hWnd);_hThis = hWnd;_EnableControl_T(ID_FilterCombo,FALSE);_HideControl_T(ID_FilterCombo);_HideControl_T(ID_FilterLabel);//We don't want the accelerator keys for the ok and cancel buttons to work, because//they are not shown on the dialog. However, we still want the buttons enabled//so that "esc" and "enter" have the behavior they used to. So, we just//clear out their text instead. _SetControlText_T(IDOK,L"");_SetControlText_T(IDCANCEL,L"");//find our button controls _SetFont_T( ID_SELECT, _GetFont_T(IDOK) );_SetFont_T( ID_CUSTOM_CANCEL, _GetFont_T(IDCANCEL));WINDOWPLACEMENT cancelLoc = _GetPlacement_T(IDCANCEL); //hide the ok and cancel buttons_HideControl_T(IDCANCEL);_HideControl_T(IDOK);//expand the file name combo to take up the space left by the OK and cancel buttons. WINDOWPLACEMENT fileNameLoc = _GetPlacement_T(ID_FileNameTextCombo); WINDOWPLACEMENT okbuttonLoc = _GetPlacement_T(IDOK);fileNameLoc.rcNormalPosition.right = okbuttonLoc.rcNormalPosition.right;_SetPlacement_T(ID_FileNameTextCombo,fileNameLoc);if(!AcceptFiles){_SetControlText_T(ID_FileNameLabel,L"Folder Name:");}WINDOWPLACEMENT parentLoc;GetWindowPlacement(_hDlg,&parentLoc);//subtract the height of the missing cancel buttonparentLoc.rcNormalPosition.bottom -= (cancelLoc.rcNormalPosition.bottom - cancelLoc.rcNormalPosition.top);SetWindowPlacement(_hDlg , &parentLoc);//move the select and custom cancel buttons to the right hand side of the window:WINDOWPLACEMENT selectLoc = _GetPlacement_T(ID_SELECT);WINDOWPLACEMENT customCancelLoc = _GetPlacement_T(ID_CUSTOM_CANCEL);WINDOWPLACEMENT ctrlLoc;GetWindowPlacement(hWnd,&ctrlLoc);ctrlLoc.rcNormalPosition.right = fileNameLoc.rcNormalPosition.right;}UINT_PTR norlib::Controls::OpenFileDialogEx::OFNHookProcOldStyle( HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam ){switch(uMsg){case WM_INITDIALOG:{InitDialog(hDlg);break;}case WM_NOTIFY:{OFNOTIFY* pNotifyData = (OFNOTIFY*)lParam;UINT_PTR results = ProcessNotifyMessage(hDlg, *pNotifyData);if (results != 0){//http://msdn.microsoft.com/ZH-CN/library/windows/desktop/ms633591(v=vs.85).aspx//::SetWindowLong(hDlg, DWL_MSGRESULT, results);//64bit http://sourceforge.net/p/bochs/bugs/1250/::SetWindowLongPtr(hDlg,DWLP_MSGRESULT,results);//If you use SetWindowLongPtr with the DWLP_MSGRESULT index to set the return value for a message processed by a dialog box procedure,//the dialog box procedure should return TRUE directly afterward. //Otherwise, if you call any function that results in your dialog box procedure receiving a window message, //the nested window message could overwrite the return value you set by using DWLP_MSGRESULT.return TRUE;}break;}case WM_SIZE:{_ResizeCustomeControl();break;}case WM_COMMAND:{HWND hParent = GetParent(hDlg);WORD code = HIWORD(wParam);WORD id = LOWORD(wParam);if (code == BN_CLICKED){switch (id){case ID_CUSTOM_CANCEL:{//The user clicked our custom cancel button. Close the dialog.SendMessage(hParent, WM_CLOSE, 0, 0);break;}case ID_SELECT:{_OnClickSelect(hParent,WM_COMMAND,IDOK,NULL);break;}}}break;}}return 0;}
app.rc檔案
// Microsoft Visual C++ generated resource script.//#include "resource.h"#define APSTUDIO_READONLY_SYMBOLS///////////////////////////////////////////////////////////////////////////////// Generated from the TEXTINCLUDE 2 resource.//#include "afxres.h"/////////////////////////////////////////////////////////////////////////////#undef APSTUDIO_READONLY_SYMBOLS/////////////////////////////////////////////////////////////////////////////// English (United States) resources#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US///////////////////////////////////////////////////////////////////////////////// Icon//// Icon with lowest ID value placed first to ensure application icon// remains consistent on all systems.1 ICON "app.ico"#ifdef APSTUDIO_INVOKED///////////////////////////////////////////////////////////////////////////////// TEXTINCLUDE//1 TEXTINCLUDE BEGIN "resource.h\0" "\0"END2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0"END3 TEXTINCLUDE BEGIN "\0"END#endif // APSTUDIO_INVOKED///////////////////////////////////////////////////////////////////////////////// Dialog//IDD_CustomOpenDialog DIALOGEX 0, 0, 177, 17STYLE DS_SETFONT | DS_3DLOOK | DS_CONTROL | WS_CHILD | WS_CAPTION | WS_TABSTOPFONT 8, "MS Sans Serif", 0, 0, 0x0BEGIN DEFPUSHBUTTON "&Select",ID_SELECT,3,0,50,15 PUSHBUTTON "&Cancel",ID_CUSTOM_CANCEL,59,0,50,15END///////////////////////////////////////////////////////////////////////////////// DESIGNINFO//#ifdef APSTUDIO_INVOKEDGUIDELINES DESIGNINFOBEGIN IDD_CustomOpenDialog, DIALOG BEGIN RIGHTMARGIN, 174 ENDEND#endif // APSTUDIO_INVOKED#endif // English (United States) resources/////////////////////////////////////////////////////////////////////////////
resource.h檔案
//{{NO_DEPENDENCIES}}// Microsoft Visual C++ generated include file.// Used by app.rc//#define IDD_CustomOpenDialog 101#define IDI_ICON1 105#define ID_SELECT 1001#define ID_CUSTOM_CANCEL 1002// Next default values for new objects// #ifdef APSTUDIO_INVOKED#ifndef APSTUDIO_READONLY_SYMBOLS#define _APS_NEXT_RESOURCE_VALUE 102#define _APS_NEXT_COMMAND_VALUE 40001#define _APS_NEXT_CONTROL_VALUE 1000#define _APS_NEXT_SYMED_VALUE 101#endif#endif#define IDOK 1#define IDCANCEL 2//control aliases that actually make sense....#define ID_FilterCombo 0x0470#define ID_FilterLabel 0x0441#define ID_FileNameLabel 0x0442#define ID_FileNameTextBox 0x0480#define ID_FileNameTextCombo 0x047c#define ID_FileList 0x0461
裡面的一些技術細節非常簡單,無非就是隱藏原有的2個ok,cancel按鈕,然後替換我們自己的按鈕,不懂的可以問。
原始碼就這些了。
編譯完成後就可以用在.net上了