--- tags: 視窗程式設計 --- # 實作:記事本(開啟檔案) 接下來我們讓同學繼續應用檔案讀取的概念,實作基本的記事本程式。記事本相信同學都不陌生,只要是純文字檔案(*.txt),都可以用記事本開啟。 我們用這個範例,讓同學更進一步瞭解「**資料流(FileStream)**」、「**例外狀況處理(try...catch...)**」,並且學習如何用到基本的開啟檔案與存檔,之後我們會更進一步帶同學製作可以編輯文字格式的文字編輯器。 ## 程式試用 你可以到課堂雲端硬碟連結,下載完成的程式檔,了解裡面的內容。 [課堂雲端硬碟連結](https://fgu365-my.sharepoint.com/:f:/g/personal/chlu_vdi_fgu_edu_tw/ErAp2L7jZPFLtKxozH19ldgBeQeTxDRNXuwtRZ8blrB9qQ?e=9qSWiv) ## 開啟新專案 開啟新專案之前有教學過,如果你忘記了或不太熟悉,請點選[這裡](https://hackmd.io/sFFV6T2oT0-ysHotbIybhw?view#WPF-%E7%9A%84%E5%B0%88%E6%A1%88%E8%A8%AD%E5%AE%9A)。 :::success 我們的專案名稱可以叫做「NotePad」,簡單就好。 ::: ## 開始吧! 還記得第二部分的基本視窗程式設計嗎?同樣的,我們依照之前說過的兩個主要程式設計流程來設計: 1. 介面基本設計 2. 程式撰寫 因此,我們也是先初步設計介面,再來進行程式撰寫。 # 第一步:進行介面基本設計 和過去的範例程式都類似,請你先從工具箱拉控制項元件進來,需要兩個按鍵,和一個「RichTextBox(豐富文件輸入文字框)」。 程式背景可以設定一個淺一點的顏色,可以和按鍵做出區別即可。 也請記得,將每一個控制項設定一個名稱,例如以下的範例名稱: :::info 1. 開啟檔案按鍵:btnOpen 2. 存檔按鍵:btnSave 3. 文字編輯區(RichTextBox):rtbText 4. 對話方塊(Dialog):openFileDialog1 ::: ![](https://i.imgur.com/lexN2Oo.jpg) ![](https://i.imgur.com/xZYW0Bx.png) ## 控制項:豐富文件輸入文字框 (RichTextBox) 這邊介紹一個新的控制項,就是「**RichTextBox**」,有人翻譯叫「豐富文件輸入文字框」或「豐富文件」,但目前是沒有統一的中文翻譯,通常我們簡單就是說「**rtb控制項**」。 我們以前用過類似的文字框控制項,就是「TextBox」,因此 RichTextBox 也是一種輸入文字框,和 TextBox 最大的差別在於,RichTextBox 中的文字,你可以設定字形、字體大小、粗體或斜體等等文字樣式,TextBox 就不能設定文字樣式。 因此如果要製作類似 WORD 軟體類似的文字編輯器,現在你大概就知道需要有什麼樣的輸入文字框了,這個之後我們會跟同學進一步的介紹。 ## 控制項:對話方塊(Dialog) 你應該早就知道甚麼是對話方塊,你可以看看下圖就會知道甚麼是對話方塊?對話方塊除了讓你可以開啟檔案、儲存檔案外,還有一是用來瀏覽資料夾、顯示字型設定、選擇色彩、列印文件等等。 這邊我們設計不同按鍵要出現不同對話方塊,以下是兩個按鍵中的開啟對話方塊的內容。這次跟同學說明你可以開啟兩種對話方塊,一個是「**開啟檔案對話方塊**」,另一個是「**儲存檔案對話方塊**」。 以下就是一個存檔與開啟檔案對話方塊的範例。我們先以「開啟檔案對話方塊」來做說明。 ![](https://i.imgur.com/QUdifhd.png) ![](https://i.imgur.com/VW4jw9e.png) 要在你的視窗畫面中增加開啟對話方塊很簡單,請到「工具箱」中,找尋「對話方塊」中的「OpenFileDialog」,再直接拖拉到你的視窗就可以了。 ![](https://imgur.com/dfJZYIh.png) 拖進去之後,OpenFileDialog 會顯示在視窗下方,就像下圖的樣子,請特別注意這個儲存對話框的名稱,為「openFileDialog1」,假如你想修改這個名稱,也可以在屬性視窗來做修改。 ![](https://imgur.com/YyncA66.png) 同樣的,儲存檔案對話方塊的加入也很簡單,只要將「SaveFileDialog」拖入即可,同樣的請注意它的名稱是「saveFileDialog1」。 ## 事件綁定 我們這範例的事件綁定很簡單,**只需要將事件綁定到兩個按鍵的「Click」事件即可**,請分別對兩個按鍵設定事件綁定。 # 第二部分:程式碼撰寫 完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。我們先寫將一般純文字文件檔讀入的程式,讓純文字文件的內容顯示在 RichTextBox 中。 ## 程式碼 其中真正顯示的程式片段是「**dlg.ShowDialog()**」,並且如果開啟對話框後,使用者按下確認按鍵,dlg.ShowDialog()會等於「true」,亦即使用者的確是選擇特定檔案,才會進行以下檔案資料流的部分。 請將以下的程式內容放進你的程式碼之中。 :::spoiler ```csharp= using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; // 使用 IO 函式庫 namespace NotePad { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnOpen_Click(object sender, EventArgs e) { // 設置對話方塊標題 openFileDialog1.Title = "選擇檔案"; // 設置對話方塊篩選器,限制使用者只能選擇特定類型的檔案 openFileDialog1.Filter = "文字檔案 (*.txt)|*.txt|所有檔案 (*.*)|*.*"; // 如果希望預設開啟的檔案類型是文字檔案,可以這樣設置 openFileDialog1.FilterIndex = 1; // 如果希望對話方塊在開啟時顯示的初始目錄,可以設置 InitialDirectory openFileDialog1.InitialDirectory = "C:\\"; // 允許使用者選擇多個檔案 openFileDialog1.Multiselect = true; // 顯示對話方塊,並等待使用者選擇檔案 DialogResult result = openFileDialog1.ShowDialog(); // 檢查使用者是否選擇了檔案 if (result == DialogResult.OK) { try { // 使用者在OpenFileDialog選擇的檔案 string selectedFileName = openFileDialog1.FileName; //// 使用 FileStream 打開檔案 //// 建立一個檔案資料流,並且設定檔案名稱與檔案開啟模式為「開啟檔案」 //FileStream fileStream = new FileStream(selectedFileName, FileMode.Open, FileAccess.Read); //// 讀取資料流 //StreamReader streamReader = new StreamReader(fileStream); //// 將檔案內容顯示到 RichTextBox 中 //rtbText.Text = streamReader.ReadToEnd(); //// 關閉資料流與讀取資料流 //fileStream.Close(); //streamReader.Close(); // 使用 using 與 FileStream 打開檔案 using (FileStream fileStream = new FileStream(selectedFileName, FileMode.Open, FileAccess.Read)) { // 使用 StreamReader 讀取檔案內容 using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8)) { // 將檔案內容顯示到 RichTextBox 中 rtbText.Text = streamReader.ReadToEnd(); } } //// 更為簡單的做法,將檔案內容顯示到 RichTextBox 中 //string fileContent = File.ReadAllText(selectedFileName); //rtbText.Text = fileContent; } catch (Exception ex) { // 如果發生錯誤,用MessageBox顯示錯誤訊息 MessageBox.Show("讀取檔案時發生錯誤: " + ex.Message, "錯誤訊息", MessageBoxButtons.OK, MessageBoxIcon.Error); } } else { MessageBox.Show("使用者取消了選擇檔案操作。", "訊息", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation); } } } } ``` ::: ## 程式說明 ![](https://imgur.com/N4nMAor.png) 開啟檔案對話方塊必須要寫程式才能使用,以下是程式的主要內容: :::info 1. 開啟檔案對話方塊的基本設定 2. 顯示對話方塊,等待使用者選擇檔案 3. 如果使用者選擇了檔案,則以檔案流讀取檔案,再將檔案流內容放進RichTextBox顯示檔案內容 4. 如果使用者不選擇檔案,則關閉開啟檔案對話框 ::: ### 開啟檔案對話方塊的基本設定 要使用開啟檔案對話方塊,通常我們都會先設定一些基礎設定,這部分其實不一定要設置,但可以對於使用者來說會更加便利。 ```csharp= // 設置對話方塊標題 openFileDialog1.Title = "選擇檔案"; // 設置對話方塊篩選器,限制使用者只能選擇特定類型的檔案 openFileDialog1.Filter = "文字檔案 (*.txt)|*.txt|所有檔案 (*.*)|*.*"; // 如果希望預設開啟的檔案類型是文字檔案,可以這樣設置 openFileDialog1.FilterIndex = 1; // 如果希望對話方塊在開啟時顯示的初始目錄,可以設置 InitialDirectory openFileDialog1.InitialDirectory = "C:\\"; // 允許使用者選擇多個檔案 openFileDialog1.Multiselect = true; ``` ### 顯示對話方塊,等待使用者選擇檔案 設定好對話方塊之後,以下的語法就能跳出對話方塊,我們一般來說都會設置一個對話方塊的結果變數,用來記錄使用者是否選擇了檔案,或者取消選擇檔案。 ```csharp= // 顯示對話方塊,並等待使用者選擇檔案 DialogResult result = openFileDialog1.ShowDialog(); // 檢查使用者是否選擇了檔案 if (result == DialogResult.OK) { ... 如果使用者選擇了檔案 } else { MessageBox.Show("使用者取消了選擇檔案操作。", "訊息"); } ``` 其中對話方塊使用者選擇的結果會有以下幾種常見的項目,其實就是去捕捉使用者按下哪一個按鍵?你可以再使用判斷式依據使用者的選擇,撰寫不同程式來處理。 |項目|說明|圖示| |--------|--------|--------| |OK|按下確定按鍵|![](https://hackmd.io/_uploads/ryoYXnp16.png)| |Cancel|按下取消按鍵|![](https://hackmd.io/_uploads/Sk45XhTya.png)| |Yes|按下是|![](https://hackmd.io/_uploads/ryM1V3pyT.png)| |No|按下否|![](https://hackmd.io/_uploads/B1TkV2a1a.png)| 我們的範例就是假如使用者按下確定按鍵,就執行開啟檔案的程式,如果按下取消或關閉開啟檔案對話方塊,就顯示一個「**跳出訊息對話方塊**」。 ![](https://imgur.com/GFxdXAu.png) :::success 「**跳出訊息對話方塊(MessageBox)**」是一個很常使用的對話方塊。 ::: 當在執行視窗程式時,若發生錯誤操作,或者需要提示使用者,我們會利用 MessageBox 顯示一個錯誤或警告訊息的對話方塊,以提醒使用者注意。MessageBox 所提供的 Show 可產生一個包含訊息、按鈕、特殊符號的對話方塊,用來告知和提示使用者。以下是MessageBox的語法: ```csharp== MessageBox.Show(要顯示的訊息, 視窗標題, 按鍵組合, 小圖示, 預設按鍵); ``` 因為用法很多,同學可以參考這個[網站](https://crmne0707.pixnet.net/blog/post/335335529-c%23-messagebox),了解其他的設定。 ### 資料流的概念:檔案讀取的資料暫存 這裡我們則要先介紹:**一般的電腦程式是如何讀取檔案?** :::success **檔案通常不能直接讀取與寫入**,需要先把檔案讀進一個「暫存區」,通常會稱為「**資料流**」,然後要讀取與修改都在這個暫存區去做,確定讀取與修改完成的時候,才把暫存區的資料關閉,或者將修改後的內容改寫回去檔案之中。 ::: 就像下圖: ``` mermaid graph TD; 開啟檔案-->讀取到暫存區; 讀取到暫存區-->程式對暫存區讀取; 程式對暫存區讀取-->完成讀取; 完成讀取-->關閉暫存區; 關閉暫存區-->關閉檔案; 讀取到暫存區-->進行暫存區寫入; 進行暫存區寫入-->暫存區資料回存到檔案; 暫存區資料回存到檔案-->關閉暫存區; ``` **為什麼要這樣設計?** 因為檔案如果讀寫的過程中發生錯誤,小則檔案被程式占住,大則檔案毀損,會造成別的程式無法順利讀取。因此讀取、寫入檔案不是這麼直覺,中間要有一個暫存區來做緩衝,避免發生不當的錯誤。 **如果其他人要對檔案讀取或修改,都需要等檔案被釋放,確定沒有人佔用,因此一般來說一個檔案通常不會讓一個以上的程式來讀取。** :::info 如果你希望實現的是同時好幾個使用者或程式共用一個檔案,就必須要能做到追蹤不同使用者或程式修改的內容(像是Google文件),或者將檔案資料以資料庫的形式來記錄。 ::: ### 電腦怎麼讀取文字檔? 如果電腦順利可以讀取檔案,就會依照**由上而下,一行一行讀取**的方式進行,並且**讀取每一行的時候,再一個字元一個字元讀取**。如果是依照下圖的範例,檔案中的每一個字元讀取的順序會是: > uid,account,password > 1,abc,123 > 2,def,456 ![](https://hackmd.io/_uploads/S1pZ_H_H2.png =500x) 現在你應該可以大致理解讀取檔案的複雜性,不過 Visual Studio 提供一些工具幫助我們設計檔案開啟的機制,例如等一下要講的「對話框」,就可以簡化這個流程。 ### 檔案資料流 接下來再跟同學說明如何讀取檔案,並且放到檔案資料流中(暫存區),請先參考以下程式碼。 ==請注意,一定要加入完整程式第 10 行「using System.IO」,使用「**IO 函式庫**」,這樣才能使用「**FileStream**」物件。==「**IO 函式庫**」的用途能夠讓你做檔案存取,至於什麼是函式庫?上述的小節就有說明,你可以回去再稍微閱讀一下。 ```csharp= try { // 使用者在OpenFileDialog選擇的檔案 string selectedFileName = openFileDialog1.FileName; // 使用 FileStream 打開檔案 // 建立一個檔案資料流,並且設定檔案名稱與檔案開啟模式為「開啟檔案」 FileStream fileStream = new FileStream(selectedFileName, FileMode.Open, FileAccess.Read); // 讀取資料流 StreamReader streamReader = new StreamReader(fileStream); // 將檔案內容顯示到 RichTextBox 中 rtbText.Text = streamReader.ReadToEnd(); // 關閉資料流與讀取資料流 fileStream.Close(); streamReader.Close(); } catch (Exception ex) { // 如果發生錯誤,用MessageBox顯示錯誤訊息 MessageBox.Show("讀取檔案時發生錯誤: " + ex.Message, "錯誤訊息", MessageBoxButtons.OK, MessageBoxIcon.Information); } ``` 開啟檔案一定要建立一個「**檔案資料流**」,檔案資料流的物件是「**FileStream**」。並且你要新增這樣的物件,需要指定OpenFileDialog的檔案(selectedFileName),以及開啟檔案的模式(FileMode.Create 或 FileMode.Open)。 然後你會需要在「StreamReader」讀取資料流,再將讀取後的文字放到 RichTextBox 中。 ### 關閉資源的重要性 最後很重要的是,請記得將所建立的「**FileStream**」關閉起來,語法只需要加上一個「Close()」就可以了,必須要將資料流關閉,才能真正的關閉檔案。使用「Close」方法,關閉(資源)是一種良好的程式設計習慣,有助於確保系統的穩定性、性能和可靠性。主要有以下理由: 1. 釋放電腦資源:許多資源,如檔案、資料庫連接、網路連線等,都是有限的,並且在使用完畢後需要釋放以便其他程式使用。未關閉的資源可能會導致資源泄漏,進而導致系統資源不足或性能下降。 2. 避免電腦資源競爭:當資源被多個程式共享時,未關閉的資源可能會導致資源競爭,例如同時對檔案進行讀取和寫入,這可能導致未預期的行為或損壞資料。 ### 例外狀況處理 你可能會注意到程式碼有一個「try...catch」,那是甚麼呢?這是「**例外狀況處理(Exception Handling)**」的陳述式,可以處理一些未知且發生錯誤的情況。在程式執行過程中,其實難免會遇到異常或錯誤,這時就要對這些異常或錯誤進行處理,避免程式當掉或者資料損毀,造成不可逆的結果。 **在程式執行過程中,可能會發生各種不可預測的錯誤**,例如:輸入不合法的值(型別轉換出錯)、找不到要開啟的資料、檔案不存在、資料庫連接失敗、網路連線中斷等等。這些錯誤可能會導致程式當掉、產生未預期的結果、甚至整個作業系統崩潰。為了提高程式的可靠性和穩定性,我們可以使用例外狀況處理來處理這些異常情況。 其實你之前看過類似的東西,還記得「**TryParse**」試著把「**文字**」轉換「**double浮點數**」的這個函式嗎。如果你忘記了轉型的概念,可以參考[這裡](https://hackmd.io/gDNqhQODQ_qiAtcXVe27mw?view#%E8%B3%87%E6%96%99%E5%9E%8B%E6%85%8B%E8%BD%89%E6%8F%9B%EF%BC%88%E8%BD%89%E5%9E%8B%EF%BC%89)。「**TryParse**」它會先「試著轉換型別」,視成功與否回傳true或false,如果試著轉換成double是成功的,回傳true,不成功,就會傳false。使用TryParse可以避免在轉換型別的時候出錯,造成程式當掉的現象。 因此假使你很難確定寫的程式一定不會有問題,或者你預期到可能出現不能預測的錯誤,這時可以考慮使用「**try…catch**」陳述式,將有可能出問題的程式放在 try 區塊,如果有問題,可以藉由 catch 區塊以 Exception 擷取錯誤,再以適當的程式處理(例如跳出一個對話方塊說明可以找哪位程式設計師解決問題),避免程式整個當掉。 流程可以參考下圖。 ``` mermaid graph TD; try-->執行程式; 執行程式-->執行完成; 執行完成-->結束; 執行程式-->執行失敗; 執行失敗-->移向catch; 移向catch-->執行catch內的程式; 執行catch內的程式-->結束; ``` #### 使用 using 與 FileStream 打開檔案 你一定會注意到,當你打開一個檔案,還要在結束的階段記得關掉它,其實很麻煩。 我們可以使用「using」,你可以更簡潔的寫這個程式,並且不需要特別關閉,讓電腦程式可以自動幫你處理。使用 using 的好處在於它可以簡化程式碼,使得程式更易讀、更易於維護。尤其是在釋放資源方面,using 可以確保資源被及時釋放,即使在發生例外的情況下也是如此,這樣可以增加程式的可靠性。 ```csharp= try { // 使用者在OpenFileDialog選擇的檔案 string selectedFileName = openFileDialog1.FileName; // 使用 using 與 FileStream 打開檔案 using (FileStream fileStream = new FileStream(selectedFileName, FileMode.Open, FileAccess.Read)) { // 使用 StreamReader 讀取檔案內容 using (StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8)) { // 將檔案內容顯示到 RichTextBox 中 rtbText.Text = streamReader.ReadToEnd(); } } } catch (Exception ex) { // 如果發生錯誤,用MessageBox顯示錯誤訊息 MessageBox.Show("讀取檔案時發生錯誤: " + ex.Message, "錯誤訊息", MessageBoxButtons.OK, MessageBoxIcon.Information); } ``` #### 更為簡單的用法 如果你只是要簡單地從純文字檔案中讀取並顯示在 RichTextBox 中,使用「**File.ReadAllText**」是更簡潔和方便的方法。 ```csharp= try { // 使用者在OpenFileDialog選擇的檔案 string selectedFileName = openFileDialog1.FileName; // 更為簡單的做法,將檔案內容顯示到 RichTextBox 中 string fileContent = File.ReadAllText(selectedFileName); rtbText.Text = fileContent; } catch (Exception ex) { // 如果發生錯誤,用MessageBox顯示錯誤訊息 MessageBox.Show("讀取檔案時發生錯誤: " + ex.Message, "錯誤訊息", MessageBoxButtons.OK, MessageBoxIcon.Information); } ``` 你可能會問,使用「FileStream 和 StreamReader」,和這裡的「File.ReadAllText」,哪一種方法比較好?以下是簡單的比較 使用「FileStream 和 StreamReader」: 1. 可以對檔案的讀取過程進行更細緻的控制,例如指定編碼方式、處理大型檔案時的效能更好等。 2. 可以及時釋放資源,確保資源得到適當的釋放,避免資源泄漏。 使用「File.ReadAllText」: 1. 程式碼簡潔明瞭,一行就可以完成檔案的讀取和顯示。 2. 對於簡單的檔案讀取操作,這種方法足夠方便且效能良好。 因此哪一種比較好要看情況,就看你對於開發的需求是甚麼? ### 函式庫概念 剛剛前面的程式碼有特別說明: :::success 請注意,一定要加入完整程式第 10 行「using System.IO」,使用「**IO 函式庫**」,這樣才能使用「**FileStream**」物件。==「**IO 函式庫**」的用途能夠讓你做檔案存取,至於什麼是函式庫?上述的小節就有說明,你可以回去再稍微閱讀一下。 ::: 到這裡你可能會好奇,什麼是「**函式庫**」?你可以大概從以下[維基百科的定義](https://zh.wikipedia.org/zh-tw/%E5%87%BD%E5%BC%8F%E5%BA%AB)瞭解。 :::info 函式庫(英語:library)是在電腦科學中用於開發軟體的子程式集合。函式庫和可執行檔的區別是,它不是獨立的電腦程式,而是向其他程式提供服務的代碼。 ::: 函式庫就像是很多程式的工具箱,你如果需要使用什麼特殊的功能,只要「引用」,就可以使用它的工具箱,你可以發現,基本上程式從第一行就引用了不少函式庫。