f

2017-08-20

How to implement Drag and Drop on JavaFX

JavaFX 8で,GUIの使い勝手を大幅に向上させる,ドラッグ・ドロップを勉強する。Oracleの以下の公式ガイドを参照した。内容は同一で,自分が理解しやすいように整理し直した。

7 ドラッグ・アンド・ドロップ操作(リリース8)

概念

ドラッグ・ドロップに登場する概念を整理しておく。

オブジェクトとデータ型

ドラッグ・ドロップには2個のオブジェクトが登場する。

ジェスチャーソース
ドラッグ元
ジェスチャーターゲット
ドロップ先

これらのドラッグ・ドロップの対象となるのは,以下の2個のオブジェクトとなる。

  • ノード
  • シーン

ドラッグ・ドロップは以下の一連の動作を表す。

  1. ドロップ元でマウスボタンをクリック
  2. マウスをドラッグ
  3. ドロップ先にマウスを移動
  4. ボタンを離す

ドラッグ・ドロップはjavafx.scne.input.DragEventクラスが基本クラスとなる。

転送モード

ドラッグ元とドロップ先の間で実行される転送のタイプは,転送モードによって以下の3種類に決まる。

  1. COPY
  2. MOVE
  3. LINK

JavaFXだと,ドラッグ元がなんであっても特に気にしなくても大丈夫な模様。ファイルでもドロップを受け付けられた。

ドラッグ・ドロップの実装手順

大きく以下の6手順でドラッグ・ドロップを実装する。

ステップ手順概要イベントコールメソッド備考
1ドラッグ元とドロップ先オブジェクトを作成


2ドラッグ元オブジェクトを設定DRAG_DETECTED (setOnDragDetected)startDragAndDrop転送モードを指定し,クリップボードに内容を格納する。
3ドロップ先オブジェクトを設定DRAG_OVER (setOnDragOver)acceptTransferModesドラッグ・ドロップで受け付ける転送モードを指定する。
4ドラッグ・ドロップの視覚効果を設定DRAG_ENTERED (setOnDragEntered)
DRAG_EXIT (setOnDragExite)


5ドロップ後処理を設定DRAG_DROPPED (setOnDragDropped)setDropCompletedクリップボードの内容を受け取る。
6ドロップ完了処理を設定DRAG_DONE (setOnDragDone)

この内必須なのは2, 3, 5の手順である。その他はおまけであるが,可能であれば実装したほうがよいだろう。

ドラッグ元とドロップ先オブジェクトを作成

ドラッグ・ドロップのドラッグ元とドロップ先のオブジェクトを作る。

    // まずはオブジェクトを作成
    final private Text source = new Text(50, 100, "DRAG ME");
    final private Text target = new Text(50, 100, "DROP HERE");

WindowsのエクスプローラーやLinuxのNautilus,MacのFinderのようにファイルマネージャーからドラッグ・ドロップを行う場合は,ドラッグ元またはドロップ先のどちらかのオブジェクトは省略可能。

ドラッグ元オブジェクトを設定

ドラッグ元オブジェクトのDRAG_DETECTEDにイベントハンドラーを割り当てる。(ドラッグ開始の設定)

        // ハンドラーを設定
        // ジェスチャーソースのDRAG_DETECTEDイベントを処理する
        source.setOnDragDetected(event -> dragDrop(event));

ドラッグドロップをstartDragAndDrop()で開始して,コンテンツをダッシュボードに格納する。

    public void dragDrop(MouseEvent event){
        // 転送モードを指定して,startDragAndDropメソッドでドラッグ・ドロップを開始
        // COPY, MOVE, LINK, ANY, COPY_OR_MOVE, NONE
        Dragboard db = source.startDragAndDrop(TransferMode.ANY);
        
        // クリップボードを作成して格納
        ClipboardContent content = new ClipboardContent();
        content.putString(source.getText());
        db.setContent(content);
        
        event.consume();        
    }    

ドロップ先オブジェクトを設定

DRAG_OVERイベント・ハンドラーを処理する。

        // ジェスチャーターゲットのDRAG_OVERイベントを処理
        target.setOnDragOver(event -> dragOver(event));

ダッシュボードとドラッグ中のオブジェクトが妥当であるかをチェックしてから,acceptTransferModesを実行する。

    public void dragOver(DragEvent event){
        // 現在のダッシュボードのデータ型から受け入れるか判定
        // 最初のevent.getGesutureSourceでオブジェクトの判定
        // getDragboard()でダッシュボードに格納されているデータにアクセス
        if (event.getGestureSource() != target &&
                event.getDragboard().hasString()){
            // ドロップの受け入れ関数を実行
            // 受け入れる転送モードを指定する
            event.acceptTransferModes(TransferMode.MOVE);
        }
        event.consume();
    }

ドラッグ・ドロップの視覚効果を設定

ドラッグジェスチャーがジェスチャーターゲット候補の境界に入ると,DRAG_ENTEREDイベントが発生する。また,境界から出るとDRAG_EXITEDイベントが送信される。これらのイベントハンドラーを利用して,ドラッグドロップの視覚効果を設定する。

        // ドラッグドロップの視覚効果を設定
        target.setOnDragEntered(event -> dragEntered(event));
        target.setOnDragExited(event -> dragExited(event));
    public void dragEntered(DragEvent event){
        // ドラッグ対象範囲内に入った視覚効果を設定
        if (event.getGestureSource() != target &&
                event.getDragboard().hasString())
        {
            target.setFill(Color.GREEN);
        }
        event.consume();
    }

    public void dragExited(DragEvent event){
        // ドラッグ対象範囲外に入った視覚効果を設定
        target.setFill(Color.BLACK);
        event.consume();
    }

ドロップ後処理を設定

ドロップ先でDRAG_OVERイベントが受け入れられた後,マウスボタンが離されると,DRAG_DROPPEDイベントが発生する。イ ベント発生時にsetDropCompleted(Boolean)メソッドをコールしてドラッグ・ドロップジェスチャーを完了させる。これを呼ばないとジェスチャーは失敗とみなされる。

        // ドロップ後の処理を設定
        target.setOnDragDropped(event -> dragDropped(event));
    public void dragDropped(DragEvent event){
        // ドロップ時の処理を設定
        Dragboard db = event.getDragboard();
        final boolean HAS_DB_STRING = db.hasString();
        if (HAS_DB_STRING){
            target.setText(db.getString());
        }
        
        event.setDropCompleted(HAS_DB_STRING);
        event.consume();        
    }

ドロップ完了処理を設定

ドラッグ・ドロップが完了したら,ドロップ元にDRAG_DONEイベントが送信される。このイベントハンドラーで,getTransferMode()により転送 モードを取得できる。転送モードがNULLの場合,転送が失敗したとみなされる。この転送モードは,受け入れ先の転送モードが使われるようだ。

        // ドラッグドロップ完了時のドロップ元の設定
        source.setOnDragDone(event -> dragDone(event));
    public void dragDone(DragEvent event){
        // ドロップ後のドロップ元の処理を設定
        if (event.getTransferMode() == TransferMode.MOVE){
            source.setText("");
        }        
        event.consume();
    }

サンプルアプリ

以下のリポジトリーに今回のサンプルコードと画像を格納している。

practice/JavaFX/DragDrop at master · lamsh/practice

動作イメージ

サンプルアプリの動作イメージを以下に示す。

DRAG MEと表示された部分をドラッグして,DROP HEREに移動させると,DROP HEREにドラッグ元の内容が表示され,ドラッグ元は空文字に変更する。DROP HEREには外部からファイルをドロップすることもでき,その場合はそのファイルの絶対パスが表示される。

01. 起動直後04. ドロップ後
02. ドラッグ開始05. ファイルドラッグ
03. ドロップオーバー時の視覚効果06. ファイルドロップ後

参考ソース全体

/// \file DragDrop.java
package dragdrop; import javafx.application.Application; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.input.ClipboardContent; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.stage.Stage; public class DragDrop extends Application { // まずはオブジェクトを作成 final private Text source = new Text(50, 100, "DRAG ME"); final private Text target = new Text(250, 100, "DROP HERE"); @Override public void start(Stage stage) throws Exception { // ジェスチャーソースのDRAG_DETECTEDイベントを処理する source.setOnDragDetected(event -> dragDrop(event)); // ジェスチャーターゲットのDRAG_OVERイベントを処理 target.setOnDragOver(event -> dragOver(event)); // ドラッグドロップの視覚効果を設定 target.setOnDragEntered(event -> dragEntered(event)); target.setOnDragExited(event -> dragExited(event)); // ドロップ後の処理を設定 target.setOnDragDropped(event -> dragDropped(event)); // ドラッグドロップ完了時のドロップ元の設定 source.setOnDragDone(event -> dragDone(event)); // その他レイアウトなどの設定 stage.setTitle("Hello Drag and Drop"); Group root = new Group(); Scene scene = new Scene(root, 400, 200); scene.setFill(Color.LIGHTGREEN); source.setScaleX(2.0); source.setScaleY(2.0); target.setScaleX(2.0); target.setScaleY(2.0); root.getChildren().addAll(source, target); stage.setScene(scene); stage.show(); } public void dragDrop(MouseEvent event){ // 転送モードを指定して,startDragAndDropメソッドでドラッグ・ドロップを開始 // COPY, MOVE, LINK, ANY, COPY_OR_MOVE, NONE Dragboard db = source.startDragAndDrop(TransferMode.ANY); // クリップボードを作成して格納 ClipboardContent content = new ClipboardContent(); content.putString(source.getText()); db.setContent(content); event.consume(); } public void dragOver(DragEvent event){ // 現在のダッシュボードのデータ型から受け入れるか判定 // 最初のevent.getGesutureSourceでオブジェクトの判定 // getDragboard()でダッシュボードに格納されているデータにアクセス if (event.getGestureSource() != target && event.getDragboard().hasString()){ // ドロップの受け入れ関数を実行 // 受け入れる転送モードを指定する event.acceptTransferModes(TransferMode.MOVE); } event.consume(); } public void dragEntered(DragEvent event){ // ドラッグ対象範囲内に入った視覚効果を設定 if (event.getGestureSource() != target && event.getDragboard().hasString()) { target.setFill(Color.GREEN); } event.consume(); } public void dragExited(DragEvent event){ // ドラッグ対象範囲外に入った視覚効果を設定 target.setFill(Color.BLACK); event.consume(); } public void dragDropped(DragEvent event){ // ドロップ時の処理を設定 Dragboard db = event.getDragboard(); final boolean HAS_DB_STRING = db.hasString(); if (HAS_DB_STRING){ target.setText(db.getString()); } event.setDropCompleted(HAS_DB_STRING); event.consume(); } public void dragDone(DragEvent event){ // ドロップ後のドロップ元の処理を設定 if (event.getTransferMode() == TransferMode.MOVE){ source.setText("DROP FINISH"); } event.consume(); } }

Difference of exercise, drill, practice, rehearse, train

日本語の「練習」に訳されることのある英単語(exercise, drill, practice, rehearse, train)の違いをまとめた。

LONGMANの英英辞典で,これらの単語の意味を調べ,それぞれのイメージを把握して使い分けを考えた。各単語の英英辞典の記載内容を引用し,それぞれのイメージを説明しまとめた。なお,英語は動詞がメインの言語であるので,英英辞典は動詞の意味を参照した。

exercise

1 use something [transitive] formal to use a power, right, or quality that you have
2 do physical activity [intransitive] to do sports or physical activities in order to stay healthy and become stronger
3 use part of your body [transitive] to make a particular part of your body move in order to make it stronger
4 animal [transitive] to make an animal walk or run in order to keep it healthy and strong
5 make somebody think [transitive] formal
a) to make someone think about a subject or problem and consider how to deal with it
b) British English if something exercises someone, they think about it all the time and are very anxious or worried – often used humorously
exercise | meaning of exercise in Longman Dictionary of Contemporary English | LDOCE

身体的な動きを伴うイメージ。スポーツや楽器の練習の意味に適している。

drill

1 [intransitive, transitive] to make a hole in something using a drill
2 [transitive] to teach students, sports players etc by making them repeat the same lesson, exercise etc many times
3 [transitive] to train soldiers to march or perform other military actions
4 [transitive] to plant seeds in rows using a machine
drill | meaning of drill in Longman Dictionary of Contemporary English | LDOCE

学生やスポーツ選手などの集団に,教えたり,何回も反復させること。学校での課題演習など,反復練習の意味で使うのに適している。

practise/practice

イギリス英語ではpractiseをpracticeの動詞として使う。アメリカ英語は同じ。

1 [intransitive, transitive] to do an activity, often regularly, in order to improve your skill or to prepare for a test
2 [transitive] to use a particular method or custom
3 [intransitive, transitive] to work as a doctor or lawyer
4 [transitive] if you practise a religion, system of ideas etc, you live your life according to its rules
5 → practise what you preach
practise | meaning of practise in Longman Dictionary of Contemporary English | LDOCE

技能向上の目的のための活動。一般的な練習のイメージ。

rehearse

1 [intransitive, transitive] to practise or make people practise something such as a play or concert in order to prepare for a public performance
2 [transitive] to practise something that you plan to say to someone
3 [transitive] formal to repeat an opinion that has often been expressed before
rehearse | meaning of rehearse in Longman Dictionary of Contemporary English | LDOCE

何かの準備のために行うイメージ。演劇,コンサート,ショーなどの公開発表の練習の意味に適している。説明の中に,practiseの単語があるように,practiceの特殊なケース。

train

1 teach somebody [intransitive, transitive] to teach someone the skills of a particular job or activity, or to be taught these skills → training
2 teach an animal [transitive] to teach an animal to do something or to behave correctly
3 prepare for sport [intransitive, transitive] to prepare for a sports event or tell someone how to prepare for it, especially by exercising → training
4 aim something [transitive] to aim something such as a gun or camera at someone or something
5 develop something [transitive] to develop and improve a natural ability or quality
6 plant [transitive] to make a plant grow in a particular direction by bending, cutting, or tying it
train | meaning of train in Longman Dictionary of Contemporary English | LDOCE

指導(教える,教わる)が入るイメージ。exerciseに指導が入った場合もこれになる。いわゆるダンベルなどの(指導員のつかない)自主トレーニングはexerciseであり,部活動など監督者のいる運動はtrainになると思われる。

Summary

ここまでの英単語のイメージを以下の表にまとめた。

練習を意味する各単語のイメージ
Word Image
exercise 身体的な運動を伴うもの
drill 集団への反復が伴うもの
practice 技能向上のためのもの
rehearse 準備として行うもの
train 指導が入るもの

個人的なイメージとしては,practiceが最上位に位置し,その他の単語はpracticeの特殊な事例のように感じた。

プログラミングや語学学習や資格試験のための「練習」はpracticeで問題ないように思った。

2017-08-17

How to Ignore case with cscope in Vim

cscopeはデフォルトのままだと大文字小文字を区別する。しかし,うろ覚えの記憶を頼りに検索をする場合,大文字小文字の区別はしないでほしいことがよくある。そこでVimからcscopeを使うときに,大文字小文字を無視する方法を記す。

解決策がないか調べてみたところ,このフォーラムでBob Harrisが言及している方法を使えば解決できそうだ。

I have 2 ideas.
:help cscope
says
:cs add {file|dir} [pre-path] [flags]
So you should be able to include -C in the [flags] field
:cs add cscope.out /some/path/src -C
And if that doesn't work, then write a script to invoke cscope
with the -C option
#!/bin/sh
cscope -C "$@"
And set the csprg variable to point to your script
:set csprg=/path/to/your/cscope.sh
Bob Harris
Bob Harris - About -C switch of cscope - Google グループ

cscopeには検索時の大文字小文字を無視する-Cオプションが存在しており,vimから使うときは初回のデータベース読み込み時に-Cオプションを指定すればいい。

Bob Hrrisは2番目の方法として-Cオプションを常に使うためにシェルスクリプトを間に挟む方法を提案している。しかし,この方法は依存関係が増え,手間が余計にかかるので,1番目の方法でうまくやるようにする。

[pre-path]にはcscopeのデータベースファイル(cscope.out)で参照されている相対パスのルートディレクトリーを指定する必要がある。

手動で毎回指定するのは煩雑なので,以下のコードに示すとおりに,vimの起動時のディレクトリーか,環境変数のパスを取得してやれば解決する。

Run cscope with ignore case in Vim
if executable('cscope')
  if filereadable('cscope.out')
    execute 'cscope add cscope.out ' . expand('%:p:h') . ' -C'
  elseif filereadable($CSCOPE_DB)
    execute 'cscope add $CSCOPE_DB ' . matchstr($CSCOPE_DB, '.*[/\\]') . ' -C'
  endif
endif

expand('%:p:h')で現在バッファーのディレクトリを取得している。また,環境変数から参照する場合は正規表現により取得している。Windows環境でも対応できるように,パス区切りに\が来ても大丈夫なようにした。

これでVimからcscopeを使ったソースコード探索がさらに楽になる。

なお,大文字小文字を区別したくなった場合は,手間だが以下のようなコマンドでcscopeのデータベースとの接続を解除して,再接続してやる。

:cscope kill #1
:cscope add cscope.out

2017-08-12

Solution for Error while taking the screenshot. capturing active window in Shutter

Linuxの画面キャプチャーソフトであるShutterで,現在画面の画面キャプチャーの取得をショートカットキーに割り当てて実行する際に生じる問題の解決策を記す。

Introduction

Linuxの画面キャプチャーソフトであるShutterには,コマンドラインから実行するためのオプションがある。これを利用して,Alt-PrintScreenキーに現在画面のスクリーンショットを撮るように,以下のコマンドを割り当てた。

shutter --active

しかし,Ubuntu 16.04のshutter 0.93.1でAlt-PrintScreenキーで画面キャプチャーを撮ろうとすると以下のエラーが出てしまった。

Shutter
Error While taking the screenshot.
No window with name pattern 'shutter' detected.

範囲選択(shutter -s)などではこの問題は発生しない。しかし,現在画面のキャプチャーは利用頻度が高いので,困っていた。

1年ほど前からこの問題が発生していたのが,原因がわからずに放置していた。気が向いたので解決策を調べてみた。

Solution

この問題は「Bug #1418149 “Shutter fails to detect active window” : Bugs : Shutter」で議論されており,「Andreas Wehler - Comment #9 : Bug #1418149 : Bugs : Shutter」の回答で解決した。

結論としては,ショートカットキーの割当コマンドに以下を記入すればよい。

sh -c 'pkill shutter && shutter --active'

Ubuntu 16.04であれば,以下のコマンドでキーボードショートカットの設定画面を開くことができる。

unity-control-center keyboard shortcuts

[Custom Shortcuts]に追加してやればよい。

Discussion

この解決策のポイントは以下2点となる。

  1. 外部シェルから起動
  2. 事前にshutterのプロセスを終了

1点目の本質的な理由はよく理解できていない。ただし,2点目のことから逆説的に必要となる。

2点目。これはshutterが起動した状態でさらにshutterで現在画面をキャプチャーしようとすると,shutterの初回起動時の現在画面を取得してしまうためだ。そのため,一度shutterのプロセスを終了させてからコマンドでキャプチャーを取得している。

ショートカットキーに複数のコマンドを割り当てる場合,;&&によりコマンドを1行でつなげる必要がある。しかし,どうやらここで指定する内容は文字列として解釈されてしまうようで,これらの予約語が有効でなかった。そのため,sh-cオプションで文字列を解釈させて対応した。

手順書や備忘録を記入する際に,画面キャプチャーは重要であるため頻繁に取得する。そのため,Shutterで現在画面を短絡キーで取得できなくて今まで手間だった。これで快適に作業できるだろう。