---
# System prepended metadata

title: 實作：記事本
tags: [110-2視窗程式設計]

---

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

![](https://i.imgur.com/lexN2Oo.jpg)

![](https://i.imgur.com/xZYW0Bx.png)

### 控制項：豐富文件輸入文字框

這邊介紹一個新的控制項，就是「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

![](https://hackmd.io/_uploads/S1pZ_H_H2.png =500x)


現在你應該可以大致理解讀取檔案的複雜性，不過 Visual Studio 提供一些工具幫助我們設計檔案開啟的機制，例如等一下要講的「對話框」，就可以簡化這個流程。

### 開啟對話框

這邊我們設計不同按鍵要出現不同對話框，以下是兩個按鍵中的開啟對話框的內容。這次跟同學說明你可以開啟兩種對話框，一個是存檔對話框，另一個則是開啟檔案對話框。

以下就是一個存檔與開啟檔案對話框的範例。

![](https://i.imgur.com/QUdifhd.png)

![](https://i.imgur.com/VW4jw9e.png)


其中真正顯示的程式片段是「**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()」就可以了，必須要將資料流關閉，才能真正的關閉檔案。

