命令模式和OnPaint事件问题

4

我正在尝试将命令模式应用于具有撤销功能的简单绘画应用程序中。但是,我在撤销操作的OnPaint事件上遇到了问题。以下是代码:

[已解决] 详见帖子末尾

interface ICommand {
    void Execute();
    void UnExecute();
}

class DrawLineCommand : ICommand {
    private SimpleImage simpleImage;
    private Image prevImage;
    public DrawLineCommand(SimpleImage simpleImage) {
        this.simpleImage = simpleImage;
        this.prevImage = simpleImage.Image;
    }
    public void Execute() {
        simpleImage.DrawLine();
    }
    public void UnExecute() {
        simpleImage.Image = prevImage;
    }
}

class CommandManager {
    private Stack undoStack = new Stack();
    public void ExecuteCommand(ICommand command) {
        command.Execute();
        undoStack.Push(command);
    }
    public void UnExecuteCommand() {
        if (undoStack.Count > 0) {
            ICommand command = (ICommand)undoStack.Pop();
            command.UnExecute();
        }
    }
}

class SimpleImage {
    private Point startPoint;
    private Point endPoint;
    private PictureBox pictureBox;
    public SimpleImage(PictureBox pictureBox) {
        this.pictureBox = pictureBox;
        pictureBox.Paint += new PaintEventHandler(pictureBox_Paint);
    }
    void pictureBox_Paint(object sender, PaintEventArgs e) {
        // this code shows the line during drawing
        // this code is under "if operation == drawLine" block
        Graphics graphics = e.Graphics;
        graphics.DrawLine(Pens.Red, startPoint, endPoint);

        // how can i refresh picturebox after undo operation?
        // "if operation == undo" then ??
    }
    public void DrawLine() {
        // this code actually saves finally drawn line
        Image img = Image;
        Graphics graphics = Graphics.FromImage(img);
        graphics.DrawLine(Pens.Red, startPoint, endPoint);
        Image = img;
    }

    public void Invalidate() {
        pictureBox.Invalidate();
    }
    public Image Image {
        get { return pictureBox.Image; }
        set { pictureBox.Image = value; }
    }
    public Point StartPoint {
        get { return startPoint; }
        set { startPoint = value; }
    }
    public Point EndPoint {
        get { return endPoint; }
        set { endPoint = value; }
    }
}

public partial class FormMain : Form {
    private PictureBox pictureBox;
    private SimpleImage simpleImage;
    private CommandManager commandManager;
    public FormMain() {
        InitializeComponent();
        simpleImage = new SimpleImage(this.pictureBox);
        commandManager = new CommandManager();
    }
    void pictureBox_MouseDown(object sender, MouseEventArgs e) {
        if (e.Button != MouseButtons.Left)
            return;

        simpleImage.StartPoint = e.Location;
    }
    void pictureBox_MouseMove(object sender, MouseEventArgs e) {
        if (e.Button != MouseButtons.Left)
            return;

        simpleImage.EndPoint = e.Location;
        simpleImage.Invalidate();
    }
    void pictureBox_MouseUp(object sender, MouseEventArgs e) {
        simpleImage.Invalidate();
        commandManager.ExecuteCommand(new DrawLineCommand(simpleImage));
    }
}

实际上,它画了一条线,执行命令并将其推入堆栈。我无法实现工作中的UNDO。我的意思是,逐步调试时,我看到对象从堆栈中弹出,然后执行OnPaint。但是实际上没有显示“先前”的图像。
我已经阅读了许多网站,并且还从其中一个codeproject网站/文章中获得了示例应用程序。它像狗一样工作。唯一的区别是这种残忍的OnPaint方法。
提前感谢任何建议!
[编辑]匆忙之中,我忘记了将一个引用类型分配给另一个不是复制它(创建独立对象),更改如下:
this.prevImage = simpleImage.Image;

在少数地方解决了问题。现在一切都正常运作。


如果您在得到答案之前解决了问题,通常希望将解决方案作为对自己问题的答案发布。其他人可以对此进行投票等操作。 - Kaleb Pederson
哦,这很有道理;)感谢你的提示。 - Krzysztof Szynter
2个回答

2
这里的重点不是直接在画布上作画,而是有一个表示你的绘画的数据结构。然后你会向这个绘画对象添加一条线,画布的主循环将从数据结构中绘制适当的图形。然后你的撤销/重做方法只需要操作数据结构,而不是进行绘画。你需要像这样的东西:
interface IPaintable // intarface for Lines, Text, Circles, ...
{
    void OnPaint(Image i); // does the painting
}

interface IPaintableCommand // interface for commands 
{
    void Do(ICollection<IPaintable> painting); // adds line/text/circle to painting
    void Undo(ICollection<IPaintable> painting);  // removes line/text/circle from painting    
}

你的主要应用程序只需保留一个列表,并在命令更改绘画集合时重新绘制画布。

感谢您的意见,我一定会改变我的方法。 - Krzysztof Szynter

1

看起来你有一个别名问题。在你的DrawLineCommand中,你在操作之前引用了图像并将其存储为这样:

this.prevImage = simpleImage.Image;

现在您有两个指向同一对象的引用。当您对该图像进行绘制线操作时,它会发生:

Image img = Image; // Now a third reference to the same image object
Graphics graphics = Graphics.FromImage(img);
graphics.DrawLine(Pens.Red, startPoint, endPoint);
Image = img; // and you set the Image reference back to the same object

以上代码使得 img 对象对于 Image 类的引用变得不必要。但是,你仍然在命令中有另一个对 Image 类的引用。垃圾回收器运行后,你将只剩下对同一图像对象的两个引用。接着,Undo 执行以下操作:
simpleImage.Image = prevImage;

这里你没有改变图像,只是让Image引用了同一个它已经引用的对象。

虽然我非常赞同m0sa的观点,在这种情况下,解决方法是在创建命令时将prevImage设置为原始图像的副本。以下假设实现了Image.Clone(),尽管我自己从未尝试过:

this.prevImage = simpleImage.Image.Clone();

注意:如果您使用此方法,处理大型图像或许多命令时可能会很快耗尽内存。


是的,我理解这种方法的缺点。特别是内存使用方面。这只是一个学习命令模式的示例应用程序。我会尝试修改代码并实施给出的建议。谢谢。 - Krzysztof Szynter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接