---
tags: 視窗程式設計
---
# 實作:記事本
接下來我們讓同學繼續應用檔案讀取的概念,實作基本的記事本程式。記事本相信同學都不陌生,只要是純文字檔案(*.txt),都可以用記事本開啟。
我們用這個範例,讓同學更進一步瞭解「資料流(FileStream)」,並且學習如何用到基本的開啟檔案與存檔,之後我們會更進一步帶同學製作可以編輯文字格式的文字編輯器。
## 程式功能
這次我們會設計一個簡單的純文字編輯器,可以開啟與存檔純文字檔案,也就是副檔名為「txt」的檔案。
## 試用軟體
同學你可以在以下連結,先把完整的程式下載回去使用看看。
[下載連結](https://fgu365-my.sharepoint.com/:f:/g/personal/chlu_vdi_fgu_edu_tw/ErAp2L7jZPFLtKxozH19ldgBUePawwFRqANx_wo7HsQj9Q?e=fWApLK)
## 開啟新專案
開啟新專案之前有教學過,如果你忘記了或不太熟悉,請點選[這裡](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
:::


### 控制項:豐富文件輸入文字框
這邊介紹一個新的控制項,就是「RichTextBox」,有人翻譯叫豐富文件輸入文字框或豐富文件,但目前是沒有統一的中文翻譯,但是有人跟你講 rtb 控制項,多半就是它了。
這個也是一種輸入文字框,不過可以帶格式,也就是說,在 RichTextBox 中的文字,你可以設定字形、字體大小、粗體或斜體等等設定。
如果要製作類似 Word 一樣的文字編輯器,現在你大概就知道需要有什麼樣的輸入文字框了,這個之後我們會跟同學進一步的介紹。
### 事件綁定
我們這範例的事件綁定很簡單,只需要將事件綁定到兩個按鍵的「Click」事件即可,請分別對兩個按鍵設定事件綁定。
## 第二部分:程式碼撰寫
完成基本介面設計後,我們就要把程式寫進來,讓程式可以運作。請將以下的程式內容放進你的程式碼之中。
```csharp=
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO; // 要引用這個IO函式庫,才能作檔案處理
namespace NotePad
{
/// <summary>
/// MainWindow.xaml 的互動邏輯
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
// 開啟一個存檔對話框
Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
// 設定檔案過濾,可以選擇只顯示純文字檔(*.txt)
dlg.Filter = "純文字資料 (*.txt)|*.txt|All files (*.*)|*.*";
// ShowDialog() 來顯示對話框,如果點選確認按鍵,會等於 true
if (dlg.ShowDialog() == true)
{
// 建立一個檔案資料流,並且設定檔案名稱與檔案開啟模式為「新增檔案」
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Create);
// 取得rtbText元件中文字的範圍,取得的範圍是「全部文字」
TextRange range = new TextRange(rtbText.Document.ContentStart, rtbText.Document.ContentEnd);
// 儲存檔案,並且設定為純文字文件檔案(*.txt)
range.Save(fileStream, DataFormats.Text);
// 關閉檔案資料流
fileStream.Close();
}
}
private void btnOpen_Click(object sender, RoutedEventArgs e)
{
// 開啟一個開啟檔案對話框
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
dlg.Filter = "純文字資料 (*.txt)|*.txt|All files (*.*)|*.*";
// ShowDialog() 來顯示對話框,如果點選開啟按鍵,會等於 true
if (dlg.ShowDialog() == true)
{
// 建立一個檔案資料流,並且設定檔案名稱與檔案開啟模式為「開啟檔案」
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Open);
TextRange range = new TextRange(rtbText.Document.ContentStart, rtbText.Document.ContentEnd);
// 將檔案資料流以純文字格式,放進rtbText元件之中顯示
range.Load(fileStream, DataFormats.Text);
fileStream.Close();
}
}
}
}
```
### 程式說明
其實你可以發現,兩個按鍵的事件綁定寫法都很類似,一個是存檔,另一個是開啟檔案。
這邊進一步跟同學介紹:**一般的電腦程式是如何讀取檔案?**
:::success
**檔案通常不能直接讀取與寫入**,需要先把檔案讀進一個類似暫時儲存的地方,然後要讀取與修改都先存取這個暫存區,確定要修改或關閉檔案的時候,才把暫存區的資料改寫回去檔案之中。
:::
就像下圖:
``` mermaid
graph TD;
開啟檔案-->讀取到暫存區;
讀取到暫存區-->程式對暫存區讀取;
程式對暫存區讀取-->完成讀取;
完成讀取-->關閉暫存區;
關閉暫存區-->關閉檔案;
讀取到暫存區-->進行暫存區寫入;
進行暫存區寫入-->暫存區資料回存到檔案;
暫存區資料回存到檔案-->關閉暫存區;
```
為什麼要這樣設計?因為檔案如果讀寫的過程中發生錯誤,小則檔案被程式占住,大則檔案毀損,會造成別的程式無法順利讀取。因此讀取、寫入檔案不是這麼直覺,中間要有一個暫存區來做緩衝,避免發生不當的錯誤。
因此檔案的讀取都需要等檔案被釋放,確定沒有人佔用,因此檔案通常不會讓一個以上的程式來讀取。
:::info
如果你希望實現的是同時好幾個使用者或程式共用一個檔案,就必須要能做到追蹤不同使用者或程式修改的內容(像是Google文件),或者將檔案資料以資料庫的形式來記錄。
:::
如果電腦順利可以讀取檔案,就會依照**由上而下,一行一行讀取**的方式進行,並且**讀取每一行的時候,再一個字元一個字元讀取**。如果是依照下圖的範例,檔案中的每一個字元讀取的順序會是:
> uid,account,password
> 1,abc,123
> 2,def,456

現在你應該可以大致理解讀取檔案的複雜性,不過 Visual Studio 提供一些工具幫助我們設計檔案開啟的機制,例如等一下要講的「對話框」,就可以簡化這個流程。
### 開啟對話框
這邊我們設計不同按鍵要出現不同對話框,以下是兩個按鍵中的開啟對話框的內容。這次跟同學說明你可以開啟兩種對話框,一個是存檔對話框,另一個則是開啟檔案對話框。
以下就是一個存檔與開啟檔案對話框的範例。


其中真正顯示的程式片段是「**dlg.ShowDialog()**」,並且如果開啟對話框後,使用者按下確認按鍵,dlg.ShowDialog()會等於「true」,亦即使用者的確是選擇特定檔案,才會進行以下檔案資料流的部分。
儲存檔案對話框:
```csharp=
// 開啟一個存檔對話框
Microsoft.Win32.SaveFileDialog dlg = new Microsoft.Win32.SaveFileDialog();
// 設定檔案過濾,可以選擇只顯示純文字檔(*.txt)
dlg.Filter = "純文字資料 (*.txt)|*.txt|All files (*.*)|*.*";
// ShowDialog() 來顯示對話框,如果點選存檔按鍵,會等於 true
if (dlg.ShowDialog() == true)
{
// 放入你要處理的事情
}
```
開啟檔案對話框:
```csharp=
// 開啟一個開啟檔案對話框
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
dlg.Filter = "純文字資料 (*.txt)|*.txt|All files (*.*)|*.*";
// ShowDialog() 來顯示對話框,如果點選開啟按鍵,會等於 true
if (dlg.ShowDialog() == true)
{
// 放入你要處理的事情
}
```
### 函式庫概念
仔細閱讀上一節的程式碼,你可以看到要新增(new)一個的對話框物件,需要寫「Microsoft.Win32.OpenFileDialog()」,例如以下的程式碼:
```csharp
// 開啟一個開啟檔案對話框
Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
```
你可以在引用函式庫的段落,加上這一行。
```csharp
using Microsoft.Win32; // 引用視窗物件函式庫
```
這樣以上的程式碼就可以簡化為:
```csharp
// 開啟一個開啟檔案對話框
OpenFileDialog dlg = new OpenFileDialog();
```
到這裡你可能會好奇,什麼是函式庫?你可以大概從以下[維基百科的定義](https://zh.wikipedia.org/zh-tw/%E5%87%BD%E5%BC%8F%E5%BA%AB)瞭解。
:::info
函式庫(英語:library)是在電腦科學中用於開發軟體的子程式集合。函式庫和可執行檔的區別是,它不是獨立的電腦程式,而是向其他程式提供服務的代碼。
:::
函式庫就像是很多程式的工具箱,你如果需要使用什麼特殊的功能,只要「引用」,就可以使用它的工具箱,你可以發現,基本上程式從第一行就引用了不少函式庫。
### 檔案資料流
接下來再跟同學說明如何讀取檔案,並且放到檔案資料流中(暫存區),請先參考以下程式碼。
==請注意,完整程式的第 15 行,一定要加入「using System.IO」,使用「**IO 函式庫**」,這樣才能使用「**FileStream**」物件。==「**IO 函式庫**」的用途能夠讓你做檔案存取,至於什麼是函式庫?上述的小節就有說明,你可以回去再稍微閱讀一下。
儲存檔案模式:
```csharp=
// 建立一個檔案資料流,並且設定檔案名稱與檔案開啟模式為「新增檔案」
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Create);
// 取得rtbText元件中文字的範圍,取得的範圍是「全部文字」
TextRange range = new TextRange(rtbText.Document.ContentStart, rtbText.Document.ContentEnd);
// 儲存檔案,並且設定為純文字文件檔案(*.txt)
range.Save(fileStream, DataFormats.Text);
// 關閉檔案資料流
fileStream.Close();
```
開啟檔案模式:
```csharp=
// 建立一個檔案資料流,並且設定檔案名稱與檔案開啟模式為「開啟檔案」
FileStream fileStream = new FileStream(dlg.FileName, FileMode.Open);
TextRange range = new TextRange(rtbText.Document.ContentStart, rtbText.Document.ContentEnd);
range.Load(fileStream, DataFormats.Text);
fileStream.Close();
```
請同學稍微認真看一下這兩段程式碼,上面的是做在儲存檔案事件裡,下面的是做在開啟檔案事件裡,請特別注意。
你也可以注意到,開啟檔案一定要建立一個「**檔案資料流**」,檔案資料流的物件是「**FileStream**」。並且你要新增這樣的物件,要設定好檔案名稱(dlg.FileName),以及開啟檔案的模式(FileMode.Create 或 FileMode.Open)。
然後你會需要知道在「RichTextBox」需要讀取或寫入的範圍,這個意思是說,你在輸入文字框中需要讀取或者放入資料的範圍,這個沒有意外的話都是選擇全部文字的範圍。
```csharp
// 取得rtbText元件中文字的範圍,取得的範圍是「全部文字」
TextRange range = new TextRange(rtbText元件的起點, rtbText元件終點);
```
再來依照你需要做讀取或存檔,進行「range.Load(讀取)」或「range.Save(存檔)」
```csharp
// 儲存檔案,並且設定為純文字文件檔案(*.txt)
range.Save(資料流, DataFormats.Text);
// 將檔案資料流以純文字格式,放進rtbText元件之中顯示
range.Load(資料流, DataFormats.Text);
```
最後很重要的是,請記得將所建立的「**FileStream**」關閉起來,語法只需要加上一個「Close()」就可以了,必須要將資料流關閉,才能真正的關閉檔案。