--- tags: 視窗程式設計 --- # 實作:記事本(編輯記錄包括文字樣式) 你應該會發現到,雖然回復上一步與重作下一部功能是有的,但無法連同修改後的文字樣式一起復原與重作。 這裡我們就要跟同學介紹如何修改原來的回復與重作功能,讓原本只能回復重作文字,變成可以連格式一起回復與重作。 # 第一部分:介面設定 這次不需要改變介面,因此跳過。 # 第二部分:程式碼撰寫 ## 回復與重作堆疊重新宣告 接下來,我們將原來的回復與重作堆疊修改,原本它們是僅記錄字串(String),我們改為另一種資料型態,稱為「**記憶體資料流 MemoryStream**」,是將資料直接存放在記憶體中的一種資料流。 這種資料型態可以讓你將資料存在記憶體,速度快、適合做為暫存空間,我們可以直接將整個 rtbText 豐富文字框中的文字直接記錄進去,這樣就不只是記錄文字,還可以包括文字的樣式。 ```csharp= private Stack<MemoryStream> undoStack = new Stack<MemoryStream>(); // 回復堆疊 private Stack<MemoryStream> redoStack = new Stack<MemoryStream>(); // 重作堆疊 ``` ## 建立將文字編輯狀態保存到回復堆疊的方法 其實建立一個 MemoryStream 等於就是在記憶體開一個空間,再將 rtbText 的內容直接存進去,並且同時也是 RTF 的格式。 然後我們再將這個記憶體空間,直接堆放到回復堆疊之中。 而且這是一個方法,換句話說,只要執行這個方法,就可以將文字編輯狀態保存到回復堆疊。 ```csharp= // 將文字編輯狀態保存到回復堆疊 private void SaveCurrentStateToStack() { // 創建一個新的 MemoryStream 來保存文字編輯狀態 MemoryStream memoryStream = new MemoryStream(); // 將 RichTextBox 的內容保存到 memoryStream rtbText.SaveFile(memoryStream, RichTextBoxStreamType.RichText); // 將 memoryStream 放入回復堆疊 undoStack.Push(memoryStream); } ``` ## 建立將文字狀態從記憶體中顯示到 RichTextBox 的方法 如果要將「MemoryStream」中的內容再放進 rtbText 之中,則是反過來,讀取記憶體空間後再將 MemoryStream 的內容放到到 RichTextBox。 這也是一個方法,也具備一個參數,參數則是你要顯示的 MemoryStream 資料。 ```csharp= // 將文字狀態從記憶體中顯示到 RichTextBox private void LoadFromMemory(MemoryStream memoryStream) { // 將 memoryStream 的指標重置到開始位置 memoryStream.Seek(0, SeekOrigin.Begin); // 將 memoryStream 的內容放到到 RichTextBox rtbText.LoadFile(memoryStream, RichTextBoxStreamType.RichText); } ``` ## 修改 rtbText 文字修改時記錄回復堆疊的程式 以下的程式則是 rtbText 的 TextChanged 事件綁定,跟之前一樣,也是在編輯文字的過程之中就會記錄文字修改歷程。 不過由於我們改用「MemoryStream」來記錄每一次的文字編輯記錄,因此在第 6、13、20 行都做了相對的修正。 更新 ListBox 的方法也是一樣,在第 35 行也做了修正。 ```csharp= private void rtbText_TextChanged(object sender, EventArgs e) { // 只有當isUndo這個變數是false的時候,才能堆疊文字編輯紀錄 if (isUndoRedo == false) { SaveCurrentStateToStack(); // 將當前的文本內容加入堆疊 redoStack.Clear(); // 清空重作堆疊 // 確保堆疊中只保留最多10個紀錄 if (undoStack.Count > MaxHistoryCount) { // 用一個臨時堆疊,將除了最下面一筆的文字記錄之外,將文字紀錄堆疊由上而下,逐一移除再堆疊到臨時堆疊之中 Stack<MemoryStream> tempStack = new Stack<MemoryStream>(); for (int i = 0; i < MaxHistoryCount; i++) { tempStack.Push(undoStack.Pop()); } undoStack.Clear(); // 清空堆疊 // 文字編輯堆疊紀錄清空之後,再將暫存堆疊(tempStack)中的資料,逐一放回到文字編輯堆疊紀錄 foreach (MemoryStream item in tempStack) { undoStack.Push(item); } } UpdateListBox(); // 更新 ListBox } } private void btnUndo_Click(object sender, EventArgs e) { if (undoStack.Count > 1) { isUndoRedo = true; redoStack.Push(undoStack.Pop()); // 將回復堆疊最上面的紀錄移出,再堆到重作堆疊 MemoryStream lastSavedState = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示 LoadFromMemory(lastSavedState); UpdateListBox(); isUndoRedo = false; } } private void btnRedo_Click(object sender, EventArgs e) { if (redoStack.Count > 0) { isUndoRedo = true; undoStack.Push(redoStack.Pop()); // 將重作堆疊最上面的紀錄移出,再堆到回復堆疊 MemoryStream lastSavedState = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示 LoadFromMemory(lastSavedState); UpdateListBox(); isUndoRedo = false; } } // 更新 ListBox void UpdateListBox() { listUndo.Items.Clear(); // 清空 ListBox 中的元素 // 將堆疊中的內容逐一添加到 ListBox 中 foreach (MemoryStream item in undoStack) { listUndo.Items.Add(item); } } ``` ## 測試程式 現在你測試程式,應該就可以做到不只是記錄文字編輯記錄,也同時會記錄文字樣式的修改記錄。 ![](https://imgur.com/v0L4ocT.png) 你應該會注意到「listUndo」的清單方塊內容就看不懂了,因此這個清單方塊就可以不需要,你也可以簡單地把所有程式中的「UpdateListBox()」註解或刪除掉,這樣就可以了。 # 完整程式碼 :::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(); InitializeFontComboBox(); InitializeFontSizeComboBox(); InitializeFontStyleComboBox(); } private bool isUndoRedo = false; // 是否在回復或重作階段 private Stack<MemoryStream> undoStack = new Stack<MemoryStream>(); // 回復堆疊 private Stack<MemoryStream> redoStack = new Stack<MemoryStream>(); // 重作堆疊 //private Stack<string> undoStack = new Stack<string>(); //private Stack<string> redoStack = new Stack<string>(); private const int MaxHistoryCount = 10; // 最多紀錄10個紀錄 private int selectionStart = 0; // 記錄文字反白的起點 private int selectionLength = 0; // 記錄文字反白的長度 private void btnOpen_Click(object sender, EventArgs e) { // 設置對話方塊標題 openFileDialog1.Title = "選擇檔案"; // 設置對話方塊篩選器,限制使用者只能選擇特定類型的檔案 openFileDialog1.Filter = "RTF格式檔案 (*.rtf)|*.rtf|文字檔案 (*.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; // 獲取文件的副檔名 string fileExtension = Path.GetExtension(selectedFileName).ToLower(); // 判斷副檔名是甚麼格式 if (fileExtension == ".txt") { // 使用 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(); } } } else if (fileExtension == ".rtf") { // 如果是RTF文件,使用RichTextBox的LoadFile方法 rtbText.LoadFile(selectedFileName, RichTextBoxStreamType.RichText); } } 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 = "RTF格式檔案 (*.rtf)|*.rtf|文字檔案 (*.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; string extension = Path.GetExtension(saveFileName); // 使用 using 與 FileStream 建立檔案,如果檔案已存在則覆寫 using (fileStream = new FileStream(saveFileName, FileMode.Create, FileAccess.Write)) { if (extension.ToLower() == ".txt") { // 將 RichTextBox 中的文字寫入檔案中 byte[] data = Encoding.UTF8.GetBytes(rtbText.Text); fileStream.Write(data, 0, data.Length); } else if (extension.ToLower() == ".rtf") { // 將RichTextBox中的內容保存為RTF格式 rtbText.SaveFile(fileStream, RichTextBoxStreamType.RichText); } } 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()); // 將回復堆疊最上面的紀錄移出,再堆到重作堆疊 MemoryStream lastSavedState = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示 LoadFromMemory(lastSavedState); UpdateListBox(); isUndoRedo = false; } } private void btnRedo_Click(object sender, EventArgs e) { if (redoStack.Count > 0) { isUndoRedo = true; undoStack.Push(redoStack.Pop()); // 將重作堆疊最上面的紀錄移出,再堆到回復堆疊 MemoryStream lastSavedState = undoStack.Peek(); // 將回復堆疊最上面一筆紀錄顯示 LoadFromMemory(lastSavedState); UpdateListBox(); isUndoRedo = false; } } private void rtbText_TextChanged(object sender, EventArgs e) { // 只有當isUndo這個變數是false的時候,才能堆疊文字編輯紀錄 if (isUndoRedo == false) { SaveCurrentStateToStack(); // 將當前的文本內容加入堆疊 redoStack.Clear(); // 清空重作堆疊 // 確保堆疊中只保留最多10個紀錄 if (undoStack.Count > MaxHistoryCount) { // 用一個臨時堆疊,將除了最下面一筆的文字記錄之外,將文字紀錄堆疊由上而下,逐一移除再堆疊到臨時堆疊之中 Stack<MemoryStream> tempStack = new Stack<MemoryStream>(); for (int i = 0; i < MaxHistoryCount; i++) { tempStack.Push(undoStack.Pop()); } undoStack.Clear(); // 清空堆疊 // 文字編輯堆疊紀錄清空之後,再將暫存堆疊(tempStack)中的資料,逐一放回到文字編輯堆疊紀錄 foreach (MemoryStream item in tempStack) { undoStack.Push(item); } } UpdateListBox(); // 更新 ListBox } } // 更新 ListBox void UpdateListBox() { listUndo.Items.Clear(); // 清空 ListBox 中的元素 // 將堆疊中的內容逐一添加到 ListBox 中 foreach (MemoryStream item in undoStack) { listUndo.Items.Add(item); } } // 初始化字體下拉選單 private void InitializeFontComboBox() { // 將所有系統字體名稱添加到字體選擇框中 foreach (FontFamily font in FontFamily.Families) { comboBoxFont.Items.Add(font.Name); } // 設置預設選中的項目為第一個字體 comboBoxFont.SelectedIndex = 0; } // 初始化字體大小下拉選單 private void InitializeFontSizeComboBox() { // 從8開始,每次增加2,直到72,將這些數值添加到字體大小選擇框中 for (int i = 8; i <= 72; i += 2) { comboBoxSize.Items.Add(i); } // 設置預設選中的項目為第三個大小,即12字體大小 comboBoxSize.SelectedIndex = 2; } // 初始化字體樣式下拉選單 private void InitializeFontStyleComboBox() { // 將不同的字體樣式添加到字體樣式選擇框中 comboBoxStyle.Items.Add(FontStyle.Regular.ToString()); // 正常 comboBoxStyle.Items.Add(FontStyle.Bold.ToString()); // 粗體 comboBoxStyle.Items.Add(FontStyle.Italic.ToString()); // 斜體 comboBoxStyle.Items.Add(FontStyle.Underline.ToString()); // 底線 comboBoxStyle.Items.Add(FontStyle.Strikeout.ToString()); // 刪除線 // 設置預設選中的項目為第一個樣式,即正常字體 comboBoxStyle.SelectedIndex = 0; } private void comboBox_SelectedIndexChanged(object sender, EventArgs e) { // 保存當前選擇的文字起始位置和長度 selectionStart = rtbText.SelectionStart; selectionLength = rtbText.SelectionLength; // 確保當前選擇的文字具有字型 if (rtbText.SelectionFont != null) { // 從下拉選單中獲取選擇的字型、大小和樣式 string selectedFont = comboBoxFont.SelectedItem?.ToString(); string selectedSizeStr = comboBoxSize.SelectedItem?.ToString(); string selectedStyleStr = comboBoxStyle.SelectedItem?.ToString(); // 確保字型、大小和樣式都已選擇 if (selectedFont != null && selectedSizeStr != null && selectedStyleStr != null) { // 將選擇的大小字串轉換為浮點數 float selectedSize = float.Parse(selectedSizeStr); // 將選擇的樣式字串轉換為 FontStyle 枚舉值 FontStyle selectedStyle = (FontStyle)Enum.Parse(typeof(FontStyle), selectedStyleStr); // 獲取當前選擇的文字的字型 Font currentFont = rtbText.SelectionFont; FontStyle newStyle = currentFont.Style; // 檢查是否需要應用新的樣式,並更新樣式 if (comboBoxStyle.SelectedItem.ToString() == FontStyle.Bold.ToString()) newStyle = FontStyle.Bold; else if (comboBoxStyle.SelectedItem.ToString() == FontStyle.Italic.ToString()) newStyle = FontStyle.Italic; else if (comboBoxStyle.SelectedItem.ToString() == FontStyle.Underline.ToString()) newStyle = FontStyle.Underline; else if (comboBoxStyle.SelectedItem.ToString() == FontStyle.Strikeout.ToString()) newStyle = FontStyle.Strikeout; else newStyle = FontStyle.Regular; // 創建新的字型並應用到選擇的文字 Font newFont = new Font(selectedFont, selectedSize, newStyle); rtbText.SelectionFont = newFont; } } // 恢復選擇狀態 rtbText.Focus(); rtbText.Select(selectionStart, selectionLength); } // 將文字編輯狀態保存到回復堆疊 private void SaveCurrentStateToStack() { // 創建一個新的 MemoryStream 來保存文字編輯狀態 MemoryStream memoryStream = new MemoryStream(); // 將 RichTextBox 的內容保存到 memoryStream rtbText.SaveFile(memoryStream, RichTextBoxStreamType.RichText); // 將 memoryStream 放入回復堆疊 undoStack.Push(memoryStream); } // 將文字狀態從記憶體中顯示到 RichTextBox private void LoadFromMemory(MemoryStream memoryStream) { // 將 memoryStream 的指標重置到開始位置 memoryStream.Seek(0, SeekOrigin.Begin); // 將 memoryStream 的內容放到到 RichTextBox rtbText.LoadFile(memoryStream, RichTextBoxStreamType.RichText); } } } ``` :::