C++/CLI/VC.NET 实现OpenFileDialog的定制,使他可以选择文件夹
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_strInitPath InitFolderPath = InitPath; FileName = L""; if (IO::File::Exists(InitPath)) { InitFolderPath = IO::Path::GetDirectoryName(InitPath); if (AcceptFiles) { FileName = IO::Path::GetFileName(InitPath); } } #pragma endregion pin_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.txt if( 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: ///有两层结构见InitDialog HWND _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 { //处理Folder String^ 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 button parentLoc.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" END 2 TEXTINCLUDE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Dialog // IDD_CustomOpenDialog DIALOGEX 0, 0, 177, 17 STYLE DS_SETFONT | DS_3DLOOK | DS_CONTROL | WS_CHILD | WS_CAPTION | WS_TABSTOP FONT 8, "MS Sans Serif", 0, 0, 0x0 BEGIN DEFPUSHBUTTON "&Select",ID_SELECT,3,0,50,15 PUSHBUTTON "&Cancel",ID_CUSTOM_CANCEL,59,0,50,15 END ///////////////////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO BEGIN IDD_CustomOpenDialog, DIALOG BEGIN RIGHTMARGIN, 174 END END #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上了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能