Отрисовка компонента в BufferedImage приводит к повреждению изображения

Я использую компонент JScrollNavigator, описанный здесь, чтобы предоставить окно навигации на большой «холстовый» компонент САПР, который я встроил в файл JScrollPane.

Я попытался адаптировать JScrollNavigator для рисования эскиза холста, чтобы предоставить пользователю дополнительный контекст. Однако это действие приводит к повреждению рендеринга основного фрейма моего приложения. В частности, это действие вызова paint(Graphics) для компонента окна просмотра (т. е. моего основного холста), передачи объекта Graphics, созданного BufferedImage, что вызывает последующее повреждение отображения; если я закомментирую эту строку, все будет работать нормально.

Ниже приведен переопределенный метод paintComponent JScrollNavigator:

@Override
protected void paintComponent(Graphics g) {
    Component view = jScrollPane.getViewport().getView();
    BufferedImage img = new BufferedImage(view.getWidth(), view.getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d = img.createGraphics();

    // Paint JScrollPane view to off-screen image and then scale.
    // It is this action that causes the display corruption!
    view.paint(g2d);
    g2d.drawImage(img, 0, 0, null);
    Image scaled = img.getScaledInstance(getWidth(), getHeight(), 0);

    super.paintComponent(g);
    g.drawImage(scaled, 0, 0, null);
}

У кого-нибудь есть какие-либо предположения относительно причины коррупции? Я бы подумал, что рисование внеэкранного изображения не должно влиять на существующие операции рисования.

ИЗМЕНИТЬ

Чтобы предоставить некоторые дополнительные детали: JScrollNavigator образует подпанель слева от JSplitPane. JScrollPane, связанный с навигатором, находится справа. «Повреждение» приводит к тому, что сплиттер больше не отображается, а полосы прокрутки не видны (они кажутся белыми). Если я изменю размер JFrame, раздел JMenu также станет белым. Если я пытаюсь использовать навигатор или взаимодействовать с полосами прокрутки, они становятся видимыми, но сплиттер остается белым. Это как если бы настройки непрозрачности различных компонентов были затронуты рендерингом окна просмотра в закадровое изображение.

Кроме того, если я заставлю JScrollNavigator появиться в совершенно отдельном JDialog, все будет работать правильно.

ИЗМЕНИТЬ 2

Я могу воспроизвести проблему последовательно, выполнив следующие действия:

Добавьте JMenuBar к mFrame:

JMenuBar bar = new JMenuBar();
bar.add(new JMenu("File"));
mFrame.setJMenuBar(bar);

В методе main() из JScrollNavigator замените:

jsp.setViewportView(textArea);

... с участием:

jsp.setViewportView(new JPanel() {
  {
    setBackground(Color.GREEN);
    setBorder(BorderFactory.createLineBorder(Color.BLACK, 5));
  }
});

Убедитесь, что JScrollNavigator встроена как панель в mFrame, а не отображается как отдельная JDialog:

mFrame.add(jsp, BorderLayout.CENTER);
mFrame.add(nav, BorderLayout.NORTH);

Теперь, когда приложение запускается, JMenuBar больше не отображается; акт рисования представления (т.е. зеленого JPanel с толстой черной рамкой) для Graphics2D, возвращаемого BufferedImage.createGraphics(), на самом деле, кажется, отображает его на экране, возможно, из верхнего левого угла JFrame, таким образом скрывая другие компоненты. Кажется, это происходит только в том случае, если в качестве окна просмотра используется JPanel, а не другой компонент, такой как JTextArea, JTable и т. д.

ИЗМЕНИТЬ 3

Похоже, у этого человека возникла та же проблема (хотя решение не опубликовано): http://www.javaworld.com/community/node/2894/

ИЗМЕНИТЬ 4

Вот методы main и paintComponent, которые приводят к воспроизводимой ошибке, описанной в Редактировании 2:

public static void main(String[] args) {
    JScrollPane jsp = new JScrollPane();
    jsp.setViewportView(new JPanel() {
        {
            setBackground(Color.GREEN);
            setBorder(BorderFactory.createLineBorder(Color.BLACK, 5));
        }
    });

    JScrollNavigator nav = new JScrollNavigator();
    nav.setJScrollPane(jsp);

    JFrame mFrame = new JFrame();

    JMenuBar bar = new JMenuBar();
    bar.add(new JMenu("File"));
    mFrame.setJMenuBar(bar);

    mFrame.setTitle("JScrollNavigator Test");

    mFrame.setSize(800, 600);

    mFrame.setLayout(new GridLayout(1, 2));

    mFrame.add(jsp);
    mFrame.add(nav);
    Dimension screenDim = Toolkit.getDefaultToolkit().getScreenSize();
    mFrame.setLocation((screenDim.width - mFrame.getSize().width) / 2, (screenDim.height - mFrame.getSize().height) / 2);

    mFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    mFrame.setVisible(true);
}

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Component view = jScrollPane.getViewport().getView();

    if (img == null) {
        GraphicsConfiguration gfConf = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();
        img = new BufferedImage(view.getWidth(), view.getHeight(), BufferedImage.TYPE_INT_ARGB);
    }

    Graphics2D g2d = img.createGraphics();
    view.paint(g2d);

    Image scaled = img.getScaledInstance(getWidth(), getHeight(), 0);

    g.drawImage(scaled, 0, 0, null);
}

ИЗМЕНИТЬ 5

Кажется, что у других возникают проблемы с воссозданием точной проблемы. Я бы попросил людей запустить код, вставленный здесь. Когда я впервые запускаю этот пример, я вижу следующее:

Поврежденное изображение 1

Ни JScrollNavigator, ни JMenuBar не были нарисованы; эти области кадра прозрачны.

После изменения размера вижу следующее:

Поврежденное изображение 2

JMenuBar все еще не нарисовано, и кажется, что JPanel в какой-то момент был отрендерен в (0,0) (где должен быть JMenuBar). Вызов view.paint внутри paintComponent является прямой причиной этого.


person Adamski    schedule 31.07.2012    source источник
comment
Что вы подразумеваете под «коррупцией». Я только что запустил этот пример, и он отлично работает для меня. Мне нужно было только вызвать super.paintComponent(g); в начале переопределенного метода paintComponent.   -  person Xeon    schedule 31.07.2012
comment
Я отправил код здесь.   -  person Xeon    schedule 31.07.2012


Ответы (3)


Резюме: исходный JScrollNavigator использует Swing opacity для отображения удобного зеленого NavBox поверх масштабированного эскиза компонента в соседний JScrollPane. Поскольку он расширяет JPanel, использование (общим) делегатом пользовательского интерфейса opacity конфликтует с использованием прокручиваемого компонента. Изображения, показанные в редактировании 5 выше, типизируют связанный артефакт рендеринга, также показанный здесь. Решение состоит в том, чтобы позволить NavBox, JScrollNavigator и прокручиваемому компоненту расширять JComponent, как это предлагается во втором дополнении ниже. Затем каждый компонент может управлять своими свойствами индивидуально.

введите здесь описание изображения

Я не вижу необычных артефактов рендеринга в вашем коде, опубликованном на моей платформе, Mac OS X, Java 1.6. Извините, явных нарушений переносимости не вижу.

первое изображение

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

  • Даже если вы используете setSize(), в этом случае вы все равно должны pack() заключать Window.

    f.pack();
    f.setSize(300, 200);
    
  • Для удобства add() перенаправляет компонент на панель содержимого.

    f.add(nav, BorderLayout.WEST);
    
  • Предпочтение от StringBuilder до StringBuffer.

  • Рассмотрим ComponentAdapter вместо ComponentListener.

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

изображение два

editPane.insertIcon(UIManager.getIcon("OptionPane.errorIcon"));
editPane.insertIcon(UIManager.getIcon("OptionPane.warningIcon"));
...
@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Component view = jScrollPane.getViewport().getView();
    BufferedImage img = new BufferedImage(view.getWidth(),
        view.getHeight(), BufferedImage.TYPE_INT_ARGB);
    Graphics2D off = img.createGraphics();
    off.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    off.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    view.paint(off);
    Graphics2D on = (Graphics2D)g;
    on.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    on.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    on.drawImage(img, 0, 0, getWidth(), getHeight(), null);
}

Приложение secundum: Похоже, делегат JPanel UI не сотрудничает. Одним из обходных путей является расширение JComponent, чтобы вы могли контролировать непрозрачность. . Это лишь немного больше работы, чтобы управлять backgroundColor. NavBox и JScrollNavigator также являются кандидатами на подобное лечение.

введите здесь описание изображения

jsp.setViewportView(new JComponent() {

    {
        setBackground(Color.red);
        setBorder(BorderFactory.createLineBorder(Color.BLACK, 16));
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(getBackground());
        g.fillRect(0, 0, getWidth(), getHeight());
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(300, 300);
    }
});
person trashgod    schedule 31.07.2012
comment
Спасибо - TBH Я внес эти изменения в JScrollNavigator уже при импорте кода. Я заметил, что проблема с повреждением возникает только в том случае, если я встраиваю JScrollNavigator в качестве подпанели фрейма приложения; Если у меня есть навигатор как отдельный JDialog, он работает нормально. Я буду копать. - person Adamski; 01.08.2012
comment
@Adamski: этот ответ напомнил мне об артефакте передискретизации, упомянутом в цитируемой статье здесь. - person trashgod; 01.08.2012
comment
Спасибо за советы по изменению размера. См. мое второе редактирование для примера того, как воспроизвести проблему коррупции в 100% случаев. Похоже, что это связано с использованием JPanel в качестве окна просмотра, а не, например. JTextArea и т. д. - person Adamski; 01.08.2012
comment
Извините, я не вижу этого в данный момент, используя, например. GridLayout со стороны навигации. Вы можете попробовать разделить левую половину разделенной панели по вертикали. - person trashgod; 01.08.2012
comment
Спасибо; Я попытался перейти на GridLayout, но он все еще выглядит не очень хорошо - JMenuBar закрашен. Я опубликовал точные методы main и paintComponent, которые я использовал. - person Adamski; 01.08.2012
comment
Я делаю setSize() после pack(); и я вызываю super в начале paintComponent(), как в оригинале. Извините, мой умственный diff слаб. :-) В Ubuntu тоже выглядит нормально. - person trashgod; 01.08.2012
comment
Не могли бы вы попробовать использовать методы main и paintComponent, которые я перечисляю в конце моего вопроса? Проблема проявляется только тогда, когда представление области просмотра представляет собой JPanel; для JTextArea и т. д. все работает нормально. - person Adamski; 02.08.2012
comment
Прохладный. Я изменил макет и заменил getPreferredSize() в анонимном JPanel, а также изменил цвет, чтобы видеть зеленый overBox. Все работало корректно, так как исправлено из оригинал. - person trashgod; 02.08.2012
comment
Ну ладно - Большое спасибо за попытку. На самом деле я только что связался с точным кодом, который я запускаю, со скриншотами. До сих пор я пытался использовать JDK 1.6.0_21 и 1.7.0_05 в Windows 7. Также попробую на Linux и посмотрю, сохраняется ли проблема. - person Adamski; 03.08.2012
comment
Я вижу нормальный рендеринг на Mac с использованием вашего последнего кода. Артефакт строки меню на снимке экрана выглядит так, будто contentPane фрейма был изменен, возможно, заменен, а не добавлен. Кстати, setSize() в конструкторе JScrollNavigator может быть лишним. - person trashgod; 03.08.2012
comment
Я предполагаю, что это должна быть проблема, специфичная для Windows - я попросил двух коллег запустить код, одного в Fedora, другого в Windows 7. У обоих были одинаковые проблемы с перерисовкой, и только с JPanel в качестве окна просмотра, а не JTextArea (где все работает нормально). - person Adamski; 03.08.2012
comment
Хорошо, я вижу это и в Ubuntu. Я предложил альтернативу выше. - person trashgod; 04.08.2012
comment
Отличный материал - Большое спасибо! Я принял твой ответ. Как вы думаете, действительно ли это ошибка, о которой стоит сообщить в Oracle? - person Adamski; 06.08.2012
comment
Рад, что это было полезно. Я не уверен, что это ошибка: делегату пользовательского интерфейса JPanel разрешено управлять непрозрачностью. Замена NavBox и JScrollNavigator на расширение JComponent оказалась простой задачей. Я еще не уверен, как это согласуется с выводами @Nick Rippe. - person trashgod; 06.08.2012
comment
Кстати, ..увидеть несопоставимый эффект на изображениях и проверить. быть ..увидеть несопоставимый эффект на изображениях и тексте. (s-›x)? - person Andrew Thompson; 12.08.2012

Я также не уверен, что вы подразумеваете под искажением, но я заметил, что изображение с передискретизацией намного лучше, если вы укажете Image.SCALE_SMOOTH в качестве подсказки масштабирования:

Image scaled = img.getScaledInstance(getWidth(), getHeight(), Image.SCALE_SMOOTH);

Может быть, это то, что вы ищете...

person lbalazscs    schedule 31.07.2012
comment
Ах, этот артефакт. Также рассмотрите этот ответ об артефакте передискретизации; +1. - person trashgod; 01.08.2012

Я смог воспроизвести вашу проблему и получить результат, который вы ищете. Проблема в том, что рисунок изображения не был завершен к моменту повторной перерисовки, поэтому рисовались только части изображения. Чтобы исправить это, добавьте это поле в свой класс JScrollNavigator (как замок):

/** Lock to prevent trying to repaint too many times */
private boolean blockRepaint = false;

Когда мы перекрашиваем компонент, эта блокировка будет активирована. Он не будет выпущен до тех пор, пока мы не сможем успешно покрасить панель — тогда можно будет выполнить еще одну покраску.

paintComponent необходимо изменить, чтобы соблюдать блокировку и использовать ImageObserver при рисовании панели навигации.

@Override
protected void paintComponent(final Graphics g) {
    super.paintComponent(g);
    if(!blockRepaint){
        final Component view = (Component)jScrollPane.getViewport().getView();
        BufferedImage img = new BufferedImage(view.getWidth(), view.getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // Paint JScrollPane view to off-screen image and then scale.
        // It is this action that causes the display corruption!
        view.paint(g2d);
        ImageObserver io = new ImageObserver() {

            @Override
            public boolean imageUpdate(Image img, int infoflags, int x, int y,int width, int height) {
                boolean result = true;
                g.drawImage(img, 0, 0, null);
                if((infoflags & ImageObserver.FRAMEBITS) == ImageObserver.FRAMEBITS){
                    blockRepaint = false;
                    result = false;
                }

                return result;
            }
        };

        Image scaled = img.getScaledInstance(getWidth(), getHeight(), 0);
        blockRepaint = g.drawImage(scaled, 0, 0, io);
    }
}
person Nick Rippe    schedule 03.08.2012
comment
Я попытался поменять ваш paintComponent на пример, который я вставил, но я вижу тот же результат. Я не совсем понимаю, как масштабирование может быть проблемой: даже если я закомментирую код масштабирования, проблема все равно возникает; проблема заключается в рисовании закадрового изображения. - person Adamski; 06.08.2012
comment
Я предполагаю, что вы добавили замок как поле (иначе код не будет работать). Если это так, ваш флаг, возвращаемый на ImageObserver, может быть другим. Строка, которую вы указываете, является проблемой, кажется проблемой только потому, что, когда вы ее комментируете, она не позволяет другим шагам что-либо делать - это строка g.drawImage(...), выполнение которой занимает некоторое время, потому что она обновляет пользовательский интерфейс. У меня есть рабочий код: pastebin.com/MccHaaFj. Я добавил несколько операторов печати в paintComponent, чтобы вы могли точно видеть, какие флаги передаются (и при необходимости корректировать) - person Nick Rippe; 06.08.2012
comment
@NickRippe: я не мог заставить это работать на Ubuntu/OpenJDK, но я никогда не пытался изменить порядок рендеринга таким образом. Мое решение состояло в том, чтобы избежать PanelUI, расширив JComponent; Я приветствовал бы любое критическое понимание, которое Вы можете предложить. - person trashgod; 06.08.2012
comment
@trashgod - у меня только что были проблемы с определением реальной проблемы. Я думал, что нашел его, но теперь, когда я возвращаюсь к нему, похоже, что я просто исправлял отдельную проблему с рендерингом. :P Ой вай! Я потратил на это достаточно времени. Ваш ответ дает хороший обходной путь (я просто надеялся решить проблему). - person Nick Rippe; 07.08.2012
comment
@NickRippe: Спасибо. Я только что заметил, что BasicPanelUI является общим, что может быть фактором. - person trashgod; 07.08.2012