詰まって数時間が吹き飛ばされたので、メモ代わりに。Swingを使っている人ならだれでも一度は通る失敗(らしい)。
どんなのをつくっていたか
Javaで自作シェルっぽいものを作ろうとしていて、JScrollPaneを自動スクロールさせようとしていた。調べながら書いたソースコードがこれ。
class MyShell extends JFrame implements ActionListener{
MyShell{
//(中略)
String now = new File(".").getAbsoluteFile().getParent().toLowerCase();//カレントディレクトリ
JTextField text = new JTextField();//入力エリア
text.addActionListener( this );
JTextArea area = new JTextArea(now+"->");//表示エリア
JScrollPane scrollpane = new JScrollPane(area);
JPanel p = new JPanel()
p.add(text,BorderLayout.SOUTH);
p.add(scrollpane);
//(中略)
}
public void actionPerformed(ActionEvent e){
String input = text.getText().toLowerCase();
if(input.equals("exit")){System.exit(0);}//強制終了用
area.append(text.getText()+"\nPC->"+mani(input.split(" "))+"\n\n"+now+"->");//描画エリアに追記
text.setText("");//入力エリアの内容を消去
JScrollBar scrollBar = scrollpane.getVerticalScrollBar();
scrollBar.setValue(scrollBar.getMaximum());
}
public String mani(String[] input){
//コマンド解釈用関数
}
//(中略)
}
※分かりやすいように、コンストラクタの中で宣言しているように書いていますが、実際はインスタンスフィールドとして各種変数を宣言しています。
一応、解説しておくとactionPerformedは入力用テキストフィールド内でエンターキーが押されたときに呼び出されます(参考リンク)。そして、エンターキーを押した直後にコマンドの解釈が行われ、描画エリアに追記。その後、スクロールバーの高さの分だけスクロールされて、JTextAreaの一番下が表示されている......はずでした。
なにがいけなかったのか
結論から言うと、イベントハンドラとして登録された処理は、そのすべての処理が終わってから描画されます。つまり、scrollBar.getMaximum()
で取得できる大きさは再描画前の大きさなので、再描画前のJTextAreaの一番下の場所にしか行けないのです。actionPerformed
の最後の行にThread.sleep(1000)
でも挿入してみると、挙動がよく分かると思います。
まあ、よくよく考えれば、描画処理はできるだけ少ない回数で済んだほうが良いですし、当然といえば当然なんですね。
解決法
(ネットはちゃんと調べると、いくらでも情報が出てきますね......。)
単一のイベントハンドラーでは状態の変更とただ一回のrepaintの呼び出しのみが許されると思ってください。複数回repaintを呼び出してもpaintComponentメソッドは一度しか呼び出されません。
引用元:https://teratail.com/questions/75183
更に調べていると、こんな記事が......。同じことで詰まっている人いましたね......。この記事にあるコードをactionPerformed
に入れてやると無事動きました。
また、Swingはシングルスレッドでの設計のようでした。
invokeLater() メソッドは(中略)保留中のすべての AWT イベントが処理されたあとに発生します
引用元:http://wisdom.sakura.ne.jp/system/java/swing/swing4.html
まとめ
- なんでもかんでもイベントハンドラで行おうとしない
- イベントハンドラでは終了時に1度だけ描画処理を行うのが基本
- Swingでイベントハンドラのスレッド以外から何らかの描画処理をしたい場合はinvokeLater()を使う
余談
余談ですが、paintImmediately
も使ってみました。どうやら、描画処理だけで、それ以外の数値等の更新はやはりイベントハンドラ終了後に行われるようです。
actionPerformed(ActionEvent e){
System.out.println(scrollbar.getValue()+"#"+scrollbar.getMaximum()+"#"+area.getHeight());
p.paintImmediately(0,0,5000,5000);
System.out.println(scrollbar.getValue()+"#"+scrollbar.getMaximum()+"#"+area.getHeight());
}
以上。