質問: JavaFXのScrollPaneについて質問です。 画面上に表示しているScrollPaneの縦スクロール位置を、ユーザの操作ではなく、プログラム側で変更するには、どうすれば良いでしょうか? --- (※説明の為に、変数名は実際のプログラムから一部変更しています。) 私がJavaFXで現在作成しているアプリケーションでは、縦スクロールのScrollPane(scroll)の中にFlowPane(listField)があります。 ユーザの操作によって、このlistFieldの中にGridPaneの表(list)を表示しています。 しかしこれだと、新しいlistをlistFieldの中に表示する度に、毎回scrollが一番上にスクロールした状態になってしまいます。 その為、scrollの縦スクロール位置を一旦変数vValueに入れ、それからlistFieldの中に新しく作ったlistを表示し、最後にscrollの縦スクロール位置をvValueに合わせれば良いと考えました。 下記プログラムの通り、getVvalueメソッドでscrollの縦スクロール位置は取得出来ました。 ``` double vValue=scroll.getVvalue(); listField.getChildren().clear(); //listField内全削除 GridPane list=createList(); //list生成 listField.getChildren().addAll(list); //listFieldにlistを表示 scroll.setVvalue(vValue); //新listのスクロール位置を旧listと同じ位置に変える ``` しかし、setVvalueメソッドを使っても、scrollの縦スクロール位置を画面上で移動させる事が出来ませんでした。 確認してみた所、setVvalueメソッドの後は、listの縦スクロール位置の数値自体は変化していました。 調べた所、恐らく同様の質問をされていると思われる下記ページが見付かりました。 参考サイト 英語な事もあり詳しくは分かりませんでしたが、ScrollPaneのlayoutメソッドというのを使ってから、setVvalueメソッドで縦スクロール数値を指定すべきだという意味だと解釈しました。 ``` double vValue=scroll.getVvalue(); listField.getChildren().clear(); //listField内全削除 GridPane list=createList(); //list生成 listField.getChildren().addAll(list); //listFieldにlistを表示 scroll.layout(); //????? scroll.setVvalue(vValue); //新listのスクロール位置を旧listと同じ位置に変える ``` しかしこれでも、scrollの縦スクロール位置が画面上で移動する事はありませんでした。 どの様にすれば縦スクロール位置を変更出来るでしょうか?
回答: ずいぶん時間が経ってしまいましたが・・・ 残念ながら今現在「こうすべき」という適切な対処はわからないのですが、ヒントになるかも知れない情報をコメントしてみます。 ### 原因(のヒント) 参照しておられるstackoverflowのページは質問者さんがやろうとしていることとほぼ同様のことをしていると思います。そちらのコードで試してみました。自分の環境(JavaSE 9.0.4, Windows 10)ではstackoverflowの回答にあるlayoutをやってもNGでした。 ふと思いついてvvaluePropertyの変化をChangeListenerで調べたところ元のvValueの値を再設定した後、しばらくしてvvaluePropertyの値が0.0にリセットされていることがわかりました。0.0にリセットされた際のスタックトレースをみるとpulse(JavaFXランタイムが定期的に行う処理)の中でfocusCleanupという処理が行われており、そこから巡り巡ってScrollPaneSkin(ScrollPaneの外観/内部的な振る舞いを制御するSkinクラス)のupdatePosYというメソッドでvvaluePropertyの値が再設定(再調整?)されており、そこで0.0が設定されてしまうのが原因のようでした。 本来、pulseのようなJavaFXランタイムの内部で行われる定期処理のタイミングを意識して制御するようなことはアプリケーションプログラマーが考慮する必要がないように設計されているべきな気がするのですが、実際pulseの中でvvaluePropertyの値が調整されてしまう事実がある以上は「どうすべきなのか」自分にはわかりませんでした。 ### 対処(のヒント?) 対処というよりイマイチな回避策というべきですが、「pulseが1回以上実行された(であろう)後にvvaluePropertyの値を設定する」という方法を試してみました。 ``` public class Main extends Application { ... boolean timerRestart = false; AnimationTimer timer = new AnimationTimer() { long start; @Override public void handle(long now) { if (timerRestart) { start = now; timerRestart = false; return; } if (now - start > 60_000_000) { System.out.println("now recover original vValue"); scroll.setVvalue(vValue); timer.stop(); } } }; public void returnToResults() { resultsBox.getChildren().clear(); resultsBox.getChildren().addAll(results); // (A) returnToResults.setDisable(true); timerRestart = true; timer.start(); } ... } ``` おおまかな動作は以下の通りです。 (1) scrollPaneの内容を(A)で入れ替えた際にAnimationTimerをstart (2) AnimationTimerをstart後適当な時間が経過するまで待つ(上の例ではとりあえず60ms) pulseはJavaFXのアプリケーションスレッドをよほど高負荷にしない限り1秒間に数十回は起動されると思いますので、とりあえず適当に60msとしましたがもっと短い間隔でもよいはずと思います。 (3) pulseが実行されたであろう時間経過の後vvaluePropetyを元の値に設定し、AnimationTimerをstopする。 なんとも不格好な回避策ですが、やってみたところ一応期待通りの動作になりました。