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上了