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