15. 檔案的輸出與輸入
如何重新命名、複製或是刪除檔案?
Visual Basic有三個陳述式可以用來重新命名、複製以及刪除檔案:Name用來對檔案重新命名,FileCopy用來複製檔案,而Kill用來刪除檔案。我們用以下這段程式介紹如何使用上述三個陳述式。
當Form_Click被執行時,這個程式會產生一個簡單的文字檔FILEA,把FILEA重新命名為FILEB,複製FILEB到這個程式所在的目錄,新檔取名為FILEC.TXT,然後顯示FILEC.TXT的內容,最後刪除FILEB和FILEC.TXT。
Option Explicit Private Sub Form_Click() Dim strA As String Dim intFileNum As Integer `Create a small file intFileNum = FreeFile Open "FILEA" For Output As #intFileNum Print #intFileNum, "This is a testDear John, How Do I... " Close #intFileNum `Rename file Name "FILEA" As "FILEB" `Copy file FileCopy "FILEB", App.Path & "\FILEC.TXT" `Read and display resulting file Open App.Path & "\FILEC.TXT" For Input As #intFileNum Line Input #intFileNum, strA Close #intFileNum MsgBox strA, vbOKOnly, "Contents of the Renamed and Copied File" `Delete files Kill "FILEB" Kill App.Path & "\FILEC.TXT" End Sub
如圖15-1所示,檔案的內容被顯示在一個訊息方塊中,當你按下「確定」鈕之後,FILEB和FILEC.TXT就會被刪除。
圖15-1 FILEC.TXT的內容 |
如何處理目錄和路徑?
Visual Basic有幾個陳述式用來摩仿MS-DOS中有關處理目錄和路徑的指令,以下幾個小節的重點在討論這些陳述式。
MkDir、ChDir和RmDir
MkDir陳述式會產生新的目錄,ChDir變換目前的工作目錄,而RmDir移除一個目錄。不管是在MS-DOS的模式下或是在Visual Basic的應用程式中,這三個指令的使用方法都是一樣的。Visual Basic的線上說明對它們有十分詳細的說明。
CurDir和App.Path
CurDir函式可以用來判斷目前的工作目錄,而APP物件的Path屬性則提供了應用程式所在的目錄。一般而言,對我們比較有用的資訊是應用程式所在的目錄,與應用程式相關的資料檔都會存放在這裡。以下這兩行程式可以幫你判斷目前的工作目錄以及應用程式所在的目錄。
strCurDirectory = CurDir strAppDirectory = App.Path
Dir
對於檔案或目錄的搜尋,Visual Basic的Dir函式是一個相當有用的工具,在這裡我們要特別介紹Dir函式的一個特性。假設在某個程式中第一次呼叫Dir函式時,我們用路徑名稱或檔案名稱作為函式的引數,而同一個程式中第二次呼叫Dir函式時不加任何引數,那麼第一次的呼叫會傳回第一個合乎搜尋條件的檔案名稱,而第二次的呼叫則會傳回下一個合乎同一個搜尋條件的檔案名稱,這個特性使得我們可以對目錄內的檔案作循序式的搜尋,我們用以下這段程式介紹這個特性。這個程式列出所有的一般檔案(不含隱藏檔、系統檔和目錄檔),把這些檔案名稱存在一個叫FILETREE.TXT的文字裡。
現在請你在一張新的表單上加入以下這兩個程序,然後加以執行。
Option Explicit Sub RecurseTree(CurrentPath As String) Dim intN As Integer, intDirectory As Integer Dim strFileName As String, strDirectoryList() As String `First list all normal files in this directory strFileName = Dir(CurrentPath) Do While strFileName <> "" Print #1, CurrentPath & strFileName strFileName = Dir Loop `Next build temporary list of subdirectories strFileName = LCase(Dir(CurrentPath, vbDirectory)) Do While strFileName <> "" `Ignore current directory, parent directory, and `Windows NT page file If strFileName <> "." And strFileName <> ".." And _ strFileName <> "pagefile.sys" Then `Ignore nondirectories If GetAttr(CurrentPath & strFileName) _ And vbDirectory Then intDirectory = intDirectory + 1 ReDim Preserve strDirectoryList(intDirectory) strDirectoryList(intDirectory) = CurrentPath _ & strFileName End If End If strFileName = Dir `Process other events DoEvents Loop `Recursively process each directory For intN = 1 To intDirectory RecurseTree strDirectoryList(intN) & "\" Next intN End Sub Private Sub Form_Load() Dim strStartPath As String Me.Show Print "WorkingDear John, How Do I... " Me.MousePointer = vbHourglass strStartPath = "C:\" Open "C:\FILETREE.TXT" For Output As #1 RecurseTree strStartPath Close #1 Me.MousePointer = vbDefault Unload Me End Sub
這個程式也說明了Visual Basic中遞迴程序(Recursive Procedure)的用法,程式中遞迴呼叫的次數取決於次目錄的階層深度。你也許會有一個疑問:為什麼在RecurseTree中,我們要用一個動態陣列來存放次目錄的名稱,最後再一一對每個次目錄進行搜尋,而不是每遇到一個次目錄就遞迴呼叫RecursTree一次?如果按照後面這方式來處理次目錄,第二個不加引數的Dir函式呼叫會造成問題。我們知道,不加引數的Dir函式會依照找到前一個檔案的條件去搜尋下一個檔案,但它不會記住目前的工作目錄。因此,當程式由遞迴程序中返回之後,Dir函式就找不回原來工作目錄中它原來所在的位置了。
如果按照現有的處理方式,在每一次遞迴呼叫中,用一個字串保留住所有在該目錄中的次目錄,那麼Dir函式不會有迷路的問題。
圖15-2是我們用「記事本」打開FILETREE.TXT時所顯示的內容。
圖15-2 WordPad中的FILETREE.TXT |
如何執行快速的檔案輸出輸入?
要增進檔案輸出入工作的速度,最好的方法就是儘量使用二進位檔案,即使是純文字檔,你也可以把它當作二位進位檔案來處理。
另外,在複製檔案時,請記得盡量利用FileCopy指令,而不要在程式載入一整個檔案,然後寫到另一個檔案中去。
如何使用二進位檔?
Visual Basic對於二進位檔的處理十分地有效率,筆者偏好以二進位檔模式下的Get和Put指令處理各種類型檔案。雖然以二進位檔模式來處理一般的檔案需要做額外資料處理動作,但就整個檔案處理的工作而言,使用二進位檔模式下的指令會比使用傳統的Input及Output方式更有效率。到了現在的Visual Basic,UDT資料結構以及大容量的字串(如在32位元作業系統中的字串)更使得二進位檔模式的檔案處理方法更具效率。
UDT資料結構
二進位檔中UDT資料結構的讀寫以一整個記憶區塊作為一次讀寫的單位,我們用以下的範例來說明這一點。當cmdPut指令按鈕被按下時,範例程式將同一筆人事資料寫入10次到EMPLOYEE.TXT檔案中;當cmdGet被按下時,程式把EMPLOYEE.TXT中的10筆記錄全部讀出。
Option Explicit Private Type Employee FirstName As String * 20 MiddleInitial As String * 1 LastName As String * 20 Age As Byte Retired As Boolean Street As String * 30 City As String * 20 State As String * 2 Comments As String * 200 End Type Private Sub cmdPut_Click() Dim Emp As Employee Dim intN Open App.Path & "\EMPLOYEE.TXT" For Binary As #1 For intN = 1 To 10 Emp.Age = 14 Emp.City = "Redmond" Emp.Comments = "He is a smart guy." Emp.FirstName = "Willy" Emp.LastName = "Doors" Emp.MiddleInitial = "G" Emp.Retired = False Emp.State = "WA" Emp.Street = "One Macrohard Way" Put #1, , Emp Next intN Close #1 End Sub Private Sub cmdGet_Click() Dim Emp(1 To 10) As Employee Dim intN As Integer Open App.Path & "\EMPLOYEE.TXT" For Binary As #1 For intN = 1 To 10 Get #1, , Emp(intN) Print Emp(intN).FirstName Next intN Close #1 End Sub
圖15-3所顯示的是Put Data和Get Data兩個指令按鈕被按下後的結果。
圖15-3 在二進位檔案模式下以UDT資料結構讀取資料 |
當程式執行時,EMPLOYEE.TXT檔被讀取了10次,總共讀入10筆資料錄,每次Get函式都讀取一筆Employee資料錄,其長度為296位元位組,因此,這個程序總共讀取了2960位元組。
警告:
按照通則,在二進位檔案模式下,讀出與寫入UDT資料結構時應該要用同一個結構才能確保資料不致流失。從Visual Basic 4以後,為了配合Unicode和32位元的作業環境,Visual Basic對記憶體中及UDT結構中的資料儲存配置(Data Layout)方式作了一些內部的改變,這些改變造成了一些棘手且不可預測的問題。
32位元版的Visual Basic在內部把所有的字串資料以Unicode來處理(包括UDT資料結構中的字串),也就是說,每個字元以2個位元組表示。當程式進行檔案輸出入處理時,Visual Basic會把Unicode字串轉換成每字元一個位元組的ANSI字串,這個轉換使得包含字串型別的UDT資料結構完全改變了其總長度。
另外,在32位元的作業系統中,為了達到輸出入處理的最大效率,UDT結構以每四位元組當作一個記憶體區段,配合這種記憶體區段架構所產生的充填位元(Padding)也改變了UDT資料結構中實際的資料儲存配置情形。
字串
利用字串可以從檔案中快速而有彈性地讀取大量的資料,其實際的做法是以二進位檔案模式開啟一個檔案,然後用Get陳述式把整個區塊的資料一次讀出放入字串變數中。這個字串變數的長度是Get陳述式一次讀取的位元組數目。因此,如果字串變數是一個位元組,Get陳述式只會從檔案中讀取一位元組的資料,如果字串變數是用一萬位元組,則Get陳述式將會讀取一萬個位元組,我們用以下的程式來介紹這個技巧:
Option Explicit Private Sub Form_Click() Dim strA As String * 1 Open App.Path & "\TEST.TXT" For Binary As #1 Get #1, , strA Print strA Close #1 End Sub Private Sub Text1_Click() Dim strB As String Open App.Path & "\TEST.TXT" For Binary As #1 strB = Space(LOF(1)) Get #1, , strB Text1.Text = strB Close #1 End Sub
在Form_Click事件程序中,我們用一個一位元組長的固定字串變數從TEST.TXT中讀取一位元組的資料;另一方面,在Text1_Click事件程序中,字串變數的長度為TEST.TXT的檔案長度,因此,若Get陳述式配合著strB變數使用時,Get陳述式便會讀取整個TEST.TXT的資料,然後存放在strB字串變數裡。
注意:
Microsoft警告過,Unicode字元轉換可能會毀掉從二進位檔載入字串變數中的資料,因此,上述的程式本來應該是不能成功地運作的。然而,經過測試之後我們發現,在檔案中所有256個可能的位元組值都不會產生不可預期的結果。在受測試的檔案中,每一個位元組都被假定為ANSI字元,而且當這些位元組被載入字串變數中以後,每個位元組都多加了一個位元組的0,讓每個字元都變成了Unicode字元。這個Unicode轉換過程並沒有發生如Microsoft所警告的資料流失的問題。雖然一切就如同舊版本的Visual Basic一樣可以安全地運作,但是,未來的版本可能就不是這樣了。為了防止這個問題發生,我們可以用二進位資料的陣列來取代字串變數。
在早期16位元版本的Microsoft Windows中,字串長度大約限制在65535位元組左右,到了今天,Windows 95和Windows NT已經把限制放寬到了二十億個位元組,這表示在多數情況下,你可以把整個檔案的內容用Get陳述式存入一個字串變數中。要注意的是,必須把字串長度設定為檔案的長度,就如同上例中我們用LOF函式來檢查檔案長度一樣。
參考資料:
請參閱
第三十四章"進階應用程式" 中的Secret應用程式有關二進位檔輸出入的部分。
Byte陣列
如果要針對檔案中的每一個位元組逐一進行處理,我們可以使用兩種方法,第一種是把整個檔案載入一個字串變數中(如前一小節所討論的),然後用ASC函式把字串中的每一個字元轉換成它相對應的ASCII值;第二種方式是採用Byte陣列,這種方式會更有效率。
在以下的程式中,我們宣告了一個動態的Byte陣列,它可以根據檔案大小而調整其陣列長度,因此我們可以把用Get陳述式把整個檔案載入到這個陣列中。
Option Explicit Private Sub Form_Click() Dim intN As Integer Open App.Path & "\TEST.TXT" For Binary As #1 ReDim bytBuf(1 To LOF(1)) As Byte Get #1, , bytBuf() For intN = LBound(bytBuf) To UBound(bytBuf) Print Chr(bytBuf(intN)); Next intN Close #1 End Sub
字串與Byte陣列的關係
上面的範例暗示著字串和Byte陣列之間有某種的關聯性,事實上,愈來愈多以前要靠字串完成的工作將會被Byte陣列所取代。
你可以把Byte陣列加入到原來只用字串處理的函式中,例如,在以下的程式中,你可以把字串指定給Byte陣列,也可以把Byte陣列指定給字串變數。
Option Explicit Private Sub Form_Click() Dim intN As Integer Dim strA As String, strC As String Dim bytB() As Byte strA = "ABC" bytB() = strA `Displays 65 0 66 0 67 0 For intN = LBound(bytB) To UBound(bytB) Print bytB(intN); Next intN Print strC = bytB() `Displays ABC Print strC `Notice actual size of string Print strA, Len(strA), LenB(strA) End Sub
圖15-4所顯示的是範例程式執行的結果
圖15-4 字串變數與Byte陣列 |
一般而言,當你想要把字串指定給Byte陣列時,這個Byte陣列應該被宣告為動態陣列,Visual Basic才能自動地擴展或縮小陣列的大小,以配合字串的長度。
請仔細地研讀以上的程式,你會對記憶體中的Unicode字元有更徹底的了解。在程式中,"ABC"字串的長度是三個位元組,程式將字串"ABC"以字串變數strA定給Byte陣列bytB(),然後列印出bytB() 的每一個位元組。預期中列印的結果應該是65、66、67 (ABC的ASCII值),然而,你可以發現事實上bytB() 包含了6個位元組。這是因為這3個位元組長的字串在記憶體中是以6個位元組的Unicode格式儲存的,而且在指定字串給Byte陣列之後,我們並沒有把它轉換為原來的ANSI字串。
接下來,我們把這6個位元組長的Byte陣列指定給另一個字串變數strC在程式的最後,我們用一個Print陳述式印出strA的內容,它的字元數以及它實際的位元組長度,後面的這兩個數字分別由Len和LenB函式所傳回。
StrConv函式
StrConv函式是一個相當有用的函式,它可以在字串資料與Unicode格式之間來去自如地轉換。以下這個程式告訴你如何使用StrConv函式將3個位元組長的字串轉換為3個位元組長的Byte陣列,然後將這個Byte陣列轉換為字串。
Option Explicit Private Sub Form_Click() Dim intN As Integer Dim strA As String Dim bytB() As Byte strA = "ABC" bytB() = StrConv(strA, vbFromUnicode) `Displays 65 66 67 For intN = LBound(bytB) To UBound(bytB) Print bytB(intN); Next intN Print strA = bytB() `This displays a question mark Print strA strA = StrConv(bytB(), vbUnicode) `Displays ABC 3 6 Print strA, Len(strA), LenB(strA) End Sub
圖15-5所顯示的是程式執行的結果。
圖15-5 以StrConv函式處理字串和Byte陣列 |
注意:
當你指定一個Byte陣列給字串變數時,如果Byte陣列中沒有額外的充填位元組(這是Unicode格式所需要的),會產生什麼樣的結果呢?這些位元組仍然可以轉換成字串,只是Visual Basic在列印字串時會無法辨認這些陌生字元,以致你看到結果是一個問號"?"。
VbFromUnicode和vbUnicode是兩個定義給StrConv函式使用的常數,你可以在線上說明中找到其他許多和這個函式相關的常數。
在了解了Byte陣列和字串的關係以及了解了如何控制ANSI/Unicode格式轉換之後,你就更能夠掌控如何利用二進位檔案模式來進行快速有效率的檔案處理工作了!
如何使用FileSystemObject物件?
Scripting動態連結程式庫(SCRRUN.DLL)讓你能透過FileSystemObject物件來建立、刪除或是複製資夾和檔案。如果要使用FileSystemObject物件,請看以下這三個步驟:
FileSystemObject物件提供了四個次物件讓你達成以下的工作:
FileSystemObject物件的用途 |
物件集合 | 物件 | 工作 |
---|---|---|
Drives | Drive | 取得磁碟的資訊並且存取其次物件 |
Folders | Folder | 建立、複製、移動和刪除資料夾;取得Tempary、Windows和System等資料夾;瀏覽各個資料夾 |
Files | File | 建立、複製、移動和刪除檔案;取得檔案的屬性資訊以及修改屬性 |
N/A | TextStream | 在檔案中讀寫純文字資料 |
檔案系統物件的組織沒有太多的階層,舉例來說,你可以用短短幾行程式碼就能產生一個暫時的文字檔:
Sub WriteTempFile() Dim fsysTemp As New FileSystemObject Dim tstrTemp As TextStream Set tstrTemp = fsysTemp.CreateTextFile(fsysTemp.GetTempName) tstrTemp.Write "This is some temp text." End Sub
檔案系統物件的一個限制是它們不提供讀寫二進位檔案的物件方法。顧名思義,TextStream物件的用途在於讀寫純文字檔。
後面的幾個小節會用一個File System Demo(FILESYS.VBP) 範例程式來示範如何以FileSystemObject物件來處理磁碟、資料夾和檔案。圖15-6中所顯示的是File System Demo執行的情形。
圖15-6 File System Demo應用程式以FileSystemObject物件處理磁碟、資料夾和檔案 |
處理磁碟
FileSystemObject物件的Drive方法可以檢查所有磁碟的狀態。然而,在查詢磁碟資訊之前,你必須檢查IsReady屬性以確定磁碟機已經安置妥當而且裡面也有磁片。以下這段程式碼會產生一個FileSystemObject物件,檢查每一個Drives物件集合裡的元素,然後顯示每一個可用磁碟的剩餘空間。
`Create a new FileSystemObject object Dim mfsysObject As New Scripting.FileSystemObject Private Sub cmdCheckDrives_Click() `Declare a Drive object Dim drvItem As Drive `Add headers to text box txtData = "Drive" & vbTab & "Free space" & vbCrLf `Change mouse pointer. This can take a while MousePointer = vbHourglass `Check each drive For Each drvItem In mfsysObject.Drives `Update text box DoEvents `If drive is ready, you can get free space If drvItem.IsReady Then txtData = txtData & _ drvItem.DriveLetter & vbTab & _ drvItem.FreeSpace & vbCrLf `Otherwise, display "Drive not ready." Else txtData = txtData & _ drvItem.DriveLetter & vbTab & _ "Not Ready." & vbCrLf End If Next drvItem `Go back to the normal mouse pointer MousePointer = vbDefault End Sub
檢查磁碟狀態時需要較長的處理時間,為了因應這個問題,在上例中我們用了Mouse Pointer和Do Events指令。通常可抽取式磁碟、光碟機和網路磁碟機的檢查時間都比較長。如果只想檢查硬碟,你可以檢查DriveType屬性,就像這樣:
`Speed things up! If drvItem.DriveType = Fixed Then `Check free space. . . End If
請注意,不要在同一個If指令中同時檢查DriveType屬性和IsReady屬性,因為這兩個測試動作都會執行,在速度上並不會加快。
處理資料夾
FileSystemObject物件可以很方便地取得Windows、System和Temporary資料夾的名稱,卻不需要在程式中使用Windows API函式。FileSystemObject物件不提供取得目前目錄的方法,因為Visual Basic已經提供了CurDir函式。以下這段程式將會顯示某些資料夾的資訊:
Sub cmdListFolders_Click() Dim fldObject As Folder `Display folder information txtData = "Windows folder: " & _ mfsysObject.GetSpecialFolder(WindowsFolder) & vbCrLf & _ "System folder: " & _ mfsysObject.GetSpecialFolder(SystemFolder) & vbCrLf & _ "Temporary folder: " & _ mfsysObject.GetSpecialFolder(TemporaryFolder) & vbCrLf & _ "Current folder: " & CurDir & vbCrLf `Get the current folder object Set fldObject = mfsysObject.GetFolder(CurDir) `Display some information about it txtData = txtData & "Current directory contains " & _ fldObject.Size & " bytes." End Sub
GetFolder物件方法會傳回一個Folder物件,取得Folder物件後,你便可以利用Folder物件的方法來複製、移動或是刪除資料夾。以下這段程式會把目前的工作目錄移到根目錄下再移回原處。
`Moves the current directory up to the root, then back Sub MoveFolder() Dim fldCurrent As Folder `Get the current folder Set fldCurrent = mfsysObject.GetFolder(CurDir) `Move the folder to the root fldCurrent.Move "\" `Move the folder back where it belongs fldCurrent.Move CurDir End Sub
你可以一步一步地追蹤觀察,看看目錄是否被移到根目錄下之後又被移回原處。這些Folder物件的Move、Copy和Delete方法執行的速度快得讓人不太敢相信。
處理檔案
使用FileSystemObject物件來產生和修改檔案時,你必須了解File物件和TextStream物件的差別:File物件可以存取修改檔案屬性和檔案位置,而TextStream物件則用來在檔案中讀寫純文字資料。
以下這段程式先用CommonDialog控制項取得一個檔名,再將檔案打開,最後把檔案文字的內容顯示在一個文字方塊中。
Sub cmdOpenFile_Click() `Declare a text stream object Dim tstrOpen As TextStream Dim strFileName As String `Display the Save common dialog dlgFile.ShowOpen strFileName = dlgFile.FileName `Check if a filename was specified If strFileName = "" Then Exit Sub `Check if the file already exists If Not mfsysObject.FileExists(strFileName) Then Dim intCreate As Integer intCreate = MsgBox("File not found. Create it?", vbYesNo) If intCreate = vbNo Then Exit Sub End If End If `Open a text stream Set tstrOpen = mfsysObject.OpenTextFile(strFileName, _ ForReading, True) `Check if file is zero length If tstrOpen.AtEndOfStream Then `Clear text box, but don't read -- file is `zero length txtData = "" Else `Display the text stream txtData = tstrOpen.ReadAll End If `Close the stream tstrOpen.Close End Sub
上例中值得一提的是FileExists物件方法。我們常常會用Dir及函式去檢查某個檔案是否存在。其實,FileExists方法做的是同一件事,但是用FileExists方法比用Dir函式更容易讓看程式的人了解你在做什麼。
另外,在檔案開啟後用AtEndStream檢查檔案中是否有資料可供讀取,這一點很重要,因為這樣才不致於讀到一個空檔案而造成錯誤。
存檔的程式也很類似前面的例子,請看以下這段程式:
Sub cmdSaveFile_Click() `Declare a text stream object Dim tstrSave As TextStream Dim strFileName As String `Display the Save common dialog dlgFile.ShowSave strFileName = dlgFile.FileName `Check if a filename was specified If strFileName = "" Then Exit Sub `Check if the file already exists If mfsysObject.FileExists(strFileName) Then Dim intOverwrite As Integer `Prompt before overwriting an existing file intOverwrite = MsgBox("File already exists. " & _ "Overwrite it?", vbYesNo) `If the user chose No, exit this procedure If intOverwrite = vbNo Then Exit Sub End If End If `Open a text stream Set tstrSave = mfsysObject.OpenTextFile(strFileName, _ ForWriting, True) `Save the text stream tstrSave.Write txtData `Close the stream tstrSave.Close End Sub
在使用TextStream物件和File物件時,經常會用到檔案名稱。TextStream物件不提供取得檔名的物件方法,然而可以取得File物件的GetFile方法卻需要檔名,因此,你必須保留一個暫存檔名。以下這段程式會告訴你為什麼需要保留一個暫存檔名才能刪除檔案:
`Creates sample temp file Sub WriteTempFile() Dim fsysTemp As New FileSystemObject Dim tstrTemp As TextStream Dim strTempName As String `Get a temporary filename strTempName = fsysTemp.GetTempName `Create a text stream Set tstrTemp = fsysTemp.CreateTextFile(strTempName) `Write to the stream tstrTemp.Write "This is some temp text." `Close it tstrTemp.Close `Delete the file fsysTemp.GetFile(strTempName).Delete End Sub