You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 04:12:21