---
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
:::


## 控制項:豐富文件輸入文字框 (RichTextBox)
這邊介紹一個新的控制項,就是「**RichTextBox**」,有人翻譯叫「豐富文件輸入文字框」或「豐富文件」,但目前是沒有統一的中文翻譯,通常我們簡單就是說「**rtb控制項**」。
我們以前用過類似的文字框控制項,就是「TextBox」,因此 RichTextBox 也是一種輸入文字框,和 TextBox 最大的差別在於,RichTextBox 中的文字,你可以設定字形、字體大小、粗體或斜體等等文字樣式,TextBox 就不能設定文字樣式。
因此如果要製作類似 WORD 軟體類似的文字編輯器,現在你大概就知道需要有什麼樣的輸入文字框了,這個之後我們會跟同學進一步的介紹。
## 控制項:對話方塊(Dialog)
你應該早就知道甚麼是對話方塊,你可以看看下圖就會知道甚麼是對話方塊?對話方塊除了讓你可以開啟檔案、儲存檔案外,還有一是用來瀏覽資料夾、顯示字型設定、選擇色彩、列印文件等等。
這邊我們設計不同按鍵要出現不同對話方塊,以下是兩個按鍵中的開啟對話方塊的內容。這次跟同學說明你可以開啟兩種對話方塊,一個是「**開啟檔案對話方塊**」,另一個是「**儲存檔案對話方塊**」。
以下就是一個存檔與開啟檔案對話方塊的範例。我們先以「開啟檔案對話方塊」來做說明。


要在你的視窗畫面中增加開啟對話方塊很簡單,請到「工具箱」中,找尋「對話方塊」中的「OpenFileDialog」,再直接拖拉到你的視窗就可以了。

拖進去之後,OpenFileDialog 會顯示在視窗下方,就像下圖的樣子,請特別注意這個儲存對話框的名稱,為「openFileDialog1」,假如你想修改這個名稱,也可以在屬性視窗來做修改。

同樣的,儲存檔案對話方塊的加入也很簡單,只要將「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);
}
}
}
}
```
:::
## 程式說明

開啟檔案對話方塊必須要寫程式才能使用,以下是程式的主要內容:
:::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|按下確定按鍵||
|Cancel|按下取消按鍵||
|Yes|按下是||
|No|按下否||
我們的範例就是假如使用者按下確定按鍵,就執行開啟檔案的程式,如果按下取消或關閉開啟檔案對話方塊,就顯示一個「**跳出訊息對話方塊**」。

:::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

現在你應該可以大致理解讀取檔案的複雜性,不過 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)是在電腦科學中用於開發軟體的子程式集合。函式庫和可執行檔的區別是,它不是獨立的電腦程式,而是向其他程式提供服務的代碼。
:::
函式庫就像是很多程式的工具箱,你如果需要使用什麼特殊的功能,只要「引用」,就可以使用它的工具箱,你可以發現,基本上程式從第一行就引用了不少函式庫。