Java/Swing: владение системным буфером обмена

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

  1. запустить внешний инструмент и дождаться его
  2. скопировать изображение из буфера обмена
  3. скопировать строку в буфер обмена

Это моя программа прекрасно умеет делать. Однако я хотел бы использовать Swing/AWT для представления пользовательского интерфейса. Я использую значок на панели задач, но для простоты это может быть и JButton во фрейме. Когда кнопка нажата, должен быть выполнен описанный выше процесс. Сделав это в первый раз, все работает как надо. Изображение копируется, вставляется на диск, а строка копируется в буфер обмена. Затем, когда кнопка нажимается во второй раз, моя программа как будто не понимает, что буфер обмена был обновлен, поскольку она все еще видит свою собственную строку с первого раза. Только после этого мой класс обработки буфера обмена теряет право собственности, и, по сути, каждая вторая попытка процедуры терпит неудачу.

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;

import javax.swing.JButton;
import javax.swing.JFrame;

public class Main {
    private static BufferedImage image; //the image from clipboard to be saved

    public static void main(String[] args) throws InterruptedException, IOException {
        new GUI();
    }

    public static void run(String filename) throws IOException, InterruptedException {
        CBHandler cbh = new CBHandler();

        //run tool, tool will copy an image to system clipboard
        Process p = Runtime.getRuntime().exec("C:\\Windows\\system32\\SnippingTool.exe");
        p.waitFor();

        //copy image from clipboard
        image = cbh.getClipboard();
        if(image == null) {
            System.out.println("No image found in clipboard.");
            return;
        }

        //save image to disk...

        //copy file link to clipboard
        String link = "http://somedomain.com/" + filename;
        cbh.setClipboard(link);
    }
}

class CBHandler implements ClipboardOwner {
    public BufferedImage getClipboard() {
        Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);

        try {
            if(t.isDataFlavorSupported(DataFlavor.imageFlavor))
                return (BufferedImage) t.getTransferData(DataFlavor.imageFlavor);
        }
        catch(Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public void setClipboard(String str) {
        StringSelection strsel = new StringSelection(str);
        Toolkit.getDefaultToolkit().getSystemClipboard().setContents(strsel, this);
    }

    @Override
    public void lostOwnership(Clipboard arg0, Transferable arg1) {
        System.out.println("Lost ownership!");
    }
}

class GUI extends JFrame {
    public GUI() {
        JButton button = new JButton("Run");
        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                try {
                    Main.run("saveFile.png");
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        add(button);
        pack();
        setVisible(true);
    }
}

Если вы попытаетесь запустить его, обратите внимание, что при втором запуске метод lostOwnership вызывается только ПОСЛЕ попытки копирования образа. Я предполагаю, что это источник моей проблемы, и я понятия не имею, почему это происходит, за исключением того, что это происходит только при запуске события Swing. Любая помощь в решении этой проблемы приветствуется.


person Per    schedule 27.08.2011    source источник


Ответы (2)


Одно предположение: вы выполняете всю свою обработку (вызывая другой процесс) в потоке отправки событий AWT (например, непосредственно из ActionListener или аналогичного).

Сообщения об изменении буфера обмена также будут обрабатываться виртуальной машиной в EDT ... но только после того, как вы нажмете кнопку.

Мораль: не делайте длительные вещи (и вещи, которые имеют эффекты, которые должны быть поставлены в очередь событий) в EDT — вместо этого создайте для этого новый поток.

person Paŭlo Ebermann    schedule 27.08.2011
comment
Не могли бы вы привести пример (псевдокод или что-то другое) того, как это сделать в моем сценарии? - person Per; 28.08.2011

Ключ к пониманию проблемы утраты права собственности находится в этой строке

Toolkit.getDefaultToolkit().getSystemClipboard().setContents(strsel, this);

Второй параметр, который вы передаете, это ClipboardOwner. JavaDocs для clipboard.setContents говорит

Если существует существующий владелец, отличный от владельца аргумента, этот владелец уведомляется о том, что он больше не владеет содержимым буфера обмена, посредством вызова ClipboardOwner.lostOwnership() для этого владельца. Реализация setContents() может не вызывать функцию lostOwnership() непосредственно из этого метода. Например, функция lostOwnership() может быть вызвана позже в другом потоке. То же самое относится к FlavorListeners, зарегистрированным в этом буфере обмена.

Итак, что происходит? Когда вы передаете владельца, буфер обмена теперь имеет ссылку на этот объект. В данном случае это CBHandler. Затем вы создаете новый и снова пытаетесь установить содержимое. Затем буфер обмена возвращается к старому владельцу (вашему исходному экземпляру) и сообщает ему: «Эй, ты больше не владелец».

public synchronized void setContents(Transferable contents, ClipboardOwner owner) {
    final ClipboardOwner oldOwner = this.owner;
    final Transferable oldContents = this.contents;

    this.owner = owner;
    this.contents = contents;

    if (oldOwner != null && oldOwner != owner) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                oldOwner.lostOwnership(Clipboard.this, oldContents);
            }
        });
    }
    fireFlavorsChanged();
}

Вам нужно будет предоставить более подробную информацию о другой проблеме: «как будто моя программа не понимает, что буфер обмена был обновлен, поскольку она все еще видит свою собственную строку с первого раза».

person Preston    schedule 27.08.2011