Подтвердить что ты не робот

Является ли хранение объектов Graphics хорошей идеей?

В настоящее время я пишу программу рисования в Java, предназначенную для гибких и комплексных функций. Это было связано с моим окончательным проектом, который я написал за ночь накануне. Из-за этого у него появилось множество тон и ошибок, которые я решал один за другим (например, я могу сохранять только файлы, которые будут пустыми, мои прямоугольники не рисуются правильно, но мои круги...).

На этот раз я пытался добавить функции отмены/повтора в свою программу. Однако я не могу "отменить" то, что я сделал. Поэтому у меня возникла идея сохранить копии моего 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?
  • Есть ли другой способ, которым я могу это сделать?
4b9b3361

Ответ 1

Нет, сохранение объекта Graphics обычно плохое.: -)

Вот почему: Обычно экземпляры Graphics недолговечны и используются для рисования или рисования на какой-либо поверхности (обычно (J)Component или BufferedImage). Он содержит состояние этих операций рисования, таких как цвета, ход, масштаб, вращение и т.д. Однако он не выполняет результат операций рисования или пикселей.

Из-за этого это не поможет вам добиться отмены функциональности. Пиксели принадлежат компоненту или изображению. Таким образом, возврат назад к "предыдущему" объекту Graphics не будет изменять пиксели обратно в предыдущее состояние.

Здесь некоторые подходы, которые я знаю, работают:

  • Используйте "цепочку" команд (шаблон команды) для изменения изображения. Шаблон команды работает очень хорошо с отменой/повторением (и реализован в Swing/AWT в Action). Выполните все команды последовательно, начиная с оригинала. Pro: Состояние в каждой команде обычно не так велико, что позволяет вам иметь много шагов отменить-буфер в памяти. Con: После многих операций он замедляется...

  • Для каждой операции сохраните весь BufferedImage (как вы изначально сделали). Pro: Легко реализовать. Con: У вас быстро закончится память. Совет. Вы можете сериализовать изображения, что делает отмену/повторение меньшим объемом памяти за счет увеличения времени обработки.

  • Комбинация вышеизложенного с использованием идеи шаблона/цепочки команд, но оптимизирующая рендеринг с "моментальными снимками" (как BufferedImages), когда это разумно. Это означает, что вам не нужно делать все с самого начала для каждой новой операции (быстрее). Также очистите/снимите эти снимки на диск, чтобы избежать нехватки памяти (но сохраните их в памяти, если сможете, для скорости). Вы также можете сериализовать команды на диск, для практически неограниченного отмены. Pro: Отлично работает, когда все сделано правильно. Con: Потребуется некоторое время, чтобы получить право.

PS: Для всего вышесказанного вам нужно использовать фоновый поток (например, SwingWorker или аналогичный) для обновления отображаемого изображения, сохранения команд/изображений на диске и т.д. в фоновом режиме, чтобы поддерживать отзывчивый интерфейс.

Удачи!: -)

Ответ 2

Идея №1, сохраняющая объекты Graphics, просто не сработает. Graphics не следует рассматривать как "удерживание" некоторой отображаемой памяти, а скорее как дескриптор для доступа к области памяти дисплея. В случае BufferedImage каждый объект Graphics всегда будет дескриптором одного и того же заданного буфера памяти изображений, поэтому все они будут представлять одно и то же изображение. Что еще более важно, вы ничего не можете сделать с сохраненным Graphics:. Поскольку они ничего не хранят, нет никакого способа, чтобы они могли "перезаписывать" что-либо.

Идея № 2, клонирование BufferedImage - это гораздо лучшая идея, но вы действительно будете тратить память и быстро исчерпывать ее. Это помогает сохранить только те части изображения, на которые влияет ничья, например, используя прямоугольные области, но все равно стоит много памяти. Буферизация этих изображений отмены на диск может помочь, но это сделает ваш пользовательский интерфейс медленным и невосприимчивым, и это плохо; кроме того, это делает приложение более сложным и подверженным ошибкам.

Моей альтернативой было бы хранить в хранилище изменения изображения в списке, отображаемом от первого до последнего поверх изображения. Операция отмены затем просто состоит из удаления модификации из списка.

Для этого требуется "подтвердить" модификации изображения, т.е. создать класс, который реализует одну модификацию, предоставив метод void draw(Graphics gfx), который выполняет фактический чертеж.

Как вы сказали, случайные изменения создают дополнительную проблему. Однако ключевой проблемой является использование Math.random() для создания случайных чисел. Вместо этого выполните каждую произвольную модификацию с помощью Random, созданной из фиксированного начального значения, так что последовательности (псевдо) случайных чисел одинаковы при каждом вызове draw(), т.е. Каждая ничья имеет точно такие же эффекты. (Вот почему их называют "псевдослучайными" - сгенерированные числа выглядят случайными, но они так же детерминированы, как и любая другая функция.)

В отличие от технологии хранения изображений, которая имеет проблемы с памятью, проблема с этим методом заключается в том, что многие модификации могут привести к медленному графическому интерфейсу, особенно если изменения являются вычислительно интенсивными. Чтобы этого избежать, самым простым способом было бы установить соответствующий максимальный размер списка отмененных изменений. Если этот предел будет превышен путем добавления новой модификации, удалите самую старую модификацию списка и примените ее к самой поддержке 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 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));
            }
        }
    }
}

Ответ 3

В большинстве игр (или программ) сохраняются только необходимые части и то, что вы должны делать.

  • прямоугольник может быть представлен шириной, высотой, цветом фона, штрихом, контуром и т.д. Таким образом, вы можете просто сохранить эти параметры вместо фактического прямоугольника. "прямоугольник цвет: красный ширина: 100 высота 100"

  • для случайных аспектов вашей программы (случайный цвет на кистях) вы можете либо сохранить семя, либо сохранить результат. "случайное семя: 1023920"

  • Если программа позволяет пользователю импортировать изображения, вы должны скопировать и сохранить изображения.

  • заполнители и эффекты (масштабирование/преобразование/свечение) могут быть представлены параметрами точно так же, как и фигуры. например. "масштаб масштаба: 2" "угол поворота: 30"

  • чтобы сохранить все эти параметры в списке, а затем, когда вам нужно отменить, вы можете пометить параметры как удаленные (но фактически не удалять их, так как вы также хотите их переделать). Затем вы можете стереть весь холст и воссоздать изображение на основе параметров минус те, которые были помечены как удаленные.

* для подобных строк вы можете просто сохранить свои местоположения в списке.

Ответ 4

Вам захочется попытаться сжать ваши изображения (с помощью PNG это хороший старт, у него есть хорошие фильтры и сжатие zlib, которые действительно помогают). Я думаю, что лучший способ сделать это -

  • сделайте копию изображения, прежде чем изменять его.
  • изменить его
  • сравнить копию с новым измененным изображением
  • для каждого пикселя, который вы не изменили, сделайте этот пиксель черным, прозрачным пикселем.

Это должно сжать действительно, очень хорошо в PNG. Попробуйте черно-белый и посмотрите, есть ли разница (я не думаю, что будет, но убедитесь, что вы установили значения rgb в одно и то же, а не только значение альфа, чтобы оно лучше сжалось).

Вы можете получить еще лучшую производительность за счет обрезки изображения на часть, которая была изменена, но я не уверен, насколько вы выиграете от этого, учитывая сжатие (и тот факт, что вам нужно будет сохранять и запоминать смещение).

Затем, поскольку у вас есть альфа-канал, если они отменены, вы можете просто вернуть изображение отмены поверх текущего изображения и установить его.