私は現在、柔軟で包括的な機能を持つように設計されたJavaでペイントプログラムを作成しているところです。それは、前日に一晩書いた私の最後のプロジェクトに端を発しています。そのため、私が1つずつ取り組んできたバグがたくさんあります(たとえば、空になるファイルしか保存できず、長方形は正しく描画されませんが、円は描画されます...)。
今回は、プログラムに元に戻す/やり直し機能を追加しようとしています。ただし、自分が行ったことを「元に戻す」ことはできません。したがって、BufferedImage
イベントが発生するたびに、mouseReleased
のコピーを保存するというアイデアが浮かびました。ただし、一部の画像は1920x1080の解像度になるため、これは効率的ではないと考えました。画像を保存するには、おそらくギガバイトのメモリが必要になります。
単純に同じものを背景色でペイントして元に戻すことができない理由は、Math.random()
に基づいてペイントするさまざまなブラシがあり、(単一の中に)多くの異なるレイヤーがあるためです。層)。
次に、ペイントに使用するGraphics
オブジェクトをBufferedImage
に複製することを検討しました。このような:
ArrayList<Graphics> revisions = new ArrayList<Graphics>();
@Override
public void mouseReleased(MouseEvent event) {
Graphics g = image.createGraphics();
revisions.add(g);
}
私はこれまでこれを行ったことがないので、いくつか質問があります。
BufferedImages
のクローンを作成するなど、これを行うことで無意味なメモリを無駄にすることはありますか?いいえ、通常、Graphics
オブジェクトを保存することはお勧めできません。 :-)
その理由は次のとおりです。通常、Graphics
インスタンスは短命であり、ある種のサーフェス(通常は(J)Component
またはBufferedImage
)。色、ストローク、スケール、回転など、これらの描画操作の状態を保持します。ただし、描画操作またはピクセルの結果は保持しません。
このため、元に戻す機能を実現するのに役立ちません。ピクセルはコンポーネントまたは画像に属します。したがって、「前の」Graphics
オブジェクトにロールバックしても、ピクセルは前の状態に戻りません。
これが私がうまくいくと知っているいくつかのアプローチです:
コマンドの「チェーン」(コマンドパターン)を使用して、イメージを変更します。コマンドパターンは、元に戻す/やり直しで非常にうまく機能します(そして、Action
のSwing/AWTに実装されています)。元のコマンドから順に、すべてのコマンドをレンダリングします。長所:通常、各コマンドの状態はそれほど大きくないため、メモリ内にアンドゥバッファの多くのステップを含めることができます。短所:多くの操作の後、遅くなります...
すべての操作について、BufferedImage
全体を保存します(最初に行ったように)。長所:実装が簡単です。短所:メモリがすぐに不足します。ヒント:処理時間を増やす代わりに、画像をシリアル化して、元に戻す/やり直しのメモリを少なくすることができます。
上記の組み合わせ。コマンドパターン/チェーンのアイデアを使用しますが、妥当な場合は「スナップショット」(BufferedImages
として)を使用してレンダリングを最適化します。つまり、新しい操作ごとに最初からすべてをレンダリングする必要はありません(高速)。また、これらのスナップショットをディスクにフラッシュ/シリアル化して、メモリ不足を回避します(ただし、速度を上げるために、可能であればメモリに保持します)。コマンドをディスクにシリアル化して、事実上無制限に元に戻すこともできます。長所:正しく実行するとうまく機能します。短所:正しくなるまでに少し時間がかかります。
PS:上記のすべてについて、レスポンシブUIを維持するために、バックグラウンドスレッド(SwingWorker
など)を使用して表示された画像を更新し、コマンド/画像をディスクなどにバックグラウンドで保存する必要があります。
幸運を! :-)
アイデア#1、Graphics
オブジェクトの保存は単純に機能しません。 Graphics
は、一部のディスプレイメモリを「保持」していると見なすべきではなく、ディスプレイメモリの領域にアクセスするためのハンドルと見なすべきです。 BufferedImage
の場合、各Graphics
オブジェクトは常に同じ特定の画像メモリバッファへのハンドルになるため、すべて同じ画像を表します。さらに重要なのは、保存されたGraphics
:では実際には何もできないため、何も保存されないため、何かを「復元」できる方法はありません。
アイデア#2、BufferedImage
sのクローンを作成することははるかに優れたアイデアですが、実際にはメモリを浪費し、すぐにメモリを使い果たしてしまいます。長方形の領域を使用するなど、描画の影響を受ける画像の部分のみを保存するのに役立ちますが、それでも多くのメモリが必要です。これらの元に戻す画像をディスクにバッファリングすると役立つ場合がありますが、UIが遅くなり、応答しなくなります。これはbad;です。さらに、それはあなたのアプリケーションをより多くします複雑でエラーが発生しやすい。
私の代替案は、画像の変更をリストに保存し、画像の上に最初から最後までレンダリングすることです。元に戻す操作は、リストから変更を削除するだけです。
これには、画像の変更を「具体化」する、つまり、実際の描画を実行するvoid draw(Graphics gfx)
メソッドを提供することにより、単一の変更を実装するクラスを作成する必要があります。
あなたが言ったように、ランダムな変更追加の問題を引き起こします。ただし、重要な問題は、Math.random()
を使用して乱数を作成することです。代わりに、固定シード値から作成されたRandom
を使用して各ランダム変更を実行し、(疑似)乱数シーケンスがdraw()
の各呼び出しで同じになるようにします。つまり、各描画まったく同じ効果があります。 (そのため、これらは「疑似ランダム」と呼ばれます。生成された数値はランダムに見えますが、他の関数と同じように決定論的です。)
メモリの問題がある画像保存技術とは対照的に、この技術の問題は、特に変更が計算集約的である場合、多くの変更がGUIを遅くする可能性があることです。これを防ぐための最も簡単な方法は、適切な元に戻せる変更のリストの最大サイズを修正することです。新しい変更を追加してこの制限を超える場合は、リストで最も古い変更を削除し、それをバッキングBufferedImage
自体に適用します。
次の単純なデモアプリケーションは、これがすべて一緒に機能すること(およびその方法)を示しています。また、元に戻されたアクションをやり直すための優れた「やり直し」機能も含まれています。
package stackoverflow;
import Java.awt.*;
import Java.awt.event.ActionEvent;
import Java.awt.event.ActionListener;
import Java.awt.image.BufferedImage;
import Java.util.LinkedList;
import Java.util.Random;
import javax.swing.*;
public final class UndoableDrawDemo
implements Runnable
{
public static void main(String[] args) {
EventQueue.invokeLater(new UndoableDrawDemo()); // execute on EDT
}
// holds the list of drawn modifications, rendered back to front
private final LinkedList<ImageModification> undoable = new LinkedList<>();
// holds the list of undone modifications for redo, last undone at end
private final LinkedList<ImageModification> undone = new LinkedList<>();
// maximum # of undoable modifications
private static final int MAX_UNDO_COUNT = 4;
private BufferedImage image;
public UndoableDrawDemo() {
image = new BufferedImage(600, 600, BufferedImage.TYPE_INT_RGB);
}
public void run() {
// create display area
final JPanel drawPanel = new JPanel() {
@Override
public void paintComponent(Graphics gfx) {
super.paintComponent(gfx);
// display backing image
gfx.drawImage(image, 0, 0, null);
// and render all undoable modification
for (ImageModification action: undoable) {
action.draw(gfx, image.getWidth(), image.getHeight());
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(image.getWidth(), image.getHeight());
}
};
// create buttons for drawing new stuff, undoing and redoing it
JButton drawButton = new JButton("Draw");
JButton undoButton = new JButton("Undo");
JButton redoButton = new JButton("Redo");
drawButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// maximum number of undo's reached?
if (undoable.size() == MAX_UNDO_COUNT) {
// remove oldest undoable action and apply it to backing image
ImageModification first = undoable.removeFirst();
Graphics imageGfx = image.getGraphics();
first.draw(imageGfx, image.getWidth(), image.getHeight());
imageGfx.dispose();
}
// add new modification
undoable.addLast(new ExampleRandomModification());
// we shouldn't "redo" the undone actions
undone.clear();
drawPanel.repaint();
}
});
undoButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!undoable.isEmpty()) {
// remove last drawn modification, and append it to undone list
ImageModification lastDrawn = undoable.removeLast();
undone.addLast(lastDrawn);
drawPanel.repaint();
}
}
});
redoButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!undone.isEmpty()) {
// remove last undone modification, and append it to drawn list again
ImageModification lastUndone = undone.removeLast();
undoable.addLast(lastUndone);
drawPanel.repaint();
}
}
});
JPanel buttonPanel = new JPanel(new FlowLayout());
buttonPanel.add(drawButton);
buttonPanel.add(undoButton);
buttonPanel.add(redoButton);
// create frame, add all content, and open it
JFrame frame = new JFrame("Undoable Draw Demo");
frame.getContentPane().add(drawPanel);
frame.getContentPane().add(buttonPanel, BorderLayout.NORTH);
frame.pack();
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
//--- draw actions ---
// provides the seeds for the random modifications -- not for drawing itself
private static final Random SEEDS = new Random();
// interface for draw modifications
private interface ImageModification
{
void draw(Graphics gfx, int width, int height);
}
// example random modification, draws bunch of random lines in random color
private static class ExampleRandomModification implements ImageModification
{
private final long seed;
public ExampleRandomModification() {
// create some random seed for this modification
this.seed = SEEDS.nextLong();
}
@Override
public void draw(Graphics gfx, int width, int height) {
// create a new pseudo-random number generator with our seed...
Random random = new Random(seed);
// so that the random numbers generated are the same each time.
gfx.setColor(new Color(
random.nextInt(256), random.nextInt(256), random.nextInt(256)));
for (int i = 0; i < 16; i++) {
gfx.drawLine(
random.nextInt(width), random.nextInt(height),
random.nextInt(width), random.nextInt(height));
}
}
}
}
ほとんどのゲーム(またはプログラム)は必要な部分だけを保存し、それがあなたがすべきことです。
長方形は、幅、高さ、背景色、ストローク、アウトラインなどで表すことができます。したがって、実際の長方形の代わりにこれらのパラメータを保存するだけで済みます。 「長方形の色:赤の幅:100の高さ100」
プログラムのランダムな側面(ブラシのランダムな色)については、シードを保存するか、結果を保存することができます。 「ランダムシード:1023920」
プログラムでユーザーが画像をインポートできる場合は、画像をコピーして保存する必要があります。
フィラーとエフェクト(ズーム/変換/グロー)はすべて、シェイプと同じようにパラメーターで表すことができます。例えば。 「ズームスケール:2」「回転角:30」
したがって、これらすべてのパラメータをリストに保存し、元に戻す必要がある場合は、パラメータを削除済みとしてマークできます(ただし、やり直したいので、実際には削除しないでください)。次に、キャンバス全体を消去し、削除済みとしてマークされたパラメーターを除いたパラメーターに基づいて画像を再作成できます。
*行のようなものについては、リストにそれらの場所を保存することができます。
あなたはあなたの画像を圧縮しようとするでしょう(PNGを使うことは良いスタートです、それは本当に役立つzlib圧縮と一緒にいくつかの素晴らしいフィルターを持っています)。これを行うための最良の方法は
それは、PNGでは本当にうまく圧縮されるはずです。黒と白を試して、違いがあるかどうかを確認してください(違いがあるとは思いませんが、アルファ値だけでなく、rgb値も同じものに設定してください。圧縮率が高くなります)。
変更された部分に画像をトリミングすることでさらにパフォーマンスが向上する可能性がありますが、圧縮(およびオフセットを保存して記憶する必要があるという事実)を考慮すると、それからどれだけ得られるかはわかりません。 。
次に、アルファチャネルがあるので、元に戻す場合は、元に戻す画像を現在の画像の上に戻すだけで設定できます。