Help us understand the problem. What is going on with this article?

JavaFX の Clipboard API を使う

More than 3 years have passed since last update.

概要

Java には java.awt.datatransfer.Clipboard という、クリップボードを操作する用のクラスが用意されています。詳細はひしだまさんが書いていらっしゃいますので、そちらの記事をお読みください。

ひしだま's 技術メモページ - クリップボード

私は 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()

保持しているコンテンツの種別を取得します。

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()

保持しているコンテンツ種別を判定します。

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 を使います。Dat*a*Format です。名前にご注意ください。

単一の値

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 メソッドがあります。

Clipboard.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 の下記リポジトリに置いてあります。

https://github.com/toastkidjp/clipboard_observer_app/tree/qiita

スクリーンショット

sample.png

クリップボードに保持された文字列か画像をウィンドウに表示して、それらをクリックするとクリップボードに再保持される、という雑な GUI アプリケーションです。

クリップボードのイベント監視

javafx.scene.input.Clipboard にはイベントリスナー等の反応的な API が用意されていないので、RxJava を使って1秒間隔で中身を確認するという方法で実装しました。

クリップボードを監視するObservable
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() メソッドがあります。

toastkidjp
Web 系企業の Android アプリ開発者です。
https://github.com/toastkidjp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした