calling a method when content of clipboard is chan

2019-01-17 18:45发布

问题:

I'm trying to make a little desktop app that should show the contents of the clipboard (if it is a string). I have done a constructor that does that and it works well, now I just want to make a call to a similar method whenever a text is copied into the clipboard in the OS. I'm quite new to this so any help would be appreciated! Something tells me I should use interrupts in some way...

package pasty;

import java.awt.FlowLayout;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class PastyFrame implements KeyListener {

    String currentClipboardString;
    JLabel clipboardLabel = new JLabel();

    public PastyFrame() {
        JFrame frame = new JFrame();
        frame.setVisible(true);

        try {
            currentClipboardString = (String) Toolkit.getDefaultToolkit().getSystemClipboard().getData(DataFlavor.stringFlavor);
        } catch (UnsupportedFlavorException | IOException ex) {
            Logger.getLogger(PastyFrame.class.getName()).log(Level.SEVERE, null, ex);

            currentClipboardString = "";
        }
        if (currentClipboardString.isEmpty()) {
            currentClipboardString = "The clipboard is empty";
        }
        frame.setSize(400, 100);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.setLayout(new FlowLayout());


        clipboardLabel.setText(currentClipboardString);
        frame.add(clipboardLabel);
}

回答1:

You can call Clipboard.addFlavorListener to listen for clipboard updates from the OS:

Toolkit.getDefaultToolkit().getSystemClipboard().addFlavorListener(new FlavorListener() { 
   @Override 
   public void flavorsChanged(FlavorEvent e) {

      System.out.println("ClipBoard UPDATED: " + e.getSource() + " " + e.toString());
   } 
}); 

Some Side Notes:

  • For launching your application, consider using initial threads.
  • Call JFrame.pack to set the frame size.
  • Key Bindings are preferred over KeyListeners for mapping KeyEvents in Swing.


回答2:

I use this. The whole class.

public class ClipBoardListener extends Thread implements ClipboardOwner{
Clipboard sysClip = Toolkit.getDefaultToolkit().getSystemClipboard();  


    @Override
  public void run() {
    Transferable trans = sysClip.getContents(this);  
    TakeOwnership(trans);       

  }  

    @Override
  public void lostOwnership(Clipboard c, Transferable t) {  

  try {  
    ClipBoardListener.sleep(250);  //waiting e.g for loading huge elements like word's etc.
  } catch(Exception e) {  
    System.out.println("Exception: " + e);  
  }  
  Transferable contents = sysClip.getContents(this);  
    try {
        process_clipboard(contents, c);
    } catch (Exception ex) {
        Logger.getLogger(ClipBoardListener.class.getName()).log(Level.SEVERE, null, ex);
    }
  TakeOwnership(contents);


}  

  void TakeOwnership(Transferable t) {  
    sysClip.setContents(t, this);  
  }  

public void process_clipboard(Transferable t, Clipboard c) { //your implementation
    String tempText;
    Transferable trans = t;

    try {
        if (trans != null?trans.isDataFlavorSupported(DataFlavor.stringFlavor):false) {
            tempText = (String) trans.getTransferData(DataFlavor.stringFlavor);
            System.out.println(tempText);  
        }

    } catch (Exception e) {
    }
}

}

When other program takes ownership of the clipboard it waits 250 ms and takes back clipboard's ownership with updated content.



回答3:

Below is an SSCCE... you can run it and select text and go Ctrl-C, multiple times ... the text selected gets printed out.

As you can see it is a little more involved than Reimius' answer. You do in fact have to clear the clipboard (which is tricky!) for the flavour listener to respond each time you copy some new text.

Furthermore you probably need to suppress output caused by the "flavour change" when you clear the clipboard... though there may be a cleverer solution than mine.

public class ClipboardListenerTest {

    public static void main(String[] args) throws InvocationTargetException, InterruptedException {

        SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
                clipboard.addFlavorListener(new FlavorListener() {
                    // this is needed to prevent output when you clear the clipboard
                    boolean suppressOutput = false;

                    // this is a specially devised Transferable - sole purpose to clear the clipboard
                    Transferable clearingTransferable = new Transferable() {
                        public DataFlavor[] getTransferDataFlavors() {
                            return new DataFlavor[0];
                        }
                        public boolean isDataFlavorSupported(DataFlavor flavor) {
                            return false;
                        }
                        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
                            throw new UnsupportedFlavorException(flavor);
                        }
                    };

                    @Override
                    public void flavorsChanged(FlavorEvent e) {
                        Transferable contentsTransferable = clipboard.getContents(null);
                        // NB the Transferable returned from getContents is NEVER the same as the 
                        // clearing Transferable!

                        if (!suppressOutput) {
                            System.out.println(String.format("# clipboard UPDATED, src %s, string %s, clearingT? %b", e.getSource(), e.toString(),
                                    contentsTransferable == clearingTransferable));
                            try {
                                String stringData = (String)clipboard.getData(DataFlavor.stringFlavor);
                                System.out.println(String.format("# string data |%s|", stringData ));
                            } catch (UnsupportedFlavorException | IOException e1) {
                                // TODO Auto-generated catch block
                                e1.printStackTrace();
                            } 
                        }

                        else {
                            // my experiments seem to show that you have to spawn a new Runnable if you want 
                            // to leave suppressOutput long enough for it to prevent the "CLEAR" operation 
                            // producing output...
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    suppressOutput = false;
                                }
                            });

                        }
                        suppressOutput = true;
                        clipboard.setContents(clearingTransferable, null);
                    }
                });

            }

        });

        int i = 0;
        while (i < 100) {
            Thread.sleep(500L);
            System.out.println("# pibble");
            i++;
        }

    }

}


回答4:

I came up with another solution to this problem: The following listener continuously reads the clipboard content using a loop. If a text is detected, it will be compared to the previous content of the clipboard, which is cached. When the clipboard contains a new text that was not cached previously, it may perform some action like 'notify observers', as in this example, which may prompt a GUI to update itself.

In this example, content changes, where the clipboard contains something other than a string, are ignored.

Other than just detecting content type changes (i.e. by using the FlavorListerner), this solution detects changes by continuous string comparison. By just read-accessing the clipboard, I'd expect this code to cause less interference with other applications than e.g. by taking ownership of the clipboard.

Suggestions are welcome.

package gui;

import java.awt.HeadlessException;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Observable;

/**
 * @author Matthias Hinz
 */
class ClipboardTextListener extends Observable implements Runnable {

    Clipboard sysClip = Toolkit.getDefaultToolkit().getSystemClipboard();

    private volatile boolean running = true;

    public void terminate() {
        running = false;
    }

    public void run() {
        System.out.println("Listening to clipboard...");
        // the first output will be when a non-empty text is detected
        String recentContent = "";
        // continuously perform read from clipboard
        while (running) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                // request what kind of data-flavor is supported
                List<DataFlavor> flavors = Arrays.asList(sysClip.getAvailableDataFlavors());
                // this implementation only supports string-flavor
                if (flavors.contains(DataFlavor.stringFlavor)) {
                    String data = (String) sysClip.getData(DataFlavor.stringFlavor);
                    if (!data.equals(recentContent)) {
                        recentContent = data;
                        // Do whatever you want to do when a clipboard change was detected, e.g.:
                        System.out.println("New clipboard text detected: " + data);
                        setChanged();
                        notifyObservers(data);
                    }
                }

            } catch (HeadlessException e1) {
                e1.printStackTrace();
            } catch (UnsupportedFlavorException e1) {
                e1.printStackTrace();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ClipboardTextListener b = new ClipboardTextListener();
        Thread thread = new Thread(b);
        thread.start();
    }
}


回答5:

Reimeus recommended using Clipboard.AddFlavorListener. I'd like to expand on his answer slightly. The way I've been doing this in my program is like this:

final Clipboard SYSTEM_CLIPBOARD = Toolkit.getDefaultToolkit().getSystemClipboard();

SYSTEM_CLIPBOARD.addFlavorListener(listener -> {

    string clipboardText = (String) SYSTEM_CLIPBOARD.getData(DataFlavor.stringFlavor);

    SYSTEM_CLIPBOARD.setContents(new StringSelection(clipboardText), null);

    System.out.println("The clipboard contains: " + clipboardText);
}

This remedies the problem of the listener only being fired when a new app copies contents to the clipboard, by essentially making the program itself become the app that copies the text to the clipboard.

The caveat of this being the listener will get called twice for each copy event, but there are ways to deal with that, of course. At least, in this situation, the clipboard event will be called properly every time something is copied to the clipboard.



回答6:

I'm not completely sure if it is correct and optimal approach, but at least it worked for me just fine. Below is the sample of the clipboard handler which implements clipboard owner interface and reads clipboard buffer each time it loses the ownership. Then it gets the ownership back to be able to lose it next time when new entry comes to the clipboard to read it again. It also allows to set clipboard contents pragmatically (in following example providing new stdin line).

public static class ClipboardHandler implements ClipboardOwner, Runnable {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
    private final Consumer<String> bufferConsumer;

    public ClipboardHandler(Consumer<String> bufferConsumer) {
        this.bufferConsumer = bufferConsumer;
    }

    @Override
    public void lostOwnership(Clipboard clipboard, Transferable notUsed) {
        Transferable contents = clipboard.getContents(this);
        if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
            try {
                String string = (String) contents.getTransferData(DataFlavor.stringFlavor);
                bufferConsumer.accept(string);
            } catch (Exception e) {
                logger.error("Unable to read clipboard buffer.", e);
            }
        }
        getOwnership(contents);
    }

    @Override
    public void run() {
        Transferable transferable = clipboard.getContents(this);
        getOwnership(transferable);
    }

    public void setBuffer(String buffer) {
        getOwnership(new StringSelection(buffer));
    }

    private void getOwnership(Transferable transferable) {
        clipboard.setContents(transferable, this);
    }

}

public static void main(String[] args) {
    ClipboardHandler clipboardHandler = new ClipboardHandler(System.out::println);
    EventQueue.invokeLater(clipboardHandler);

    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNextLine()) {
        String buffer = scanner.nextLine();
        if (!buffer.trim().isEmpty()) {
            clipboardHandler.setBuffer(buffer);
        }
    }

}