概要
Java には java.awt.datatransfer.Clipboard という、クリップボードを操作する用のクラスが用意されています。詳細はひしだまさんが書いていらっしゃいますので、そちらの記事をお読みください。
私は JavaFX に入っている方の Clipboard API である javafx.scene.input.Clipboard を調べることにしました。
実行環境
Java SE | 1.8.0_u131 |
---|---|
OS | Windows 10 |
IDE | IntelliJ IDEA 2017.1.3 |
javafx.scene.input.Clipboard
切取り、コピー、貼付けなどの操作中にデータを配置できる、オペレーティング・システムのクリップボードを表します。
まあ、クリップボード関連のあれこれができるクラスということでしょう。
Let's try!
試しにコードを書いてみます。
import javafx.scene.input.Clipboard;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
final Clipboard cb = Clipboard.getSystemClipboard();
System.out.println(cb.getString());;
}
}
失敗
パッケージ階層からわかる通り、javafx.scene.input.Clipboard は JavaFX のクラスなので、通常のスレッドからは利用できません。
Exception in thread "main" java.lang.IllegalStateException: This operation is permitted on the event thread only; currentThread = main
at com.sun.glass.ui.Application.checkEventThread(Application.java:443)
at com.sun.glass.ui.ClipboardAssistance.<init>(ClipboardAssistance.java:40)
at com.sun.javafx.tk.quantum.QuantumToolkit.getSystemClipboard(QuantumToolkit.java:1200)
at javafx.scene.input.Clipboard.getSystemClipboardImpl(Clipboard.java:413)
at javafx.scene.input.Clipboard.getSystemClipboard(Clipboard.java:178)
at jp.toastkid.sandbox.Sandbox.main(Sandbox.java:10)
というわけで、通常のアプリケーションでクリップボード操作をしたい時は java.awt.datatransfer.Clipboard を使いましょう。おしまい
動作するコード例
JavaFX アプリケーションであれば使えるので、main メソッドのクラスを javafx.application.Application のサブクラスにします。
import javafx.application.Application;
import javafx.scene.input.Clipboard;
import javafx.stage.Stage;
public class MainFx extends Application {
public static void main(String[] args) {
Application.launch(SandboxFx.class);
}
@Override
public void start(Stage primaryStage) throws Exception {
Clipboard cb = Clipboard.getSystemClipboard();
System.out.println(cb.getString());
}
}
このコードを動かすと、現在クリップボードに保持されている文字列が出力されます。
final Clipboard cb = Clipboard.getSystemClipboard();
System.out.println(cb.getString());
メソッドについて
getContentTypes()
保持しているコンテンツの種別を取得します。
System.out.println(cb.getContentTypes());
テキストの場合
[[text/uri-list], [JAVA_DATAFLAVOR:application/x-java-jvm-local-objectref; class=com.intellij.codeInsight.editorActions.FoldingData], [text/plain], [ms-stuff/oem-text], [text/rtf], [text/html], [ms-stuff/locale]]
画像ファイルの場合
[[FileName], [message/external-body;access-type=clipboard;index=1;size=28453;name="650contribution.png"], [AsyncFlag], [DataObjectAttributesRequiringElevation], [application/x-java-file-list, java.file-list], [FileNameW], [ms-stuff/preferred-drop-effect], [message/external-body;access-type=clipboard;index=2;size=220376;name="sample_images_1.jpg"], [text/uri-list], [message/external-body;access-type=clipboard;index=0;size=119242;name="sample_images_2.jpg"], [Shell Object Offsets], [Shell IDList Array], [DataObjectAttributes]]
インターネット上の画像の場合
[[text/uri-list], [application/x-java-rawimage], [text/_moz_htmlinfo], [text/html;cf=49474], [application/x-java-file-list, java.file-list], [text/_moz_htmlcontext], [application/x-moz-nativeimage], [text/html], [application/x-moz-file-promise-url], [application/x-moz-file-promise-dest-filename], [cf17], [ms-stuff/preferred-drop-effect]]
hasXX()
保持しているコンテンツ種別を判定します。
System.out.println(cb.hasString());
System.out.println(cb.hasUrl());
System.out.println(cb.hasHtml());
System.out.println(cb.hasRtf());
System.out.println(cb.hasImage());
System.out.println(cb.hasFiles());
実行結果
T: true
F: false
Method name | Text | Local image file | Web image file | URL on Browser |
---|---|---|---|---|
hasString() | T | F | F | T |
hasUrl() | T | T | T | F |
hasHtml() | T | F | T | F |
hasRtf() | T | F | F | F |
hasImage() | F | F | T | F |
hasFiles() | F | T | T | F |
setContent(Map)
値を取り出すだけでなく、差し込むことも可能です。差し込みには Map と DataFormat を使います。DataFormat です。名前にご注意ください。
単一の値
final Map<DataFormat, Object> content = new HashMap<>();
content.put(DataFormat.PLAIN_TEXT, "Orange");
cb.setContent(content);
アプリケーションの実行後にテキストエディタ上で Ctrl+V を押すと "Orange" というテキストがペーストされます。
複数の値
では複数の値を content に持たせたらどうなるでしょうか?
final Map<DataFormat, Object> content = new HashMap<>();
content.put(DataFormat.URL, "https://www.yahoo.co.jp");
content.put(DataFormat.PLAIN_TEXT, "Toast");
cb.setContent(content);
アプリケーションの実行後にテキストエディタ上でCtrl+V を押すと "Toast" というテキストがペーストされます。1つしかクリップボードには保持できないようです。
clear()
こちらの Clipboard API には clear メソッドがあります。
System.out.println(clipboard.getString());
clipboard.clear();
System.out.println(clipboard.getString());
holding text
null
この API を使って何ができるか?
JavaFX の API なので、用途も必然的に GUI との組み合わせになります。
1. 今クリップボードに持っている値を見せる
まあ、そのままですね。
2. エディタアプリケーションでのマルチクリップボードの実装
マルチクリップボードというのはその名の通り複数の値をクリップボードに保持しておける機能で、UNIX のテキストエディタには標準的に備わっている機能だそうです。この機能について迂闊に人に話したらどうなるか、『プロダクティブ・プログラマ』にこんな話が載っていました。
サンダル履きの男に「俺は高校の時からもう20年もそんなの使ってるよ」的な話を1時間くらい聞かされるはめになるかもしれません
3. クリップボード監視アプリケーションの実装
例えば、画像が保持されたらそれを自動でファイルに保存するとか、URL だったら WebView のウィンドウを出すとか……
簡単なクリップボード監視アプリケーションの実装
というわけで、ごく簡単なものを実装してみました。ソースコードは GitHub の下記リポジトリに置いてあります。
スクリーンショット
クリップボードに保持された文字列か画像をウィンドウに表示して、それらをクリックするとクリップボードに再保持される、という雑な GUI アプリケーションです。
クリップボードのイベント監視
javafx.scene.input.Clipboard にはイベントリスナー等の反応的な API が用意されていないので、RxJava を使って1秒間隔で中身を確認するという方法で実装しました。
Observable.interval(1, TimeUnit.SECONDS)
.observeOn(JavaFxScheduler.platform())
.map(l -> getStringOrEmpty())
.filter(str -> !str.isEmpty())
.subscribe(stringConsumer, Throwable::printStackTrace);
RxJavaFX の JavaFxScheduler
RxJavaFX で用意されている RxJava の Scheduler で、これを使うと JavaFX アプリケーションスレッドで RxJava 中の処理を実行させることができます。Consumer 等であれば標準で用意されている Platform.runLater(Runnable) を使えばよいのですが、 map や filter のように値を返さないといけない場合は、この JavaFxScheduler があると便利です。
RxJava2 の map等での null 非許容
RxJava2 では map で null を return すると NullPointerException が発生します。
java.lang.NullPointerException: The mapper function returned a null value.
at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39)
at io.reactivex.internal.operators.observable.ObservableMap$MapObserver.onNext(ObservableMap.java:59)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.drainNormal(ObservableObserveOn.java:200)
at io.reactivex.internal.operators.observable.ObservableObserveOn$ObserveOnObserver.run(ObservableObserveOn.java:252)
at io.reactivex.rxjavafx.schedulers.JavaFxScheduler$JavaFxWorker$QueuedRunnable.run(JavaFxScheduler.java:87)
at io.reactivex.rxjavafx.schedulers.JavaFxScheduler$JavaFxWorker.run(JavaFxScheduler.java:158)
at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:748)
これを回避するために Empty オブジェクトを用意しています。
private static final Image EMPTY_IMAGE = new WritableImage(1, 1);
まとめ
JavaFX の Clipboard API の紹介と、これを用いた簡単なアプリケーションの実装について説明しました。 JavaFX アプリケーションを作るのでないなら、こちらではなく java.awt.datatransfer.Clipboard を使いましょう。JavaFX の Clipboard API には clear() メソッドがあります。