---
# System prepended metadata

title: 實作：記事本（重做下一步）
tags: [視窗程式設計]

---

---
tags: 視窗程式設計
---

# 實作：記事本（重做下一步）

我們已經實作了基本的復原上一步功能，讓記事本程式可以記錄文字編輯過程，並且可以回到前十次的文字修改紀錄。

不過這類的文字編輯軟體，都可以有一個重作功能，讓你回復到上一步之後，又能重作一次之前的內容。

# 第一部分：介面設定

這次的介面設計也很簡單，再多加一個「**重作下一步**」按鍵就可以了，名稱可以是以下。

:::info
1. 重作下一步按鍵：btnRedo
:::

同樣的，也要做 Click 的事件綁定。畫面參考如下。

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

# 第二部分：程式碼撰寫

完成基本介面設計後，我們就要來實現個「**重作下一步**」的功能。

我們也一樣使用堆疊的資料結構，不一樣的地方在於，我們使用兩個堆疊來儲存文字修改記錄：

:::success
1. 回復（undo）堆疊：undoStack
2. 重作（redo）堆疊：redoStack
:::

當使用者進行「回復上一步」操作時，將會把回復上一步的內容，堆疊移到重作堆疊；當使用者「重作下一步」操作時，將狀態從重作堆疊移回回復堆疊。就像以下圖片的概念。

1. 假如使用者輸入一段文字「John」，回復紀錄已經呈現以下堆疊狀態，重作堆疊還是空的。

![](https://imgur.com/TEm8pXp.png =400x)

2. 使用者進行「回復上一步」，就會顯示「Joh」，也就是將回復堆疊最上一筆的「John」移出（Pop），再移到重作堆疊（Push）。

![](https://imgur.com/JTAEyuH.png =500x)

3. 使用者再進行一次「回復上一步」，就會顯示「Jo」。

![](https://imgur.com/9DEutzo.png =500x)

4. 這時使用者進行「重作下一步」，就會顯示「Joh」，也就是將重作堆疊最上一筆的「Joh」移出（Pop），再移到回復堆疊（Push）。

![](https://imgur.com/ueqdD4T.png =600x)

## 程式碼

如果大概的概念你理解了，接下來就是程式碼的加入，我們需要修改的地方滿多的，請你仔細好好檢查再加入程式碼

1. 原來的「isUndo」改成「isUndoRedo」

```csharp=
private bool isUndoRedo = false;
```

這個變數也是要用於避免在回復或重作的時候，重新又將文字的改變記錄到回復堆疊中。

2. 回復上一步按鍵的 Click 事件綁定

```csharp=
private void btnUndo_Click(object sender, EventArgs e)
{
    if (undoStack.Count > 1)
    {
        isUndoRedo = true;
        redoStack.Push(undoStack.Pop()); // 將回復堆疊最上面的紀錄移出，再堆到重作堆疊
        rtbText.Text = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示
        UpdateListBox();
        isUndoRedo = false;
    }
}
```

程式重點：第 6 行，將回復堆疊最上面的紀錄移出，再堆到重作堆疊

3. 重作下一步按鍵的 Click 事件綁定

```csharp=
private void btnRedo_Click(object sender, EventArgs e)
{
    if (redoStack.Count > 0)
    {
        isUndoRedo = true;
        undoStack.Push(redoStack.Pop()); // 將重作堆疊最上面的紀錄移出，再堆到回復堆疊
        rtbText.Text = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示
        UpdateListBox();
        isUndoRedo = false;
    }
}
```

程式重點：第 6 行，將重作堆疊最上面的紀錄移出，再堆到回復堆疊

4.  rtbText 中的「TextChanged」事件綁定

```csharp=
private void rtbText_TextChanged(object sender, EventArgs e)
{
    // 只有當isUndo這個變數是false的時候，才能堆疊文字編輯紀錄
    if (isUndoRedo == false)
    {        
        undoStack.Push(rtbText.Text); // 將當前的文本內容加入堆疊
        redoStack.Clear();            // 清空重作堆疊

        // 確保堆疊中只保留最多10個紀錄
        if (undoStack.Count > MaxHistoryCount)
        {
            // 用一個臨時堆疊，將除了最下面一筆的文字記錄之外，將文字紀錄堆疊由上而下，逐一移除再堆疊到臨時堆疊之中
            Stack<string> tempStack = new Stack<string>();
            for (int i = 0; i < MaxHistoryCount; i++)
            {
                tempStack.Push(undoStack.Pop());
            }
            undoStack.Clear(); // 清空堆疊
                               // 文字編輯堆疊紀錄清空之後，再將暫存堆疊（tempStack）中的資料，逐一放回到文字編輯堆疊紀錄
            foreach (string item in tempStack)
            {
                undoStack.Push(item);
            }
        }
        UpdateListBox(); // 更新 ListBox
    }
}
```

其實紀錄文字編輯歷程沒有太大變化，僅在第 7 行中，增加了一段清空重作堆疊的指令。

## 測試看看

這時你應該發現可以執行重作功能，試試看吧！

# 完整程式碼

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

namespace NotePad
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private bool isUndoRedo = false;                           // 是否在回復或重作階段
        private Stack<string> undoStack = new Stack<string>();     // 回復堆疊
        private Stack<string> redoStack = new Stack<string>();     // 重作堆疊
        private const int MaxHistoryCount = 10;                    // 最多紀錄10個紀錄

        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);
            }
        }

        private void btnSave_Click(object sender, EventArgs e)
        {
            // 設置對話方塊標題
            saveFileDialog1.Title = "儲存檔案";
            // 設置對話方塊篩選器，限制使用者只能選擇特定類型的檔案
            saveFileDialog1.Filter = "文字檔案 (*.txt)|*.txt|所有檔案 (*.*)|*.*";
            // 如果希望預設儲存的檔案類型是文字檔案，可以這樣設置
            saveFileDialog1.FilterIndex = 1;
            // 如果希望對話方塊在開啟時顯示的初始目錄，可以設置 InitialDirectory
            saveFileDialog1.InitialDirectory = "C:\\";

            // 顯示對話方塊，並等待使用者指定儲存的檔案
            DialogResult result = saveFileDialog1.ShowDialog();

            //建立 FileStream 物件
            FileStream fileStream = null;

            // 檢查使用者是否選擇了檔案
            if (result == DialogResult.OK)
            {
                try
                {
                    // 使用者指定的儲存檔案的路徑
                    string saveFileName = saveFileDialog1.FileName;

                    // 使用 FileStream 建立檔案，如果檔案已存在則覆寫
                    fileStream = new FileStream(saveFileName, FileMode.Create, FileAccess.Write);
                    // 將 RichTextBox 中的文字寫入檔案中
                    byte[] data = Encoding.UTF8.GetBytes(rtbText.Text);
                    fileStream.Write(data, 0, data.Length);

                    //// 使用 using 與 FileStream 建立檔案，如果檔案已存在則覆寫
                    //using (fileStream = new FileStream(saveFileName, FileMode.Create, FileAccess.Write))
                    //{
                    //    // 將 RichTextBox 中的文字寫入檔案中
                    //    byte[] data = Encoding.UTF8.GetBytes(rtbText.Text);
                    //    fileStream.Write(data, 0, data.Length);
                    //}

                    //// 將 RichTextBox 中的文字儲存到檔案中
                    //File.WriteAllText(saveFileName, rtbText.Text);

                    MessageBox.Show("檔案儲存成功。", "訊息", MessageBoxButtons.OK, MessageBoxIcon.Information);
                }
                catch (Exception ex)
                {
                    // 如果發生錯誤，用 MessageBox 顯示錯誤訊息
                    MessageBox.Show("儲存檔案時發生錯誤: " + ex.Message, "錯誤訊息", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                finally
                {
                    // 關閉資源，如果使用 using 或者直接以 File.WriteAllText 儲存文字檔，可以不需要。
                    fileStream.Close();
                }
            }
            else
            {
                MessageBox.Show("使用者取消了儲存檔案操作。", "訊息", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);
            }
        }

        private void btnUndo_Click(object sender, EventArgs e)
        {
            if (undoStack.Count > 1)
            {
                isUndoRedo = true;
                redoStack.Push(undoStack.Pop()); // 將回復堆疊最上面的紀錄移出，再堆到重作堆疊
                rtbText.Text = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示
                UpdateListBox();
                isUndoRedo = false;
            }
        }

        private void btnRedo_Click(object sender, EventArgs e)
        {
            if (redoStack.Count > 0)
            {
                isUndoRedo = true;
                undoStack.Push(redoStack.Pop()); // 將重作堆疊最上面的紀錄移出，再堆到回復堆疊
                rtbText.Text = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示
                UpdateListBox();
                isUndoRedo = false;
            }
        }

        private void rtbText_TextChanged(object sender, EventArgs e)
        {
            // 只有當isUndo這個變數是false的時候，才能堆疊文字編輯紀錄
            if (isUndoRedo == false)
            {
                undoStack.Push(rtbText.Text); // 將當前的文本內容加入堆疊
                redoStack.Clear();            // 清空重作堆疊

                // 確保堆疊中只保留最多10個紀錄
                if (undoStack.Count > MaxHistoryCount)
                {
                    // 用一個臨時堆疊，將除了最下面一筆的文字記錄之外，將文字紀錄堆疊由上而下，逐一移除再堆疊到臨時堆疊之中
                    Stack<string> tempStack = new Stack<string>();
                    for (int i = 0; i < MaxHistoryCount; i++)
                    {
                        tempStack.Push(undoStack.Pop());
                    }
                    undoStack.Clear(); // 清空堆疊
                                       // 文字編輯堆疊紀錄清空之後，再將暫存堆疊（tempStack）中的資料，逐一放回到文字編輯堆疊紀錄
                    foreach (string item in tempStack)
                    {
                        undoStack.Push(item);
                    }
                }
                UpdateListBox(); // 更新 ListBox
            }
        }

        // 更新 ListBox
        void UpdateListBox()
        {            
            listUndo.Items.Clear(); // 清空 ListBox 中的元素

            // 將堆疊中的內容逐一添加到 ListBox 中
            foreach (string item in undoStack)
            {
                listUndo.Items.Add(item);
            }
        }
    }
}
```
:::