HOME> SWT

このドキュメントはEclipsetechnical articlesにある Creating Your Own Widgets using SWT を私、Takayukiが日本語に翻訳したものです。誤訳がある場合はtakayuki at users.sourceforge.jpまでお願いします。


Copyright © 2001 Object Technology International, Inc.

Eclipse Corner Article

SWTでカスタムウィジェットを作る
Creating Your Own Widgets using SWT

要約
アプリケーションを書くとき、通常はSWTの標準ウィジェットを使用するでしょうし、 時には自分のカスタムウィジェットを作成する必要があるでしょう。 たとえば、標準のウィジェットによって提供されない新しいウィジェットを加えたいか、 既存のウィジェットの機能を拡張したいと思うかもしれません。 この記事はいろいろなSWTを拡張する戦略について説明し、それらを使用する方法をあなたに教えます。

Summary
When writing applications, you typically use the standard widgets provided by SWT. On occasion, you will need to create your own custom widgets. For example, you might want to add a new type of widget not provided by the standard widgets, or extend the functionality of an existing widget. This article explains the different SWT extension strategies and shows you how to use them.

By Steve Northover & Carolyn MacLeod, OTI March 22, 2001


カスタムウィジェットを作る
Creating Your Own Widgets

概要
Overview

アプリケーションを書く場合、通常はSWTによって提供される標準のウィジェットを使用しますが、時々自分のカスタムウィジェットを作成する必要があるでしょう。 このようにしたい理由は

カスタムウィジェットは既存のウィジェットのクラス階層においてサブクラス化することによって作られます。

When writing applications, you typically use the standard widgets provided by SWT. On occasion, you will need to create your own custom widgets. There are several reasons that you might want to do this:

Custom widgets are created by subclassing in the existing widget class hierarchy.

移植性の問題
Portablity Issues

カスタムウィジェットを書く前に移植性について考えるのはとても重要なことです。 SWTは以下の方法で拡張することができます。

加えて、これらの組み合わせは異なるプラットフォームで利用できます。

It is very important to think about portability before writing a custom widget. SWT can be extended in the following ways:

In addition, a combination of these can be used on different platforms:

これはもちろん、ウィジェットを2度実装し(あるプラットフォームではネイティブコードを呼び、 他では移植性のあるコードを呼ぶ)、同一のAPIを両方で維持する必要が伴います。

This of course involves implementing the widget twice - using native calls on the one platform and portable code on the others - while maintaining the same API for both.

各SWTプラットフォームは共有ライブラリ(たとえばWindowsのDLL)とjar(Javaクラスライブラリ)の両方で成り立っています。 共有ライブラリはSWTに必要なネイティブな機能のすべてを含みますが、プラットフォームで利用可能な機能の完全なセット ではありません。したがって、SWTによって利用可能になっていないネイティブ関数やネイティブウィジェットを 利用可能にするには共有ライブラリを自分で書く必要があります。 あるプラットフォームでネイティブコードを使い、他のプラットフォームで移植性のあるコードを使う場合、 ネイティブウィジェットを使用するプラットフォームでは共有ライブラリを呼び出し、 移植性のあるウィジェットを使用するプラットフォームではjarを呼び出すことを確認してください。

Each SWT platform is shipped with both a shared library (for example, a DLL on Windows) and a jar (for the Java class files). The shared library contains all of the native function required for SWT, but it was not meant to be a complete set of the functions available on the platform. Thus to expose native function or native widgets that were not exposed by SWT, you need to write your own shared library. If you are using a combination of native code on one platform and portable code on another, make sure you call your shared library on the platform with the native widget, and your jar on the platform with the portable widget.

最後に1つ注意: 共有ライブラリへのSWTのインターフェースはSWT内部のコードです。 すべてのプラットフォームのすべての機能にアクセスする アプリケーションにフレームワークを提供するということではありません(気が遠くなる仕事でしょう)。 このドキュメントの目的はSWTとCのコードを統合する方法を示すことであり、オペレーティングシステムを模して作ることではありません。 したがって、このドキュメントでネイティブを書くためにとったアプローチとSWTでとられたアプローチとは異なります。

One final note: SWT's interface to its shared libraries is internal SWT code. It was not meant to provide a framework for applications to access all possible native function on all platforms - that would be a daunting task. One of the purposes of this document is to show how you can integrate C code with SWT, not model the operating system. As such, the approach taken to writing natives in this document is different from the approach taken by SWT.

移植性のあるウィジェットを書く
Writing Portable Widgets

SWTのライブラリは100% Javaの移植性のあるカスタムウィジェットのために2つのウィジェットクラスを提供しています。

The SWT library provides two widget classes that are typically used as the basis for a custom 100% Java portable widget:

基本ウィジェット
Basic Widgets

基本ウィジェットは他のウィジェットを含まず、他のウィジェットから作られることもありません。 基本ウィジェットはそれ自体で描画します。基本ウィジェットの例はButtonです。 他の例はTextです。基本カスタムウィジェットを作るにはCanvasをサブクラス化します。

Basic widgets do not contain any other widgets, and are not built from any other widgets. Basic widgets draw themselves. An example of a basic widget is Button. Another example is Text . To create a custom basic widget, subclass Canvas .

複合ウィジェット
Compound Widgets

複合ウィジェットは他のウィジェット含み、他のウィジェットによって作られます。 複合ウィジェットの例はComboです。TextButtonそしてListを含みます。 他の例はGroupです。 いくつも子ウィジェットを含むことができます。 複合カスタムウィジェットを作るにはCompositeをサブクラス化します。

Compound widgets contain other widgets, and/or are composed of other widgets. An example of a compound widget is Combo . It contains a Text , a Button and a List . Another example is Group . It can contain any number of children. To create a custom compound widget, subclass Composite .

抜け目無い読者はCanvasは実際にはCompositeのサブクラスであることに注目したでしょう。 これは根本的な実装によるものです。 Canvasは何かを描くために、Compositeは子ウィジェットを持つものとして扱います。 それゆえ、どちらのクラスのサブクラスにするか決めるための規則は次のようになります。 子ウィジェットを持つ場合はCompositeをサブクラスにしましょう。 子ウィジェットを持たない場合はCanvasをサブクラスにしましょう。

The astute reader may have noticed that Canvas is actually a subclass of Composite . This is an artifact of the underlying implementation. We treat Canvas as something you draw on and Composite as something that has children. Therefore the rule for deciding which class to subclass is this: If your widget has or will have children, subclass Composite . If your widget does not have and never will have children, subclass Canvas .

さらに、子ウィジェットを含み、レイアウトすることを意図している複合ウィジェットと 単に他のウィジェットから構成された複合ウィジェットを区別しないことに注意してください。 両者はCompositeのサブクラスですが、タイプ、継承ではなく、実装について記述しています。 100% Javaの移植性のあるウィジェットを書くとき、 ウィジェットのタイプに関係なくすべての基本ウィジェットはSWTクラス階層の移植性のあるエントリーポイントとして Compositeのことを考えます。

Note also that we do not distinguish between a compound widget that is intended to contain and lay out children, and one that is merely composed of other widgets. Both will be a subclass of Composite , and as such we are describing implementation, rather than type, inheritance. When writing 100% Java portable widgets, we can think of Composite as the portable entry point into the SWT class hierarchy for all compound widgets, and Canvas as the portable entry point into the SWT class hierarchy for all basic widgets, regardless of widget type.

基本ウィジェットの例
Basic Widget Example

こんな感じの右側にテキストがある画像を表示するウィジェット想像してください:

Imagine we are building an application where we need a widget that displays an image with a line of text to the right, something like this:

画像とテキストの両方を描くことを計画しているのでCanvasをサブクラスにします。

Since we plan to draw both the image and the text, we subclass Canvas.

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.events.*;

public class PictureLabel extends Canvas {
    Image image; 
    String text;
}

ウィジェットは生成される必要があります。これを行うには、少なくとも1つのコンストラクタを書かなければなりません。 SWTのウィジェットは親ウィジェットなしでは生成することができないため、コンストラクタの引数の1つは親ウィジェットをとらなければいけません。 SWTの慣例ではコンストラクターはparentstyleの2つの引数を持ちます。 スタイルビットはウィジェットの外観を制御します。 ウィジェットが生成された後、親ウィジェットとスタイルは変更することができません。 あなたのウィジェットもスタイルビットを使うことができます。

Our widget needs to be created. To do this, we must write at least one constructor. Because widgets in SWT cannot be created without a parent, the constructor must take at least one argument that is the parent. The convention in SWT is to have a constructor with two arguments, parent and style . Style bits are used to control the look of widgets. Neither the parent nor the style bits can be changed after the widget is created. Your widget can use style bits too.

    PictureLabel(Composite parent, int style) {
        super(parent, style);
    }

いかなるウィジェットの親もCompositeになります。 スタイルは整数で、いくつかのビットはすでにシステムによって使用されています。 たとえばSWT.BORDERCanvasに境界線をもたらします。

The parent of any widget must be a Composite . The style is an integer, where some bits are already used by the system. For example, SWT.BORDER will cause a Canvas to have a border.

次はウィジェットを初期化する必要があります。 SWTの慣例ではコンストラクタですべての初期化を行います。 必ず親ウィジェットやスタイルビットが必要とする初期化はここで行われなければいけません。 PictureLabelはデフォルトで白い背景を持つことを決めたのでColorフィールドを追加し、 Colorを確保し、背景を初期化する必要があります。

Next we need to initialize our widget. The convention in SWT is to do all initialization in the constructor. Certainly, any initialization that requires the parent or the style bits must be done here. We have decided that our PictureLabel widget will default to a white background, so we need to add a Color field, allocate a Color , and initialize the background.

public class PictureLabel extends Canvas {
    Image image;
    String text;
    Color white;

    PictureLabel(Composite parent, int style) {
        super(parent, style);
        white = new Color(null, 255, 255, 255);
        setBackground(white);

Colorはグラフィックスリソースなので破棄されなければなりません。 確保した白い色を破棄するにはdisposeリスナーを追加します。 すべてのウィジェットが破棄されるときの通知を提供しています。 コンストラクタでdisposeリスナーを追加します。

Colors are graphics resources that must be disposed. How can we dispose of the white color that we allocated? We add a dispose listener. Every widget provides notification when it is destroyed. We add the dispose listener in the constructor.

        addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                white.dispose();
            }
        });
    }
}

注意: Colorを解放するためにただdispose()をオーバーライドしてはいけません。 disposeが実際にウィジェットに送られた場合にのみ機能します。 Shellがこれを破棄するときには起こらず、オーバーライドしたdisposeはColorをリークします。 それがどれほど生成されたとしても、ウィジェットにイベントが通知されることを保証するには イベントを生成するメソッドをオーバーライドする代わりにイベントリスナーを追加してください。

Note: Do not just override dispose() to release the color. This only works in the case where dispose is actually sent to the widget. When the shell is disposed this does not happen, so overriding dispose will leak the color. To ensure that your widget is informed of an event no matter how it was generated, add an event listener instead of overriding methods that generate events.

ウィジェットが生成・初期化され、グラフィックリソースをリークすることなく破棄することができるようになりました。 するといくつか機能が必要になります。 画像とテキストを描く必要があり、これにはpaintリスナーが必要になります。 ウィジェットを実装するためにはたくさんのリスナーを追加する必要があります。 新しいウィジェットクラスの一部としてリスナーインターフェースを実装することができましたが、 クラスでインターフェースメソッドはpublicになります。 代わりにSWTの慣例では同じ名前の非publicメソッドにするために匿名内部クラスを利用します。 一貫性を保つために、この慣例にしたがってdisposeリスナーを書き直し、 Colorを破棄するコードをwidgetDisposedメソッドに移動します。 paintリスナーを同様に書きます。

Our widget is created and initialized, and it can be destroyed without leaking graphics resources. Now it needs some functionality. We need to draw the image and the text, and this will require another listener: the paint listener. Implementing a widget often requires adding many listeners. We could implement the listener interfaces as part of our new widget class, but that would make the interface methods public in our class. Instead, the SWT convention is to use anonymous inner classes to forward the functionality to non-public methods of the same name. For consistency, we will rewrite the dispose listener to follow this convention, moving the color dispose code into the widgetDisposed method. We write the paint listener the same way.

        addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                PictureLabel.this.widgetDisposed(e);
            }
        });

        addPaintListener(new PaintListener() {
            public void paintControl(PaintEvent e) {
                PictureLabel.this.paintControl(e);
            }
        });

同じ名前を選んだことで、だいぶ後ですることを決める場合にインターフェースを容易に実装するオプションを持ちます。 ここにウィジェットを描くpaintControlメソッドがあります。

By choosing the same names, we have the option of easily implementing the interfaces if we decide to do so later. Here is the paintControl method to draw the widget.

void paintControl(PaintEvent e) {
    GC gc = e.gc;
    int x = 1;
    if (image != null) {
        gc.drawImage(image, x, 1);
        x = image.getBounds().width + 5;
    }
    if (text != null) {
        gc.drawString(text, x, 1);
    }
}

画像とテキストを描けるようになりましたが、ユーザーにそれらを設定させる必要があります。 したがってそれらのsetとgetメソッドを書きます。

Now we can draw the image and the text, but we need to let the user set them. So we write set and get methods for each of them.

public Image getImage() {
    return image;
}

public void setImage(Image image) {
    this.image = image;
    redraw();
}

public String getText() {
    return text;
}

public void setText(String text) {
    this.text = text;
    redraw();
}

getメソッドは取るに足らないものです。 単純にフィールドを返します。 setメソッドはフィールドに設定し、その後変更を示すためにウィジェットを再描画します。 これを行う最も簡単な方法はredraw()を呼び出すことによってウィジェットに ダメージを与え、ウィジェットにpaintイベントをイベントキューに入れさせることです。 このアプローチは1つのpaintイベントですませるので画像とテキストの両方を設定するときに有利です。

The get methods are trivial. They simply answer the fields. The set methods set the fields and then redraw the widget to show the change. The easiest way to do this is to damage the widget by calling redraw() , which queues a paint event for the widget. This approach has the advantage that setting both the image and the text will cause only one paint event because multiple paints are collapsed in the event queue.

まだ終わっていません。ウィジェットは最適サイズがわかりません。 この情報はウィジェットをレイアウトするために必要です。 今回の場合、ベストサイズは単純にテキストと画像のサイズと間にちょっとしたスペースを足したサイズです。 また、周囲に1ピクセルのマージンを加えます。

We are not done yet. Our widget does not know its preferred size. This information is needed in order to lay out the widget. In our case, the best size is simply the size of the text plus the size of the image, plus a little bit of space in between. Also, we will add a 1 pixel margin all the way around.

ウィジェットの最適サイズを返すためにcomputeSizeメソッドを実装しなければいけません。 その仕事は現在の内容に基づいて最適なサイズを計算することです。 もっとも単純な実装は引数を無視して単にサイズを計算することです。

To return the preferred size of the widget, we must implement the computeSize method. The computeSize method can be quite complicated. Its job is to calculate the preferred size of the widget based on the current contents. The simplest implementation ignores the arguments and just computes the size.

public Point computeSize(int wHint, int hHint, boolean changed) {
    int width = 0, height = 0;
    if (image != null) {
        Rectangle bounds = image.getBounds();
        width = bounds.width + 5;
        height = bounds.height;
    }
    if (text != null) {
        GC gc = new GC(this);
        Point extent = gc.stringExtent(text);
        gc.dispose();
        width += extent.x;
        height = Math.max(height, extent.y);
    }
    return new Point(width + 2, height + 2);    
}

wHinthHint、そしてchangedとは何でしょうか。 ヒント引数はウィジェットに「ある特定の横幅を与えると、すべての内容を表示するためにウィジェットが必要な高さはどのくらいか」というような 質問をすることができるようにします。 たとえばワードラップするLabelウィジェットはこれを求めるでしょう。 特定のヒントについて気にしないことをクライアントに示すには特別な値SWT.DEFAULTが使われます。 以下の例は100ピクセルの横幅を与えたときの最適なサイズを求めます。

What are wHint , hHint , and changed ? The hint arguments allow you to ask a widget questions such as "Given a particular width, how high does the widget need to be to show all of the contents?" For example, a word-wrapping Label widget might be asked this. To indicate that the client does not care about a particular hint, the special value SWT.DEFAULT is used. The following example asks a label for its preferred size given a width of 100 pixels:

  Point extent = label.computeSize(100, SWT.DEFAULT, false);

PictureLabelウィジェットでは、横幅が小さすぎるとき、画像をテキストの上に重ねて描くようにしたり、 横幅のリクエストのためにテキストをラップすることもできますが、単純にそのようにはしないことにしました。 まだ、ヒントを尊重する必要はあります。 ウィジェットは切り取られるようにします。 これを行う最も簡単な方法は計算を行い、結果をふるいにかけます。

For our PictureLabel widget, we could be fancy and stack the image over the text when the width is too small, and/or wrap the text in order to meet a width request, but for simplicity we have decided not to do so. Still, we need to honour the hints. So, our widget will clip. The easiest way to do this is to perform the calculation and then filter the results.

  public Point computeSize(int wHint, int hHint, boolean changed) {
     int width = 0, height = 0;
     if (image != null) {
         Rectangle bounds = image.getBounds();
         width = bounds.width + 5;
         height = bounds.height;
     }
     if (text != null) {
         GC gc = new GC(this);
         Point extent = gc.stringExtent(text);
         gc.dispose();
         width += extent.x;
        height = Math.max(height, extent.y);
     }
     if (wHint != SWT.DEFAULT) width = wHint;
     if (hHint != SWT.DEFAULT) height = hHint;
     return new Point(width + 2, height + 2);
  }

指定されたヒントサイズを正確に返さないことに注意してください。 1ピクセルの境界線を追加しています。なぜこうするのでしょうか。すべてのウィジェットはクライアントエリアtrimをもっています。 ヒントパラメータはクライアントエリアの希望サイズを示しています。したがってcomputeSizeから 返すサイズはtrimを含んでいなければいけません。

Notice that we do not return the hint sizes exactly as specified. We have added the 1-pixel border. Why do we do this? All widgets have a client area and trim . The hint parameters specify the desired size of the client area. We must set the size of the widget so that the size of the client area is the same as the hint, so the size we return from computeSize must include the trim.

changedフラグについてはどうでしょう。 これはSWTレイアウトマネージャと連動して利用されており、基本ウィジェットでは無視されています。 これは複合ウィジェットについてお話しするときに議論しましょう。

What about the changed flag? This is used in conjunction with SWT layout managers and is ignored for basic widgets. This will be discussed when we talk about compound widgets.

複合ウィジェットの例
Compound Widget Example

ここからはPictureLabelウィジェットを複合ウィジェットとして作っていきます。 この章は基本ウィジェットの例の章を読んだことを想定していることに注意してください。 今回はウィジェットは2つのLabelを子ウィジェットとして実装します。 1つは画像を表示し、もう1つはテキストを表示します。 このウィジェットを実装するために他のウィジェットを利用するのでCompositeをサブクラス化します。

Now we will recode the PictureLabel widget as a compound widget. Note that this section assumes that you have read the basic widget example section. This time the widget will be implemented using two Label children: one to display the image, and one to display the text. Since we are using other widgets to implement our widget, we subclass Composite .

public class PictureLabel extends Composite {
  Label image, text;
  Color white;

  PictureLabel(Composite parent, int style) {
     super(parent, style);
     white = new Color(null, 255, 255, 255);
     image = new Label(this, 0);
     text = new Label(this, 0);
     setBackground(white);
     image.setBackground(white);
     text.setBackground(white);
     addDisposeListener(new DisposeListener() {
         public void widgetDisposed(DisposeEvent e) {
            PictureLabel.this.widgetDisposed(e);
         }
     });

コンストラクタの中でグラフィックスリソースを初期化するのと同様に 子ウィジェットを生成し、背景色を設定する必要があります。 一般的な間違いは子ウィジェットを親ウィジェットの子として生成することです。 これは対等なウィジェットにしましょう。 かわりにthisの子として生成して確認してください。 以前のようにdisposeリスナーはColorを解放します。

As well as initializing the graphics resources in the constructor, we need to create the child widgets and set their background color. A common mistake is to create the child widgets as children of the parent. This would make them peers of our widget. Instead, make sure to create them as children of this . The dispose listener frees the color, as before.

生成と破棄を処理したので、子ウィジェットをレイアウトする必要があります。2つの方法があります。

比べるためにここは両方で実装します。

Now that we have handled creation and destruction, we need to lay out the children. There are two possibilities:

We will implement both here for comparison.

リサイズ時に子ウィジェットを配置する
Positioning Children on Resize

はじめに、ウィジェットがリサイズされたときに子ウィジェットを配置します。 リサイズのリスナーを追加する必要があります。

First, we will position the children when the widget is resized. We need to add a resize listener.

     addControlListener(new ControlAdapter() {
         public void controlResized(ControlEvent e) {
            PictureLabel.this.controlResized(e);
         }
     });
  }
  void controlResized(ControlEvent e) {
     Point iExtent = image.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     Point tExtent = text.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     image.setBounds(1, 1, iExtent.x, iExtent.y);
     text.setBounds(iExtent.x + 5, 1, tExtent.x, tExtent.y);
  }

ウィジェットがリサイズされたとき、子ウィジェットのそれぞれのサイズを計算し、 範囲と5ピクセルのスペースと1ピクセルのマージンを使い、setBoundsを 使用して子ウィジェットを配置します。

When the widget is resized, we compute the size of each of our children, and then use their extents and our 5-pixel spacing and 1-pixel margin to position the children using setBounds .

これからsetとgetメソッドを書きます。画像とテキストを描かないのでウィジェットにダメージを与えることは 正しい振る舞いではありません。 子ウィジェットは新しい内容を見せるためにリサイズされなければなりません。 これを行うためにリサイズのリスナーからコードを resizeと呼ばれるヘルパーメソッドに移動します。

Now we will write the set and get methods. Because we are not drawing the image and text, damaging the widget will not cause the correct behavior. The children must be resized to show their new contents. To do this, we will take the code from the resize listener and move it into a helper method called resize .

  void controlResized(ControlEvent e) {
     resize();
  }

  void resize() {
     Point iExtent = image.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     Point tExtent = text.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     image.setBounds(1, 1, iExtent.x, iExtent.y);
     text.setBounds(iExtent.x + 5, 1, tExtent.x, tExtent.y);
  }

setメソッドとgetメソッド

Here are the set and get methods.

  public Image getImage() {
     return image.getImage();
  }

  public void setImage(Image image) {
     this.image.setImage(image);
     resize();
  }

  public String getText() {
     return text.getText();
  }


  public void setText(String text) {
     this.text.setText(text);
     resize();
  }

computeSizeメソッドを実装しなければなりません。 これは子ウィジェットに最適なサイズをたずねるという単純なものです。

Now we have to implement the computeSize method. This is a simple matter of asking the children for their preferred sizes.

  public Point computeSize(int wHint, int hHint, boolean changed) {
     Point iExtent = image.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     Point tExtent = text.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     int width = iExtent.x + 5 + tExtent.x;
     int height = Math.max(iExtent.y, tExtent.y);
     if (wHint != SWT.DEFAULT) width = wHint;
     if (hHint != SWT.DEFAULT) height = hHint;         
     return new Point(width + 2, height + 2);
  }

レイアウトマネージャによる子ウィジェットの配置
Positioning Children With a Layout Manager

子ウィジェットの配置にレイアウトマネージャを用いるように複合ウィジェットの例を書き換えます。 既存のSWTレイアウトマネージャRowLayoutを子ウィジェットを配置するのに使うことができますが、 computeSizeメソッドでchangedパラメータの説明をすることをお約束いたしました。 これはまたより複雑なレイアウトの必要条件ためにどのようにするかの例になります。 以下のコードの中でPictureLabelLayoutクラスはLayoutを拡張し、 書き直されたPictureLabelクラスはそのすべてでリストされます。

Now we will rewrite our compound widget example using a layout manager to position our widget's children. We could just use an existing SWT layout manager - RowLayout - to position the children, but we promised to explain the changed parameter in the computeSize method. This also gives an example of how this might be done for more complicated layout requirements. In the code that follows, the class PictureLabelLayout extends Layout , and the rewritten PictureLabel class is listed in its entirety.

レイアウトマネージャはウィジェットコンストラクター内の次の行のコードによってウィジェットに設定されます。

The layout manager is set into the widget with the following line of code in the widget constructor:

     setLayout(new PictureLabelLayout());

ウィジェットの2つのsetメソッドにおいて次のコードでレイアウトマネージャを呼び出します。

We will call the layout manager in the widget's two set methods, with the following line of code:

     layout(true);

layoutメソッドのパラメータはchangedフラグです。 もしtrueならば、ウィジェットの内容が変更された(そのまま2つのsetメソッドの場合)ことを示します。 したがってレイアウトマネージャが持っているキャッシュはフラッシュされる必要があります。 ウィジェットがリサイズされ、SWTシステムがlayout(false)をレイアウトマネージャに送ったとき、 キャッシュをフラッシュする必要はありません。 これによってレイアウトマネージャに高価な計算を必要なときだけ行わせます。

The parameter to the layout method is the changed flag. If true , it indicates that the widget contents have changed (as is the case in the two set methods), therefore any caches that the layout manager may have been keeping need to be flushed. When the widget is resized, the SWT system sends layout(false) to the layout manager, so caches do not need to be flushed. This lets the layout manager perform any expensive calculations only when necessary.

PictureLabelLayoutクラスにおいてcomposite.getChildren()は 常に2つの子ウィジェットを正確に返すことを知っています。 一般的にレイアウトマネージャは子ウィジェットが何個あっても処理しなければならず、 任意の数の子ウィジェットを持つことができるウィジェットを実装する場合、 計算するためにループする必要があるでしょう。 changedフラグをチェックし、状況に応じて 2つの範囲のキャッシュをフラッシュするクラスであることに注意してください。

In class PictureLabelLayout , we know that composite.getChildren() will always return exactly two children. In general, a layout manager will have to handle any number of children, so if you are implementing a widget that can have an arbitrary number of children you will need to loop through them to do your calculations. Note that it is in this class that we check the value of the changed flag and optionally flush our two "extent" caches.

PictureLabelクラスはレイアウトマネージャの利用によって単純化されたことに注目してください。 computeSizeresizeのコードはPictureLabelLayoutクラスに 移動し、リサイズのリスナーはもはや必要ありません。

Notice that the PictureLabel class has been simplified by using a layout manager. The code in computeSize and resize has been moved to the PictureLabelLayout class, and the resize listener is no longer needed.

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.events.*;


class PictureLabelLayout extends Layout {
  Point iExtent, tExtent; // the cached sizes

  protected Point computeSize(Composite composite, int wHint, int hHint,
     boolean changed) {
     Control [] children = composite.getChildren();
     if (changed || iExtent == null || tExtent == null) {
         iExtent = children[0].computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
         tExtent = children[1].computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     }
     int width = iExtent.x + 5 + tExtent.x;
     int height = Math.max(iExtent.y, tExtent.y);
     return new Point(width + 2, height + 2);
  }
 
  protected void layout(Composite composite, boolean changed) {
     Control [] children = composite.getChildren();
     if (changed || iExtent == null || tExtent == null) {
         iExtent = children[0].computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
         tExtent = children[1].computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     }
     children[0].setBounds(1, 1, iExtent.x, iExtent.y);
     children[1].setBounds(iExtent.x + 5, 1, tExtent.x, tExtent.y);
  }
}
 
public class PictureLabel extends Composite {
  Label image, text;
  Color white;
 
  PictureLabel(Composite parent, int style) {
     super(parent, style);
     white = new Color(null, 255, 255, 255);
     image = new Label(this, 0);
     text = new Label(this, 0);
     setBackground(white);
     text.setBackground(white);
     image.setBackground(white);
     addDisposeListener(new DisposeListener() {
         public void widgetDisposed(DisposeEvent e) {
            PictureLabel.this.widgetDisposed(e);
         }
     });
     setLayout(new PictureLabelLayout());
  }
 
  void widgetDisposed(DisposeEvent e) {
     white.dispose();
  }
    
  public Image getImage() {
     return image.getImage();
  }
 
  public void setImage(Image image) {
     this.image.setImage(image);
     layout(true);
  }
 
  public String getText() {
     return text.getText();
  }
 
  public void setText(String text) {
     this.text.setText(text);
     layout(true);
  }
}

イベントとリスナー
Events and Listeners

新しいウィジェットにイベントをサポートさせたいと思うでしょう。たとえば ウィジェットをユーザーが選択したときにリスナーに通知するようにしたいかもしれません。 または値が変わったことをリスナーに通知する編集可能なウィジェットを持ちたいかもしれません。

Often, you will want a new widget to support an event. For example, you may want your widget to notify listeners when the user selects it. Or you may have an editable widget that should notify listeners when its value has changed.

AnEventと呼ばれるイベントを実装する詳細はJava Beanリスナーを 実装することと正確に同じです。

The details to implement an event called AnEvent are exactly the same as implementing a Java Bean listener:

たとえばユーザーがマウスの左ボタンをクリックするときリスナーに通知する PictureLabelウィジェットがほしいとします。 xyフィールドを持ち、 imageClicked(ImageClickedEvent event)メソッドを持つImageClickedListenerインターフェースを 実装したImageClickedEventクラスを作ります。

Say we want PictureLabel widgets to notify listeners when the user clicks the left mouse button in the image. We create class ImageClickedEvent with x and y fields, and interface ImageClickedListener with method imageClicked(ImageClickedEvent event) .

public class ImageClickedEvent extends java.util.EventObject {
  public int x, y;
 
  public ImageClickedEvent(Object source, int x, int y) {
     super(source);
     this.x = x;
     this.y = y;
  }
}
 
public interface ImageClickedListener extends java.util.EventListener {
  public void imageClicked(ImageClickedEvent event);
}

リスナーを格納するためにVectorPictureLabelを追加します。

We add a Vector to PictureLabel to store the listeners:

  Vector imageClickedListeners = new Vector();
 
  public void addImageClickedListener(ImageClickedListener listener) {
     imageClickedListeners.addElement(listener);
  }
 
  public void removeImageClickedListener(ImageClickedListener listener) {
     imageClickedListeners.removeElement(listener);
  }

最後にPictureLabelのコンストラクターにおいてマウスリスナーを画像のLabelウィジェットに 追加します。それは画像の上でマウスの左ボタンをクリックされたときリスナーに通知する働きをします。

Finally, in PictureLabel's constructor, we add a mouse listener to the image Label widget, which does the work of notifying the listeners when the left mouse button is clicked over the image.

     
     addMouseListener(new MouseAdapter() {
         public void mouseDown(MouseEvent event) {
            if (event.button == 1) {
                PictureLabel.this.mouseDown(event);
            }
         }
     });
 
  public void mouseDown(MouseEvent event) {
     ImageClickedEvent e = new ImageClickedEvent(this, event.x, event.y);
     int size = imageClickedListeners.size();
     for (int i = 0; i < size; i++) {
         ImageClickedListener listener =
            (ImageClickedListener) imageClickedListeners.elementAt(i);
         listener.imageClicked(e);
     }
  }

サンプルアプリケーション
Sample Application

例となるアプリケーションで新しいウィジェットを使うことにします。 アプリケーションは単純にPictureLabelを子ウィジェットとしてshellを生成します。 その後、PictureLabelのイメージに小さな赤い四角とテキストに「Hi there!」を設定します。 shellにはPictureLabelのサイズを設定するためのLayoutマネージャはありません。 イメージがクリックされたら、テキストを「Red!」に変えます。

Now we will use the new widget in an example application. The application simply creates a shell with a PictureLabel child. Then it sets the PictureLabel's image to a little red square, and text to "Hi there!". There is no Layout manager for the shell, so we will set the PictureLabel's size. When the image is clicked, we change the text to "Red!".

アプリケーションのソースコードです。

Here is the application code:

import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;
 
public class PictureLabelExample {
  public static void main(String [] args) {
     Image image = new Image(null, 20, 20);
     Color red = new Color(null, 255, 0, 0);
     GC gc = new GC(image);
     gc.setBackground(red);
     gc.fillRectangle(image.getBounds());
     gc.dispose();
     red.dispose();
 
     Shell shell = new Shell();
     PictureLabel label = new PictureLabel(shell, 0);
     label.setImage(image);
     label.setText("Hi there!");
     Point size = label.computeSize(SWT.DEFAULT, SWT.DEFAULT, false);
     label.setSize(size);
 
     label.addImageClickedListener(new ImageClickedListener() {
         public void imageClicked(ImageClickedEvent event) {
            ((PictureLabel) event.getSource()).setText("Red!");
         }
     });
 
     shell.open();
     Display display = shell.getDisplay();
     while (!shell.isDisposed()) {
         if (!display.readAndDispatch()) display.sleep();
     }
     image.dispose();
  }
}
 

そしてこれが実行したときに見えるものです。

And here is what you see when you run it:

shellの中のPictureLabel

A PictureLabel in a Shell

高度な問題
Advanced Issues

カスタムウィジェットを作る基礎をお話ししてきました。 それはCanvasCompositeをサブクラス化し、生成、初期化、ウィジェットのリソースの 設定と取得、描画、グラフィックリソースの破棄、サイズとレイアウト、そしてイベントとリスナーの提供についてです。 新しいウィジェットを実装する場合に処理する必要がある、いくつかの高度な問題があります。

We have discussed the basics of creating a custom widget: subclassing Canvas or Composite , creating and initializing, setting and getting the widget's resources, drawing, disposing graphics resources, size and layout, and providing events and listeners. There are some advanced issues that you may need to deal with when you implement a new widget.

SWTのイベント機構
SWT Event Mechanism

SWTは一般のJavaの"typed"リスナーと同様の低レベルなリスナー機構を提供しています。 すべてのSWTウィジェットはaddListener(int eventType, Listener listener)notifyListeners(int eventType, Event event)を理解します。 eventType定数はSWTクラスで定義されています。 イベントが発生したとき、ウィジェットは 適切なtype定数を含む SWTのEventオブジェクトを生成します。 notifyListenersメソッドはListenerのために handleEvent(Event event)を呼び出します。 既存のSWTイベントを再利用したい場合、典型的にはこの機構を利用します。

SWT provides a low-level listener mechanism as well as the usual Java "typed" listeners. Every SWT widget understands addListener(int eventType, Listener listener) and notifyListeners(int eventType, Event event) . The eventType constants are defined in class SWT . When an event occurs, the widget creates an SWT Event object containing the appropriate type constant. The notifyListeners method calls handleEvent(Event event) for the Listener . If you need to reuse an existing SWT event, you would typically use this mechanism.

たとえば、あなたのウィジェットがselectionイベントを実装する場合、 以下のように「typed」を追加、削除するメソッドを実装することができます。

For example, if your widget implements a selection event, you could implement your "typed" add and remove methods as follows:

public void addSelectionListener(SelectionListener listener) {
  addListener(SWT.Selection, new TypedListener(listener));
}

public void removeSelectionListener(SelectionListener listener) {
  removeListener(SWT.Selection, listener);
}

「selection イベント」があなたのウィジェットで発生したとき (たとえばchild1が選択されたとき)、notifyListenersを使って アプリケーションのselectionリスナーに通知します。

When the "selection event" occurs in your widget (say, when child1 is selected), you notify the application's selection listeners using notifyListeners .

  child1.addListener(SWT.Selection, new Listener() {
     public void handleEvent(Event e) {
         notifyListeners(SWT.Selection, new Event());
     }
  });

リスナーを追加するとき、最初に TypedListenerにラップすることに注意してください。 これはTypedListenerhandleEvent(Event event)メソッドが Eventのtypeに基づく適切なTypedEventのサブクラスを生成し、 typedイベントに応じて適切なメソッドを呼び出します。 この方法で、アプリケーションはJavaのtypedリスナーをウィジェットに追加できますが、 ウィジェットの実装はより効率的な低レベル機構を使うことができます。 あなたのウィジェット実装がtypedリスナーAPIを提供しているかを確認してください。 しかしながら、アプリケーションはローレベルのリスナーメソッドを呼び出していてはいけません。 typedリスナーメソッドはすべてのウィジェットがすべての種類のイベントを処理することができるとか、 Eventクラスのすべてのフィールドがすべてのイベントで有効であると 仮定するような予想外のプログラミングエラーを防止します。

Note that when we add the listener we first wrap it in a TypedListener . This is because TypedListener's handleEvent(Event event) method creates the appropriate TypedEvent subclass based on the type in the Event , and then calls the appropriate method for the typed event. In this way, applications can add Java typed listeners to widgets, but widget implementations can use the more efficient low-level listener mechanism. Make sure that your widget implementation provides a typed listener API, however. Applications should not be calling low-level listener methods. The typed listener methods prevent accidental programming errors such as assuming that all widgets can handle all types of event, or that all fields in the Event class are valid for all events.

SWTのウィジェットをラップする
Wrapping an SWT Widget

ときどき、既存のSWTウィジェットをラッピングすることによって 新しいウィジェットを実装するという最良の方法を見つけるかもしれません。 たとえば、TableTreeを実装するためにTableを使いたいかもしれません。 これを行うにはTableTreeCompositeのサブクラスとして生成し、 TableTreeのコンストラクタでTableを子ウィジェットとして生成します。 このように作ったウィジェットはラップされたウィジェットのAPIを呼び出すので100% Javaの移植性があります。

Occasionally, you may find that the best way to implement a new widget is by wrapping an existing SWT widget. For example, to implement a TableTree , you might want to use a Table . To do this, create TableTree as a subclass of Composite , and then in the TableTree constructor create a Table child. The resulting widget will be 100% Java portable because you call the wrapped widget's API.

SWTウィジェットをラッピングするためのいくつかの指針があります。

Here are some guidelines for wrapping SWT widgets:

TableTreeウィジェットをTableでラッピングすることで実装した完全な例は Appendix A: TableTree and TableTreeItemで提供されています。 この例はTableTreeItemがいかにしてItemをサブクラス化し、TableItemを ラッピングしているかを示します。 設計の決定は次のとおりです。

A complete example of a TableTree widget that was implemented by wrapping a Table is provided in Appendix A: TableTree and TableTreeItem . This example also shows how TableTreeItem was implemented by subclassing Item and wrapping a TableItem . Some of the design decisions that were made are:

次のページは3列のTableTreeを表示するアプリケーションのコードを示します。 TableTreeTableTreeItemの全ソースコードはAppendix A: TableTree and TableTreeItemです。

Shellの中のTableTree

The following page shows an example three-column TableTree , and the application code that created it. The full source listing for TableTree and TableTreeItem is in Appendix A: TableTree and TableTreeItem .

A TableTree in a Shell

public static void main(String [] args) {
  Shell shell = new Shell();
  Image image = new Image(null, 20, 20);
  Color red = new Color(null, 255, 0, 0);
  GC gc = new GC(image);
  gc.setBackground(red);
  gc.fillRectangle(image.getBounds());
  gc.dispose();
  red.dispose();
  TableTree tableTree = new TableTree(shell, SWT.BORDER);
  tableTree.setSize(320, 200);
  Table table = tableTree.getTable();
  table.setHeaderVisible(true);
  table.setLinesVisible(true);
  for (int col = 0; col < 3; col++) {
     TableColumn column = new TableColumn(table, SWT.NONE, col);
     column.setText("Column " + col);
     column.setWidth(100);

 }
  for (int iRoot = 0; iRoot < 8; iRoot++) {
     TableTreeItem root = new TableTreeItem(tableTree, SWT.NONE);
     root.setText("Root " + iRoot);
     for (int iBranch = 0; iBranch < 4; iBranch++) {
         TableTreeItem branch = new TableTreeItem(root, SWT.NONE);
         branch.setText("Branch " + iBranch);
         for (int col = 1; col < 3; col++) {
            branch.setImage(col, image);
            branch.setText(col, "R"+iRoot+"B"+iBranch+"C"+col);
         }
         for (int iLeaf = 0; iLeaf < 4; iLeaf++) {
            TableTreeItem leaf = new TableTreeItem(branch, SWT.NONE);
            leaf.setText("Leaf " + iLeaf);
            for (int col = 1; col < 3; col++) {
                leaf.setImage(col, image);
                leaf.setText(col, "R"+iRoot+"B"+iBranch+"L"+iLeaf+C"+col);
            }
         }
     }
  }
  shell.pack(); shell.open();
  Display display = shell.getDisplay();
  while (!shell.isDisposed()) {
     if (!display.readAndDispatch()) display.sleep();
  }
}

ウィジェットを直接サブクラス化すること
Subclassing Widgets Directly

極端な状況ではCanvasComposite以外のウィジェットをサブクラスにする必要があるかもしれません。 もし他のすべての手段を調査し、不毛であった場合にのみ推奨します。 サブクラス化する前にウィジェットをラップすることを最初に試してください。その理由は次のようになります。

In extreme circumstances, you may need to subclass a widget other than Canvas or Composite . We recommend against doing this unless all other avenues have been explored and exhausted. Try to wrap the widget first, before subclassing it. Here is why:

TableTreeの例を考慮してください。 私たちはTableをサブクラス化するよりラップすることを選択しました。 最もよいテストはTableTree "is-a" Tableであるかどうかたずねることです。 答えは明らかに、そうではありません。Tableを使ったTableTreeを実装するために 単純な選択を行いました。 TableTreeの「行」またはインデックスについては言えません。 そしてTableの最初の列についてはツリーのために予約されています。 Tableのためのたくさんの操作はTableTreeには意味がありません。 たとえばgetSelectionIndex()getTopIndex()

Consider the example of TableTree . We chose to wrap Table rather than subclass it. The best test is to ask whether a TableTree "is-a" Table . The answer is definitely not - we have simply chosen to implement TableTree using Table . We cannot talk about the "rows" of a TableTree , or index into a TableTree ; and the first column of the Table is reserved for the "tree". Many of the operations for a Table do not make sense for a TableTree , for example getSelectionIndex() and getTopIndex() .

CanvasCompositeをサブクラス化することはあなたのウィジェットがすべての SWTプラットフォームで動作することを保証する最もよい手段です。 このケースでの"is-a"テストはあなたのウィジェットはis-aベースか複合ウィジェット かどうかをテストします。 新しいウィジェットはサブクラス化されたタイプのSWTネイティブウィジェットであるかを 尋ねる必要があります。 たとえば100% Javaの移植性のあるPictureLabelはSWTネイティブのLabelではありません。

Subclassing Canvas or Composite is the best way to ensure that your widget works on all SWT platforms. The "is-a" test in this case tests whether your widget is-a basic or compound widget. Subclassing anything else requires asking if the new widget is-an SWT native widget of the type being subclassed . For example, a 100% Java portable PictureLabel is not an SWT native Label .

CompositeCanvas以外にサブクラス化をする場合、 protected void checkSubclass()メソッドが何もしないようにオーバーライドしなければなりません。 オーバーライドする前にメソッドのコメントを読んで確認してください。

When subclassing anything other than Composite or Canvas you must override the method protected void checkSubclass() to do nothing. Make sure you read the method comment before overriding it.

* *

ネイティブウィジェットをラップする
Wrapping a Native Widget

Sometimes, an application requires a native widget that is not provided by SWT. This may be a platform widget, or a third party widget, or any other widget in a shared library. In this section, we will describe how to interface to a native widget on the Windows and Motif platforms. This section assumes that you have some understanding of the Java Native Interface , or JNI . Two good books on JNI are:

This section also assumes that you have done some platform programming before, and are proficient in C and in the use of makefiles. You must have platform documentation available, such as the MSDN Library on Windows, and Motif 2.1 documentation or "man pages" for your Motif Unix/Linux system. If you are programming to a third party widget, you will need to know its API.

In this section, we will create a shared library and load it using:

  System.loadLibrary("mywidget");

On Windows, this loads a Dynamic Link Library or DLL file called "mywidget.dll".

On Motif, this loads a Shared Object Library or SO file called "libmywidget.so".

For our example, we will be building a widget we will call Spinner . On Windows, the native widget we will use is called an UpDown control, and on Motif we will be using an XmSimpleSpinBox . They look like this:

Our Spinner will be numeric, and we want to be able to set and get the maximum and minimum value, as well as the current value (which we will call the "selection", to conform to SWT convention). We also want to be able to set the font. When the user clicks on one of the arrows, we want to notify listeners that the selection has changed, so we will need to implement a selection listener. The test code for our widget looks something like this (the full source listing for class SpinnerTest is in Appendix B: SpinnerTest and Spinner

:
  final Spinner spinner = new Spinner(shell, 0);
  spinner.setMaximum(999);
  spinner.setSelection(500);
  spinner.setMinimum(100);
  Font font = new Font(display, "Courier", 20, SWT.NORMAL);
  spinner.setFont(font);
  spinner.addSelectionListener(new SelectionAdapter() {
         public void widgetSelected(SelectionEvent e) {
                System.out.println(spinner.getSelection());
         }
  });

The next step is to write the API in Java. Since we know we will be writing the native interface file (we'll call it "spinner.c") twice - once on Windows and once on Motif - we try to write the Java code only once so that it is easier to maintain. We start by creating a subclass of Composite , and we load the shared library in a static initializer:

  static {
     System.loadLibrary("spinner");
  }

This will load a file called "spinner.dll" on Windows, and "libspinner.so" on Motif. Since we know that creating a widget returns a "handle" on both platforms, we define an instance variable to hold the handle:

  int handleSpinner;

Since we will be providing a listener, we know that we will need to map this handle back to the Java Spinner object when the platform calls in to Java to notify us of the event. So we create a static Hashtable that will contain Spinner handles as keys and Spinner objects as values:

  static Hashtable table = new Hashtable();

Now we write the constructor. We add the handle to the table after the widget is created, and we remove it when the widget is destroyed. Note that we don't create the widget in Java. We will create it later, in the native createControl method. We also forward all controlResized and focusGained events to the native resizeControl and setFocus methods, and set our font to the default font using the native setFont method. There is one more thing to explain in the constructor. You may notice that we are actually creating two widgets: a Composite parent named handle in the call to super, and a Spinner child named handleSpinner in the call to createControl . This wraps the native control in an SWT parent, allowing it to participate in the SWT system.

  public Spinner(Composite parent, int style) {
     super(parent, style);
     int handleParent = handle;
     handleSpinner = createControl(handleParent);
     if (handleSpinner == 0) SWT.error(SWT.ERROR_NO_HANDLES);
     table.put(new Integer(handleSpinner), this);
     addDisposeListener(new DisposeListener() {
         public void widgetDisposed(DisposeEvent e) {
            Spinner.this.widgetDisposed(e);
         }
     });
     addControlListener(new ControlAdapter() {
         public void controlResized(ControlEvent e) {
            Spinner.this.controlResized(e);         
         }
     });
     addFocusListener(new FocusAdapter() {
         public void focusGained(FocusEvent e) {
            Spinner.this.focusGained(e);         
         }
     });
     Font font = getFont();
     setFont(handleSpinner, font.handle);
  }
 
  public void widgetDisposed(DisposeEvent e) {
     table.remove(new Integer(handleSpinner));
     handleSpinner = 0;
  }
 
  public void controlResized(ControlEvent e) {
     Rectangle rect = getClientArea();
     resizeControl(handleSpinner, rect.x, rect.y, rect.width, rect.height);        
  }
 
  public void focusGained(FocusEvent e) {
     setFocus(handleSpinner);
  }
 
  static final native int createControl(int handleParent);
  static final native void resizeControl(int handle, int x, int y, int width, int height);
  static final native void setFocus(int handle);
 

We write the setFont API method to call the native setFont method with the Spinner handle and the font handle. Call super.setFont because some superclasses need to set fonts for things like titles - it will not actually be needed in this case, but by convention widgets usually inform their superclass of font and color changes.

  public void setFont(Font font) {
     super.setFont(font);
     int hFont = 0;
     if (font != null) hFont = font.handle;
     setFont(handleSpinner, hFont);
  }
 
  static final native void setFont(int handle, int hFont);

In a similar manner, we write the remaining set and get API methods and declare the corresponding native methods. The checkWidget method simply checks that the widget is still valid.

  public void setSelection(int selection) {
     checkWidget();
     setPosition(handleSpinner, selection);
  }
 
  public int getSelection() {
     checkWidget();
     return getPosition(handleSpinner);
  }
 
  public void setMaximum(int maximum) {
     checkWidget();
     setMaximum(handleSpinner, maximum);
  }
 
  public int getMaximum() {
     checkWidget();
     return getMaximum(handleSpinner);
  }
 
  public void setMinimum(int minimum) {
     checkWidget();
     setMinimum(handleSpinner, minimum);
  }
 
  public int getMinimum() {
     checkWidget();
     return getMinimum(handleSpinner);
  }
 
  static final native void setPosition(int handle, int position);
  static final native int getPosition(int handle);
  static final native void setMaximum(int handle, int max);
  static final native int getMaximum(int handle);
  static final native void setMinimum(int handle, int min);
  static final native int getMinimum(int handle);

As with all widgets we implement, we need to provide a computeSize method to compute the preferred size of the widget. This one also forwards to a native to do the work. In this case, the native needs to return two integers (width and height) so we use an int array of size 2 to hold the returned values:

  public Point computeSize(int wHint, int hHint, boolean changed) {
     checkWidget();
     int [] result = new int [2];
     computeSize(handleSpinner, result);
     if (wHint != SWT.DEFAULT) result [0] = wHint;
     if (hHint != SWT.DEFAULT) result [1] = hHint;
     int border = getBorderWidth();
     return new Point(result [0] + border * 2, result [1] + border * 2);
  }
 
  static final native void computeSize(int handle, int [] result);

Finally, we need to define the listener interface. The addSelectionListener method simply adds an SWT.Selection listener using the SWT low-level listener mechanism. The widgetSelected method is special. It is the entry point into Java when the event occurs. In other words, we will be calling this method from C. When the method is called, it determines the Spinner for the event by looking in the table, and then forwards to the SWT event mechanism by calling notifyListeners .

  public void addSelectionListener(SelectionListener listener) {
     if (listener == null) throw new SWTError(SWT.ERROR_NULL_ARGUMENT);
     addListener(SWT.Selection, new TypedListener(listener));
  }
 
  static void widgetSelected(int handle) {
     Spinner spinner = (Spinner) table.get(new Integer(handle));
     if (spinner == null) return;
     spinner.notifyListeners(SWT.Selection, new Event());
  }

Notice that the Event object that we create for the Spinner selection listener does not need to have any fields set. If you need to return more information for your event, such as the x and y coordinates of the event or the key that was pressed, then your call-in method ( widgetSelected , in this case) will have to have more parameters than just the handle.

One more thing to note before we write the C code. All of our native methods have been defined as static methods, and all of them have the handle passed as the first parameter. This consistency makes it easier to write the C native code.

The full source code listing for the Java Spinner class is in in Appendix B: SpinnerTest and Spinner . Now we need to write the native interface methods in C. First we will write the native interface for Windows, and then we will write it for Motif.

Windows Native Code

Now we really get into JNI and platform programming. We will write a C file called "is-a". It needs to implement the following Java methods from class spinner. Spinner :

  static final native int createControl(int handleParent);
  static final native void computeSize(int handle, int [] result);
  static final native void resizeControl(int handle, int x, int y, int width, int height);
  static final native void setPosition(int handle, int position);
  static final native int getPosition(int handle);
  static final native void setMaximum(int handle, int max);
  static final native int getMaximum(int handle);
  static final native void setMinimum(int handle, int min);
  static final native int getMinimum(int handle);
  static final native void setFont(int handle, int hFont);
  static final native void setFocus(int handle);

It also needs to call in to the following Java method when the selection changes:

  static void widgetSelected(int handle);

The first thing we need to do is to include at least these three files. Your control may require additional files.

#include <jni.h>
#include <windows.h>
#include <commctrl.h>

We will start with the createControl method. Recall that we decided to use a Windows UpDown control. If we create an Edit control first, and then create the UpDown control with UDS_AUTOBUDDY and UDS_SETBUDDYINT flags set, then the Edit control will automatically be associated with the UpDown control's arrows. We can retrieve the Edit control by sending UDM_GETBUDDY to the /UpDown /control. We will show you the complete createControl method after we explain how to call in to Java.

Calling in to Java:

The first time we ever call createControl , we initialize some static variables:

static DWORD tlsIndex = 0;
static jobject javaClass;
static jmethodID mid;
static WNDPROC oldProc;

We use one of them ( tlsIndex ) as a flag to make sure we initialize them only once. Here is the initialization code from createControl :

  if (tlsIndex == 0) {
     tlsIndex = TlsAlloc();
     if (tlsIndex == -1) return (jint) 0;
     javaClass = (*env)->NewGlobalRef(env, (jobject) that);
     mid = (*env)->GetStaticMethodID(env, (jobject) that, "widgetSelected", "(I)V");
     oldProc = (WNDPROC) GetWindowLong((HWND) hwndParent, GWL_WNDPROC);
  }
  TlsSetValue(tlsIndex, (LPVOID) env);

These variables are needed to implement call-in. As this is important code, we will describe each variable that is initialized:

     tlsIndex = TlsAlloc();
     if (tlsIndex == -1) return (jint) 0;
    
  }
  TlsSetValue(tlsIndex, (LPVOID) env);

Here, we allocate a Windows /Thread Local Storage/ (TLS) index, and then (for each Spinner ) we use the TLS index to store a pointer called env . Notice that *JNIEnv env is the first parameter passed to every JNI method. It is a pointer to a function table, and it is only valid in the thread associated with it. We know that we need to call in to Java when the user changes the Spinner value, and that we will be calling in from a Windows "window procedure" or WNDPROC. The WNDPROC does not know about the Java environment. When the WNDPROC is invoked, we will need "env". So we have to save it on creation so that we have it when we need to call in. We also need the class and method ID to call in to, and we can store these in statics because they will be the same across all threads:

     javaClass = (*env)->NewGlobalRef(env, (jobject) that);
     mid = (*env)->GetStaticMethodID(env, (jobject) that, "widgetSelected", "(I)V");

When the user changes the Spinner value, the UpDown sends a WM_VSCROLL to its parent control. In order to see this message, it is necessary to "subclass the window proc" of the parent. In Windows, this means replacing the window proc of the parent with our own window proc. The new WNDPROC will look for WM_VSCROLL (in order to notify the Java code that the Spinner value has been changed) and then call the previous WNDPROC to handle other messages that our control is not interested in. Note that it is important to call the previous WNDPROC, or the parent window will not behave properly (i.e. it will not paint or resize, etc.) We store the previous WNDPROC in oldProc:

     oldProc = (WNDPROC) GetWindowLong((HWND) hwndParent, GWL_WNDPROC);

The last line in createControl before we return the new handle installs a WNDPROC called WindowProc :

  SetWindowLong((HWND) hwndParent, GWL_WNDPROC, (long) WindowProc);

Here is the code for our WindowProc . First we retrieve env from Thread Local Storage and check if an exception has occurred. Then we see if this is an " UpDown value changed" event (a WM_VSCROLL message with SB_THUMBPOSITION in the low order bits of wParam ). If it is, we use env to call in to the Java static method called "widgetSelected", passing lParam as the handle of the UpDown control. Otherwise, we just forward to the parent control's window procedure.

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
  JNIEnv *env = TlsGetValue(tlsIndex);
  if (env != NULL) {
     /* If an exception has already occurred,
      * allow the stack to unwind so that the
      * exception will be thrown in Java. */
     if ((*env)->ExceptionOccurred(env)) return 0;
     switch (msg) {
         case WM_VSCROLL:
            if ((wParam & 0xFFFF) == SB_THUMBPOSITION) {
                return (LRESULT) ((*env)->CallStaticIntMethod(env, javaClass, mid, lParam));
            }
            break;
     }
  }
  return CallWindowProc(oldProc, hwnd, msg, wParam, lParam);
}

And finally, here is the code for createControl :

JNIEXPORT jint JNICALL Java_spinner_Spinner_createControl
  (JNIEnv *env, jclass that, jint hwndParent)
{
  HWND hwndText, hwndUpDown;
  if (tlsIndex == 0) {
     tlsIndex = TlsAlloc();
     if (tlsIndex == -1) return (jint) 0;
     javaClass = (*env)->NewGlobalRef(env, (jobject) that);
     mid = (*env)->GetStaticMethodID(env, (jobject) that, "widgetSelected", "(I)V");
     oldProc = (WNDPROC) GetWindowLong((HWND) hwndParent, GWL_WNDPROC);
  }
  TlsSetValue(tlsIndex, (LPVOID) env);
 
  hwndText = CreateWindowEx(
     WS_EX_CLIENTEDGE,
     "EDIT",
     NULL,
     WS_CHILD | WS_VISIBLE | WS_TABSTOP,
     0, 0, 0, 0,
     (HWND) hwndParent,
     0,
     GetModuleHandle(NULL),
     NULL);
  if (hwndText == 0) return (jint) 0;
  hwndUpDown = CreateWindowEx(
     0,
     UPDOWN_CLASS,
     NULL,
     WS_CHILD | WS_VISIBLE | UDS_AUTOBUDDY | UDS_SETBUDDYINT
	 | UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,
     0, 0, 0, 0,
     (HWND) hwndParent,
     0,
     GetModuleHandle(NULL),
     NULL);
  if (hwndUpDown == 0) return (jint) 0;
  SetWindowLong((HWND) hwndParent, GWL_WNDPROC, (long) WindowProc);
  return (jint) hwndUpDown;
}

The set and get methods we need to implement are much simpler than the createControl and WindowProc methods. Here are setPosition and getPosition . They simply send the UDM_SETPOS or UDM_GETPOS message to the UpDown handle. The remaining set and get methods are similar, and they are listed in Appendix C: Spinner for Windows . The only interesting one is setFont , which sets the font of the Edit control, which it gets by sending UDM_GETBUDDY to the UpDown handle.

JNIEXPORT void JNICALL Java_spinner_Spinner_setPosition
  (JNIEnv *env, jclass that, jint hwnd, jint position)
{
  SendMessage((HWND) hwnd, UDM_SETPOS, 0, position);
}
 
JNIEXPORT jint JNICALL Java_spinner_Spinner_getPosition
  (JNIEnv *env, jclass that, jint hwnd)
{
  return (jint) SendMessage((HWND) hwnd, UDM_GETPOS, 0, 0) & 0xFFFF;
}

The resizeControl method positions the Edit control (the buddy) and the UpDown (the arrow buttons) using the specified coordinates and size. For the width of the arrow buttons, we use the width of a typical vertical scrollbar.

JNIEXPORT void JNICALL Java_spinner_Spinner_resizeControl
  (JNIEnv *env, jclass that, jint hwndUpDown, jint x, jint y, jint
width, jint height)
{
  HWND hwndText = (HWND) SendMessage((HWND) hwndUpDown, UDM_GETBUDDY, 0, 0);
  UINT flags = SWP_NOZORDER | SWP_DRAWFRAME | SWP_NOACTIVATE;
  int upDownWidth = GetSystemMetrics(SM_CXVSCROLL);
  SetWindowPos(hwndText, (HWND) 0, x, y, width - upDownWidth + 2, height, flags);
  SetWindowPos((HWND) hwndUpDown, (HWND) 0, x + width - upDownWidth, y, upDownWidth, height, flags);        
}

The final method we need to write is computeSize . This is typically a complex method, and our computeSize is no exception. We construct a string of digits the same length as the maximum value, and measure its height and width if drawn in the Edit control's font. We make sure our control is no shorter than a combo box, and we add in text margins, and the width of the arrow buttons. In order to return the computed height and width values in the result array, we need to lock down the array using the JNI function GetIntArrayElements to protect it from moving as a result of garbage collection.

JNIEXPORT void JNICALL Java_spinner_Spinner_computeSize
  (JNIEnv *env, jclass that, jint hwndUpDown, jintArray result) {
  int width, height;
  TEXTMETRIC tm;
  RECT rect;
  int comboHeight;
  int max, digits;
  UINT flags;
  char text[64];
  HWND hwndText = (HWND) SendMessage((HWND) hwndUpDown, UDM_GETBUDDY, 0, 0);
  HDC hDC = GetDC(hwndText);
  HFONT oldFont = 0;
  HFONT newFont = (HFONT) SendMessage(hwndText, WM_GETFONT, 0, 0);
 
  jint *result1 = NULL;
  result1 = (*env)->GetIntArrayElements(env, result, NULL);
 
  if (newFont != 0) oldFont = SelectObject(hDC, newFont);
  GetTextMetrics(hDC, &tm);
  comboHeight = GetSystemMetrics(SM_CYVSCROLL);
  height = (comboHeight > tm.tmHeight) ? comboHeight : tm.tmHeight;
  max = SendMessage((HWND) hwndUpDown, UDM_GETRANGE, 0, 0) & 0xFFFF;
  if (max > 0) {
     digits = 0;
     while (max > 0) {
         text[digits] = '0';
         max /= 10;
         digits++;
     }
     flags = DT_CALCRECT | DT_EDITCONTROL | DT_NOPREFIX;
     DrawText(hDC, (LPCTSTR) text, digits, (LPRECT) &rect, flags);
     width = rect.right - rect.left + 3;
  } else {
     width = 10;
  }
  if (newFont != 0) SelectObject(hDC, oldFont);
  ReleaseDC(hwndText, hDC);
  width += GetSystemMetrics(SM_CXVSCROLL);
  SendMessage(hwndText, EM_GETRECT, 0, (LPARAM) &rect);
  if (rect.top == 0) rect.top = 1; // windows bug fix
  width += (rect.left + 1) * 2;
  height += (rect.top + 1) * 2;
    
  result1 [0] = width;
  result1 [1] = height;
  (*env)->ReleaseIntArrayElements(env, result, result1, 0);
}

The full source listing for the Windows C code and makefile are in Appendix C: Spinner for Windows . A batch file sets environment variables and calls make to create the DLL. Options for your compiler and linker may differ, but you will have to link in the win32 libs: comctl32.lib, user32.lib, and gdi32.lib.

Motif Native Code

Now we need to write "is-a" for Motif. In this section, we will only point out the differences between the Motif "is-a" and the Windows one. The full source listing for the Motif C code and makefile are in Appendix D: Spinner for Motif. A shell script sets environment variables and calls make.

The Motif equivalent to Thread Local Storage is called Thread-Specific Data (TSD), and its functions are defined in pthread.h . You will need to include at least the following files:

#include <jni.h>
#include <Xm/XmAll.h>
#include <pthread.h>

In order to store the env pointer in Thread-Specific Data, you first create a key:

static pthread_key_t envKey;
    pthread_key_create(&envKey, NULL);

and then you store into and retrieve from TSD as follows:

  pthread_setspecific(envKey, env);
  JNIEnv *env = (JNIEnv *) pthread_getspecific(envKey);

As you compare the Windows and Motif "spinner.c" listings, you will notice that the JNI portions of the code are identical: method templates, the use of JNI functions like GetIntArrayElements and ReleaseIntArrayElements for locking/releasing an array of integers, and NewGlobalRef , GetStaticMethodID , and CallStaticIntMethod to call in to Java.

The platform code, however, is completely different. On Motif, we create the native control using XmCreateSimpleSpinBox . The Text widget is created automatically and stored in the XmNtextField resource of the SimpleSpinBox . You can retrieve the Text (for setting the font or computing the preferred size) using:

  Arg arg;
  Widget handleText;
  XtSetArg(arg, XmNtextField, &handleText);
  XtGetValues((Widget) handle, &arg, 1);

After creating the SimpleSpinBox , we have to "manage" it, and then we add an XtCallbackProc for the valueChanged callback:

  XtManageChild(handleSpinBox);
  XtAddCallback(handleSpinBox, XmNvalueChangedCallback, (XtCallbackProc)Callback, NULL);

The computeSize method is about as complex as the one for Windows, with height and width based on the maximum number of digits, with margins and shadows added in. Unfortunately, we had to guess a nice width for the arrow buttons, as this value could not be retrieved from a SimpleSpinBox .

Mixing Native and Java Widgets

Occasionally it is necessary to implement a widget using a native widget on one platform, and emulating the widget behavior with Java code on another platform. SWT does this for several widgets; for example, Tree is native on Windows, and emulated on Motif. All of the code to implement Tree on Motif is shipped in the SWT jar for Motif. On Windows, the API for Tree is shipped in the Windows SWT jar and the interface to the native control is shipped in the Windows SWT shared library (SWT.DLL).

When emulating a widget on one platform and wrapping a native or third party widget on another, it is important to subclass at the same point in the Widget hierarchy on both platforms (i.e. Composite or Canvas ). It is also important to make certain that the public API is identical for both widgets. This makes it possible for applications to run on any platform without recompiling.

Looking at the Spinner example in the previous section, suppose we now want to have a Spinner on a third platform that does not have a native SpinBox or UpDown control. Since we subclassed Composite on Windows and Motif, we again subclass Composite on the new platform. Then we create three children: a Text and two arrow Buttons . Then we fill in the API and write our widget as described earlier in the Compound Widget Example section. The full source code listing for this 100% Java Spinner is in Appendix E: Spinner for Any Platform.

要約

Summary

SWTは新しいウィジェットを実装するためにさまざまな方法を提供しています。 もっとも単純で、典型的に使うであろう方法はCanvasCompositeをサブクラス化し、 仕事を行うためのリスナーとメソッドを追加することです。

SWT provides several different ways to implement new widgets. The simplest method, and the one you will typically use, is to subclass Canvas or Composite and add listeners and methods to get the job done.

ある場合には、新しいウィジェットの仕様はあなたの実装において使いたいと思うウィジェットに 既存のSWTウィジェットはよく似ているでしょう。 この場合のお勧めの方法はCompositeのサブクラスでSWTウィジェットをラップし、 ラップされたウィジェットに転送することによってラップされたウィジェットのメソッドのサブセットを 注意深く実装することです。

In certain cases the specification for your new widget will so closely resemble a single existing SWT widget that you will want to use that widget in your implementation. The recommended way to do this is to wrap the SWT widget in a subclass of Composite , and implement a carefully determined subset of the wrapped widget's methods by forwarding to the wrapped widget.

たまに、プラットフォームやサードパーティーのウィジェットを呼び出すプラットフォーム特有の 共有ライブラリを書くことでラップする必要があるかもしれません。

Occasionally, you may need to wrap a platform or third party widget by writing a platform-specific shared library that makes calls to this widget. You can then subclass Composite and provide a Java native interface to your library code.

最後に、とても特別でまれな状況では、既存のSWTのウィジェットをサブクラス化することができますが、推奨されません。

Finally, in very special and rare circumstances, you can subclass an existing SWT widget, but this is not recommended.

Example Code

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.

Microsoft, Windows, Windows NT, and the Windows logo are trademarks of Microsoft Corporation in the United States, other countries, or both.