咖啡日语论坛

 找回密码
 注~册
搜索
楼主: bgx5810

Java SE 6完全攻略

[复制链接]
 楼主| 发表于 2007-5-25 15:14:15 | 显示全部楼层
ネイティブ・アプリケーションとの連携


Javaから他のネイティブ・アプリケーションを起動するには,J2SE 5.0ではProcessBuilderクラス,それ以前ではRuntimeクラスを使用します。これでこと足りる場合も多くあります。
しかし,これらのクラスでは困ることもあります。問題になるのは以下の二つの場合です。
  • 起動するアプリケーションがどこにあるかわからない
  • どのアプリケーションを起動するのかがわからない
自分のマシンであれば,アプリケーションがインストールされている場所はわかります。しかし,他の人のマシンでは,どこにインストールされているかは必ずしもわかりません。インストールされているかどうかすらわからいないことも少なくありません。
このような場合,Javaだけではどうしようもありません。Java Native Interface(JNI)を使用してレジストリを調べるといった方法を採らなくてはならなくなります。
もっと問題なのは,起動するアプリケーションがわからない場合です。
例えば,Webブラウザを考えてみましょう。ブラウザには様々なものがあります。Windowsの場合はFirefox,InternetExplorer(IE),Operaなどなど。多くのユーザーはIEを使用しているかもしれませんが,断定はできません。つまり「ブラウザを起動する」といっただけでは,どのブラウザを起動すればいいのかわからないのです。加えて,起動しようとしたブラウザによってはインストールされていない可能性もあります。
この問題はメーラーやエディタでも同じです。
OSによっては,MIMEタイプごとにデフォルトで起動するアプリケーションが決まっています。HTMLファイルであればIE,メーラはOutlookなどが設定されています。Javaからこの情報を扱えればいいのですが,JNIを使用しない限り調べることができませんでした。
そこで登場したのが,Java SE 6のjava.awt.Desktopクラスです。
ブラウザの起動手始めにブラウザを起動してみましょう。
サンプルはここからダウンロードできます:DesktopSample1.java
Desktop.クラスはstaticメソッドのgetDesktopメソッドで取得できます。あとはbrowseメソッドをコールするだけです。
注意すべき個所は,browseメソッドの引数がURLクラスではなく,URIクラスだというところだけです。
      public DesktopSample1(String uri) {
    // Desktopオブジェクトの取得
    Desktop desktop = Desktop.getDesktop();

    try {
        // ブラウザの起動
        desktop.browse(new URI(uri));
    } catch (URISyntaxException ex) {
        ex.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}
DesktopSample1クラスを実行すると,IEをデフォルトに設定してある場合はIEが,他のブラウザをデフォルトに設定してある場合はそのブラウザが起動するはずです。
他のアプリケーションの起動他のアプリケーションも,ブラウザと同様に起動できます。Desktopクラスで扱える機能を表1に示しました。
  
表1 Desktopクラスで扱える機能
                機能          メソッド名                          ブラウズ          browse                          メール          mail                          編集          edit                          オープン          open                          印刷          print         

mailメソッドの引数の型はURIクラス,もしくは引数なしです。その他の三つのメソッドの引数の型はすべてFileクラスです。
これらのメソッドを使ったサンプルを作ってみました。
サンプルはここからダウンロードできます:DesktopSample2.java
機能とロケーションを指定してDesktopクラスの適切なメソッドをコールするだけのクラスです。
  public DesktopSample2(String method, String location) {
    Desktop desktop = Desktop.getDesktop();

    try {
        if (method.equals("browse")) {
            desktop.browse(new URI(location));
        } else if (method.equals("mail")) {
            desktop.mail(new URI(location));
        } else if (method.equals("edit")) {
            desktop.edit(new File(location));
        } else if (method.equals("open")) {
            desktop.open(new File(location));
        } else if (method.equals("print")) {
            desktop.print(new File(location));
        }
    } catch (URISyntaxException ex) {
        ex.printStackTrace();
    } catch (IOException ex) {
        ex.printStackTrace();
    }
}
筆者の環境では,ブラウザとしてIE,メーラーとしてOutlook Express,エディタとしてメモ帳が起動しました。皆さんの環境ではいかがですか?
この機能は,ファイル・マネージャのようなアプリケーションを作成するといった様々な用途に利用できるはずです。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:14:59 | 显示全部楼层
システムトレイにアクセスする


Swingも熟成が進み,様々な機能が使えるようになりました。パフォーマンスも初期のころに比べると各段の進歩をとげています。工夫次第では,昨年の9月の記事でご紹介したAerithのようにクールなユーザー・インタフェースを実現することもできます。
しかし,いまだに制約があることも確かです。その制約の一つがシステムトレイへのアクセスでした。
システムトレイは,Windowsのデスクトップの右下にある小さいアイコンが表示される領域です。
Java SE 6では,ようやくシステムトレイにアクセスできるようになりました。
システムトレイを表すクラスはjava.awt.SystemTrayクラスです。システムトレイ上に表示されるアイコンはjava.awt.TrayIconクラスで表されます。
アイコンの表示まずはシステムトレイにアイコンを登録してみます。
サンプルはこちらからダウンロードできます:SystemTraySample1.java
以下にシステムトレイにアイコンを登録するコードを示します。
  [size=0.9em]    public SystemTraySample1() throws IOException, AWTException {
    // SystemTrayオブジェクトの取得
    SystemTray tray = SystemTray.getSystemTray();

    // アイコンの生成と登録
    TrayIcon icon = new TrayIcon(ImageIO.read(new File("duke-icon.png")));
    tray.add(icon);
}  
      [tr]        図1 アイコン画像(duke-icon.png)      [/tr]      [tr]        図2 SystemTraySample1クラスの実行結果       [/tr]
  

SystemTrayオブジェクトはシングルトンであり,SystemTray#getSystemTrayメソッドで取得できます。
TrayIconクラスのコンストラクタは3種類あります。ここでは引数にImageオブジェクトを取るものを使用しました。
Windowsでは,システムトレイに表示するイメージはたて16ピクセル,よこ16ピクセルのサイズにします。また,アイコンのイメージは背景が透明のほうが見ばえがいいので,JPEGよりはPNGもしくはGIFを使いましょう。
ここで使用したduke-icon.pngを図1に示します。
TrayIconオブジェクトが生成できたら,SystemTrayオブジェクトにaddメソッドで登録します。
簡単ですね。SystemTraySample1クラスを実行すると図2のように表示されます。
アイコンのイベント単にアイコンを表示するだけではつまらないので,イベント処理を付け加えてみましょう。
サンプルはこちらからダウンロードできます:SystemTraySample2.java
以下にアイコンに関連する部分を示します。
      public SystemTraySample2() throws IOException, AWTException {
    // SystemTrayオブジェクトの取得
    SystemTray tray = SystemTray.getSystemTray();

    Image image = ImageIO.read(new File("duke-icon.png"));

    // 右クリック時に表示されるポップアップメニュー
    PopupMenu menu = new PopupMenu();
    menu.add(new MenuItem("Duke"));
    menu.add(new MenuItem("Fang"));

    // アイコンの生成
    // ツールチップとポップアップメニューを指定する
    icon = new TrayIcon(image, "I'm Duke!", menu);

    // マウス・イベント
    icon.addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent event) {
            if (event.getButton() == MouseEvent.BUTTON1) {
                // ポップアップメッセージの表示
                icon.displayMessage("from Duke", "Hello, World!",
                                    TrayIcon.MessageType.INFO);
            }
        }
    });

    tray.add(icon);
}
      [tr]        図3 ツールチップの表示      [/tr]      [tr]        図4 ポップアップメニューの表示      [/tr]      [tr]        図5 ポップアップメッセージの表示      [/tr]
  

SystemTraySample2クラスでは,ポップアップメニュー,ツールチップ,マウス・イベントを扱います。ポップアップメニューはアイコンをマウスで右クリックしたときに表示されます。
ツールチップとポップアップメニューは,TrayIconクラスのコンストラクタの引数で指定します。第2引数がツールチップ,第3引数がポップアップメニューです。それぞれTrayIcon#setToolTipメソッド,TrayIcon#setPopupMenuメソッドで設定することも可能です。
TrayIconクラスはマウス・イベントも扱うことができます。イベントの処理はAWTのイベント処理と同じです。
ここでは左クリックされたときに,ポップアップメッセージを表示させてみました。ポップアップメッセージはTrayIcon#displayMessageメソッドで表示することができます。
さて,SystemTraySample2を実行してみましょう。図3にツールチップの表示,図4にポップアップメニューの表示,図5にポップアップメッセージの表示を示しました。ポップアップメニューはSwingのJPopupMenuクラスが使用できないので外観はいまいちですが,機能的には十分です。
ここでは行いませんでしたが,状況に応じてイメージを変更することも可能です。システムトレイは,常駐アプリケーションの状態表示などいろいろな使い道がありそうですね。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:16:39 | 显示全部楼层
文字に対するアンチエイリアス


[tr]        図1 アンチエイリアス      [/tr]
  「Swingの文字は汚い」と多くの方が思っているのではないでしょうか。では,なぜSwingの文字は汚い感じがするのでしょう。
その理由の一つがアンチエイリアスです。
アンチエイリアスは一種のぼかし処理です。例えば,線を描画する場合,ピクセルの大きさが有限であるため,ギザギザ(ジャギー)になります(これがエイリアスです)。このジャギーを解消させるのがアンチエイリアスです。具体的には,色の中心からの距離に応じて色を薄くするという処理を行います。
例えば,図1では,上の線はアンチエイリアス処理を行っておらず,下はアンチエイリアス処理を行っています。拡大すると,アンチエイリアスをしている線は,境界がぼやけています。
このように,アンチエイリアスを使用すると描画がなめらかになります。特に文字の描画では大きい効果が得られます。
アンチエイリアスはJ2SE 1.2のころから使用できました。ところが,Swingのコンポーネントでの文字描画ではアンチエイリアスを適用できませんでした注1
Java SE 6になって,ようやくSwingで文字描画にアンチエイリアスが使用されるようになりました。
この効果をさっそくサンプルで確かめてみましょう。
  
  サンプルのソース:AntialiasFontSample1.java
  
このサンプルは,文字の大きさを変化させて表示を行っているだけです。
  JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

for (int i = 12; i <= 32 ; i += 4) {
  Font font = new Font("SansSerif", Font.PLAIN, i);
  JLabel label = new JLabel(i + "pt: ABCあいう");
  label.setFont(font);

  panel.add(label);
}
Windowsの実行結果を図2,Linux(Fedora Core 5)での実行結果を図3に表示します。それぞれ右上が通常の表示で,それを2倍に拡大したものが左下の部分になります。Linuxはさざなみフォントを使用しています。
  
    [tr]        図2 Windowsでの実行結果      [/tr]      [tr]        図3 Linuxでの実行結果      [/tr]
  

Windowsではある程度大きい文字にアンチエイリアスが適用されています。小さい文字にアンチエイリアスをかけると逆に見にくくなることがあるためです。Linuxでは小さい文字でもアンチエイリアスが適応されています。
皆さんも,自分のパソコンで試してみてください。もしかしたら,これらとは違う結果になっているかもしれません。
というのも,これらの実行結果は,今となってはめずらしいCRTディスプレイで出力したものだからです。図4は,同じサンプルをWindowsで液晶ディスプレイ(LCD)で出力したものです。ちょっとわかりにくいかもしれないので,こちらは4倍に拡大してあります。
  
    [tr]        図4 Windowsでの実行結果(LCD)      [/tr]
  

図4をよく見てみると,文字の色は黒なのに,文字の左側には赤,右側には青でぼかしがはいっています。
なぜ,もともとの色とは違う色でアンチエイリアスを適用するのでしょうか。それは,液晶の色の並び方が規則的であることを利用するためです。
液晶ではだいたいの場合,色の並びは左から赤,緑,青の順番で並んでいます。この色の並びを利用して,1ピクセルを構成する三つの色を分解して扱うのです。
例えば,左側の緑と青の部分は0にして赤だけセットすることにより,3色をまとめて扱うよりも細かくアンチエイリアスの制御を行うことができます。
このように,1ピクセルを構成する三つの色を分解したものをサブピクセルと呼びます。Windowsでは,サブピクセルを利用したアンチエイリアスをClearTypeと呼びます。
Java SE 6では,サブピクセルを利用したアンチエイリアスが可能になりました。このため,LCDでサンプルを動作させると,図4のように赤と青でアンチエイリアスされたのです。
LCDであっても上記のような表示にならない場合は,Clear Typeが有効になっていないと考えられます。
WindowsXPのClearTypeの設定は,コントロールパネルの[画面]から行います。[デザイン]タブの[効果]ボタンをクリックすると,図5のようなダイアログが表示されます。「次の方法でスクリーンフォントの縁を滑らかにする(S)」では,[標準]が通常のアンチエイリアス,[ClearType]がClearTypeになります。
  
    [tr]        図5 ClearTypeの設定      [/tr]
  

Linuxはディストリビューションによって設定方法が異なります。Fedora Core 5の場合はタスクバーの[デスクトップ]-[設定]-[フォント]で行います(図6)。
  
    [tr]        図6 サブピクセルアンチエイリアスの設定(Fedora Core5)      [/tr]
  

もし,システムの設定と異なる方法でアンチエイリアスを行いたい場合は,次のようにします。
      >java -Dawt.useSystemAAFontSettings=on [クラス名]
       
awt.useSystemAAFontSettingsの取りうる値
  off      アンチエイリアスを行わない
  on       アンチエイリアスを行う
  gasp     小さい文字ではアンチエイリアスを行わない
  lcd      サブピクセルのアンチエイリアス
  lcd_hrgb サブピクセルのアンチエイリアス 横方向RGB
  lcd_hbgr サブピクセルのアンチエイリアス 横方向BGR
  lcd_vrgb サブピクセルのアンチエイリアス 縦方向RGB
  lcd_vbgr サブピクセルのアンチエイリアス 縦方向BGR
   
lcd_hrgb以下の四つは後で説明します。
アンチエイリアスな文字を描画するさて,自作のコンポーネントではサブピクセルのアンチエイリアスはどのように行えばいいのでしょうか。
アンチエイリアスはjava.awt.RenderingHintsクラスで表します。Java SE 6ではこのRenderingHintsクラスにLCDのアンチエイリアスの定義が加わりました注2
  
  サンプルのソース:AntialiasFontSample2.java
  
LCD用のアンチエイリアスの定義はVALUE_TEXT_ANTIALIAS_LCDで始まる4種類です。Hが頭に付いているのは水平方向に並んでいるもの,Vは垂直方向です。RGBとBGRは色の並び方の違いを表しています。
これらの値はawt.useSystemAAFontSettingsの値であるlcd_hrgb,lcd_hbgr,lcd_vrgb,lcd_bgrに相当します。
ヒントの設定には,Graphics2D#setRenderingHintメソッド,もしくは,複数のヒントをまとめて設定するGraphics2D#addRenderingHintsメソッドを使用します。
ヒントのキーはKEY_TEXT_ANTIALIASINGです。
  [size=0.9em]public void paintComponent(Graphics g) {
  Graphics2D g2d = (Graphics2D)g;

  Font font = new Font("SansSerif", Font.PLAIN, 32);
  g2d.setFont(font);

  // HRGB
  g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                       RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
  g2d.drawString("HRGB:" + text, 10, 40);

  // HBGR
  g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                       RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HBGR);
  g2d.drawString("HBGR:" + text, 10, 80);

  // VRGB
  g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                       RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VRGB);
  g2d.drawString("VRGB:" + text, 10, 120);

  // VBGR
  g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                       RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_VBGR);
  g2d.drawString("VBGR:" + text, 10, 160);
}
実行してみると,先ほどの結果と同じように左に赤,右に青が出力されるのはHRBGだということがわかります(図7)。つまり,-Dawt.useSystemAAFontSettings=lcdとして実行したときには,常にHRGBになります。
実をいうと,Javaからはピクセルの色のパターンはわからないことがあるので,このような決め打ちになっているようです。
例えば,LCDを90度回転させて縦長の状態で使用していたら,正しくアンチエイリアスできません。こうしたときには,-Dawt.useSystemAAFontSettings=lcd_vrgbといったように指定しなくてはなりません。
  
    [tr]        図7 ヒントを変化させて描画      [/tr]
  

ここで示したように,Java SE 6ではサブピクセルを使用したアンチエイリアスが可能になりました。ただ,効果が薄いと感じられることも多いので,使うか使わないかはユーザーに委ねるのがよさそうです。
例えば,前述したawt.useSystemAAFontSettingsの値を使用するのがいいかもしれません。この場合はSystem#getPropertyメソッドを使用すればawt.useSystemAAFontSettingsの値を取得できます。
とはいうものの,アンチエイリアスは効果が高いので,デフォルトではヒントをRenderingHints.VALUE_TEXT_ANTIALIAS_GASPにしておき,awt.useSystemAAFontSettingsに値が設定してあればそれを使用するのがいいと思います。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:17:27 | 显示全部楼层
丸がちゃんと丸になる


今週はまず次のプログラムをJ2SE 5.0で動作させてみてください。
  
  サンプルのソース:TinyCircleSample.java
  
このサンプルは3種類の方法で直径が1から20までの円を描画しています。描画の部分を次に示します。
  public void paintComponent(Graphics g) {
    Graphics2D g2d = (Graphics2D)g;

    g2d.drawString("Graphics#drawOval", 10, 15);
    g2d.drawString("Graphics2D#draw(Ellipse.FLoat)", 10, 60);
    g2d.drawString("Graphics2D#draw(Ellipse.Double)", 10, 105);

    int x = 10;
    for (int i = 1; i < 20; i++) {
        x += (i * 1.5);

        // AWT で描画
        g.drawOval(x, 20, i, i);

        // Java2D で描画(単精度)
        Ellipse2D ellipse = new Ellipse2D.Float(x, 65, i, i);
        g2d.draw(ellipse);

        // Java2D で描画(倍精度)
        ellipse = new Ellipse2D.Double(x, 110, i, i);
        g2d.draw(ellipse);
    }
}
はじめにAWTで描画し,次にJava2Dで単精度,最後にJava2Dで倍精度で描画しています。
図1にLinux(Fedora Core 5)での実行結果を示します。
  
    [tr]        図1 サンプルの実行結果(J2SE 5.0)      [/tr]
  

なんとなく変な感じがしますね。わかりやすいように図2に拡大図を示しました。
  
    [tr]        図2 実行結果の拡大図(J2SE 5.0)      [/tr]
  

一番上のAWTで描画しているものはちゃんと円になっていますが,Java2Dを使用したものはとうてい円とはいえないようなものまで含まれています。
Java2Dではなぜこんないびつな円になってしまうのでしょう。
Java2Dでは描画に実数を使用できます。円を表すEllipse2Dクラスでも,Ellipse2D.DoubleクラスとEllipse2D.Floatという精度の異なる二つの内部クラスが定義されています。
実数が使用できるのはいいのですが,問題は最終的に整数にしなくてはならないということです。floatやdoubleからintに単純に変換すると,小数点以下は切り捨てられてしまいます。これが,円がいびつになる原因です。
さて,このサンプルをJava SE 6で実行してみましょう(図3)。
  
    [tr]        図3 実行結果の拡大図(Java SE 6)      [/tr]
  

ちゃんと円になりました。今までこれができなかったことのほうが不思議です。
小さい形状が正確に描画されるだけでなく,Java2Dの内部表現としてもPath2D.Doubleクラスを導入し,倍精度で扱うことができるようになりました。
ところで,サンプルの実行にLinuxを使用したのには理由があります。Windowsではまだ円がいびつなままなのです。
Windowsでは,Java2Dの描画にDirectXの一部であるDirect3Dをデフォルトで使用します。これがどうやらいまいちのようなのです。
次に示すように,Direct3Dを使用せずにOpenGLを使用すると,ちゃんと丸が丸として描画されます(図4,図5)。
これからは,Javaで描画するときはWindowsでもLinuxでもOpenGLを使用するのが常道になるのかもしれません。
      >java -Dsun.java2d.opengl=true TinyCircleSample  
            [tr]        図4 Direct3Dでの実行結果        図5 OpenGLでの実行結果[/tr]
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:18:11 | 显示全部楼层
より細かいモーダル制御


JavaでDialogクラスやJDialogクラスを使用してダイアログを表示するとしましょう。
ダイアログを表示したときに,ダイアログを表示させた元のフレームなどが操作できなくなることがあります。このようなダイアログをモーダルなダイアログといいます。
モーダルなダイアログは,そのダイアログで入力などの操作が必要であることをユーザーにわからせるのに効果的です。
DialogクラスやJDialogクラスは,コンストラクタの引数でモーダルかどうかを指定できます。また,setModalメソッドで指定することも可能です。
一方,JOptionPaneクラスのshowXXXXDialogメソッド(XXXXの部分はInputやConfirmなどが入ります)で表示した場合は,ダイアログはモーダルになります。
J2SE 5.0までは,モーダルの有無しか扱えませんでした。通常はこれだけで問題ありません。しかし,一つのアプリケーションで複数のウィンドウを扱う場合はちょっと困ってしまうこともあります。
例えば,通常のオペレーションを行うウィンドウ以外に,ヘルプを表示するウィンドウがある場合を考えてみます。メインのウィンドウでモーダルのダイアログを表示すると,ヘルプ・ウィンドウへのアクセスもブロックされてしまいます。
ユーザーとしては,表示されたダイアログのヘルプを見たくても,ブロックされてしまっているのでヘルプ・ウィンドウを操作できません。これはちょっと困りものです。
かといって,ダイアログをモーダルにしないと,本来のモーダルの意味がなくなってしまいます。
このような場合に備えて,Java SE 6ではより細かいモーダルの制御を行えるようになりました。
新しいモーダル・モデルJava SE 6ではダイアログのモーダルをDialog.ModalityTypeというenumで表します。取りうる値は次の4種類です。
  
表1 Dialog.ModalityType
                値          説明                          MODELESS          ウィンドウのブロックを行わない                          APPLICATION_MODAL          アプリケーションのすべてのウィンドウをブロックする
          ただし,ダイアログの子供のウィンドウはブロックしない                          DOCUMENT_MODAL          自身の親のウィンドウはブロックするが,他のウィンドウはブロックしない
          ただし,ダイアログの子供のウィンドウはブロックしない                          TOOLKIT_MODAL          同じToolkitオブジェクトを使用して作成されたウィンドウをブロックする
          ただし,ダイアログの子供のウィンドウはブロックしない         

J2SE 5.0までのモーダルは,Dialog.ModalityTypeではAPPLICATION_MODALに相当します。
一つのアプリケーションで複数のToolkitオブジェクトを使うことはほとんどないので,通常,モーダルとして使用されるのはAPPLICATION_MODALとDOCUMENT_MODALになります。
DOCUMENT_MODALを使用すれば,前述したヘルプ・ウィンドウの問題を解決できます。
さっそくこれをサンプルで確かめてみましょう。
  
  サンプルのソースコード:ModalSample1.java
  
このサンプルは二つのフレームを生成し,それぞれにボタンがついています。ボタンをクリックするとダイアログが表示されます。
private void init(String title, int x, int y) {
    JFrame frame = new JFrame(title);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setBounds(x, y, 100, 75);

    JButton button = new JButton(title);
    frame.add(button);

    button.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            JButton source = (JButton)e.getSource();
            showDialog(source);
        }
    });

    frame.setVisible(true);
}
ダイアログを表示するshowDialogメソッドは次のようになります。
private void showDialog(JButton parent) {
    JOptionPane pane = new JOptionPane(parent.getText(),
                               JOptionPane.INFORMATION_MESSAGE);
         
    JDialog dialog = pane.createDialog(parent, parent.getText());

    // モーダルの設定
    dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL);

    dialog.setVisible(true);
}
ここではJOptionPaneクラスを使用してダイアログを生成していますが,もちろん直接JDialogオブジェクトを生成してもかまいません。
新しいモーダルはDialog#setModalityTypeメソッドで設定します。
はじめはAPPLICATION_MODALで実行してみましょう。
    [tr]        図1 APPLICATION_MODALでの実行      [/tr]
  

どちらか一方のダイアログが表示されていると,二つのフレームとも操作できません。これは今までのモーダルと同じ動作です。
それでは,モーダルをDOCUMENT_MODALに変更してみます。
  private void showDialog(JButton parent) {
    JOptionPane pane = new JOptionPane(parent.getText(),
                               JOptionPane.INFORMATION_MESSAGE);

    JDialog dialog = pane.createDialog(parent, parent.getText());

    // モーダルの設定
    dialog.setModalityType(Dialog.ModalityType.DOCUMENT_MODAL);

    dialog.setVisible(true);
}

    [tr]        図2 DOCUMENT_MODALでの実行      [/tr]
  

一方のフレームでダイアログを表示していても,他方のフレームは操作できます。とはいうものの,ダイアログの親となるフレームの操作はブロックされます。
ここではどちらのダイアログも同じモーダルを設定しましたが,異なるモーダルを設定することも可能です。
ところで,多くのダイアログを使用するアプリケーションでは,個々のダイアログにモーダルの設定をするのは面倒です。例えば,ヘルプ・ウィンドウだけはいつでも操作できるようにしたいというような場合は,ダイアログではなくウィンドウに設定を行うことができます。
つまり,ダイアログのモーダルの除外規則をウィンドウに設定します。このために,Dialog.ModalExclusionTypeというenumが導入されました。
Dialog.ModalExclusionTypeの取りうる値を表2に示します。
   表2 Dialog.ModalExclusionType
                値          説明                          NO_EXCLUDE          除外しない(デフォルト)                          APPLICATION_EXCLUDE          APPLICATION_MODALなダイアログからのブロックを除外する                          TOOLKIT_EXCLUDE          APPLICATION_MODALもしくはTOOLKIT_MODALなダイアログからのブロックを除外する         
この動作もサンプルで確認してみましょう。
  
  サンプルのソースコード:ModalSample2.java
  
ModalExclusionTypeはWindow#setModalExclusionTypeメソッドで設定します。そこで,ModalSample2クラスでは片方のフレームにのみModalExclusionTypeを設定します。
  public ModalSample2() {
    JFrame frame1 = init("Document 1", 200, 100);

    JFrame frame2 = init("Document 2", 500, 100);

    // frame2 のみ ModalExclusionType を設定する
    frame2.setModalExclusionType(
        Dialog.ModalExclusionType.APPLICATION_EXCLUDE);
}
残りの部分はModalSample1とほぼ同一です。もちろん,ダイアログはAPPLICATION_MODALにしてあります。
実行すると,ダイアログが表示されていてもDocument 2のフレームは操作できます。自分の子供のダイアログに対しても,この除外規則は適用されるので,ダイアログをいくつも表示させることが可能になります。
  
    [tr]        図3 ModalExculsionTypeの適用      [/tr]
  

ダイアログは使用頻度も高いので,柔軟にモーダルを設定できるのはうれしいですね。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:18:54 | 显示全部楼层
AWT/Java2Dの細かい新機能


今月の最後は,AWT/Java2Dの細かい改良点,新機能をいくつか紹介していきます。
ウィンドウ・リサイズ中のレイアウトこの機能の恩恵は,Java SE 6を使用するだけで得られます。
まずは,J2SE 5.0で実行しているアプリケーションのウィンドウをリサイズしてみてください。図1はJ2SE 5.0でJDKに付属しているサンプルJava2Demoをリサイズしています。
図1を見て,何か変だと思いませんか。
リサイズ中,表示されているコンポーネントはそのままで変化がありません。リサイズが終了して,ウィンドウのサイズが確定すると,コンポーネントの再レイアウトが行われます。
  
    [tr]        図1 J2SE 5.0でのウィンドウ・リサイズ      [/tr]
  

さて,全く同じアプリケーションをJava SE 6で動作させてみましょう。同じように,ウィンドウのリサイズを行ってみてください。
気がつきましたか?
そうです,図2に示したように,Java SE 6ではリサイズ動作中でもダイナミックにコンポーネントのレイアウト変更が行われます。
このため,最終的にウィンドウのサイズをどの程度にすればよいかの判断がつきやすくなります。
機能としてはたいしたことはないのですが,これだけでユーザビリティがかなり向上しますよ。
  
    [tr]        図2 Java SE 6でのウィンドウ・リサイズ      [/tr]
  

マウスの絶対座標取得マウス・イベントが発生した場合,MouseEventオブジェクトからマウス・ポインタの位置を取得できます。ところが,取得したマウス・ポインタの位置は,イベントが発生したコンポーネントからの相対位置になっています。
普通はこれで構わないのですが,まれにマウス・ポインタの絶対位置を必要とするときがあります。
J2SE 5.0までは,イベントが発生したコンポーネントの絶対位置を取得し,それを利用してマウス・ポインタの絶対位置を計算しなくてはなりませんでした。
Java SE 6では,MouseEventクラスにgetLocationOnScreenメソッド,getXOnScreenメソッド,getYOnScreenメソッドという三つのメソッドが導入され,絶対位置を直接取得できるようになりました。
単に使い方が簡単になったというだけですが,それでもあるとないとでは大違いです。
描画パフォーマンスなどもちろん,Java SE 6ではGUIの描画パフォーマンスも向上しています。
OpenGLでは,描画を行うパイプラインをシングルスレッド化し,効率を向上させています。また,Windowsで使われるDirect3Dでの描画も同様に最適化されました。
また,9月に解説したJOGLからJava2Dにアクセスする機能もJava SE 6の新しい機能です。9月の記事ではXTransを紹介しましたが,Chris Campbell氏のblogに新しいサンプルが紹介されています。ソースコードも公開されているので,興味のある方はご参照ください。
さて,2カ月にわたってAWTとJava2Dの機能を紹介してきました。来月からはSwingの新機能についてご紹介します。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:19:52 | 显示全部楼层
ベースラインでのレイアウト


先月までは,AWT/Java2Dの新機能を紹介してきました。今月からはSwingの新機能に移りましょう。とはいうものの,今週と来週はAWTとSwingの両方で通用する機能を紹介します。
SwingやAWTで悩ましい問題として,コンポーネントのレイアウトがあります。
どのレイアウト・マネージャを使えばいいのか,どのように配置すればいいのか,思ったように配置できない,などなど。面倒になってレイアウト・マネージャをnullにしてしまった経験をお持ちの方も多いと思います。
Java SE 6では,レイアウトの機能も拡張されました。
そこで,今週は既存のレイアウト・マネージャの拡張機能,来週は新しいレイアウト・マネージャであるGroupLayoutクラスについて解説します。
文字の属性      [tr]        図1 アルファベットの構成      [/tr]

コンポーネントを水平方向にレイアウトする場合を考えてみましょう。このような場合,垂直方向の位置合わせにはコンポーネントの上,下,中央のいずれかが使用されます。例えば,FlowLayoutクラスでは,コンポーネントの中央にそろえて配置されます。
通常はそれで十分です。しかし,文字,特にアルファベットを表示する場合,考慮しなくてはならないことがあります。
英語をはじめて習ったときに,4線のノートでアルファベットを書いたのを覚えていますか?
アルファベットを書くときには,この4線の下から2番目の線を基準として表記します。しかし,gやyなどの文字は,その線の下にはみ出てしまいます。これがやっかいなところです。
図1に示したように,アルファベットで書くときに基準となる線のことをベースライン(Baseline)といいます。そして,ベースラインから上の部分をアセント,下の部分をディセントといいます。
      [tr]        図2 中央で整列      [/tr]      [tr]        図3 ベースラインで整列      [/tr]

さて,ここでGUIに戻りましょう。図2は三つのコンポーネントが並んでいるGUIです。それぞれのコンポーネントは中央にそろえて配置されています。しかし,文字の大きさが異なるため,整然と並んでいる感じを受けません。
中央でそろえるのがおかしいならば,上で合わせますか? それとも下で合わせますか?
こうした文字が表示されているコンポーネントを並べる場合には,さきほど説明したベースラインでそろえるのが最適です。
図2のGUIをベースラインでそろえたものを図3に示しました。どうですか? ずっとすっきりしたと思いませんか?
ここで示したように,Java SE 6ではベースラインで整列ができるようになりました。とはいうものの,java.awt.GridLayoutクラスのように,プリファードサイズを使用しないレイアウト・マネージャではベースラインを使用することはできません。
そこで,今回はjava.awt.FlowLayoutクラス,java.awt.GridBagLayoutクラス,javax.swing.SpringLayoutクラスでのベースライン整列について解説していきます。
FlowLayoutクラスの場合図2や図3で示したGUIはレイアウト・マネージャにFlowLayoutクラスを使用しています。FlowLayoutクラスでのベースラインによる整列はとても簡単。たった1行付け加えるだけです。
  サンプルのソースコード BaselineLayoutSample1.java
コンテナにレイアウト・マネージャを適用する部分を以下に示します。まずは通常の使い方です。
JPanel panel = new JPanel();
panel.setBorder(new TitledBorder("FlowLayout"));

// FlowLayout
FlowLayout layout = new FlowLayout();
panel.setLayout(layout);
これを実行したのが図2です。何もしなければ前述したように中央で整列します。
ベースラインで整列させるには次のように1行加えます。
JPanel panel = new JPanel();
panel.setBorder(new TitledBorder("FlowLayout"));

// FlowLayout
FlowLayout layout = new FlowLayout();

// ベースラインで整列
layout.setAlignOnBaseline(true);
panel.setLayout(layout);
これだけで,図3のようにベースラインで整列させることができます。
GridBagLayoutクラスの場合FlowLayoutクラスはちょっとしたレイアウトには重宝しますが,少し複雑なGUIになると太刀打ちできません。そうしたときによく使われるのが,GridBagLayoutクラスです。
GridBagLayoutクラスは,GridBagConstraintsクラスを用いてレイアウトの付随情報を表します。「ベースラインで整列する」という情報も,GridBagConstraintsクラスに記述します。
  サンプルのソースコード BaselineLayoutSample2.java
ベースラインで整列させるかどうかはGridBagConstraints.anchorに記述します。
GridBagConstraints constraints
    = new GridBagConstraints();
constraints.insets = new Insets(5, 5, 5, 5);

// ベースライン整列
constraints.anchor = GridBagConstraints.BASELINE;

JLabel label = new JLabel("<html><h1>GridLayout</h1></html>");
layout.setConstraints(label, constraints);
panel.add(label);
GridBagConstraints.BASELINEはJava SE 6で新たに定義された定数です。その他のコンポーネントもanchorをBASELINEにすることでベースラインで整列させることができます。
実行結果を図4に示しました。
anchorで使用できる定数として,BASELINE以外にABOVE_BASELINE,BELOW_BASELINEなどが定義されました。
図5は,ラベルをBASELINE,テキスト・フィールドをABOVE_BASELINE,ボタンをBELOW_BASELINEとしたときの実行結果です。
  
    [tr]        図4 GridBagLayoutでの実行結果      [/tr]      [tr]        図5 ベースラインを基準にしたレイアウト       [/tr]
  

SpringLayoutクラスの場合      [tr]        図6 SpringLayout      [/tr]
  

SpringLayoutクラスは,J2SE 1.4から導入された比較的新しいレイアウト・マネージャです。新しいということもあって,使用される頻度が低い気がします。この解説ではじめてSpringLayoutの存在を知る方も多いのではないでしょうか。
でも,結構便利なレイアウト・マネージャなんです。
SpringLayoutクラスは自分自身のコンポーネントとその周りのコンポーネントの位置関係を利用してレイアウトを行います。
SpringLayoutのおもしろいところは,位置関係をバネで表すところです。そう,SpringLayoutのSpringは文字通りバネのことです。
例えば,コンポーネントc1とコンポーネントc2を水平方向に並べるには,c1の右端(EAST)とc2の左端(WEST)を,スプリングで結び付けるための記述を行います(図6)。例えば,最短で5ピクセル,推奨は10ピクセル,最大に伸びたときに20ピクセルのスプリングで結び付けるには以下のように記述します。
springLayout.putConstraint(SpringLayout.EAST, c1,
                     Spring.constant(5,10, 20);
                     SpringLayout.WEST, c2);
ここで変数springLayoutは,SpringLayoutオブジェクトです。
このようにコンポーネント間をスプリングで結び付けていきます。ベースラインについても同じようにスプリングで表します。とはいうものの,ベースラインで整列させるのであれば,長さ0のスプリングで結び付けなければなりません。
  サンプルのソースコード BaselineLayoutSample3.java
サンプルでは,ラベルとテキスト・フィールド,ボタンの三つをベースラインで整列させています。
// ベースラインで整列
layout.putConstraint(SpringLayout.BASELINE, field,
                     Spring.constant(0),
                     SpringLayout.BASELINE, label);
layout.putConstraint(SpringLayout.BASELINE, field,
                     Spring.constant(0),
                     SpringLayout.BASELINE, button);
      [tr]        図7 SpringLayoutでの実行結果      [/tr]
  

Spring.constant(0)は長さ0のスプリングを生成します。
今週は既存の三つのレイアウト・マネージャでベースラインでの整列を行ってみました。
来週はJava SE 6で新たに導入されたGroupLayoutクラスについて解説します。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:21:15 | 显示全部楼层
新しいレイアウト・マネージャ


先週に引き続き,今週もコンポーネントのレイアウトに関する話題です。Java SE 6で新たに導入されたレイアウト・マネージャjavax.siwng.GroupLayoutクラスを紹介します。
GroupLayoutクラスは,もともとNetBeans 5.0で導入されたMatesseというGUIビルダーで使用されていたものです。NetBeansではGUIのデザインを直感的に行うことができ,複雑な配置でも容易に作成できます。
Matisseで導入されたGroupLayoutクラスは次のような特徴を持っています。
  
  • レイアウトの自由度が高い
  • コードの自動生成が容易
Java SE 6にGroupLayoutクラスが取り入れられたので,今後は他のGUIビルダーでもGroupLayoutクラスが採用されることが予想されます。
GroupLayoutクラスはツールで使われることが前提かもしれませんが,そこをあえて手で書いてみましょう。
GroupLayoutはコンポーネントを水平方向と垂直方向の両方からグループ化してレイアウトします。
      [tr]        図1 グループの構成      [/tr]
  

グループにはシーケンシャルなグループとパラレルなグループの2種類があります。シーケンシャルなグループは,グループの要素を順々に配置するグループです。一方のパラレルなグループは,センタリングやベースライン整列などを行います。
例えば,先週と同じようにラベル,テキスト・フィールド,ボタンが水平方向に並んだGUIを考えてみましょう。
まず水平方向です。
水平方向にはラベル,テキスト・フィールド,ボタンと順々に並べていくので,シーケンシャルなグループが一つあればOKです。
垂直方向で見ると,三つのコンポーネントは同じ位置になるので,これはパラレルな一つのグループとして扱うことができます。
言葉だけだとわかりにくいので,図で表してみます(図1)。図中の赤の実践が水平方向のシーケンシャル・グループ,青の点線が垂直方向のパラレル・グループです。しかし,図にしてもわかりにくいですね(笑)。
他の例も見るうちに,だんだんとわかるようになるので,今はこのままで進みましょう。
それでは,これを実際にサンプルで確かめてみます。
  
  サンプルのソースコード GroupLayoutSample1.java
  
GroupLayoutクラスで扱うグループはGroupLayoutクラスの内部クラスGroupで表します。シーケンシャル・グループはGroupクラスの派生クラスであるSequentialGroupクラスで表します。このSequentialGroupクラスもGroupLayoutクラスの内部クラスです。
同様にパラレル・グループはParallelGroupクラスで表します。こちらもGroupLayoutクラスの内部クラスです。
SequentialGroupクラスもParallelGroupクラスも直接オブジェクトを生成することはできません。その代わりGroupLayoutクラスのcreateSequentialGroupメソッド,createParallelGroupメソッドを使用します。
グループにコンポーネントを登録する場合はGroup#addComponentメソッドを使用します。ここでは使用しませんが,グループは入れ子にできます。その場合はGroup#addGroupメソッドを使用してグループを入れ子にします。
最終的に,グループをレイアウト・マネージャに登録するときにはGroupLayout#addHorizontalGroupメソッドとaddVerticalGroupメソッドを使用します。
まずはGroupLayoutクラスのオブジェクト生成部分です。
  // GroupLayout の生成
GroupLayout layout = new GroupLayout(panel);
panel.setLayout(layout);

// 自動的にコンポーネント間のすき間をあけるようにする
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
GroupLayoutオブジェクトはコンストラクタの引数にコンテナを指定します。
setAutoCreateGapsメソッドは,レイアウトするコンポーネント間に自動的にすき間を入れるように設定するためのメソッドです。同様にsetAutoCreateContainerメソッドは,コンテナとコンテナのエッジに最も近いコンポーネント間にすき間を自動的に入れるためのメソッドです。
どちらもデフォルトではすき間を入れないようになっています。
次に水平方向のグループです。
  // 水平方向のグループ
GroupLayout.SequentialGroup hGroup
    = layout.createSequentialGroup();

// グループにコンポーネントを追加
hGroup.addComponent(label)
      .addComponent(field)
      .addComponent(button);

// レイアウト・マネージャに登録
layout.setHorizontalGroup(hGroup);
はじめに,GroupLayout#createSequentialGroupメソッドでシーケンシャル・グループを生成します。
そして,生成したSequentialGroupオブジェクトにaddComponentメソッドを使用して,コンポーネントを追加していきます。addComponentメソッドの戻り値はSequentialGroupオブジェクトなので,そのまま続けて記述できます。
このようなところが,GUIビルダーで扱いやすくなっている部分です。つまり,コードを自動生成しやすいようにメソッドが設計されているのです。
最後にsetHorizontalGroupメソッドでグループをレイアウト・マネージャに登録します。
垂直方向も同じように記述します。
// 垂直方向のグループ
GroupLayout.ParallelGroup vGroup
    = layout.createParallelGroup();

// グループにコンポーネントを追加
vGroup.addComponent(label)
      .addComponent(field)
      .addComponent(button);

// レイアウト・マネージャに登録
layout.setVerticalGroup(vGroup);
      [tr]        図2 実行結果      [/tr]
  

垂直方向はパラレル・グループなので,GroupLayout#createParallelGroupメソッドを使用してParallelGroupオブジェクトを生成します。
後は水平方向と同じです。
ソースコードができあがったので,実行してみましょう(図2)。意図したようにレイアウトができました。ParallelGourpクラスは,どうやらデフォルトではセンターで整列するようです。
とはいうものの,先週解説したように,文字を表示するコンポーネントはベースラインでそろえるほうが整然と見えます。
このサンプルもベースラインで整列するように改造してみましょう。
  
  サンプルのソースコード GroupLayoutSample2.java
  
ParallelGroupクラスで,ベースラインで整列させるためにはGroupLayoutクラスのcreateParallelGroupメソッドの引数にGroupLayout.Alignment.BASELINEを指定します。
  // 垂直方向のグループ
GroupLayout.ParallelGroup vGroup
    = layout.createParallelGroup(
            GroupLayout.Alignment.BASELINE);

// グループにコンポーネントを追加
vGroup.addComponent(label)
      .addComponent(field)
      .addComponent(button);

// レイアウト・マネージャに登録
layout.setVerticalGroup(vGroup);
      [tr]        図3 ベースラインでの整列      [/tr]
  

これで実行した結果が図3です。ちゃんとベースラインで整列されていることがわかります。
BASELINE以外にはCENTER,LEADING,TRAILINGが定義されています。BASELINEは水平方向専用ですが,その他の定数は垂直方向の整列にも用いることができます。
次に,もう少し複雑な例を取り上げてみましょう。
名字と名前を別々に入力するGUIです。二つのラベル,二つのテキスト・フィールドから構成されています。
これをグループで構成してみます(図4)。
  
    [tr]        図4 グループに分割      [/tr]
  

まず水平方向(赤線)です。水平方向は全体をシーケンシャル・グループで構成します。シーケンシャル・グループの中にはラベル二つで構成するパラレル・グループと,テキスト・フィールド二つで構成するパラレル・グループを含みます。
垂直方向(青線)も全体はシーケンシャル・グループです。その中に,名字のラベルとテキスト・フィールドを含むパラレル・グループと,名前のラベルとテキスト・フィールドを含むパラレル・グループを含みます。
これをコードで表してみましょう。
  
  サンプルのソースコード GroupLayoutSample3.java
  
グループ分けができれば,コードにするのは簡単です。
  // 水平方向のグループ
GroupLayout.SequentialGroup hGroup
    = layout.createSequentialGroup();

// ラベルを含むパラレル・グループを追加
hGroup.addGroup(layout.createParallelGroup()
                .addComponent(lastNameLabel)
                .addComponent(firstNameLabel));

// テキスト・フィールドを含むパラレル・グループを追加
hGroup.addGroup(layout.createParallelGroup()
                .addComponent(lastNameField)
                .addComponent(firstNameField));

layout.setHorizontalGroup(hGroup);

// 垂直方向のグループ
GroupLayout.SequentialGroup vGroup
    = layout.createSequentialGroup();

// 名字のラベル,テキスト・フィールドを含む
// パラレル・グループを追加
vGroup.addGroup(layout.createParallelGroup(
                        GroupLayout.Alignment.BASELINE)
                .addComponent(lastNameLabel)
                .addComponent(lastNameField));

// 名前のラベル,テキスト・フィールドを含む
// パラレル・グループを追加
vGroup.addGroup(layout.createParallelGroup(
                        GroupLayout.Alignment.BASELINE)
                .addComponent(firstNameLabel)
                .addComponent(firstNameField));

layout.setVerticalGroup(vGroup);
      [tr]        図5 実行結果      [/tr]
  

グループにグループを追加するにはGroup#addGroupメソッドを使用します。それさえわかれば,後は前のサンプルとたいして違いません。
実行結果を図5に示します。
最後にもうちょっと複雑な例を示しましょう。名字と名前の下に年齢を示すコンボボックス,そして性別を表すラジオボタンを配置してみます。ラジオボタンは横にテキストを表示できますが,上部に表示させたかったので,わざとラベルを使ってみました(図6)。
  
    [tr]        図6 グループ構成      [/tr]
  

グループ化のポイントは,テキスト・フィールドと同じグループに,ラジオボタンなどを含んだグループを含ませているところです。ラジオボタンを含むグループはシーケンシャル・グループで,テキスト・フィールドと同じ幅の中に二つのパラレル・グループを含めています。
このとき,二つのパラレル・グループがくっつき過ぎないように,グループの間にギャップを入れてあります。
同様に,コンボボックスの後ろにもギャップを入れて,テキスト・フィールドとは長さが異なるようにしてあります。
  
  サンプルのソースコード GroupLayoutSample4.java
  
垂直方向は単純なので,水平方向のソースコードだけを示します。
  GroupLayout.SequentialGroup hGroup
    = layout.createSequentialGroup();

hGroup.addGroup(layout.createParallelGroup()
        .addComponent(lastNameLabel)
        .addComponent(firstNameLabel)
        .addComponent(ageLabel)
        .addComponent(sexLabel));

hGroup.addGroup(layout.createParallelGroup(
                    GroupLayout.Alignment.LEADING)
        .addComponent(lastNameField)
        .addComponent(firstNameField)
        .addGroup(layout.createSequentialGroup()
              .addComponent(ageComboBox)
              .addGap(10, 20, 40))
        .addGroup(layout.createSequentialGroup()
              .addGap(5, 10, 20)
              .addGroup(layout.createParallelGroup(
                      GroupLayout.Alignment.CENTER)
                  .addComponent(maleLabel)
                  .addComponent(maleButton))
              .addGap(5, 10, 20)
              .addGroup(layout.createParallelGroup(
                      GroupLayout.Alignment.CENTER)
                  .addComponent(femaleLabel)
                  .addComponent(femaleButton))
              .addGap(5, 10, 20)));

layout.setHorizontalGroup(hGroup);
      [tr]        図7 実行結果      [/tr]
  

ギャップを追加するにはGroupLayout#addGapメソッドを使用します。引数は最小,推奨,最大の長さになります。
コードが複雑に見えるかもしれませんが,順に追っていけば容易に理解できるはずです。
実行結果を図7に示しました。
ここまでできれば,たいていのことはできるはずです。
例えば,このレイアウトにGridBagLayoutクラスを使用すれば,コードはもっと複雑になるはずです。
簡単で,しかも柔軟なレイアウトができるのがGroupLayoutクラスの特徴なのです。
ところで,GroupLayoutクラスはNetBeansのMatisseで使われていると上で述べました。実際にこのサンプルと同じGUIを作ってみましょう。
NetBeansでの作業を図8,図9に示しました。図9は図8を拡大したものです。これを見てもわかるように,グラフィカルにGUIを組み立てることができます。
ありがたいのは,ベースラインで整列や右詰めがとても楽にできることです。図9にベースラインの補助線が表示されていますが,自動的にあのような補助線が表示され,そこにコンポーネントが吸いつくような感じになります。
完成したものが図10です。
  
    [tr]        図8 NetBenasのGUIビルダー      [/tr]      [tr]        図9 NetBenasのGUIビルダー(拡大)      [/tr]      [tr]        図10 完成したGUI      [/tr]
  

それでは,NetBeansが自動生成したコードも見てみましょう。以下に水平方向のコードを示します。
  layout.setHorizontalGroup(
    layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
    .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
            .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel1)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 105, javax.swing.GroupLayout.PREFERRED_SIZE))
            .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel2)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField2))
            .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel3)
                    .addComponent(jLabel4))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, 71, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addGroup(layout.createSequentialGroup()
                        .addGap(10, 10, 10)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(jLabel5)
                            .addComponent(jRadioButton1))
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(jRadioButton2)
                            .addComponent(jLabel6))))))
        .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
自動生成したコードなので,見づらいかもしれません。しかし,ほとんど手書きで書いたものと同じ構成になっていることがわかるはずです。違いといえば,ギャップを省略せずに記述してあることぐらいです。
GroupLayoutクラスはGUIビルダーで使用することが前提かもしれません。しかし,柔軟なレイアウトを行うことができる数少ないレイアウト・マネージャなのですから,どんどん使ってみましょう。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:22:01 | 显示全部楼层
JTableクラスでのソーティング


JTableクラスはJava SEのバージョンが上がるたびに機能拡張されています。例えば,J2SE 5.0では印刷のサポート,不連続セルの選択などの機能拡張が行われています。
Java SE 6でも以下の2点の機能が拡張されました。
  
  • カラムのセルのソーティング
  • カラムのセルのフィルタリング
そこで,今週はソーティング,来週はフィルタリングについて解説します。
J2SE 5.0でも,がんばればJTableでソーティングをすることは可能でした。例えば,Java Swing HacksのHack #22では「JTableにソートを実行させる」と題して,ソートを扱っています。
とはいうものの,デフォルトでソーティングができるのは大歓迎です。しかも,単純なソートであれば,あっという間です。
  
  サンプルのソースコード TableSortingSample1.java
すべてのカラムをソートするには,以下のように1行加えるだけです。
      TableModel model = new DefaultTableModel(data, columnNames);
JTable table = new JTable(model);

// テーブルをソート可にする
table.setAutoCreateRowSorter(true);  
これだけで,ソーティングが可能です。簡単すぎて拍子抜けしますね。
とりあえず実行してみましょう。
図1がデフォルトの表示です。ここで,ヘッダの[Title]をクリックしてみます。するとタイトルが昇順でソートされ,ヘッダには昇順を示す三角が描画されました(図2)。もう1回クリックすると,今度は降順でソートされます(図3)。他のカラムでも同じようにソートできます。
残念なことに,デフォルトの並び順に戻すことはできないようです。
  
    [tr]        図1 デフォルト画面      [/tr]      [tr]        図2 昇順でソート      [/tr]      [tr]        図3 降順でソート      [/tr]
  

気をつけなくてはならないのが,表示はソートされていても,TableModelオブジェクトは変更されていないということです。
例えば,図2では2行目にCoffee & Cigarettesが表示されていますが,TableModelオブジェクトではCoffee & Cigarettesはあいかわらず7行目として扱われます。
このために,表示の行からテーブル・モデルでの行への変換も用意されています。これもサンプルで確かめてみましょう。
  
  サンプルのソースコード TableSortingSample2.java
  
サンプルでは,セルをクリックすると表示上の行と,テーブル・モデルでの行を表示します。
      TableModel model = new DefaultTableModel(data, columnNames);
JTable table = new JTable(model);
table.setAutoCreateRowSorter(true);

table.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent event) {
        JTable table = (JTable)event.getSource();
        int[] selection = table.getSelectedRows();
        for (int i = 0; i < selection.length; i++) {
                       
            // テーブル・モデルの行数に変換
            int modelRow = table.convertRowIndexToModel(selection);
                               
            System.out.println("View Row: " + selection
                               + " Model Row: " + modelRow);
        }
    }});  
表示上の行から,テーブル・モデルでの行に変換するのがJTableクラスのconvertRowIndexToModelメソッドです。
これとは逆に,テーブル・モデルの行から,表示上の行に変換するためにconvertRowIndexToViewメソッドも用意されています。
さて,サンプルを実行して,図2の状態でCoffee & Cigarettesをクリックしてみました。
      C:\temp>java TableSortingSample2
       
View Row: 1 Model Row: 6  
JTableクラスは0行からはじまるので,1と6になりましたが,ちゃんと変換が行われていることがわかります。
  javax.swing.table.TableRowSorterクラス今までのサンプルはJTableクラスが勝手にソートしてくれていました。
ところで,自動でソートするためのメソッドであるsetAutoCreateRowSorterメソッドの名前が気になりませんか。単にソートするのであればsetAutoSortとかにすればいいと思うのですが,この長ったらしい名前になったのはなぜでしょう。
その理由はソートを行うのがRowSorterクラスだということです。setAutoCreateRowSorterメソッドはRowSorterオブジェクトの自動的生成を設定するメソッドだったのです。
自動で生成してくれるのはいいのですが,自分でソートをコントロールしたい場合には,TableRowSorterオブジェクトの生成を自分で記述する必要があります。
  
  サンプルのソースコード TableSortingSample3.java
  
TableRowSorterオブジェクトの生成部分を示します。
      TableRowSorter<TableModel> sorter
    = new TableRowSorter<TableModel>(model);

JTable table = new JTable(model);
table.setRowSorter(sorter);  
TableRowSorterクラスはジェネリクスでパラメータ化されています。定義ではTableModelSorter<M extends TableModel>となっており,自作したテーブル・モデルでも使用できるようになっています。
TableRowSorterクラスのコンストラクタはM型のオブジェクトを取ります。後は,JTableクラスのsetRowSorterメソッドで指定するだけです。
この記述だけだとsetAutoCreateRowSorterメソッドで指定したのと変わらないので,少しカスタマイズしてみましょう。
まずはソートできないカラムを作ってみます。
      // Main Performerはソート不可にする
sorter.setSortable(2, false);  
setSortableメソッドは第1引数がカラム数,第2引数でソートの可/不可を決めます。
これで実行すると,[Main Performer]のカラムがソートできなくなります。
次に,ソートしたときのイベントを扱ってみましょう。ソート時に発生するイベントはjavax.swing.event.RowSorterEventクラスで,リスナーはjavax.swing.event.RowSorterListenerインタフェースです。
      // ソートされたときに発生するイベント処理
sorter.addRowSorterListener(new RowSorterListener() {
    public void sorterChanged(RowSorterEvent event) {
        System.out.println("Type: " + event.getType());
    }
});  
RowSorterEventクラスのgetTypeメソッドが返すのは列挙型のRowSorterEvent.Typeです。RowSorterEvent.TypeにはSORT_ORDER_CHANGEDとSORTEDが定義されています。SORT_ORDER_CHANGEDはソートの順序(昇順,降順)が変化したことを示し,SORTEDはソートが行われたことを示しています。
TableRowSorterクラスの動作TableRowSorterクラスはソートにComparatorインタフェースを使用します。適切なComparatorオブジェクトを選択しないと,ソートのパフォーマンスが大幅に変化します。
そのためにも,Comparatorオブジェクトの選択の手順を理解しましょう。
Comparatorオブジェクトの手順は次のようになります。
  
  • TableRowSorter#setComparatorメソッドでComparatorオブジェクトが指定されていれば,それを使用する
  • TableModelクラスのgetColumnClassメソッドの戻り値がStringクラスであれば,java.text.CollatorオブジェクトをCollator.getInstanceメソッドで取得して使用する
  • TableModel#getColumnClassメソッドの戻り値がComparableなクラスであれば,Comparable#compareToメソッドをコールするだけのComparatorクラスを使用する
  • TableModel#getColumnClassメソッドの戻り値がそれ以外の場合,TableRowSorter#setStringConverterメソッドでTableStringConverterオブジェクトが指定されていれば,それを使用してモデルの当該オブジェクトをStringオブジェクトに変換し,Collatorクラスを使用してソートを行う
  • TableStringConverterオブジェクトも設定されていない場合,セルの当該オブジェクトをtoStringメソッドでStringオブジェクトに変換し,Collatorクラスを使用してソートを行う
ちょっと複雑ですね。
基本的にはTableRowSorter#getComparatorでComparatorオブジェクトを指定しておくか,TableModel#getColumnClassメソッドでComparableなクラスを返すようにします。
例えば,今まで使っていたサンプルは[Year]カラムはIntegerクラスですが,DefaultTableModelクラスをそのまま使っているのでgetColumnClassメソッドはOjectクラスが返ってしまいます。このため,Integerオブジェクトに対してtoStringメソッドをコールし,文字列にしてから比較を行っています。これは明らかに無駄な処理です。
そこで,getColumnClassメソッドをオーバーライドしてみましょう。
      TableModel model = new DefaultTableModel(data, columnNames) {
    public Class getColumnClass(int column) {
        if (column == 1) {
            // カラム1ならInteger
            return Integer.class;
        } else {
            // その他はString
            return String.class;
        }
    }
};  
このサンプルは列数が少ないので,ソートはあっという間に終わってしまい,効果はわかりません。しかし,列数が多ければ多いほど,パフォーマンスの違いが明確になります。
使うにはちょっとくせがありますが,簡単にソートできるのはとても便利です。Java Swing Haksには「実際のところ,SwingのAPIが大規模であることを考えると,この機能をSwingがまだ提供していないのは一種の驚きです」と記述されています。
待ちに待っていた機能がやっと取り入れられたという感じですね。
さて,来週はJTableクラスのもう一つの拡張機能であるフィルタリングを取り上げます。
"Java Swing Hacks" Joshua Marinacci, Chris Adamson 著, 神戸博之, 島田秋雄 監訳, 加藤慶彦 訳, O'Reilly ISBN4-87311-278-8
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:22:56 | 显示全部楼层
JTableクラスでのフィルタリング


先週に引き続き,JTableクラスの機能拡張を取り上げます。今週はフィルタリングです。
フィルタリングといっても,使うのは先週のソーティングと同じjavax.swing.table.TableRowSorterクラスです。
実際にフィルタリングを行うのはjavax.swing.RowFilterクラスです。TableRowSorterオブジェクトにsetRowFilterメソッドで指定します。
RowFilterクラスはabstractクラスで,それ自身をインスタンス化することはできません。その代わり,用途に応じたstaticなメソッドが定義されているので,それを使用します。
表1 Dialog.ModalityType
  メソッド名  説明  regexFilter(String regx, int... indices)  正規表現を使用したフィルタリングを行うRowFilterオブジェクトを返す  numberFilter(RowFilter.ComparisonType type, Number number, int... indices)  数値によるフィルタリングを行うRowFilterオブジェクトを返す  dateFilter(RowFilter.ComparisonType type, Date date, int... indices)  日付によるフィルタリングを行うRowFilterオブジェクトを返す  andFilter(Iterable filters)  複数のRowFilterオブジェクトのANDを取るRowFilterオブジェクトを返す  orFilter(Iterable filters)  複数のRowFilterオブジェクトのORを取るRowFilterオブジェクトを返す  norFilter(RowFilter filter)  指定されたRowFilterオブジェクトのNOTを取るRowFilterオブジェクトを返す

regexFilterメソッド,numberFilterメソッド,dateFilterメソッドの最後の引数であるindicesには,フィルタリングを適用するカラムのインデックスを指定します。複数のカラムを指定することもできますし,何も指定しないことも可能です(可変長引数なので省略できます)。何も指定しないと,すべてのカラムがフィルタリングの対象となります。
まずは数字でフィルタリングしてみましょう。
  
  サンプルのソースコード TableFilteringSample1.java
  
TableFilterlingSample1クラスはフィルタリング以外の部分は先週使用したサンプルと同一です。フィルタリングの設定をする部分を以下に示します。
TableRowSortersorter
    = new TableRowSorter[table](model);

// 2003年以降を表示
sorter.setRowFilter(
    RowFilter.numberFilter(RowFilter.ComparisonType.AFTER,
                           2003, 1));
第2引数が閾値となる数値,第1引数で閾値以上であることを指定します。RowFilter.ComparisonTypeは列挙型で,AFTER以外にBEFOREとEQUALE,NOT_EQUALが定義されています。
第3引数がカラム数を示しています。
さて,これで実行してみましょう。ソートはヘッダをクリックしないとソートされませんでしたが,フィルタリングはデフォルトの表示でも行われます。
  
    [table]                  図1 数値によるフィルタリング        

正しくフィルタリングできたことがおわかりでしょうか。2003年以降で指定しましたが,これだと2003年は含まれないようです。
もう少し複雑なフィルタリングの条件をつけてみましょう。2000年から2004年までに公開したものを表示するようにしてみます。
[size=0.9em]// 2000年から2004年までを表示
List<RowFilter<Object, Object>> filters
  = new ArrayList<RowFilter<Object, Object>>();
filters.add(RowFilter.numberFilter(RowFilter.ComparisonType.AFTER, 2000, 1));
filters.add(RowFilter.numberFilter(RowFilter.ComparisonType.BEFORE, 2004, 1));

sorter.setRowFilter(RowFilter.andFilter(filters));
このような複合条件を扱うためにandFilterメソッドやorFilterメソッドがあります。ここでは,andFilterメソッドを使用しました。
andFilterメソッドの引数はIterableインタフェースなので,ArrayListクラスを使用しました。
さて,これで実行してみましょう。
  
    [tr]図2 数値によるフィルタリング      [/tr]
  

2000<Year<2004なので,2003年の映画が表示されました。
次は,正規表現でフィルタリングするサンプルです。
  
  サンプルのソースコード TableFilteringSample2.java
  
正規表現を使用する場合はRowFilter.regexFilterメソッドを使用します。
// 題名の頭文字がAからMに入るもの
sorter.setRowFilter(
    RowFilter.regexFilter("[a-mA-M].*", 0));
はじめの文字がaからm,もしくはAからMの題名を持つ映画を抽出します。ところが,このままだとうまくいきません。
  
    [tr]図3 正規表現によるフィルタリング(誤用)      [/tr]
  

調べてみると,java.util.regex.Matcherクラスのfindメソッドを使用してマッチングしているようです。findクラスは文字列の先頭からマッチする部分を検索するので,"[a-mA-M].*"という正規表現は,文字列中にaからm,もしくはAからMの文字が使われていればマッチしてしまいます。
そこで,先頭を示すために次のように表記します。
// 題名の頭文字がAからMに入るもの
sorter.setRowFilter(
    RowFilter.regexFilter("^[a-mA-M]", 0));
正規表現で"^"は先頭を表します。これならばうまくいくはずです。
  
    [tr]図4 正規表現によるフィルタリング       [/tr]
  

このサンプルではフィルタははじめから定義してありますが,フィルタを動的に差し替えることもできます。
  
  サンプルのソースコード TableFilteringSample3.java
  
TableFilteringSample3クラスでは,テキスト・フィールドに正規表現を入力して,フィルタリングを実行できます。
TableRowFilter#setRowFilterメソッドは,コールされるとすぐに適用されるので,動的にフィルタの入れ替えが可能です。フィルタリングを行わないようにするにはsetRowFilterメソッドの引数をnullにします。
TableFilteringSample3では,ボタンのイベント処理でこれを行っています。
  JButton filterButton = new JButton("Filter");
filterButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        // フィルタの変更
        sorter.setRowFilter(RowFilter.regexFilter(regex));
    }
});

JButton resetButton = new JButton("Reset");
resetButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        // nullを代入することでフィルタリングを
        // 行わないようにする
        sorter.setRowFilter(null);
    }
});
RowFilter.regexFilterメソッドの引数でカラム番号を指定していないので,すべてのカラムに対してフィルタリングを行います。
実行結果を図5,図6に示しました。図5が初期画面,図6がフィルタリング後の画面です。
  
    [tr]図5 動的なフィルタリング(フィルタリング前)      [/tr]      [tr]図6 動的なフィルタリング(フィルタリング後)      [/tr]
  

今までは,標準で定義されていたフィルタを使用してきましたが,フィルタを自作することも可能です。試しに正規表現でフィルタリングするフィルタを作ってみましょう。
  
  サンプルのソースコード TableFilteringSample4.java
  
フィルタを自作する場合,RowFilterクラスのincludeメソッドをオーバーロードします。
sorter.setRowFilter(new RowFilter<Object, Object>() {
    private Pattern pattern = Pattern.compile(regex);

    @Override
    public boolean include(
        RowFilter.Entry<? extends Object,
                        ? extends Object> entry) {
        for (int i = 0; i < entry.getValueCount(); i++) {
            Object value = entry.getValue(i);
            String valueText;
            if (value instanceof String) {
                valueText = (String)value;
            } else {
                valueText = value.toString();
            }
            
            Matcher matcher = pattern.matcher(valueText);
            if (matcher.find()) {
                return true;
            }
        }

        return false;
    }
});
includeメソッドの引数はRowFilter.Entryオブジェクトです。RowFilter.Entryはフィルタリングの対象となる複数のカラムの値を保持しています。
保持している値の個数はRowFilter.Entry#getValueCountメソッドで取得でき,値はgetValueメソッドで取得できます。
値が取得できれば,後は正規表現でマッチさせるだけです。includeメソッドの戻り値は,booleanでマッチする場合はtrue,しない場合はfalseになります。
いかがでしょうか。フィルタリングも簡単に適用できることがおわかりになったと思います。フィルタを自作するのも,それほど難しいことではありません。
先週のソーティングと同様にフィルタリングもぜひ活用してください。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:23:37 | 显示全部楼层
タブにコンポーネントを貼る


多くの情報を切り替えて参照/操作するとき,タブはとても便利です。最近,Internet Explorer 7(IE 7)がタブを用いたGUIを採用したため,ほとんどのブラウザがタブ対応になりました。
このようにタブを用いたGUIはよく使われています。JavaでもJTabbedPaneクラスを使用すれば,タブを用いたGUIを作成できます。
しかし,何かが足りません。
FirefoxでもIE 7でもいいので,タブのところを観察してみてください(図1)。
  
    [tr]        図1 クローズ・ボタンのあるタブ      [/tr]

        
  

そう,クローズ・ボタンが付いているのです。Eclipseが採用しているSWTだとタブにボタンを付けることができるのですが,従来のSwingでは無理でした。
ここで,Java SE 6の登場です。Java SE 6ではJTabbedPaneクラスのタブにSwingのコンポーネントを貼ることができるようになったのです。これを利用すれば,タブのクローズ・ボタンを実現できます。
まずは簡単なサンプルで,タブにコンポーネントを貼ってみましょう。
  
  サンプルのソースコード TabbedPaneSample1.java
10個のタブを生成し,そこにJLabelオブジェクトを貼るサンプルです。
pane = new JTabbedPane();

for (int i = 0; i < 10; i++) {
    JLabel label = new JLabel("Tab " + i);
    pane.addTab(null, label);

    // タブに貼るコンポーネントを生成
    JLabel tabLabel
        = new JLabel("tab " + i,
                     new ImageIcon("T"+(i+1)+".gif"),
                     JLabel.LEADING);

    // タブに貼るコンポーネントを設定
    pane.setTabComponentAt(i, tabLabel);
}
今までのJTabbedPaneクラスの使い方では,addTabメソッドの第1引数にタブに表示する文字列を指定していました。しかし,タブにコンポーネントを貼るときには,第1引数をnullにしておきます。
次に,setTabComponentAtメソッドを使用して,タブに貼るコンポーネントを設定します。第1引数がタブのインデックス,第2引数がタブに貼るコンポーネントです。
addTabメソッドでタブに表示する文字列もしくはアイコンを指定していたとしても,setTabComponentAtメソッドで設定したコンポーネントが優先されます。
このサンプルを実行した結果が図2です。
ここではJLabelクラスを使いましたが,Swingのコンポーネントであれば何でも貼ることができます。とはいっても,JTableクラスやJTreeクラスを貼るのはほとんど意味がありませんが。
  
    [tr]        図2 タブにラベルを貼る      [/tr]
  

では,クローズ・ボタンのあるタブのサンプルを作ってみましょう。
  
  サンプルのソースコード TabbedPaneSample2.java
  
タブに貼るのはJPanelオブジェクトで,そこにラベルとクローズ・ボタンを配置します。
ボタンといっても,ここではJLabelクラスを使用しています。JButtonクラスでもイメージを使用できるのですが,イメージのサイズに比べてボタンのサイズが大きくなってしまいます。もちろん,setPreferredSizeメソッドでサイズを調整すればいいのですが,少し面倒です。そこで,マウス・イベントが扱えてサイズの調整が不要なJLabelクラスをここでは使用しました。
// ラベルとボタンを貼るパネル
JPanel panel = new JPanel();
panel.setOpaque(false);
panel.setLayout(new BorderLayout(5, 5));

// ラベルの生成
// アイコンはデフォルトで用意されているものを使用
JLabel label = new JLabel(title,
                    UIManager.getIcon("FileView.fileIcon"),
                    JLabel.LEFT);
panel.add(label, BorderLayout.CENTER);

// タブのクローズを行うボタン
// マウスイベントが使用できればいいので,
// ここではラベルを使用
ImageIcon icon = new ImageIcon("close.gif");
JLabel closeLabel = new JLabel(icon);
panel.add(closeLabel, BorderLayout.EAST);

// タブにパネルを設定
pane.setTabComponentAt(index, panel);
ラベルは,Swingで標準的に使用できるアイコンを使用しました。UIManagerクラスのgetIconメソッドを使えば,標準で提供されているアイコンを取得できます。
FileView.fileIconはファイルを表すときに使われるアイコンであり,白い四角形の右上が少し折れたイメージになっています。
このアイコン以外にも,ディレクトリを表すFileView.directoryIconなどを使用できます。
クローズ・ボタンには赤の×印のイメージを用意しました。これをパネルの右側に配置します。あとは最後の行で,このパネルをタブに設定しています。
さて,肝心なのはクローズ・ボタンをクリックされたときのイベント処理です。
// クローズ・ボタンが押されたときの処理
closeLabel.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent event) {
        // ボタンが押されたタブを選択
        Component tabComp
            = ((Component)event.getSource()).getParent();
        int index = pane.indexOfTabComponent(tabComp);

        // タブを削除
        pane.remove(index);
    }
});
クローズ・ボタンはイベントのgetSourceメソッドで得られるので,クローズ・ボタンが貼られているパネルは,getParentメソッドで取得できます。
タブに貼られたコンポーネントのインデックスを取得するにはJTabbedPaneクラスのindexOfTabComponentメソッドを使用します。インデックスが取得できれば,そのインデックスのタブを削除します。
実行してクローズ・ボタンを押してみてください。ちゃんとタブが削除されるはずです(図3)。
クローズ・ボタン以外にも,タブにフォーカスされたらアイコンを変化させるなどの応用が可能です。また,タブに対するイベント処理が可能になったため,Eclipseなどのように,タブをダブルクリックしたら表示領域が大きくなるといった動作も実現できます。
アプリケーションのユーザビリティを高めるためにも,いろいろな応用を考えてみてはいかがでしょうか。
            [tr]        図3 クローズ・ボタンつきのタブ[/tr]
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:24:44 | 显示全部楼层
テキスト・コンポーネントの印刷


[tr]        図1 TextCompPrintSample1      [/tr]
Javaで実装するのが面倒なものの一つに印刷があります。
慣れてしまえばどうということはないのですが,印刷に特有の知識が必要になるので,なかなか手が出ません。
そこで,印刷の知識がなくても簡単に印刷を実現できる機能がJ2SE 5.0から徐々に取り入れられてきています。J2SE 5.0ではJTableクラスに簡易印刷機能が導入されました。
Java SE 6では,JTextAreaクラスやJEditorPaneクラスなど文字列を扱うテキスト・コンポーネントに簡易印刷機能が導入されました。
今週はこの簡易印刷機能を試してみましょう。
簡易印刷機能は,JEditorPaneクラスなどの親クラスであるjavax.swing.text.JTextComponentクラスに導入されました。したがって,この機能を使用できるのは,JTextFieldクラス,JTextAreaクラス,JEditorPaneクラスの三つのクラスです。
使い方はJTableクラスの簡易印刷機能とほとんど同じです。
  
  サンプルのソースコード TextCompPrintSample1.java
  
このサンプルはJEditorPaneクラスを使用して,自分自身のソースコードを表示します(図1)。フレームの一番下には印刷ボタンがあり,クリックすると印刷を行います。
コンストラクタを以下に示します。
public TextCompPrintSample1()
            throws MalformedURLException, IOException {
    frame = new JFrame("Text Component Print Sample");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    File file = new File("TextCompPrintSample1.java");
    URL url = file.toURI().toURL();
    editorPane = new JEditorPane(url);

    frame.add(new JScrollPane(editorPane));

    JButton printButton = new JButton("印刷");
    printButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent event) {
            try {
                // テキストコンポーネントの印刷
                editorPane.print();

            } catch (PrinterException ex) {
                JOptionPane.showMessageDialog(
                    frame,
                    "印刷に失敗しました",
                    "印刷失敗",
                    JOptionPane.WARNING_MESSAGE);
            }
        }
    });
    frame.add(printButton, BorderLayout.SOUTH);

    frame.setSize(600, 700);
    frame.setVisible(true);
}
      [tr]        図2 実行結果(PDF)      [/tr]

JEditorPaneクラスの引数にドキュメントのURLを与えれば,自動的にドキュメントをロードしてくれます。この機能を使用してローカル・ファイルを読み込むこともできます。そのために,FileオブジェクトからURLオブジェクトを生成しています。URLオブジェクトに直接変換できないため,間にURIオブジェクトを挟んでいます。
JEditorPaneオブジェクトに表示された文字列の印刷は,printButtonのイベント処理で行います。印刷はとても簡単。赤字で示しているように,単にprintメソッドをコールするだけです。
印刷に失敗したら,ダイアログを表示します。
実行してPDFを作成してみたのが図2です。何の飾りもない,素っ気ない出力になりました。
これではちょっと寂しいので,ヘッダーとフッターを付けてみましょう。
  
  サンプルのソースコード TextCompPrintSample2.java
  
印刷処理以外の部分はTextCompPrintSample1クラスとほとんど変わらないので,印刷処理の部分だけを示します。
printButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        try {
            // テキスト・コンポーネントの印刷
            // ヘッダー,フッター付き
            editorPane.print(
                new MessageFormat(file.getName()),
                new MessageFormat("Page {0}"));
        } catch (PrinterException ex) {
            JOptionPane.showMessageDialog(
                frame,
                "印刷に失敗しました",
                "印刷失敗",
                JOptionPane.WARNING_MESSAGE);
        }
    }
});
      [tr]        図3 実行結果(PDF)      [/tr]

TextCompPrintSample1クラスでは引数のないprintメソッドをコールしていましたが,ここでは引数があります。
第1引数がヘッダー,第2引数がフッターです。両方とも型はjava.text.MessageFormatクラスです。
MessageFormatクラスを使っているのは,ページ数を埋め込むためです。このコードではフッターにページ数が印刷されるようにしてみました。{0}の部分がページ数に置き換えられます。
印刷してみた結果が図3です。
若干ヘッダーのフォントサイズが大きいような気もしますが…。
残念ながら,ヘッダーやフッターのフォント・サイズや表示位置は固定です。
printメソッドにはオーバロードされたメソッドがもう一つあります。このメソッドでは,印刷部数や紙の大きさ,向きなどを指定できます。
とはいうものの,この印刷機能はあくまでも簡易的なものです。ヘッダーやフォント・サイズを変えられませんし,総ページ数を印刷することもできません。また,他のコンポーネントを一緒に印刷することもできません。
このような印刷を行うのであれば,従来通りの印刷処理を記述しなければなりません。
だからといって,簡易印刷機能が役に立たないというわけではありません。
今までは,テキスト・ファイルの内容を印刷だけでも,フレーバーや属性などの設定を行わなければなりませんでした。JEditorPaneクラスを使えば,フレーバーを全く気にすることなく,あっという間に印刷処理を作れます。
とりあえず印刷するだけ,ということであれば,簡易印刷機能で十分な場面は多いのではないでしょうか。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:25:28 | 显示全部楼层
Swingでマルチスレッド - SwingWorker その1



今週は簡単なパズルから始めてみましょう。
  サンプルのソースコード Monologue.java
ちょっと長いですが,全文を次に示します(インポート文は省略しています)。
public class Monologue {
    private JButton button;

    public Monologue() {
        JFrame frame = new JFrame("Monologue");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setLayout(new FlowLayout(FlowLayout.CENTER));

        button = new JButton("実行");
        button.setPreferredSize(new Dimension(100, 26));

        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                // 処理中であることを示すため
                // ボタンの文字列を変更し,使用不可にする
                button.setText("処理中...");
                button.setEnabled(false);
                     
                // ながーい処理
                try {
                    TimeUnit.SECONDS.sleep(10L);
                } catch (InterruptedException ex) {}
                 
                // 処理が終了したので,文字列を元に戻し
                // ボタンを使用可能にする
                button.setText("実行");
                button.setEnabled(true);
            }
        });

        frame.add(button);
        frame.setSize(100, 70);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        new Monologue();
    }
}
このプログラムを実行すると,フレームに「実行」と書かれたボタンが表示されます。
問題はこのボタンがクリックされた直後の状態はどうなるかということです。
クリックされたときには,イベント処理としてボタンの文字列を変更し,ボタンを使用不可にします。その後,何らかの処理を行うのですが,ここでは単純化のため単にスリープさせています。
スリープから目覚めると,ボタンの文字列を元に戻し,使用可能状態に戻します。
選択肢は図1の六つです。
(1)は「ボタンをクリックしても何も表示が変わらない」,(2)は「クリックされたままの状態になる」,(3)は「文字列が変化せずに使用不可状態になる」というものです。(4),(5),(6)は文字列が「処理中」になる以外は(1),(2),(3)と変わりません。
単純に考えれば,文字列を変更して,使用不可にしているので(6)になるはずですが,それだとパズルになりませんね。
さて,どれが正解でしょうか。
  
    [tr]        図1 パズルの選択肢      [/tr]
(1)(2)(3)
(4)(5)(6)
  

この問題は,サン・マイクロシステムズのJavaエバンジェリストグループが主体になって毎月開催しているセミナー「今月の2時間で学ぶJava Hot Topic」で取り上げたのと同じものです。Javaエバンジェリストグループは5分でわかる今週のJavaホットトピックというブログでも,定期的にJavaのパズルを掲載しており,このパズルも掲載されています。
ですので,このセミナーに参加された方はもう答えを知っていはずですね。正解は(2)です。
この結果は「Swingで実装されている」というところがキーです。同じものをAWTで作り直して実行すると,結果は(6)になります(ソースコード)。
なぜSwingだともともとの意図である(6)になってくれないのでしょうか。
それはSwingがシングルスレッドで実装されているからです。
SwingやAWTはイベント駆動で動作することは皆さんご存じのはずです。イベントはFIFO(First In, First Out)のキューにためられます。これをイベントキューと呼びます。
このイベントキューからイベントを取り出してイベント処理を実行するのがイベント・ディスパッチ・スレッドです。
ここまではSwingでもAWTでも同じです。ところが,AWTでは描画処理の多くをウィンドウ・システムに委譲しているのに対し,Swingではすべて自前で描画しているという違いがあります。
ウィンドウ・システムでの描画はイベント・ディスパッチ・スレッドとは別のスレッドで行われます。このため,AWTではボタンの文字列の変更はすぐに反映されます。
一方,Swingでは,イベント・ディスパッチ・スレッドが描画処理も行います。
上記のコードでマウスがクリックされたときのイベント処理を考えてみましょう。
マウス・ボタンがクリックされるとイベントキューにはActionEventが積まれます。そして,マウス・ボタンがリリースされ,MouseEventのMOUSE_RELEASEDと,MouseEventのMOUSE_CLICKEDが続いてキューに積まれます。
イベント・ディスパッチ・スレッドはActionEventを取り出し,actionPerformedメソッドに記述された処理を実行します。
ここでJButton#setTextメソッドがコールされると,setTextメソッドの内部でrepaintメソッドがコールされます。repaintメソッドは再描画イベントを発行し,イベントキューに積みます。
次にイベント・ディスパッチ・スレッドはJButton#setEnabledメソッドをコールします。これも同様に再描画イベントをイベントキューに積みます。
この時点では,まだMouseEventのMOUSE_RELEASEDは処理されていません。つまり,まだボタンがクリックされたままの状態になっているということです。
そして,「ながーい処理」がここで始まってしまいます。
つまり,マウスがリリースされた描画処理や,文字列の変更,使用不可のイベントはキューに積まれたまま,処理がブロックされてしまうのです。
したがって,選択肢(2)のままになってしまうというわけです。
この結果から,イベント処理を行うときには次の点に注意しなければならないことがわかります。
  
  • イベント処理では時間のかかる処理を実行しない
どうしても,長い時間がかかる処理を行う場合は,別にスレッドを用意して実行する必要があります。このときに注意しなくてはならないのは,Swingはスレッドセーフで作られていないということです。
つまり,別スレッドからJButton#setTextメソッドなどをコールすることはできないということです注1。他のスレッドからSwingのメソッドをコールするには,SwintUtilities.invokeLaterメソッドを介して行います。
ただ,ユーザーがイベント処理ごとにスレッドを作成し,SwingUtilities.invokeLaterメソッドで描画の更新を行うのは大変です。
前置きが長くなりました。ここでようやく登場するのがSwingWorkerクラスです。
SwingWorkerクラスは,Swingで簡単かつ安全にマルチスレッドを使用した非同期処理を行うことができるクラスです。
SwingWorkerクラスはもともとHans Muller氏とKathy Walrath氏による1998年の記事Threads and Swingの中でサンプルとして登場しました注2。その後,java.netでSwingWorkerプロジェクトになり,それがJava SE 6に取り入れられたという経緯があります。
さて,SwingWorkerクラスを使って,上記のパズル・プログラムを書き直してみましょう。
  サンプルのソースコード Monologue2.java
public class Monologue2 {
    private JButton button;

    public Monologue2() {
        JFrame frame = new JFrame("Monologue");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new FlowLayout(FlowLayout.CENTER));
         
        button = new JButton("実行");
        button.setPreferredSize(new Dimension(100, 26));

        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                // 処理中であることを示すため
                // ボタンの文字列を変更し,使用不可にする
                button.setText("処理中...");
                button.setEnabled(false);
         
                // SwingWorkerを生成し,実行する
                SwingWorker worker = new LongTaskWorker(button);
                worker.execute();
            }
        });

        frame.add(button);
        frame.setSize(100, 70);
        frame.setVisible(true);
    }

    // 非同期に行う処理を記述するためのクラス
    class LongTaskWorker extends SwingWorker<Object, Object> {
        private JButton button;
        
        public LongTaskWorker(JButton button) {
            this.button = button;
        }
         
        // 非同期に行われる処理
        @Override
        public Object doInBackground() {
            // ながーい処理
            try {
                TimeUnit.SECONDS.sleep(10L);
            } catch (InterruptedException ex) {}
            
            return null;
        }
         
        // 非同期処理後に実行
        @Override
        protected void done() {
            // 処理が終了したので,文字列を元に戻し
            // ボタンを使用可能にする
            button.setText("実行");
            button.setEnabled(true);
        }
    }
   
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Monologue2();
            }
        });
    }
}
今週は少し長くなってしまったので,SwingWorkerクラスの解説は来週までのお楽しみということにしましょう。
今回はSwingWorkerクラスを使っている部分以外の変更点について,少しだけ言及しておきます。違いはmainメソッドのところです。
Monologueクラスのmainメソッドは,単にMonologueオブジェクトを生成していただけでした。しかし,よく考えてみると,Monologueクラスのコンストラクタで行われている処理はすべてSwingに関するものです。
しかし,Monologueクラスのコンストラクタはメインスレッドで実行されてしまいます。前述したように,Swingに関する処理はSwingのイベント・ディスパッチ・スレッドで実行しなくてはなりません。
そこで,Monologue2クラスでは,SwingUtilitiesクラスのinvokeLaterメソッドを使用しています。invokeLaterメソッドの引数はRunnableオブジェクトであり,このオブジェクトのrunメソッドはイベント・ディスパッチ・スレッドで実行されます。
これにより,Monologue2クラスのコンストラクタはイベント・ディスパッチ・スレッドで実行されます。
この部分は盲点になる場合が多いので,気をつけてください。
来週は,SwingWorkerクラスの使い方について解説します。

注1 Component#repaintメソッドなどイベント・ディスパッチ・スレッド以外のスレッドからコールされることを許されているメソッドもあります。
注2 Threads and Swingの続編として,Using a Swing Worker Threadと,Joseph Bowbeer氏によるThe Last Word in Swing Threadsがあります。どれもSwingでマルチスレッドを扱うのであれば,一読する価値がありますよ。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:26:16 | 显示全部楼层
Swingでマルチスレッド - SwingWorker その2


今週は,先週に引き続いてSwingでマルチスレッドを扱うためのSwingWorkerクラスを取り上げます。
SwingWorkerクラスはSwingのイベント・ディスパッチ・スレッドと非同期に処理を行うためのクラスです。
利用するには,SwingWorkerクラスを派生させたクラスを作成し,非同期に行う処理とイベント・ディスパッチ・スレッドで実行させる処理を記述します。
別スレッドで非同期に行う処理はdoInBackgroundメソッドに記述します。イベント・ディスパッチ・スレッドで実行させる処理は,実行のタイミングによって二つのメソッドを使い分けます。doInBackgroundメソッドの実行が終了した後に処理させる場合はdoneメソッド,doInBackgroundメソッドとパラレルに実行させる場合はprocessメソッドを使用します。
基本的な使い方それでは先週示したサンプルを使って,具体的な動作を説明しましょう。
  サンプルのソースコード Monologue2.java
先週のサンプルは,フレームにボタンが一つあり,ボタンをクリックすると長い処理が実行されるというものです。長い処理を行っている間は,ボタンの文字列を変更し,使用不可状態にしておきます。
つまり,SwingWorkerクラスを派生させたクラスを作成し,長い処理の部分をdoInBackgroundメソッド,「長い処理が終了した後にボタンの文字列を元に戻し,使用可能にする処理」をdoneメソッドに記述します。
SwingWorkerクラスはジェネリクスでパラメータ化されています。ただ,このサンプルではパラメータは使用しないので,二つのパラメータともObject型にしておきます。
doInBackgroundメソッドとdoneメソッドは親クラスで定義されているので,@Overrideアノテーションを付加しておきます。
// 非同期に行う処理を記述するためのクラス
class LongTaskWorker extends SwingWorker<Object, Object> {
    private JButton button;
   
    public LongTaskWorker(JButton button) {
        this.button = button;
    }
     
    // 非同期に行われる処理
    @Override
    public Object doInBackground() {
        // ながーい処理
        try {
            TimeUnit.SECONDS.sleep(10L);
        } catch (InterruptedException ex) {}
         
        return null;
    }
     
    // 非同期処理後に実行
    @Override
    protected void done() {
        // 処理が終了したので,文字列を元に戻し
        // ボタンを使用可能にする
        button.setText("実行");
        button.setEnabled(true);
    }
}
doInBackgroundメソッド,doneメソッドで記述されているコードは,もともとActionListener#actionPerformedメソッド内に記述していたものです。長い処理はイベント・ディスパッチ・スレッドで実行すると他の処理をブロックしてしまいます。そこで,別スレッドで動作させるため,doInBackgroundメソッドに記述しました。
ボタンの文字列を変更したうえで使用可能にする処理は,長い処理後に行えばいいので,doneメソッドに記述します。
後は,ボタンをクリックされたときのイベント処理を行うactionPerformedメソッドを書き換えます。
button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        // 処理中であることを示すため
        // ボタンの文字列を変更し,使用不可にする
        button.setText("処理中...");
        button.setEnabled(false);

        // SwingWorker を生成し,実行する
        LongTaskWorker worker = new LongTaskWorker(button);
        worker.execute();
    }
});
非同期に長い処理を行う前に,ボタンの文字列を変更し,使用不可にします。
SwingWorkerオブジェクトを生成し,SwingWorkerクラスのexecuteメソッドをコールします。
実行すると,ボタンがクリックされていた状態のままだったのが,正しく文字列が変更され,使用不可になります。
SwingWorkerクラスを使ううえでの注意点は,一度使ったSwingWorkerオブジェクトを再び使うことはできないということです。executeメソッドを一度コールした後は,もう一度コールしても何も処理されません。
つまり,非同期に行う何らかのタスクがある場合,そのつどSwingWorkerオブジェクトを生成して,使用するようにします。
二つのパラメータ      [tr]        図1 ImageViewer      [/tr]

SwingWokerはジェネリクスの二つのパラメータが使用されていると前述しました。
これらのパラメータは,非同期に実行しているスレッドからイベント・ディスパッチ・スレッドに何らかの情報を引き渡すときに使います。
例えば,非同期にデータベースにアクセスし,その結果をテーブルに表示する場合,データベースのクエリ結果をイベント・ディスパッチ・スレッドに引き渡さなければなりません。
J2SE 5.0より前のJava SEであれば,単にObjectクラスを使用して情報を引き渡し,イベント・ディスパッチ・スレッドで情報をキャストして使用していたはずです。
しかし,これではどうしてもダウンキャストを使わなくてはなりません。そこで,ジェネリクスを使用して,情報の型をパラメータ化したのです。
それでは二つあるパラメータのうち,まず一つ目のパラメータの使い方を見てきましょう。
  サンプルのソースコード ImageViewer.java
このサンプルは,クラス名のとおりイメージを表示するだけのアプリケーションです。
フレームの下部にテキスト・フィールドとボタンが二つ表示されています。テキスト・フィールドにはファイル名を記入します。[参照]ボタンをクリックすれば,ファイル・チューザでファイルを選ぶことも可能です。
[ロード]ボタンをクリックすると,イメージ・ファイルを読み込み,フレーム中央部に表示します(図1)。
ここで非同期に行うのは,イメージ・ファイルの読み込みです。
つまり,doInBackgroundメソッドでファイルを読み込み,それをdoneメソッドに引き渡します。
このときに使用する型が一つ目のパラメータになります。イメージの読み込みなのでImageクラスを使用することにしましょう。
class ImageLoadWorker extends SwingWorker<Image, Object> {
コンストラクタでは,ファイル名やイメージを表示するためのラベルなどを設定しておきます。
doInBackgroundメソッドでイメージの読み込みを行います。
@Override
public Image doInBackground() {
    Image image;
    try {
        // イメージの読み込み
        image = ImageIO.read(file);
    } catch (IOException ex) {
        // 例外が発生した場合はnullを返す
        image = null;
    }
     
    return image;
}
doInBackgroundメソッドの戻り値がImageクラスになっているのがわかるはずです。ここがパラメータ化されている部分です。Javadocを見ると,doInBackgroundメソッドの戻り値は,パラメータのTになっています。
ファイルの読み込みは何ら難しいことはありません。Image I/Oを使用してイメージを読み込んでいます。例外が発生した場合にはnullを返すことにしました。
次はdoneメソッドです。
@Override
protected void done() {
    try {
        // doInBackgroundメソッドの戻り値を取得する
        Image image = get();

        if (image != null) {
            // イメージの設定
            imageLabel.setIcon(new ImageIcon(image));
            frame.setTitle(TITLE + " - " + file.getName());
        } else {
            showErrorDialog("イメージがロードできませんでした");
        }
    } catch (InterruptedException ex) {
        showErrorDialog("タイムアウトしました");
    } catch (ExecutionException ex) {
        showErrorDialog("処理が失敗しました");
    }
doInBackgroundメソッドの戻り値は,赤字で示したようにgetメソッドを使用して取得します。getメソッドの戻り値も,doInBackgroundメソッドと同様にパラメータのTになっています。
イメージが取得できれば,それをラベルに設定するだけです。
鋭い方は,この情報のやり取りを見てjava.util.concurrent.Callableインタフェースとjava.util.concurrent.Futureを思いだされたかもしれません。実際,SwingWorkerクラスはConcurrencyUtilitiesを使用して実装されています。SwingWorkerクラスのJavadocを見ると,SwingWorkerクラスはFutureインタフェースをインプリメントしていることがわかります。
ところで,イメージを読み込んでいる最中に,またロード・ボタンをクリックされたら困りますね。かといって,ボタンやテキスト・フィールドを一つひとつ使用不可にしていくのも面倒です。
そこで,ここではグラス・ペインを使ってマウス・イベントを扱えないようにしてみました。JFrameクラスは,グラス・ペイン,コンテント・ペインという二つのコンポーネントから構成されています(実際にはもう少し複雑ですが)。
JFrameオブジェクトに貼られるSwingコンポーネントは,コンテント・ペインに貼られます。J2SE1.4.2まではコンポーネントを追加するときにframe.getContentPane().add(comp)のように書いていたことを覚えておられる方もいるはずです。
グラス・ペインはコンテント・ペインより前面に表示されるコンポーネントです。グラスという名前からもわかるように,透明で通常は表示されません。
グラス・ペインでマウス・イベント処理を行うことにより,コンテント・ペインにマウス・イベントを発生させないようにするのが,ここでの工夫です。
そのために,ImageLoadWorkerクラスのコンストラクタでリスナーを設定します。
public ImageLoadWorker(String fileLocation,
                       JLabel imageLabel,
                       JFrame frame) {
    file = new File(fileLocation);
    this.imageLabel = imageLabel;
    this.frame = frame;

    // マウス・イベントを拾わないようにするためのしかけ
    frame.getGlassPane().addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent event) {
            event.consume();
        }
    });
    frame.getGlassPane().setVisible(true);
}
mousePressedメソッドでイベントを消費(consume)し,下位のコンポーネントにイベントが伝達しないようにします。そして,グラス・ペインを表示します。
これで,doInBackgroundメソッドの処理中はマウスのクリック操作ができないようになります。
このままにしておくとずっとマウスが使えないので,doneメソッドでリスナーを取り外し,グラス・ペインを表示しないようにします。
// マウスイベントを拾えるようにする
frame.getGlassPane().setVisible(false);
MouseListener[] listeners = frame.getGlassPane().getMouseListeners();
for (MouseListener listener: listeners) {
    frame.getGlassPane().removeMouseListener(listener);
}
それでは,もう一つのパラメータに移りましょう。
非同期処理を行っている途中で,イベント・ディスパッチ・スレッドに情報を引き渡したくなることがよくあります。
例えば,プログレス・バーで経過を表示させる場合や,処理が終わった後に,処理が終わった情報の表示を一括ではなく逐次的に更新する場合などです。
このような場合の型を指定するのが,二つ目のパラメータです。
このパラメータは,publishメソッドの引数の型と,processメソッドの引数の型に使用します。
それでは,サンプルで使い方を見ていきましょう。
  サンプルのソースコード FileViewer.java
このサンプルは指定されたファイルの内容をテキストエリアに表示するアプリケーションです。ファイルをすべて読み込んだ後に表示するのではなく,表示を逐次更新するようにしています。
そのために,二つ目のパラメータはStringクラスにしました。
class FileLoadWorker extends SwingWorker<Boolean, String> {  
もう一つのパラメータは,ファイルの読み込みが成功したかどうかを示すBooleanです。
次に,doInBackgroundメソッドを示します。
@Override
public Boolean doInBackground() {
    // 注 わざとパフォーマンスの悪い書き方をしています
    FileReader reader = null;

    try {
        reader = new FileReader(filename);

        while(true) {
            try {
                java.util.concurrent.TimeUnit.MILLISECONDS.sleep(2);
            } catch (InterruptedException ex) {}

            int c = reader.read();
            if (c == -1) {
                // ファイルを最後まで読めれば成功
                return true;
            }
            
            // 読み込んだ文字をパブリッシュする
            publish(String.valueOf((char)c));
        }
    } catch (IOException ex) {
        // 例外が発生したら不成功
        return false;
    } finally {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
このファイル読み込みの処理は,逐次処理がわかりやすくなるように,わざと遅い方法で記述してあります。
readメソッドを使用して1文字ずつ読み込みます。その後,読み込んだ文字を引数にpublishメソッドをコールします。publishメソッドによって,イベント・ディスパッチ・スレッドに情報を引き渡すことができます。
情報を受けとるのはprocessメソッドです。
@Override
protected void process(List<String> chunks) {
    // パブリッシュされた文字をテキストエリアに追加
    for (String str: chunks) {
        fileContents.append(str);
    }
}
      [tr]        図2 FileViewer      [/tr]
  

processメソッドの引数はパラメータのリストになります。publishメソッドがコールされると,すぐさまprocessメソッドがコールされるわけではありません。processメソッドはイベント・ディスパッチ・スレッドの中でスケジューリングされてコールされます。
前回コールされたときから次にコールされるまでの間のpublishメソッドで引き渡された情報が,リストになってprocessメソッドに渡されるのです。
このサンプルではリストに入っているのは,ファイルから読み込んだ文字列なので,それをテキストエリアに追加していきます(図2)。
このようにすることで,非同期に行われる処理とSwingの処理を並列に処理できるようになります。
Swingは,シングルスレッドで実装されている利点と欠点の両方を併せ持っています。Java SE 6では,Swingのシングルスレッドによる欠点がSwingWorkerクラスにより低減されました。
ファイルやデータベースへのアクセス,通信など,Swingのイベント・ディスパッチ・スレッドと非同期に行うべき処理はたくさんあります。つまり,SwingWorkerクラスを使う場面は多いといえるでしょう。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2007-5-25 15:27:24 | 显示全部楼层
Swingでのドラッグ&ドロップ その1


今月はJavaにおける最大のお祭りJavaOneがサンフランシスコで行われます。櫻庭も例年のごとく参加します。せっかく参加するのですから,この連載でもJavaOneをレポートしたいと思います。
そこで,変則的なのですが,今月は今週と第4週をJava SE 6完全攻略,第2週と第4週をJavaOneレポートとさせていただきます。
さて,今月のJava SE 6完全攻略では,Swingのドラッグ&ドロップについて解説します。
Swingのドラッグ&ドロップはJ2SE 1.4から導入された機能です。ただ,Swingのドラッグ&ドロップに関する解説はまだあまりありません。そこでまずはじめに,今までのドラッグ&ドロップについて解説します。その後,Java SE 6でのドラッグ&ドロップの強化点について解説します。
Swingコンポーネントのドラッグ&ドロップのサポートはじめに,Swingのコンポーネントがドラッグ&ドロップを含めたデータ転送をどのようにサポートしているかを説明しましょう。
表1はSwingチュートリアルのIntroduction to Drag and Drop and Data Transferから引用した表です。
表1 Swingコンポーネントのデータ転送サポート                      コンポーネント        ドラッグ
        コピー        ドラッグ
        移動        ドロップ        カット        コピー        ペースト                    JColorChooser                                                                                                            JEditorPane                                                                                                                                                                                 JFileChooser                                                                                                            JFormattedTextField                                                                                                                                                                                 JList                                                                                                             JPasswordField         n/a        n/a                                  n/a        n/a                                              JTable                                                                                                             JTextArea                                                                                                                                                                                 JTextField                                                                                                                                                                                 JTextPane                                                                                                                                                                                JTree                                                                                              

JTextFieldクラスやJEditorPaneクラスなどの文字列を編集するクラスは,データ転送におけるすべての操作が可能になっています。逆にJTableクラスやJTreeクラスは限られた操作しかできません。また,この表にないJLabelクラスなどはデータ転送が行えません。
このように,コンポーネントによってデータ転送のサポートが異なることがわかります。
手始めに,JTextAreaクラスでドラッグ&ドロップを試してみましょう。
  サンプルのソースコード SimpleDnD.java
SimpleDnDクラスは三つのテキストエリアを持ちます(図1)。それぞれのテキストエリアは「ドロップのみ」「ドラッグとドロップの両方が可能」「ドラッグとドロップのどちらも不可」になっています。図1は,各テキストエリアでドロップ動作を行おうとしているところです。一番下のテキストエリアだけドロップできないことがわかります。
  
    [tr]        図1 SimpleDnDへのドロップ動作      [/tr]

  

JTextAreaクラスやJTextFieldクラスといったドラッグとドロップの両方が可能なクラスであっても,デフォルトではドラッグはできないようになっています。
表1でドラッグが可能となっているコンポーネントもデフォルトではドラッグできないのです。
ドラッグを可能にするには,次のようなコードを記述します。
// TextArea をドラッグ可能にする
area2.setDragEnabled(true);
setDragEnabledメソッドは,表1に示したドラッグが可能なコンポーネントで定義されています。
JTextAreaクラスのようにドラッグ&ドロップが可能なクラスに対して,ドラッグ&ドロップを不可にするにはどうすればいいのでしょうか。これも簡単に実現できます。
// TextAreaをドラッグ&ドロップ不可にする
area3.setTransferHandler(null);
setTransferHandlerメソッドの引数をnullにするということは,すでに設定されているTransferHandlerクラスを無効にしてしまうということです。
そう,javax.swing.TransferHandlerクラスがドラッグ&ドロップを実現するためのキーになるクラスなのです。
AWTでドラッグ&ドロップを実装したことがある人はご存じでしょうが,ドラッグ&ドロップで転送されるデータはjava.awt.datatransfer.Transferableインタフェースを実装したクラスで表されます。
AWTでは,DragSourceEventイベントやDragSourceDropEventイベント,DragSourceDragEventイベントを使用し,Transferableオブジェクトを介してドラッグ&ドロップを行います。
なんとなく面倒ですね。でも,Swingではこうした面倒なところをすべてTransferHandlerクラスが行ってくれます。
図2に示したように,ドラッグが開始されるとComponentAに関連付けられたTransferHandlerオブジェクトはComponentAから転送する情報を取得します。そして,Transferableオブジェクトを生成し,そこにComponentAから取得したデータを詰め込みます。
ComponentBにドロップされる場合,ComponentBに関連付けられたTransferableHandlerオブジェクトは,Transferableオブジェクトからデータを取り出し,それをComponentBにセットします。
  
    [tr]        図2 TransferHandlerクラスとドラッグ&ドロップ      [/tr]

  

TransferHandlerクラスの使い方それではTransferHandlerクラスを使ってドラッグ&ドロップを実現してみましょう。転送するデータが文字列の場合はとても簡単です。
サンプルとして,ドラッグ&ドロップをサポートしていないJLableクラスを扱ってみましょう。
  サンプルのソースコード JLabelDnD.java
  
JLabelクラスは文字列とアイコンを扱えますが,ここでは文字列だけを対象にします。
JLabel label = new JLabel(TEXT2);

// D&D のためのTransferHandlerオブジェクトをセット
label.setTransferHandler(new TransferHandler("text"));
TransferHandlerオブジェクトをコンポーネントにセットするにはJComponent#setTransferHandlerメソッドを使用します。
TransferHandlerクラスのコンストラクタの引数はデータ転送を行うプロパティ名です。これだけでドロップが可能になります。
次にドラッグできるようにしましょう。ドラッグを実現するには,ドラッグを開始するイベントが発生したときにデータをTransferHandlerオブジェクトに引き渡します。とはいっても,とても簡単。イベント処理で次のように記述します。
// Drag 開始時にデータの交換を行う
label.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent event) {
        JLabel label = (JLabel)event.getSource();
        TransferHandler handler = label.getTransferHandler();
        handler.exportAsDrag(label, event, TransferHandler.COPY);
    }
});
ここでは,マウスでラベルをクリックしたときをドラッグ開始としています。
イベントからソースのコンポーネントを取得し,そこからgetTransferHandlerメソッドでTransferHandlerオブジェクトを取り出します。
次に,exportAsDragメソッドをコールします。exportAsDragメソッドは第1引数がコンポーネント,第2引数が発生したイベント,第3引数がデータ転送の種類です。ここでは第3引数としてCOPYを使用していますが,ほかにはMOVEを使用できます。
実際に転送するデータはTransferHandlerクラスのコンストラクタで指定されているので,exportAsDragメソッドで指定する必要はありません。
これで,JLabelクラスでもドラッグ&ドロップが可能になりました。ぜひ,実行してみてください(図3)。
ただし,ラベルで表示している文字列の一部をドラッグ&ドロップすることはできません。そのような用途にはJTextFieldクラスやJTextAreaクラスを編集不可の状態で使用します。
  
    [tr]        図3 JLabelDnDの実行結果      [/tr]
a) ドラッグ中
b) ドロップ
  

TransferHandlerクラスを拡張するもう少し複雑な例を扱ってみましょう。
表1をもう一度見てください。JListクラス,JTableクラス,JTreeクラスなどは,ドラッグはできるもののドロップはできません。これをドロップできるようにしてみましょう。
例えばJListクラスをドロップできるようにした場合,どのように項目を追加するかといったことを決めなくてはなりません。「改行された文字列を別々の項目として扱うかどうか」「HTMLで表されていたら<ul>タグで表されるリストを項目として扱うようにする」といったことです。
このような処理を行うにはTransferHandlerクラスだけではできません。それぞれのクラスに応じてTransferHandlerクラスを派生させたクラスを作成します。
サンプルとしてJListクラスを扱いましょう。
  
  サンプルのソースコード JListDnD.javaListTransferHandler.java
  
文字列をドロップできるようにし,文字列の中に改行が含まれていれば,別々の項目として扱うことにしました。この処理を行うのがTransferHandlerクラスの派生クラスであるListTransferHandlerクラスです。今回はクリップボードを介したデータ転送(カット&ペーストやコピー&ペースト)は対象からはずし,ドラッグ&ドロップの部分だけを実装しました。
整理のために,TransferHandlerクラスのメソッドをドラッグとドロップに分けてみます。
ドラッグ
  
  • exportAsDrag
  • createTrasferable
  • exportDone
ドロップ
  
  • canImport
  • importData
exportAsDragメソッドは,JLabelDnDクラスで使ったように,イベント時にコールされます。このメソッドはTransferHandlerクラスで定義されているものをそのまま使用します。次のcreateTransferableメソッドが,Transferableオブジェクトを生成するメソッドです。このメソッドは扱うデータにより処理が異なるので,オーバーライドします。
次のexportDoneメソッドはドラッグが終了したときにコールされるメソッドです。ドラッグ元のコンポーネントの表現によって処理も異なるので,このメソッドもオーバーライドします。
ドロップに関するメソッドは二つ。canImportメソッドは,Transferableオブジェクトが保持しているデータをコンポーネントにドロップできるかどうかを判定します。もう一方のimportDataメソッドは,実際のドロップ処理を記述します。どちらのメソッドもオーバーライドします。
そのほかにオーバーライドするメソッドに,getSourceActionメソッドがあります。このメソッドはコピーと移動のどちらをサポートしているかを返すメソッドです。
それではコードを順番に見ていきましょう。まずはcreateTransferableメソッドからです。
protected Transferable createTransferable(JComponent c) {
    // ドラッグされた部分からTransferableを作成する
    // 複数行選択されていたら,改行を加えた複数の文字列とする
    JList list = (JList)c;
    indices = list.getSelectedIndices();
    Object[] values = list.getSelectedValues();
     
    StringBuilder builder = new StringBuilder();

    for (int i = 0; i < values.length; i++) {
        if (values == null) {
            // 項目データがなければ,空文字を追加
            builder.append("");
        } else {
            builder.append(values.toString());
        }

        // 最終行以外は改行を加える
        if (i != values.length - 1) {
            builder.append("\n");
        }
    }
     
    // 文字列を扱うTransferableインタフェースを実装した
    // StringSelectionオブジェクトを生成
    return new StringSelection(builder.toString());
}
行っている処理はそれほど難しいものではありません。リストで選択されている項目から複数行の文字列を作成しています。
キーとなるのが赤字で示したStringSelectionオブジェクトの生成です。java.awt.datatransfer.StringSelectionクラスは,Transferableインタフェースを実装したクラスで,文字列を扱うことができます。
文字列はデータ転送で最もよく使用されるので,標準でTransferableインタフェースを実装したクラスが提供されています。もし,イメージなど,文字列以外のデータを使用する場合には,Transferableインタフェースを実装するクラスを自作する必要があります。
さて,次にexportDoneメソッドです。
[size=0.9em]protected void exportDone(JComponent c, Transferable data, int action) {
    // 移動であれば,選択された部分を削除する
    if (action == MOVE && indices != null) {
        JList list = (JList)c;
        DefaultListModel model  = (DefaultListModel)list.getModel();
                       
        // 同じリスト内の移動であれば,インデックスを再計算する
        if (addCount > 0) {
            for (int i = 0; i < indices.length; i++) {
                if (indices > addIndex) {
                    indices += addCount;
                }
            }
        }

        // 項目の削除
        for (int i = indices.length - 1; i >= 0; i--) {
            model.remove(indices);
        }
    }

    indices = null;
    addCount = 0;
    addIndex = -1;
}
リストの項目選択には「単一」「連続した複数」「不連続な複数」の3種類があるのですが,ここでは簡略化のため,不連続な選択は対象にしていません。
引数のactionにはCOPYもしくはMOVEを指定します。MOVEのときにはドラッグされた項目をモデルから削除します。
indicesはドラッグ時に選択されていた項目のインデックスを保持している配列です。addCountとaddIndexは同じリスト内でドラッグ&ドロップで項目を移動させたときに使用しています。addCountがドロップで追加された項目数,addIndexがドロップされたインデックスを示しています。
同じリスト内で移動させる場合,ドロップの処理が呼び出された後,exportDoneメソッドがコールされます。つまり,ドロップ処理でリストに項目が追加されてからexportDoneメソッドがコールされるため,indicesが示しているドラッグ時のインデックスは,ドロップされた状態に応じて再計算しなければなりません。
再計算した後,リストのモデルからドラッグされた項目を削除します。最後にindicesやaddCount,addIndexをリセットしておきます。
これでドラッグに関する部分は実装できました。次にドロップに関する処理を実装します。
ドロップの際,Transferableオブジェクトが保持しているデータが,ドロップ先のコンポーネントで扱えるかどうかをチェックしなくてはなりません。そのために使用するのがcanImportメソッドです。
public boolean canImport(JComponent c, DataFlavor[] flavors) {
    // 文字列であればドロップ可能とする
    for (DataFlavor flavor: flavors) {
        if (DataFlavor.stringFlavor.equals(flavor)) {
            return true;
        }
    }
               
    return false;
}
Transferableオブジェクトが保持しているデータの種別(フレーバ)はjava.awt.datatransfer.DataFlavorクラスで扱います。canImportメソッドの引数がDataFlavorクラスの配列になっているのは,一つのTransferableオブジェクトを複数のフレーバで表せるからです。
例えば,プレーンな文字列とHTMLの両方で表すといったことができます。
DataFlavorクラスには,よく使用するデータのフレーバが定数として用意されています。文字であればDataFlavor.stringFlavor,イメージであればDataFlavor.imageFlavorです。もちろん,新しいフレーバを作成することもできます。
ここでは文字列を扱うので,引数のフレーバとstringFlavorを比較しています。マッチすればtrueを返し,引数のフレーバが文字列以外であればfalseを返します。
次がドロップ処理の本体であるimportDataメソッドです。
[size=0.9em]public boolean importData(JComponent c, Transferable t) {
    if (canImport(c, t.getTransferDataFlavors())) {
        try {
            Object obj = t.getTransferData(DataFlavor.stringFlavor);
            String text = (String)obj;
            importString(c, text);

            return true;
        } catch (UnsupportedFlavorException ex) {
            // 失敗したらfalseを返すだけ
        } catch (IOException ex) {
            // 失敗したらfalseを返すだけ
        }
    }

    return false;
}
canImportメソッドを先に説明してしまいましたが,コールされる順番はimportDataメソッドの方が先です。
importDataメソッドの内部でcanImportメソッドをコールし,ドロップできるかどうかを判定します。このときに,Transferableオブジェクトからフレーバを取り出すためにTransferable#getTransferDataFlavorsメソッドを使用します。
次にドロップ処理です。
ドロップするデータは,Transferable#getTransferDataメソッドを使用します。getTransferDataメソッドの引数はフレーバです。ドロップ処理は煩雑なので,別メソッドにしました。
canImportメソッドがfalseを返してきた場合,もしくは例外が発生した場合,ドロップ処理は行わず,falseを返します。
private void importString(JComponent c, String str) {
    JList list = (JList)c;
    DefaultListModel model = (DefaultListModel)list.getModel();
    int index = list.getSelectedIndex();

    // リストの選択している部分をドラッグして,
    // 選択している部分の中にドロップさせようとしたときは
    // ドロップを失敗させる
    if (indices != null
        && index >= indices[0] - 1
        && index <= indices[indices.length - 1]) {
        indices = null;
        return;
    }

    int max = model.getSize();
    if (index < 0) {
        index = max;
    } else {
        // 選択された行の次の行にドロップする
        index++;
        if (index > max) {
            index = max;
        }
    }

    addIndex = index;
    String[] values = str.split("\n");
    addCount = values.length;
    for (int i = 0; i < values.length; i++) {
        model.add(index++, values);
    }
}
同一リスト中のドラッグ&ドロップは処理が煩雑になるため,連続した項目選択範囲の中にドロップをさせるのはサポートしていません。
ここでは選択された行の次の行にドロップ処理を行っています。その際,同じリスト内でドラッグ&ドロップを行っているかもしれないので,exportDoneメソッドで使用するaddCountとaddIndexを設定しています。
後は,文字列を改行で分解し,リスト・モデルに追加しているだけです。
これでListTransferHandlerクラスができあがりました。実行してみると,ドラッグ&ドロップが可能なことを確認できるはずです(図4)。
  
    [tr]        図4 JListのドラッグ&ドロップ      [/tr]
a) リストからドラッグb) リストからドロップ
c) リストへドラッグd) リストへドロップ
  

このサンプルを操作していると,ドロップするときにちょっと意に反した動きをしませんか。例えば「項目の先頭にドロップしようとすると,最後尾にドロップされてしまう」とか「項目間にドロップさせようとしても,マウスカーソルが項目間を示すカーソルに変化しない」といった点です。
こうした動作は,不具合とまではいかなくとも,もう少しどうにかならないかなぁという感じですね。これらの問題はJava SE 6を使えば解決できます。
ちょっと間が空いてしまいますが,次回のJava SE 6完全攻略で,Java SE 6でのドラッグ&ドロップの拡張を解説していきます。
なお,今回の記事ではドラッグ&ドロップの必要最低限の部分しか解説していません。ドラッグ&ドロップやカット&ペーストについてより詳しく知りたい場合には,Swingのチュートリアル「Introduction to Drag and Drop and Data Transfer」をご覧ください。
J2SE1.2やJ2SE1.3でも,AWTの機能を用いれば,Swingでドラッグ&ドロップを実装できました。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注~册

本版积分规则

小黑屋|手机版|咖啡日语

GMT+8, 2025-1-26 06:43

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表