JLayerを使用したコンポーネントのデコレート方法


このセクションは、近く予定されているJava SE 7リリースの機能と表記規則を反映するために更新されています。 最新のJDK7スナップショットjava.netからダウンロードできます。


JLayerクラスは、柔軟でありながら強力なSwingコンポーネント向けのデコレータです。 このクラスを使用すると、基本となるコンポーネントを直接変更することなく、コンポーネント上に描画したり、コンポーネント・イベントに応答したりすることができます。

Java SE 7でのJLayerクラスは、java.netのJXLayerプロジェクトに似た性質を持ちます。 もともと、JLayerクラスはJXLayerプロジェクトをベースにしていましたが、それぞれのAPIは別々に展開されました。

このドキュメントでは、JLayerクラスの機能を示す例について説明します。 また、完全なソース・コードも提供されています。

JLayerクラスの使用

javax.swing.JLayerクラスが果たす役割は半分に過ぎず、 残りの半分はjavax.swing.plaf.LayerUIクラスによって実行されます。 ここでは、JButtonオブジェクトの上に何らかのカスタム描画を表示する(JButtonデコレート)とします。 その場合、デコレートの対象となるコンポーネントはターゲットです。

  • ターゲット・コンポーネントを作成します。
  • 描画を行うためのLayerUIサブクラスのインスタンスを作成します。
  • ターゲットとLayerUIオブジェクトをラップするJLayerオブジェクトを作成します。
  • ターゲット・コンポーネントを使用する場合とまったく同様に、JLayerオブジェクトをユーザー・インタフェースで使用します。

たとえば、JPanelサブクラスのインスタンスをJFrameオブジェクトに追加する場合、通常、次のようなコードを使用します。

JFrame f = new JFrame();

JPanel panel = createPanel();

f.add (panel);

JPanelオブジェクトをデコレートする場合は、代わりに次のようなコードを使用します。

JFrame f = new JFrame();

JPanel panel = createPanel();
LayerUI<JPanel> layerUI = new MyLayerUISubclass();
JLayer<JPanel> jlayer = new JLayer<JPanel>(panel, layerUI);

f.add (jlayer);

JPanelオブジェクトとLayerUIオブジェクトの型に互換性を持たせるため、総称を使用します。 前の例では、JLayerオブジェクトとLayerUIオブジェクトの両方がJPanelクラスに対して使用されています。

JLayerクラスは通常、そのビュー・コンポーネントの正確な型を使用して総称化されますが、LayerUIクラスは、その汎用パラメータまたは祖先のJLayerクラスとともに使用するよう設計されています。

たとえば、LayerUI<JComponent>オブジェクトをJLayer<AbstractButton>オブジェクトと一緒に使用できます。

LayerUIオブジェクトは、JLayerオブジェクトのカスタム・デコレートとイベント処理を担当します。 LayerUIサブクラスのインスタンスを作成すると、適切な総称型を持つすべてのJLayerオブジェクトに対して、カスタムの動作が適用されます。 これは、JLayerクラスがfinalであるためであり、すべてのカスタム動作はLayerUIサブクラスにカプセル化されているため、JLayerのサブクラスを作成する必要はありません。

LayerUIクラスの使用

LayerUIクラスは、その動作のほとんどをComponentUIクラスから継承します。 次に、もっともよくオーバーライドされるメソッドを示します。

  • ターゲット・コンポーネントの描画が必要になると、paint(Graphics g, JComponent c)メソッドが呼び出されます。 Swingのレンダリングと同じ方法でコンポーネントをレンダリングするには、super.paint(g, c)メソッドを呼び出します。
  • LayerUIのサブクラス・インスタンスがコンポーネントに関連付けられると、installUI(JComponent c)メソッドが呼び出されます。 ここで、必要なすべての初期化処理を実行します。 渡されるコンポーネントは、対応するJLayerオブジェクトになります。 JLayerクラスのgetView()メソッドを使用して、ターゲット・コンポーネントを取得します。
  • LayerUIのサブクラス・インスタンスとコンポーネントとの関連付けがなくなると、uninstallUI(JComponent c)メソッドが呼び出されます。 必要に応じて、ここでクリーンアップ処理を実行します。

コンポーネント上の描画

JLayerクラスを使用するには、適切なLayerUIのサブクラスが必要です。 もっとも簡単な種類のLayerUIクラスは、コンポーネントの描画方法を変更するものです。 たとえば、コンポーネント上に薄いカラー・グラデーションを描く例を次に挙げます。

class WallpaperLayerUI extends LayerUI<JComponent> {
  @Override
  public void paint(Graphics g, JComponent c) {
    super.paint(g, c);

    Graphics2D g2 = (Graphics2D) g.create();

    int w = c.getWidth();
    int h = c.getHeight();
    g2.setComposite(AlphaComposite.getInstance(
            AlphaComposite.SRC_OVER, .5f));
    g2.setPaint(new GradientPaint(0, 0, Color.yellow, 0, h, Color.red));
    g2.fillRect(0, 0, w, h);

    g2.dispose();
  }
}

paint()メソッドで、カスタムの描画が実行されます。 super.paint()メソッドを呼び出すと、JPanelオブジェクトのコンテンツが描画されます。 50%透明なコンポジットが設定されてから、カラー・グラデーションが描画されます。

LayerUIのサブクラスを定義したら、その使用は簡単です。 次に、WallpaperLayerUIクラスを使用するソース・コードを示します。

import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.LayerUI;

public class Wallpaper {
  public static void main(String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        createUI();
      }
    });
  }

  public static void createUI() {
    JFrame f = new JFrame("Wallpaper");
    
    JPanel panel = createPanel();
    LayerUI<JComponent> layerUI = new WallpaperLayerUI();
    JLayer<JComponent> jlayer = new JLayer<JComponent>(panel, layerUI);
    
    f.add (jlayer);
    
    f.setSize(300, 200);
    f.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
    f.setLocationRelativeTo (null);
    f.setVisible (true);
  }

  private static JPanel createPanel() {
    JPanel p = new JPanel();

    ButtonGroup entreeGroup = new ButtonGroup();
    JRadioButton radioButton;
    p.add(radioButton = new JRadioButton("Beef", true));
    entreeGroup.add(radioButton);
    p.add(radioButton = new JRadioButton("Chicken"));
    entreeGroup.add(radioButton);
    p.add(radioButton = new JRadioButton("Vegetable"));
    entreeGroup.add(radioButton);

    p.add(new JCheckBox("Ketchup"));
    p.add(new JCheckBox("Mustard"));
    p.add(new JCheckBox("Pickles"));

    p.add(new JLabel("Special requests:"));
    p.add(new JTextField(20));

    JButton orderButton = new JButton("Place Order");
    p.add(orderButton);

    return p;
  }
}

結果は次のとおりです。

A panel with a jazzy decoration

ソース・コード

Wallpaper NetBeans Project
Wallpaper.java

Java Web Startを使用して実行

この例を起動する

LayerUIクラスのpaint()メソッドを使用すると、コンポーネントの描画方法を完全にコントロールできます。 次に挙げたLayerUIのもう1つのサブクラスでは、Java 2Dイメージ処理を使用してパネルのコンテンツ全体を変更する方法を示します。

class BlurLayerUI extends LayerUI<JComponent> {
  private BufferedImage mOffscreenImage;
  private BufferedImageOp mOperation;

  public BlurLayerUI() {
    float ninth = 1.0f / 9.0f;
    float[] blurKernel = {
      ninth, ninth, ninth,
      ninth, ninth, ninth,
      ninth, ninth, ninth
    };
    mOperation = new ConvolveOp(
            new Kernel(3, 3, blurKernel),
            ConvolveOp.EDGE_NO_OP, null);
  }

  @Override
  public void paint (Graphics g, JComponent c) {
    int w = c.getWidth();
    int h = c.getHeight();

    if (w == 0 || h == 0) {
      return;
    }

    // Only create the offscreen image if the one we have
    // is the wrong size.
    if (mOffscreenImage == null ||
            mOffscreenImage.getWidth() != w ||
            mOffscreenImage.getHeight() != h) {
      mOffscreenImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
    }

    Graphics2D ig2 = mOffscreenImage.createGraphics();
    ig2.setClip(g.getClip());
    super.paint(ig2, c);
    ig2.dispose();

    Graphics2D g2 = (Graphics2D)g;
    g2.drawImage(mOffscreenImage, mOperation, 0, 0);
  }
}

paint()メソッド内で、パネルはオフスクリーン・イメージにレンダリングされます。 オフスクリーン・イメージは畳み込み演算子を使用して処理された後で、画面に描画されます。

ユーザー・インタフェース全体は変わらず使用できる状態にありますが、表示がぼやけています。

画像が反転したユーザー・インタフェース

ソース・コード

Myopia NetBeans Project
Myopia.java

Java Web Startを使用して実行

この例を起動する

イベントに対する応答

LayerUIのサブクラスでも、対応するコンポーネントのすべてのイベントを受け取ることができます。 しかし、JLayerインスタンスは特定のイベント・タイプに対する関心を登録する必要があります。 これを実行するには、JLayerクラスのsetLayerEventMask()メソッドを使用します。 ただし、通常このメソッドは、LayerUIクラスのinstallUI()メソッド内で実行される初期化処理によって呼び出されます。

たとえば、次に抜粋したLayerUIのサブクラスでは、マウス・イベントとマウス・モーション・イベントを取得するよう登録されています。

public void installUI(JComponent c) {
  super.installUI(c);
  JLayer jlayer = (JLayer)c;
  jlayer.setLayerEventMask(
    AWTEvent.MOUSE_EVENT_MASK |
    AWTEvent.MOUSE_MOTION_EVENT_MASK
  );
}

JLayerのサブクラスが受け取ったすべてのイベントは、イベント・タイプに名前の一致するイベント・ハンドラ・メソッドにルーティングされます。 たとえば、次の該当メソッドをオーバーライドすることで、マウス・イベントとマウス・モーション・イベントに応答できるようになります。

protected void processMouseEvent(MouseEvent e, JLayer l) {
  // ...
}

protected void processMouseMotionEvent(MouseEvent e, JLayer l) {
  // ...
}

次のLayerUIサブクラスは、パネル内でマウスが動くたびに半透明な円を描画します。

class SpotlightLayerUI extends LayerUI<JPanel> {
  private boolean mActive;
  private int mX, mY;

  @Override
  public void installUI(JComponent c) {
    super.installUI(c);
    JLayer jlayer = (JLayer)c;
    jlayer.setLayerEventMask(
      AWTEvent.MOUSE_EVENT_MASK |
      AWTEvent.MOUSE_MOTION_EVENT_MASK
    );
  }

  @Override
  public void uninstallUI(JComponent c) {
    JLayer jlayer = (JLayer)c;
    jlayer.setLayerEventMask(0);
    super.uninstallUI(c);
  }

  @Override
  public void paint (Graphics g, JComponent c) {
    Graphics2D g2 = (Graphics2D)g.create();

    // Paint the view.
    super.paint (g2, c);

    if (mActive) {
      // Create a radial gradient, transparent in the middle.
      java.awt.geom.Point2D center = new java.awt.geom.Point2D.Float(mX, mY);
      float radius = 72;
      float[] dist = {0.0f, 1.0f};
      Color[] colors = {new Color(0.0f, 0.0f, 0.0f, 0.0f), Color.BLACK};
      RadialGradientPaint p =
          new RadialGradientPaint(center, radius, dist, colors);
      g2.setPaint(p);
      g2.setComposite(AlphaComposite.getInstance(
          AlphaComposite.SRC_OVER, .6f));
      g2.fillRect(0, 0, c.getWidth(), c.getHeight());
    }

    g2.dispose();
  }

  @Override
  protected void processMouseEvent(MouseEvent e, JLayer l) {
    if (e.getID() == MouseEvent.MOUSE_ENTERED) mActive = true;
    if (e.getID() == MouseEvent.MOUSE_EXITED) mActive = false;
    l.repaint();
  }

  @Override
  protected void processMouseMotionEvent(MouseEvent e, JLayer l) {
    Point p = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), l);
    mX = p.x;
    mY = p.y;
    l.repaint();
  }
}

mActive変数は、マウスがパネルの座標内にあるかどうかを示します。 LayerUIサブクラスがマウス・イベントとマウス・モーション・イベントの取得に関心があることを示すため、installUI()setLayerEventMask()が呼ばれています。

processMouseEvent()メソッドでは、マウスの位置に応じてmActiveフラグが設定されます。 processMouseMotionEvent()メソッドでは、マウス移動の座標がmXおよびmYのメンバー変数に保管されるため、後からpaint()メソッドで使用できます。

paint()メソッドはパネルのデフォルトの外観を表示した後で、スポットライト効果として放射状のグラデーションを重ねて表示します。

マウスをなぞるスポットライト

ソース・コード

Diva NetBeans Project
Diva.java

Java Web Startを使用して実行

この例を起動する

ビジー・インジケータの動画表示

この例では、ビジー・インジケータを動画として表示します。 LayerUIサブクラス内の動画を表示するものであり、フェードイン機能とフェードアウト機能を備えています。 この例はここまでの例より複雑ですが、カスタム描画のためにpaint()メソッドを定義するという基本方針に基づく点に変わりはありません。

Place Orderボタンをクリックすると、ビジー・インジケータが4秒間表示されます。 パネルがグレーで表示され、インジケータが回転することに注目してください。 インジケータの各要素には、それぞれ異なる透明度が設定されています。

LayerUIのサブクラスであるWaitLayerUIクラスでは、コンポーネントを更新するためにプロパティ変更イベントを発生させる方法が示されています。 WaitLayerUIクラスはTimerオブジェクトを使用して、1秒間に24回、状態を更新しています。 この処理は、タイマーのターゲット・メソッドであるactionPerformed()メソッド内で実行されています。

actionPerformed()メソッドはfirePropertyChange()メソッドを使用して、内部状態の更新を通知します。 これによってapplyPropertyChange()メソッドが呼び出され、JLayerオブジェクトが再描画されます。

スムーズなビジー・インジケータ

ソース・コード

TapTapTap NetBeans Project
TapTapTap.java

Java Web Startを使用して実行

この例を起動する

テキスト・フィールドの検証

このドキュメントの最後の例では、JLayerクラスを使用して、テキスト・フィールドに有効なデータが含まれているかどうかを示すデコレーションを付与する方法を示します。 その他の例ではJLayerクラスを使用してパネルや汎用コンポーネントがラップされていましたが、この例では具体的にJFormattedTextFieldコンポーネントのラップ方法を示します。 また、複数のJLayerインスタンスに対して、1つのLayerUIサブクラス実装を使用できるということも実証します。

JLayerクラスは、無効なデータを含むフィールドを視覚的に表すために使用されます。 ValidationLayerUIクラスによってテキスト・フィールドが描画される際、フィールド・コンテンツが解析できない場合は、赤い×印が表示されます。 次にその例を示します。

不適切な入力値に対する即時のフィードバック

ソース・コード

FieldValidator NetBeans Project
FieldValidator.java

Java Web Startを使用して実行

この例を起動する