jQuery の In Place Editor プラグインを作ってみた
2010年02月21日
独立心旺盛で、新しい技術で新しいWebサービスを作りたいと思っているけれど、ひとりでやることに限界を感じているフリーのエンジニアの方。あなたの期待にこたえられる仲間と環境を、八角研究所なら提供できると思います。社員としてではない関わり方も、あるかもしれません。
この会社の特徴を知る
このカテゴリの記事はまだ投稿されていません
など 3 記事
など 24 記事
のんびり書いてる間にJava6update12が出ちゃいました。Swing関連では、AWTの連携強化とJavaFXのパフォーマンス改善のおこぼれがあるくらいでしょうか。
今回は、予告通りEDTについて書いてみたいと思います。
EDTとは、Event Disptach Threadの略で、名前の通りイベントを割り当てるためのスレッドです。EDTは、1つのスレッドなのですべての操作がいったんEDTに集められ、それを適切な処理に振り分けています。
たとえばボタンをクリックするとEDTに「ボタンが押されたよ~。」というメッセージが送信されます。EDTはメッセージを受け取り、ボタンをクリックするというアクションに関連づけられているアクションがあれば、そのアクションを呼び出します。
本当に一本のスレッドを使用しているのか簡単なプログラムを作成して見てみます。今回使用するAPIは、Thread#getCurrentThread()です。このAPIを使用すると現在実行中のスレッドに関する情報を取得することができます。
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
public class ImasaraSwing2_1 {
private static void createAndShowGUI() {
final JFrame f = new JFrame("いまさらSwing2_1");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setLayout(new FlowLayout());
for (int i = 0; i < 3; i++) {
final JButton button = new JButton("ボタン" + i);
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final Thread t = Thread.currentThread(); // ここで現在実行中のスレッドを取得
JOptionPane.showMessageDialog(f, String.format(
"%s がクリックされました\n Id: %d ,Name: %s", button.getText(), t
.getId(), t.getName()));
}
});
f.add(button);
}
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
//ちなみにメインスレッドも表示してみる
final Thread t = Thread.currentThread();
System.out.printf("Id: %d ,Name: %s\n", t.getId(), t.getName());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
}
上記コードを実行すると下記のウィンドウが表示されます。
ボタンを順にクリックして見ると、スレッドIDとスレッド名のメッセージダイアログが表示されます。IDは13、名前は”AWT-EventQueue-0”ですべて同じスレッドであることが分かります。このスレッドがEDTです。
ちなみに、今回は自力でスレッドを取得してEDTの存在を確認しましたが、EDT上で実行されていることを確認するためのユーティリティSwingUtilities#isEventDispatchThread()が用意されています。
EDT上ですべてのイベントが処理されることはわかりましたが、そのことにメリット・デメリットがあるのでしょうか?
1つのスレッドですべてのイベントが処理されるということは、どこかの処理で無限ループや時間のかかる処理を行うと他のイベントが処理できずに固まることを意味します。
試しに(ありがちですが)素数を計算するプログラムを作ってみます。(簡単のためエラトステネスのふるいは使わず、ベタに割り切れる数があるかチェックしています。)
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class ImasaraSwing2_2 {
private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);
// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);
// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
listModel.addElement(2);
for (int i = 3; i <= n; i++) {
if (isPrimeNumber(i)) {
listModel.addElement(i);
}
}
}
// 単純に割り切れる数があるかループしてチェック
private boolean isPrimeNumber(final int n) {
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
});
frame.setSize(250, 250);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
}
上記コードを実行すると下記のような画面が表示されます。数字を入力して[素数計算]ボタンをクリックすると、その数字以下すべての素数が表示されます。10000程度までなら最近のPCの性能であれば一瞬で表示されると思います。
しかし、5万,10万と数字を大きくすると次第に遅くなることがわかります。私のPCで100万を入力してウィンドウを操作すると下記のようになってしまいました。
では、時間のかかる処理は、どうすればよいでしょうか?
EDT上で実行できないなら答えは一つしかありません。EDT以外のスレッドで実行するしかないです。
というわけで、さきほどのプログラムの計算部分をベタにスレッドにしてみます。
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class ImasaraSwing2_3 {
private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_3");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);
// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);
// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
// 計算は、計算スレッドに任せる。
new CalcThread(n, listModel).start();
}
});
frame.setSize(250, 250);
frame.setVisible(true);
}
//計算を行って、結果をリストボックスに表示するスレッド
private static class CalcThread extends Thread {
private int n;
private DefaultListModel listModel;
public CalcThread(final int n, final DefaultListModel listModel) {
this.listModel = listModel;
this.n = n;
}
@Override
public void run() {
listModel.addElement(2);
for (int i = 3; i <= n; i++) {
if (isPrimeNumber(i)) {
listModel.addElement(i);
}
}
}
private boolean isPrimeNumber(final int n) {
for (int i = 2; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
}
今度は、うまく動くでしょうか?
実は、これでもうまく動かないのです。運が良ければうまく動いているように見えますが、たいていはリストボックスが更新されなかったり、意図しないタイミングでリストボックスが更新したりすると思います。
実は、Swingにはもう一つ決まり(というか制約)があって、基本的にコンポーネントの更新処理(今回の例ではリストボックスに値を表示)は、EDT上で行わなければならないのです。
ここで、第1回で”おまじない”としておいたSwingUtilities#invokeLaterが再度登場します。SwingUtilities#invokeLaterは、EDTに対して「後でEDT上で実行してね。はぁ~と。」とお願いするメソッドです。このメソッドを使用してリストボックスの更新処理を書き直してみます。
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class ImasaraSwing2_4 {
private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_4");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);
// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);
// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
// 計算は、計算スレッドに任せる。
new CalcThread(n, listModel).start();
}
});
frame.setSize(250, 250);
frame.setVisible(true);
}
private static class CalcThread extends Thread {
private int n;
private DefaultListModel listModel;
public CalcThread(final int n, final DefaultListModel listModel) {
this.listModel = listModel;
this.n = n;
}
//EDT上で更新されるようにお願いする。
private void addList(final int n) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
listModel.addElement(n);
}
});
}
@Override
public void run() {
addList(2);
for (int i = 2; i <= n; i++) {
if (isPrimeNumber(i)) {
addList(i);
}
}
}
private boolean isPrimeNumber(final int n) {
for (int i = 3; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
}
ここまでやって、ようやくまともに動作するようになります(実は、まだ問題がありますが・・・)。実際に大きな数字を入力して実行してみるとモリモリとリストボックスが大きくなっていくことがわかります。
でも、たいしたことやってない割に面倒くさすぎませんか?
このような処理を行うためにJava6からSwingWorkerというクラスが用意されました(Java5へのポーティングもあるようです)。このクラスを使用するともっとスッキリと書くことできるようになります。SwingWorkerを使用して書き直すと下記のようになります。詳しい説明は、JavaDocを参照してください。
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class ImasaraSwing2_5 {
private static void createAndShowGUI() {
final JFrame frame = new JFrame("いまさらSwing2_5");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
// 入力パネル
final JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BorderLayout());
final JTextField value = new JTextField();
final JButton calc = new JButton("素数を表示");
inputPanel.add(value, BorderLayout.CENTER);
inputPanel.add(calc, BorderLayout.EAST);
frame.add(inputPanel, BorderLayout.NORTH);
// 結果を表示するリスト
final DefaultListModel listModel = new DefaultListModel();
final JList result = new JList(listModel);
frame.add(new JScrollPane(result), BorderLayout.CENTER);
// ボタンを押した時の処理
calc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
listModel.clear();
final int n = Integer.parseInt(value.getText());
if (n < 2) {
return;
}
//SwingWorkerを使用して書き直す
new SwingWorker<Void, Integer>() {
@Override
protected Void doInBackground() throws Exception {
publish(2);
for (int i = 2; i <= n; i++) {
if (isPrimeNumber(i)) {
publish(i); //publishするとEDT上でprocessが実行される。
}
}
return null;
}
private boolean isPrimeNumber(final int n) {
for (int i = 3; i < n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
//publishされた値は、ここで処理される。このメソッドはEDT上で実行される。
protected void process(java.util.List<Integer> chunks) {
for (int i: chunks) {
listModel.addElement(i);
}
};
}.execute();
}
});
frame.setSize(250, 250);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createAndShowGUI();
}
});
}
}
コンポーネントの更新処理は、EDT上で行う必要があるということで、リストボックスの更新処理をEDT上で実行するというサンプルを用いました。実は、これには例外があってJTextFieldやJTextAreaなどのJTextComponentを継承したクラスのsetTextメソッドは、EDT以外のスレッドからも更新する事が可能です。
ちなみJTextComponent#setTextのJavaDocには以下のように書かれています。(それにしても、この日本語では何を言ってるのか分かりませんね。。。)
このメソッドはスレッドに対して安全ですが、ほとんどの Swing メソッドは違います。詳細は、「How to Use Threads」を参照してください。
次回は、EDTと並んで理解の難しい(ような気のする)レイアウトについて書きます。
コメントはありません
トラックバックはありません
メンバー紹介
2010年02月21日
2010年02月21日
2009年09月07日
2009年09月01日
2009年08月22日
2009年08月15日
2009年08月07日
2009年08月07日
2009年07月28日
この日記にコメントする