---
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);
}
}
```
## 測試程式
現在你測試程式,應該就可以做到不只是記錄文字編輯記錄,也同時會記錄文字樣式的修改記錄。

你應該會注意到「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);
}
}
}
```
:::