如何在C# WinForm项目中实现Undo撤销按钮功能?
问题描述
我是第一次提问,目前正在学习C#(Visual Studio),正在开发一款酒吧管理程序,最后一项任务是实现Undo撤销按钮。以下是当前代码:
namespace GestBar_v1._0 { public partial class Form1 : Form { string[] bebidas = { "Café", "Ice Tea", "Água", "Aguardente" }; double[] precoBebidas = { 0.8, 1.5, 1.0, 1.0 }; string[] alimentacao = { "Bolos", "Sandes Mistas", "Torrada", "Salgados" }; double[] precoAlimentacao = { 1.0, 1.5, 1.5, 1.0 }; double soma = 0; double fecho = 0; void resetTxt() { txtPreco.BackColor = Color.White; txtPreco.ForeColor = Color.Black; txtPreco.Font = new Font("Microsoft Sans Serif", 11, FontStyle.Regular); label2.Text = "Preço Final"; } private void Processo(string[] items, double[] prices, int itemIndex) { if (listaProduto.Items.Contains(items[itemIndex])) { int index = listaProduto.Items.IndexOf(items[itemIndex]); double count = double.Parse(listaUnidade.Items[index].ToString()); listaPreco.Items[index] = Math.Round(prices[itemIndex] * (count + 1), 2); listaUnidade.Items[index] = count + 1; soma += prices[itemIndex]; } else { listaProduto.Items.Add(items[itemIndex]); listaPreco.Items.Add(prices[itemIndex]); listaUnidade.Items.Add(1); soma += prices[itemIndex]; } txtPreco.Text = soma.ToString(); } public Form1() { InitializeComponent(); } private void button2_Click(object sender, EventArgs e) { resetTxt(); btn11.Visible = true; btn12.Visible = true; btn13.Visible = true; btn14.Visible = true; btn11.Text = bebidas[0] + "\n" + precoBebidas[0] + "€"; btn12.Text = bebidas[1] + "\n" + precoBebidas[1] + "€"; btn13.Text = bebidas[2] + "\n" + precoBebidas[2] + "€"; btn14.Text = bebidas[3] + "\n" + precoBebidas[3] + "€"; btn21.Visible = false; btn22.Visible = false; btn23.Visible = false; btn24.Visible = false; } private void Form1_Load(object sender, EventArgs e) { } private void btnAliment_Click(object sender, EventArgs e) { resetTxt(); btn21.Visible = true; btn22.Visible = true; btn23.Visible = true; btn24.Visible = true; btn21.Text = alimentacao[0] + "\n" + precoAlimentacao[0] + "€"; btn22.Text = alimentacao[1] + "\n" + precoAlimentacao[1] + "€"; btn23.Text = alimentacao[2] + "\n" + precoAlimentacao[2] + "€"; btn24.Text = alimentacao[3] + "\n" + precoAlimentacao[3] + "€"; btn11.Visible = false; btn12.Visible = false; btn13.Visible = false; btn14.Visible = false; } private void btnTodosP_Click(object sender, EventArgs e) { resetTxt(); btn11.Visible = true; btn12.Visible = true; btn13.Visible = true; btn14.Visible = true; btn21.Visible = true; btn22.Visible = true; btn23.Visible = true; btn24.Visible = true; btn11.Text = bebidas[0] + "\n" + precoBebidas[0] + "€"; btn12.Text = bebidas[1] + "\n" + precoBebidas[1] + "€"; btn13.Text = bebidas[2] + "\n" + precoBebidas[2] + "€"; btn14.Text = bebidas[3] + "\n" + precoBebidas[3] + "€"; btn21.Text = alimentacao[0] + "\n" + precoAlimentacao[0] + "€"; btn22.Text = alimentacao[1] + "\n" + precoAlimentacao[1] + "€"; btn23.Text = alimentacao[2] + "\n" + precoAlimentacao[2] + "€"; btn24.Text = alimentacao[3] + "\n" + precoAlimentacao[3] + "€"; } private void btn11_Click(object sender, EventArgs e) { resetTxt(); Processo(bebidas, precoBebidas, 0); } private void btn12_Click(object sender, EventArgs e) { resetTxt(); Processo(bebidas, precoBebidas, 1); } private void btn13_Click(object sender, EventArgs e) { resetTxt(); Processo(bebidas, precoBebidas, 2); } private void btn14_Click(object sender, EventArgs e) { resetTxt(); Processo(bebidas, precoBebidas, 3); } private void btn21_Click(object sender, EventArgs e) { resetTxt(); Processo(alimentacao, precoAlimentacao, 0); } private void btn22_Click(object sender, EventArgs e) { resetTxt(); Processo(alimentacao, precoAlimentacao, 1); } private void btn23_Click(object sender, EventArgs e) { resetTxt(); Processo(alimentacao, precoAlimentacao, 2); } private void btn24_Click(object sender, EventArgs e) { resetTxt(); Processo(alimentacao, precoAlimentacao, 3); } private void btnNovo_Click(object sender, EventArgs e) { resetTxt(); fecho += Convert.ToDouble(txtPreco.Text); listaProduto.Items.Clear(); listaPreco.Items.Clear(); listaUnidade.Items.Clear(); txtPreco.Clear(); soma = 0; } private void btnRetirarSelec_Click(object sender, EventArgs e) { if (listaProduto.SelectedIndex >= 0) { int index = listaProduto.SelectedIndex; double count = double.Parse(listaUnidade.Items[index].ToString()); soma -= count * precoBebidas[index]; listaUnidade.Items.RemoveAt(index); listaPreco.Items.RemoveAt(index); listaProduto.Items.RemoveAt(index); txtPreco.Text = soma.ToString(); } } private void btnReduzQnt_Click(object sender, EventArgs e) { int index = -1; if (listaProduto.SelectedIndex >= 0) { index = listaProduto.SelectedIndex; } else if (listaPreco.SelectedIndex >= 0) { index = listaPreco.SelectedIndex; } else if (listaUnidade.SelectedIndex >= 0) { index = listaUnidade.SelectedIndex; } if (index >= 0) { double count = double.Parse(listaUnidade.Items[index].ToString()); if (count > 1) { soma -= precoBebidas[index]; soma -= precoAlimentacao[index]; listaUnidade.Items[index] = count - 1; listaPreco.Items[index] = Math.Round(precoBebidas[index] * (count - 1), 2); listaPreco.Items[index] = Math.Round(precoAlimentacao[index] * (count - 1), 2); txtPreco.Text = soma.ToString(); } else { listaUnidade.Items.RemoveAt(index); listaPreco.Items.RemoveAt(index); listaProduto.Items.RemoveAt(index); txtPreco.Text = soma.ToString(); } } } private void btnFechoC_Click(object sender, EventArgs e) { txtPreco.Text = fecho.ToString(); txtPreco.BackColor = Color.Green; txtPreco.ForeColor = Color.White; txtPreco.Font = new Font("Microsoft Sans Serif", 11, FontStyle.Bold); label2.Text = "Saldo Final"; MessageBox.Show("A caixa foi Encerrada."); fecho = 0; } private void btnReturn_Click(object sender, EventArgs e) { } } }
代码里有部分葡萄牙语元素,抱歉。我之前想过用数组每次点击按钮时保存当前状态,再通过按钮更新列表框,但数组长度固定没法实现,完全不知道该怎么解决,恳请帮忙。
实现Undo撤销功能的解决方案
要实现撤销功能,核心是记录每次操作前的完整状态快照,数组长度固定不适合动态保存多个状态,用Stack<T>(栈)最合适——栈的"后进先出"特性刚好匹配撤销逻辑(最后做的操作最先撤销)。
步骤1:定义状态快照类
先创建一个类,用来保存每次操作前的订单完整状态:
// 保存订单状态的快照类 private class OrderState { public List<string> Products { get; set; } public List<double> Quantities { get; set; } public List<double> Prices { get; set; } public double Total { get; set; } }
步骤2:添加栈存储状态
在Form1类中声明栈对象,用来存储状态快照:
// 栈存储操作前的状态,支持撤销 private Stack<OrderState> _undoStack = new Stack<OrderState>();
步骤3:修改操作方法,保存状态快照
每次调用会修改订单状态的方法(比如Processo、btnRetirarSelec_Click、btnReduzQnt_Click)前,先保存当前状态到栈里。
先添加一个通用的保存状态方法:
// 保存当前订单状态到撤销栈 private void SaveCurrentState() { OrderState state = new OrderState(); state.Products = listaProduto.Items.Cast<string>().ToList(); state.Quantities = listaUnidade.Items.Cast<double>().ToList(); state.Prices = listaPreco.Items.Cast<double>().ToList(); state.Total = soma; _undoStack.Push(state); }
然后修改Processo方法,开头加上保存状态的代码:
private void Processo(string[] items, double[] prices, int itemIndex) { // 操作前保存当前状态 SaveCurrentState(); if (listaProduto.Items.Contains(items[itemIndex])) { int index = listaProduto.Items.IndexOf(items[itemIndex]); double count = double.Parse(listaUnidade.Items[index].ToString()); listaPreco.Items[index] = Math.Round(prices[itemIndex] * (count + 1), 2); listaUnidade.Items[index] = count + 1; soma += prices[itemIndex]; } else { listaProduto.Items.Add(items[itemIndex]); listaPreco.Items.Add(prices[itemIndex]); listaUnidade.Items.Add(1); soma += prices[itemIndex]; } txtPreco.Text = soma.ToString(); }
同样,在btnRetirarSelec_Click和btnReduzQnt_Click方法的开头也加上SaveCurrentState();,确保每次修改前都保存状态。
步骤4:实现撤销按钮逻辑
在btnReturn_Click方法中编写恢复状态的代码:
private void btnReturn_Click(object sender, EventArgs e) { resetTxt(); // 如果栈里有保存的状态,就弹出并恢复 if (_undoStack.Count > 0) { OrderState previousState = _undoStack.Pop(); // 清空当前列表 listaProduto.Items.Clear(); listaUnidade.Items.Clear(); listaPreco.Items.Clear(); // 恢复之前的状态 foreach (var product in previousState.Products) { listaProduto.Items.Add(product); } foreach (var quantity in previousState.Quantities) { listaUnidade.Items.Add(quantity); } foreach (var price in previousState.Prices) { listaPreco.Items.Add(price); } // 恢复总金额 soma = previousState.Total; txtPreco.Text = soma.ToString(); } else { MessageBox.Show("没有可撤销的操作"); } }
补充优化
- 点击
btnNovo_Click(新订单)或btnFechoC_Click(结账)时,需要清空撤销栈,因为这些操作是重置订单,之前的操作无需保留:
在btnNovo_Click里添加_undoStack.Clear();,btnFechoC_Click里也添加_undoStack.Clear(); - 原代码中
btnRetirarSelec_Click和btnReduzQnt_Click计算扣除金额时,直接用precoBebidas[index]可能出错(商品可能是食物),建议后续优化时,在状态快照里保存每个商品的单价,或者判断商品所属分类后再取对应价格。
内容的提问来源于stack exchange,提问作者Nuno Sanches
相关产品推荐
相关产品推荐

