---
tags: 視窗程式設計
---
# 實作:記事本(重做下一步)
我們已經實作了基本的復原上一步功能,讓記事本程式可以記錄文字編輯過程,並且可以回到前十次的文字修改紀錄。
不過這類的文字編輯軟體,都可以有一個重作功能,讓你回復到上一步之後,又能重作一次之前的內容。
# 第一部分:介面設定
這次的介面設計也很簡單,再多加一個「**重作下一步**」按鍵就可以了,名稱可以是以下。
:::info
1. 重作下一步按鍵:btnRedo
:::
同樣的,也要做 Click 的事件綁定。畫面參考如下。

# 第二部分:程式碼撰寫
完成基本介面設計後,我們就要來實現個「**重作下一步**」的功能。
我們也一樣使用堆疊的資料結構,不一樣的地方在於,我們使用兩個堆疊來儲存文字修改記錄:
:::success
1. 回復(undo)堆疊:undoStack
2. 重作(redo)堆疊:redoStack
:::
當使用者進行「回復上一步」操作時,將會把回復上一步的內容,堆疊移到重作堆疊;當使用者「重作下一步」操作時,將狀態從重作堆疊移回回復堆疊。就像以下圖片的概念。
1. 假如使用者輸入一段文字「John」,回復紀錄已經呈現以下堆疊狀態,重作堆疊還是空的。

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

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

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

## 程式碼
如果大概的概念你理解了,接下來就是程式碼的加入,我們需要修改的地方滿多的,請你仔細好好檢查再加入程式碼
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);
}
}
}
}
```
:::