【題外話】
最近做了個.NET 4.0平台的程序,一直在Win7/8上運行的好好的,結果用戶說XP上說有問題,於是我就改了下程序,增加了記log的功能然后發給用戶。log的目錄是根據Environment.CurrentDirectory得出的。要求用戶運行完程序以后將log發回給我,但用戶始終找不到這個文件。
【文章索引】
我的程序打開后需要先用OpenFileDialog打開一個文件,之后開始記錄日志,由於之前習慣使用Environment.CurrentDirectory獲取程序的當前路徑,所以這次也理所當然地把日志文件放在這個目錄之下。這個目錄在Win7/Win8下一直都是程序運行的目錄,但是在XP下卻不是。
從MSDN上可以找到Environment.CurrentDirectory作用的說明,一種是“獲取和設置當前目錄(即該進程從中啟動的目錄)的完全限定路徑.”(.NET Framework 2.0),另一種是“獲取或設置當前工作目錄的完全限定路徑.”(.NET Framework 3.5+)。初一看感覺這倆說明完全是不同的,但仔細想想也不可能出現不同,因為.NET 3.5和2.0不僅僅是同一個CLR,並且Environment.CurrentDirectory更是位於mscorlib.dll中,應該不會出現.NET 3.5和2.0不同的問題。
帶着好奇心,我去翻看mscorlib.dll的源代碼,發現Environment.CurrentDirectory是這么寫的:
1 public static String CurrentDirectory { 2 [ResourceExposure(ResourceScope.Machine)] 3 [ResourceConsumption(ResourceScope.Machine)] 4 get{ 5 return Directory.GetCurrentDirectory(); 6 } 7 8 [ResourceExposure(ResourceScope.Machine)] 9 [ResourceConsumption(ResourceScope.Machine)] 10 set { 11 Directory.SetCurrentDirectory(value); 12 } 13 }
曾經從網上看到過一些介紹.NET中獲取當前目錄方法的文章,其中關於IO.Directory.GetCurrentDirectory()的描述基本上都是獲取程序的工作目錄,或者有的文章也說不清除獲取的是什么。但對於Environment.CurrentDirectory獲取的內容,基本上都說是程序當前目錄,或者直接照搬MSDN上的說明。
從上述源代碼看,其實這倆獲取到的是一樣的內容。而IO.Directory.GetCurrentDirectory()的代碼則如下所示:
1 [ResourceExposure(ResourceScope.Machine)] 2 [ResourceConsumption(ResourceScope.Machine)] 3 public static String GetCurrentDirectory() 4 { 5 StringBuilder sb = new StringBuilder(Path.MAX_PATH + 1); 6 if (Win32Native.GetCurrentDirectory(sb.Capacity, sb) == 0) 7 __Error.WinIOError(); 8 String currentDirectory = sb.ToString(); 9 if (currentDirectory.IndexOf('~') >= 0) { 10 //省略部分代碼 11 } 12 String demandPath = GetDemandDir(currentDirectory, true); 13 new FileIOPermission( FileIOPermissionAccess.PathDiscovery, new String[] { demandPath }, false, false ).Demand(); 14 return currentDirectory; 15 }
從代碼里看到,GetCurrentDirectory其實調用的WIN32 API獲取的當前目錄,其調用的是kernel32.dll中的GetCurrentDirectory()。
1 [DllImport(KERNEL32, SetLastError=true, CharSet=CharSet.Auto, BestFitMapping=false)] 2 [ResourceExposure(ResourceScope.Machine)] 3 internal static extern int GetCurrentDirectory(int nBufferLength, StringBuilder lpBuffer);
可見,通過Environment.CurrentDirectory或者Directory.GetCurrentDirectory()獲取到的並不是由.NET控制的返回值,而是操作系統返回的。同時,這個屬性本身就不是程序的啟動目錄,而應當是程序的工作目錄,比如在快捷方式的屬性中是可以修改這個路徑的。
剛才從GetCurrentDirectory()入手我們只能知道這個值由系統控制,但並不能得知到底出了什么問題。不過,既然是因為使用OpenFileDialog后導致當前目錄發生了變化,那就從OpenFileDialog入手,網上也有類似的文章,見相關鏈接2。
OpenFileDialog有一個很有意思的屬性,叫RestoreDirectory,MSDN上的說明是“獲取或設置一個值,該值指示對話框在關閉前是否還原當前目錄. (從 FileDialog 繼承.)”,而且經過測試發現,RestoreDirectory設置為true以后再執行文件對話框是不會更改當前目錄的,所以有人推測Win7默認的RestoreDirectory為true,而XP默認的為false。
但經過測試發現,不論什么操作系統(以2k3和Win8為例),.NET中文件對話框的RestoreDirectory默認值都是False,如下圖。
圖中標題為OpenFileDialog默認的RestoreDirectory的值,文本框中第一個路徑為OpenFileDialog.Show之前的Environment.CurrentDirectory,第二個路徑為Show之后的路徑,測試的時候均手動修改了RestoreDirectory的值(即每組第一個圖手動設置為false,第二個圖設置為true),測試代碼如下:
1 private void Form1_Load(object sender, EventArgs e) 2 { 3 this.Text = String.Format("Default [RestoreDirectory]={0}", dlgOpen.RestoreDirectory); 4 5 this.chkRestoreDir.Checked = this.dlgOpen.RestoreDirectory; 6 } 7 8 private void btnTest_Click(object sender, EventArgs e) 9 { 10 StringBuilder sb = new StringBuilder(); 11 12 sb.AppendLine(String.Format("[Environment.CurrentDirectory]={0}", Environment.CurrentDirectory)); 13 sb.AppendLine(String.Format("[RestoreDirectory]={0}", dlgOpen.RestoreDirectory)); 14 15 dlgOpen.ShowDialog(); 16 sb.AppendLine(String.Format("[Dialog.FileName]={0}", dlgOpen.FileName)); 17 sb.AppendLine(String.Format("[Environment.CurrentDirectory]={0}", Environment.CurrentDirectory)); 18 19 this.txtInfo.Text = sb.ToString(); 20 } 21 22 private void chkRestoreDir_CheckedChanged(object sender, EventArgs e) 23 { 24 this.dlgOpen.RestoreDirectory = this.chkRestoreDir.Checked; 25 }
為了驗證是不是.NET的問題,我用VC++和MFC又寫了一個新的程序來重新測試一遍,參數均按照.NET上的參數創建,結果如下圖。
仍然與在.NET上測試的結果相同,看來不是.NET的原因,而是系統的原因了。同時,我又搜索了微軟公開的.NET Framework的代碼,也並未發現在FileDialog中修改了CurrentDirectory的值。 所以,如果需要使用Environment.CurrentDirectory等獲取工作目錄,那么最好設置RestoreDirectory為true,以保證在任何平台都沒有問題;反之,如果想始終獲取當前的目錄,那么遇到FileDialog時自己手動SetCurrentDirectory下吧。
附,C++測試用的關鍵代碼:
1 CString info, path, T("True"), F("False"); 2 GetCurrentDirectory(0x105, path.GetBuffer(0x105)); 3 path.ReleaseBuffer(); 4 info = "[Environment.CurrentDirectory]="; 5 info = info + path; 6 info = info + "\r\n[RestoreDirectory]=" + (chkRestoreDir.GetCheck() == TRUE ? T : F); 7 8 CFileDialog *dlgOpen; 9 dlgOpen = new CFileDialog(TRUE, (LPCTSTR)"", (LPCTSTR)"", OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | (chkRestoreDir.GetCheck() ? OFN_NOCHANGEDIR : 0), (LPCTSTR)""); 10 ; 11 if (dlgOpen->DoModal() == IDOK) 12 { 13 CString fileName; 14 fileName = dlgOpen->GetPathName(); 15 info = info + "\r\n[Dialog.FileName]=" + fileName; 16 } 17 18 delete dlgOpen; 19 20 GetCurrentDirectory(0x105, path.GetBuffer(0x105)); 21 path.ReleaseBuffer(); 22 23 info = info + "\r\n[Environment.CurrentDirectory]=" + path; 24 InfoText = info; 25 UpdateData(false);
對於WinForm應用程序,其實可以使用System.Windows.Forms.Application.StartupPath來獲取程序所在的目錄,MSDN上是這么說明的“獲取啟動了應用程序的可執行文件的路徑,不包括可執行文件的名稱.”,其實現代碼如下:
1 public static string StartupPath { 2 get { 3 if (startupPath == null) { 4 StringBuilder sb = new StringBuilder(NativeMethods.MAX_PATH); 5 UnsafeNativeMethods.GetModuleFileName(NativeMethods.NullHandleRef, sb, sb.Capacity); 6 startupPath = Path.GetDirectoryName(sb.ToString()); 7 } 8 Debug.WriteLineIf(IntSecurity.SecurityDemand.TraceVerbose, "FileIO(" + startupPath + ") Demanded"); 9 new FileIOPermission(FileIOPermissionAccess.PathDiscovery, startupPath).Demand(); 10 return startupPath; 11 } 12 }
而其中的GetModuleFileName其實也是調用的WIN32 API,具體如下:
1 [DllImport(ExternDll.Kernel32, CharSet=CharSet.Auto)] 2 public static extern int GetModuleFileName(HandleRef hModule, StringBuilder buffer, int length);
而GetModuleFileName其實是獲取執行程序的完整路徑,Application.StartupPath通過此獲取的目錄一定是程序所在的目錄。
【相關鏈接】
- Environment.CurrentDirectory 屬性:http://msdn.microsoft.com/zh-cn/library/system.environment.currentdirectory.aspx
- OpenFileDialog在XP會更改working directory:http://swaywang.blogspot.com/2012/06/copenfiledialogxpworking-directory.html