關於C#中程序當前目錄的小隨筆


【題外話】
最近做了個.NET 4.0平台的程序,一直在Win7/8上運行的好好的,結果用戶說XP上說有問題,於是我就改了下程序,增加了記log的功能然后發給用戶。log的目錄是根據Environment.CurrentDirectory得出的。要求用戶運行完程序以后將log發回給我,但用戶始終找不到這個文件。

 

【文章索引】

  1. 奇怪的目錄
  2. 奇怪的RestoreDirectory
  3. 系統的問題
  4. 其他獲取方式

 

【1、奇怪的目錄】

我的程序打開后需要先用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控制的返回值,而是操作系統返回的。同時,這個屬性本身就不是程序的啟動目錄,而應當是程序的工作目錄,比如在快捷方式的屬性中是可以修改這個路徑的。

 

【2、奇怪的RestoreDirectory】

剛才從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 }

 

【3、系統的問題】

為了驗證是不是.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);

 

【4、其他獲取方式

對於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通過此獲取的目錄一定是程序所在的目錄。

 

【相關鏈接】

  1. Environment.CurrentDirectory 屬性:http://msdn.microsoft.com/zh-cn/library/system.environment.currentdirectory.aspx
  2. OpenFileDialog在XP會更改working directory:http://swaywang.blogspot.com/2012/06/copenfiledialogxpworking-directory.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM