VS2010实现类似EditPlus目录视图
关键字:目录视图,CDockablePane,磁盘驱动目录,文件列表,文件通配符
引言
EditPlus软件的目录视图用起来十分方便,笔者经过不断试验,利用vs2010实现了类似其目录的类CFileView。
首先,让我们看下Editplus中的目录视图界面,其界面如下图所示:
可以看见,Editplus目录视图主要分为四部分:
磁盘驱动器目录下拉列表,目录树,文件列表,筛选器向上弹出列表。
如何利用VS2010实现
下图为笔者实现的目录视图效果:
要完全实现EditPlus的目录视图有两点比较困难,在这里我未实现:
第一,文件筛选器是向上弹出的,这需要自己重新编写派生自CCombox的类,比较麻烦,这里未实现。
第二,VS2010下利用CDockablePane类来实现时,并无Editplus的目录树和文件列表之间的分割线,而这条分割线可以自由调整目录树和文件列表的范围,在vs2010中实现这样可分割的面板,我估计需要使用CPaneDivider,有兴趣的可以参考MFC中附带的例子SetPaneSize,这里未实现。
实现难点:
难点1:获取磁盘驱动器目录,初始化磁盘目录下拉列表。
代码如下:
void CFileView::InitDirveBox() { TCHAR DirverName[MAX_PATH]; TCHAR szDirvers[MAX_PATH]; TCHAR szTemp[MAX_PATH]; DWORD Len=GetLogicalDriveStrings(sizeof(szDirvers)/sizeof(TCHAR),szDirvers); DWORD index=0; DWORD cnt=0; CString strDirve,strname; if(!Len) return; while(cnt<Len) { szTemp[index]=szDirvers[cnt]; if(szTemp[index]==0) { index=0; strDirve=szTemp; strDirve.MakeUpper(); if(DRIVE_CDROM==GetDriveType(szTemp)) { strname=_T(" CD驱动器"); strDirve+=strname; } else if(GetVolumeInformation(szTemp, DirverName, MAX_PATH, NULL, NULL, NULL, NULL, 0)) { strname=DirverName; strDirve+=strname; } m_CtrlDirveCombo.AddString(strDirve); } else { index++; } cnt++; } m_CtrlDirveCombo.SetCurSel(0); }
2.文件目录树的初始化
难点1:保存目录树上的内容
有两种思路,一是保存主要节点到磁盘文件,然后再程序加载时动态搜索节点对应的目录;而是将目录树整体串行化到磁盘文件。
这里我采用第二种思路。序列化目录树的函数,取自http://www.codeguru.com/上的一个例子。
串行化目录树函数代码如下:
//串行化树形控件 void CTreeCtrlEx::Serialize(CArchive& ar) { DWORD data; if (ar.IsStoring()) { // storing code ar<<m_bSaveData; HTREEITEM hti = GetRootItem(); while( hti ) { int indent = GetIndentLevel( hti ); while( indent-- ) ar.WriteString( _T("\t") ); ar.WriteString( GetItemText( hti ) + _T("\r\n"));//保存节点字符串 if(m_bSaveData) //是否保存节点的数值 ar<<GetItemData( hti ); hti = GetNextItem( hti ); } } else { // loading code CString sLine,strtext; ar>>m_bSaveData; if( !ar.ReadString( sLine ) ) return; HTREEITEM hti = NULL; int indent, baseindent = 0; while( sLine[baseindent] == '\t' ) baseindent++; do { if( sLine.GetLength() == 0 ) continue; for( indent = 0; sLine[indent] == _T('\t'); indent++ ) ; // We don't need a body sLine = sLine.Right( sLine.GetLength() - indent ); indent -= baseindent; HTREEITEM parent; int previndent = GetIndentLevel( hti ); if( indent == previndent) parent = GetParentItem( hti ); else if( indent > previndent ) parent = hti; else { int nLevelsUp = previndent - indent; parent = GetParentItem( hti ); while( nLevelsUp-- ) parent = GetParentItem( parent ); } hti = InsertItem( sLine, parent ? parent:TVI_ROOT,TVI_LAST); if(m_bSaveData) { ar>>data; SetItemData(hti,data); } }while( ar.ReadString( sLine ) ); hti = GetRootItem(); Expand(hti,TVE_EXPAND); } } int CTreeCtrlEx::GetIndentLevel( HTREEITEM hItem ) { int iIndent = 0; while( (hItem = GetParentItem( hItem )) != NULL ) iIndent++; return iIndent; } // GetNextItem - Get next item as if outline was completely expanded // Returns - The item immediately below the reference item // hItem - The reference item HTREEITEM CTreeCtrlEx::GetNextItem( HTREEITEM hItem ) { HTREEITEM hti; if( ItemHasChildren( hItem ) ) return GetChildItem( hItem ); // return first child else{ // return next sibling item // Go up the tree to find a parent's sibling if needed. while( (hti = GetNextSiblingItem( hItem )) == NULL ){ if( (hItem = GetParentItem( hItem ) ) == NULL ) return NULL; } } return hti; }
磁盘驱动目录的保存就不需要了,可以通过保存的目录树内容的根目录,来选取磁盘驱动目录,代码如下:
//填充文件视图中的控件
void CFileView::FillFileView()
{
InitDirveBox();
InitFilterBox();
InitDirTree(m_strSavePath);
HTREEITEM hroot=m_CtrlDirTree.GetRootItem();
CString strDrive=m_CtrlDirTree.GetItemText(hroot);
CString strDir;
int cnt=m_CtrlDirveCombo.GetCount();
int index=0;
//根据目录树内容 为磁盘驱动目录下拉列表选择合适的初始值
for(index=0;index<cnt;index++)
{
m_CtrlDirveCombo.GetLBText(index,strDir);
if(strDir.Find(strDrive)!=-1)
{
m_CtrlDirveCombo.SetCurSel(index);
break;
}
}
}
难点二:搜索目录
为了提高程序效率,我们应该搜索文件目录时,只搜索当前路径而不包括子目录,仅当用户双击父目录时,采取搜索子目录,这样可以避免一次把全部文件或者目录搜索到位的时间开销和不必要的处理,因为用户不一定每个文件目录都关心。
搜索目录的代码改自MSDN,如下:
void CFileView::BrowseDir(LPCTSTR pstr,HTREEITEM hParent ) { CFileFind finder; // build a string with wildcards CString strWildcard(pstr); strWildcard += _T("\\*.*"); // start working for files BOOL bWorking = finder.FindFile(strWildcard); while (bWorking) { bWorking = finder.FindNextFile(); // skip . and .. files; otherwise, we'd // recur infinitely! if (finder.IsDots()) continue; // if it's a directory, recursively search it if (finder.IsDirectory()) { CString strDir = finder.GetFilePath(); //仅取当前路径名 去掉父路径名 CString tempDir; tempDir=strDir.Mid(strDir.ReverseFind(_T('\\'))+1); //以父节点为根节点,插入新节点 m_CtrlDirTree.InsertItem(tempDir,0,0,hParent); } } m_CtrlDirTree.Expand(hParent,TVE_EXPAND); finder.Close(); }
难点三:获取指定节点路径
由于在目录树上不显示完全路径,因此用户双击或单击某个节点时,需要重新计算该节点的路径。
代码如下:
CString CFileView::GetItemParentPath(HTREEITEM htitem) { HTREEITEM hParent,hCur,hRoot; CString strdir,strTemp; strdir=_T(""); hCur=htitem; hRoot=m_CtrlDirTree.GetRootItem(); if(htitem==hRoot) return strdir; while( (hParent=m_CtrlDirTree.GetParentItem(hCur))!=NULL) { strTemp=m_CtrlDirTree.GetItemText(hParent); if(strTemp.Find(_T('\\'))==-1) strTemp+=_T('\\'); strTemp+=strdir; strdir=strTemp; hCur=hParent; } return strdir; }
通过获取父路径名,在获取自身路径名,两个字符串相加记得完整路径名。
3.文件列表的初始化
难点:匹配多个通配符
可以看出Editplus可以支持多个文件类型的筛选,因此必须支持多个通配符的筛选。
程序中指定的通配符格式为:
("Media Files (*.mp3;*.rmvb;*.avi;*.wmv;*.mp4;*.rm;*.mpg;*.flv)")
当用户选择了文件目录和通配符后,用该根据这两项内容来筛选指定目录下文件.
从用户通配符字符串解析出单个通配符的代码如下:
//从含有多个通配符的字符串解析出单个通配符 BOOL CFileView::GetWildCardFromFilterStr(LPCTSTR lpFilterStr, CStringArray &WildCardArray) { int start,curpos,end,postemp; CString StrWildCard=lpFilterStr; CString strwd; start=StrWildCard.Find(_T("(")); end=StrWildCard.Find(_T(")")); if(start==-1 || end==-1||start>end) return FALSE; curpos=start+1; while(curpos<end) { postemp=StrWildCard.Find(_T(";"),curpos); if(postemp==-1) postemp=end; strwd=StrWildCard.Mid(curpos,postemp-curpos); WildCardArray.Add(strwd); curpos=postemp+1; } return TRUE; }
根据指定目录和通配符初始化文件列表代码如下:
//根据指定路径和通配符字符串初始化文件列表 BOOL CFileView::InitFileList(LPCTSTR lpPathName,LPCTSTR lpszFilter) { TCHAR lpszOldPath[MAX_PATH]; CString strFileType=lpszFilter; CString strType; ::GetCurrentDirectory(MAX_PATH, lpszOldPath); if(::SetCurrentDirectory(lpPathName)) { m_CtrlFileList.ResetContent(); int cnt,index; CStringArray strWDArray; CString strwd; //解析出通配符,添加相应类型的文件 if(GetWildCardFromFilterStr(lpszFilter,strWDArray)) { cnt=strWDArray.GetSize(); for(index=0;index<cnt;index++) { strwd=strWDArray.GetAt(index); m_CtrlFileList.Dir(DDL_READWRITE,strwd); } } ::SetCurrentDirectory(lpszOldPath); return TRUE; } else { MessageBox(_T("无法更改当前目录!")); return FALSE; } }
4.文件类型筛选器实现
难点:自定义文件类型的支持
当用户选择筛选器中自定义类型时,弹出对话框供用户输入文件类型,效果如下图所示:
筛选后文件列表为:
实现方法,首先构造一个筛选器自定义对话框CFilterDlg(class CFilterDlg : public CDialogEx)
然后判断当用户选择自定义时弹出对话框供用户选择。
实现代码如下:
//用户选择更改文件类型的处理 void CFileView::OnCloseupFilterCombo() { // TODO: Add your control notification handler code here int cnt=m_CtrlFilterCombo.GetCount(); int cur=m_CtrlFilterCombo.GetCurSel(); //注意"自定义"字符串在筛选器中的位置放在末尾 列表控件创建时取消CBS_SORT风格防止不是在末尾位置 //用户选择自定义类型时,弹出对话框供用户输入类型 if(cur==cnt-1) { CFilterDlg filterdlg; CString strtype,strtemp,strpretype; m_CtrlFilterCombo.GetLBText(cur,strpretype); int start,end; start=strpretype.Find( _T('(') ); end=strpretype.ReverseFind( _T(')') ); filterdlg.m_strFilter=strpretype.Mid(start+1,end-start-1);//初始化输入框 if(IDOK==filterdlg.DoModal()) { strtype=filterdlg.m_strFilter; int len=strtype.GetLength(); if(strtype.GetAt(len-1)==_T(';')) strtype.Delete(len-1); strtype+=_T(")"); strtemp=_T("自定义("); strtemp+=strtype; m_CtrlFilterCombo.DeleteString(cur); m_CtrlFilterCombo.AddString(strtemp); } m_CtrlFilterCombo.SetCurSel(cur); } //根据用户类型重新筛选文件 HTREEITEM htitem = m_CtrlDirTree.GetSelectedItem(); if(NULL!=htitem) { m_CtrlDirTree.SelectItem(htitem); CString strdir=GetItemParentPath(htitem); CString strcurpath=m_CtrlDirTree.GetItemText(htitem); strdir+=_T('\\'); strdir+=strcurpath; CString strFileType=GetFilterString(); InitFileList(strdir,strFileType); } }
5.动态创建控件消息的捕获
以磁盘驱动目录下拉列表关闭事件为例,其动态捕获需要添加以下代码:
step1:消息响应函数
//选择了驱动目录 afx_msg void OnCloseupDirveCombo();
step2:消息映射
BEGIN_MESSAGE_MAP(CFileView, CDockablePane) ON_CBN_CLOSEUP(ID_COMBO_DIRVE, OnCloseupDirveCombo) END_MESSAGE_MAP()
step3:消息处理函数
void CFileView::OnCloseupDirveCombo() { // TODO: Add your control notification handler code here long index=m_CtrlDirveCombo.GetCurSel(); CString strdir; m_CtrlDirTree.DeleteAllItems(); m_CtrlDirveCombo.GetLBText(index,strdir); strdir=strdir.Left(strdir.ReverseFind(_T('\\'))+1); HTREEITEM hroot=m_CtrlDirTree.InsertItem(strdir,0,0,TVI_ROOT); BrowseDir(strdir,hroot); CString strFileType=GetFilterString(); InitFileList(strdir,strFileType); }
注意使用CDockablePane和动态创建控件时资源ID的重要性。
6.VS2010中类CDockablePane的特性的束缚
使用CDockablePane类仍然存在很多不方便的地方,容易出错.
易错点1:面板区域的更新问题
CDockablePane如果更新不好,容易出现其他窗口的重影,这里需要重载OnPaint函数,代码如下:
void CFileView::OnPaint() { CPaintDC dc(this); // device context for painting // TODO: 在此处添加消息处理程序代码 // 不为绘图消息调用 CDockablePane::OnPaint() CRect rectUpdate; GetClientRect(rectUpdate);//计算客户区的方法 不是很精确 不过方便 也可以通过计算控件坐标来精确计算更新区域 rectUpdate.InflateRect(1, 1); dc.Draw3dRect(rectUpdate, ::GetSysColor(COLOR_3DSHADOW), ::GetSysColor(COLOR_3DSHADOW)); }
易错点2:面板控件大小调整问题
当用户调整面板大小时,需要动态的调整控件大小和位置,代码如下:
//调整CListBox控件水平滚动 void CFileView::AdjusrHorzScroll() { CClientDC dc(this); CFont* pOldFont = dc.SelectObject(&afxGlobalData.fontRegular); int cxExtentMax = 0; for (int i = 0; i < m_CtrlFileList.GetCount(); i ++) { CString strItem; m_CtrlFileList.GetText(i, strItem); cxExtentMax = max(cxExtentMax, dc.GetTextExtent(strItem).cx); } m_CtrlFileList.SetHorizontalExtent(cxExtentMax); dc.SelectObject(pOldFont); } //调整各个控件大小和位置 void CFileView::AdjustLayout() { if (GetSafeHwnd() == NULL) { return; } CRect rectClient; CString msg; GetClientRect(rectClient); int height=rectClient.Height(); //一下数值并无精确计算 你可以按你需求重新计算 使用更科学方法 m_CtrlDirveCombo.SetWindowPos(NULL, rectClient.left, rectClient.top, rectClient.right,rectClient.top+30*height/100, SWP_NOACTIVATE | SWP_NOZORDER); m_CtrlDirTree.SetWindowPos(NULL, rectClient.left,rectClient.top+25, rectClient.right, rectClient.top+45*height/100, SWP_NOACTIVATE | SWP_NOZORDER); m_CtrlFilterCombo.SetWindowPos(NULL, rectClient.left, rectClient.top+45*height/100+25, rectClient.right, rectClient.top+45*height/100+60, SWP_NOACTIVATE | SWP_NOZORDER); m_CtrlFileList.SetWindowPos(NULL, rectClient.left, rectClient.top+45*height/100+47, rectClient.right, rectClient.top+height, SWP_NOACTIVATE | SWP_NOZORDER); AdjusrHorzScroll(); } //面板大小调整事件响应函数 void CFileView::OnSize(UINT nType, int cx, int cy) { CDockablePane::OnSize(nType, cx, cy); AdjustLayout(); // TODO: 在此处添加消息处理程序代码 }
疑点:CListbox控件的滚动条和位置创建难以控制
创建四个控件代码如下:
int CFileView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CDockablePane::OnCreate(lpCreateStruct) == -1) return -1; // TODO: 在此添加您专用的创建代码 const DWORD dwDirveComboStyle = WS_CHILD | WS_VISIBLE |WS_BORDER|WS_VSCROLL|CBS_DROPDOWNLIST|CBS_SORT|CBS_NOINTEGRALHEIGHT; const DWORD dwTreeStyle =WS_CHILD | WS_VISIBLE |WS_BORDER|TVS_SHOWSELALWAYS|TVS_SINGLEEXPAND|TVS_EDITLABELS; const DWORD dwListBoxStyle =WS_CHILD| WS_VISIBLE| WS_BORDER|WS_HSCROLL|WS_VSCROLL|LBS_NOINTEGRALHEIGHT|LBS_NOTIFY; const DWORD dwFilterComboStyle = WS_CHILD|WS_VISIBLE |WS_BORDER|WS_VSCROLL|WS_HSCROLL|CBS_DROPDOWNLIST|CBS_NOINTEGRALHEIGHT; CRect rectDummy; rectDummy.SetRectEmpty(); //注意这里的rectDummy实际上意义不大 最终决定控件位置的是AdjustLayout函数 // 创建驱动器目录下拉框 if (!m_CtrlDirveCombo.Create(dwDirveComboStyle, rectDummy, this, ID_COMBO_DIRVE)) { TRACE0("未能创建驱动器目录列表\n"); return -1; // 未能创建height } //创建文件目录树 if (!m_CtrlDirTree.Create(dwTreeStyle,rectDummy, this, ID_TREE_DIR)) { TRACE0("未能创建文件目录树\n"); return -1; // 未能创建 } //创建文件列表 if (!m_CtrlFileList.Create(dwListBoxStyle,rectDummy, this, ID_LISTBOX_FILE)) { TRACE0("未能创建文件列表\n"); return -1; // 未能创建 } //创建筛选器下拉列表 if (!m_CtrlFilterCombo.Create(dwFilterComboStyle,rectDummy, this, ID_COMBO_FILTER)) { TRACE0("未能创建筛选器列表\n"); return -1; // 未能创建 } // 加载视图图像 HICON icon=AfxGetApp()->LoadIcon(IDI_ICON_FOLDER); //创建图像列表控件 m_FileViewImages.Create(32,32,ILC_MASK |ILC_COLOR32,1,1); m_FileViewImages.Add(icon); //把图标载入图像列表控件 m_FileViewImages.SetBkColor (RGB(255,255,255)); m_CtrlDirTree.SetImageList(&m_FileViewImages,TVSIL_NORMAL); AdjustLayout(); FillFileView(); return 0; }
虽然一直为CListBox计算大小,设定分格,但是CListBox的滚动条和边界就是出不来,相同的创建风格在另外一个文件目录程序中却出来了带边界和滚动条的正确的效果,如下图:
不知为何,CListbox风格也尝试多遍了,反正CListBox的位置一直不正确或者CListBox的边界和滚动条不出来,因此只好把CListBox即程序中的文件列表放在最下面,本应该放在筛选器上面的,这点很无奈。
CFileView类设计如下代码所示:
// CFileView #include "TreeCtrlEx.h" class CFileView : public CDockablePane { DECLARE_DYNAMIC(CFileView) public: CFileView(); void AdjustLayout();//调整控件大小 void AdjusrHorzScroll();//调整列表控件水平滚动宽度 virtual ~CFileView(); CString GetAppPath(void);//获取应用程序路径 void SetSavePath(LPCTSTR lpPathName);//设置目录树内容保存路径 // 特性 protected: CTreeCtrlEx m_CtrlDirTree;//目录树控件 CComboBox m_CtrlDirveCombo;//磁盘驱动目录下拉列表控件 CListBox m_CtrlFileList;//文件列表 CComboBox m_CtrlFilterCombo;//文件筛选器下拉列表 CImageList m_FileViewImages;//图标 CString m_strSavePath;//目录树内容保存路径 protected: //初始化筛选器 void InitFilterBox(); //初始化文件列表 BOOL InitFileList(LPCTSTR lpPathName,LPCTSTR lpszFilter); //初始化驱动目录 void InitDirveBox(); //初始化目录树 BOOL InitDirTree(LPCTSTR lpPathName); //重通配符字符串解析出单个通配符 BOOL GetWildCardFromFilterStr(LPCTSTR lpFilterStr, CStringArray &WildCardArray); //获取目录树所选条目的路径 因为在目录上隐藏了目录的完全路径 CString GetItemParentPath(HTREEITEM htitem); //浏览目录 将子目录添加到目录树节点上 void BrowseDir(LPCTSTR pstr,HTREEITEM hParent = TVI_ROOT); //获取筛选器上的通配符 CString GetFilterString(); //填充文件视图 void FillFileView(); //保存目录树内容 void CFileView::SerializeDirTree(LPCTSTR lpPathName); protected: DECLARE_MESSAGE_MAP() public: //创建视图函数 afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); //调整视图大小 需要重载来调整控件位置 afx_msg void OnSize(UINT nType, int cx, int cy); //选择了驱动目录 afx_msg void OnCloseupDirveCombo(); //用户双击目录树控件 afx_msg void OnDblclkDirTree(NMHDR* pNMHDR, LRESULT* pResult); //用户选择了筛选器 afx_msg void OnCloseupFilterCombo(); //用户编辑目录树上条目 afx_msg void OnEndlabeleditTreeDir(NMHDR* pNMHDR, LRESULT* pResult); //用户在目录树上单击右键 afx_msg void OnRclickDirTree(NMHDR* pNMHDR, LRESULT* pResult); //用户双击文件列表 afx_msg void OnDblclkFileList(); //文件视图的更新 需要重载来重绘控件 防止其他窗口的覆盖后重影 afx_msg void OnPaint(); //文件视图的销毁 需要重载来保存目录树内容 afx_msg void OnDestroy(); };
对应的类图如下图所示: