GUIのプログラミング (2): リサジュー図形のグラフ


課題7   リサジュー図形

文字列表示用の JLabel、値設定用の JSlider、ボタン JButton、グラフ描画用の JPanel を使って、さまざまな リサジュー (Lissajous) 図形 を表示できるプログラムを作る。

リサジュー図形は互いに垂直な単振動が組み合わさってできる図形である。オシロスコープの実験をやったことがある人にはおなじみと思う。
今回は t を媒介変数、m, n, δ を整数、x を水平方向、y を鉛直方向として

x = cos(m t)
y = cos(n t + δ)

で表されるときの図形を表示させる。



Eclipse を起動し、プロジェクトが何も開いていない状態とし、

メニューの 「ファイル(F)」-「新規(N)」 の中の 「Java プロジェクト」 を選択

し、プロジェクト名を

Lissajous

とする。

また、画面の下の方に

module-info.javaファイルの作成

というチェックボックスがある。もしこれにチェックが入っていたら、クリックしてチェックを外す。

以上が設定出来たら、「完了(F)」 をクリックしてプロジェクト作成を完了させる。

次に、フレームウィンドウを作成する。

メニューの 「ファイル(F)」-「新規(N)」 の中の 「その他(O)...」 を選択

WindowBuilder の中の Swing デザイナー にある
「JFrame」

を選択する。これにより JFrame を拡張した独自クラスのフレームウィンドウが作成される。

「パッケージ(K)」 を   ap1.lissajous
「名前(M)」 を   Lissajous

として、「完了(F)」 をクリックする。これにより、プロジェクトフォルダの

src\ap1\lissajous

フォルダに、JFrame を拡張したクラス Lissajous が入ったファイル Lissajous.java が生成されることになる。

前回のGUIプログラム演習ではグラフ描画用のパネルを JPanel を継承した内部クラスとしたが、今回はメインとは別ファイルを使い、JPanel を継承した独立したクラスを定義して利用する。

メニューの 「ファイル(F)」-「新規(N)」 の中の 「その他(O)...」 を選択

WindowBuilder の中の Swing デザイナー にある
「JPanel」

を選択する。これにより JPanel を拡張した独自クラスが作成される。

「パッケージ(K)」 を   ap1.lissajous   (これはクラス Lissajous と同一)
「名前(M)」 を   GraphPanel

として、「完了(F)」 をクリックする。これにより Lissajous.java と同じフォルダに、JPanel を継承したクラス GraphPanel を持つファイル GraphPanel.java が作成され、編集可能になる。


ファイル Lissajous.java を開き、「デザイン」タブをクリックしてデザインモードに入る。

まず、フレームの title プロパティを

リサジュー図形 (自分の学籍番号と氏名)

とする。これはフレームウィンドウのタイトルバーに表示される。

部品の配置はレイアウトマネージャに従うが、contentPane の初期設定である BorderLayout (完全修飾名は java.awt.BorderLayout) では、今回の演習のような単純ではない配置を実現することは難しい。

Swing デザイナー では、

単純名完全修飾名
GridBagLayoutjava.awt.GridBagLayout
GroupLayoutjavax.swing.GroupLayout
SpringLayoutjavax.swing.SpringLayout

といった、ビジュアルデザイン向けのレイアウトマネージャを利用して、サイズを変更しても柔軟に部品の位置・サイズを変えてくれるような配置が実現できる。
ただ、上記のようなレイアウトマネージャを使ったデザインは、学習する事項が多くあり、入門者が簡単に使いこなせるものではない。
望むらくは上記のレイアウトマネージャの内のひとつ (例えば GroupLayout) を使って配置してほしいが、時間の制限もあり、難しいのではないかと思う。
そこで今回の演習では、レイアウトマネージャを使わず、絶対配置と言われる配置方法を使うものとして説明していく。

絶対配置にする場合、contentPane の Layout の設定で

絶対レイアウト

を選択する。そうすると、マウスで指定した位置に、ドラッグした大きさで部品を自由に配置できる。ただし、絶対配置の欠点として、ウィンドウのサイズ変更には対応できなくなる。

下図のように部品を配置する。
なお、プログラムの都合上 lblFormula と graphPanel は先に初期化されている必要があり、また、各スライダーの上のラベルがスライダーより先に初期化されている必要がある。従って、部品の配置の際、左側から先に、上から下に、配置していくこと

Lissajous部品配置

なお、今回は全ての部品をインスタンス変数 (フィールド) とする
部品を配置したら、下図の箇所のアイコンが下の赤から上の緑へ向かう上向き矢印の場合は

Convert local to field

ボタンをクリックして下向き矢印のアイコンに変更しておくこと。
既に下向き矢印のアイコンになっている場合はクリックしてはいけない。

JLabel である lblFormula は text プロパティとして

式:   x = cos(m t),   y = cos(n t + δ)

を設定し、「拡張プロパティの表示」ボタンをクリックして拡張プロパティを表示させ、

border プロパティを

BevelBorder (lowered)

にして、くぼんだ感じのラベルとする。fontプロパティを変更し、フォントを大きくして見栄えを良くする。


フォントは、具体的な個別フォント、例えば

MS P明朝

などを指定してもよいが、Windows以外の環境ではうまく表示されない可能性がある。
そこでJavaでは、環境に依存しないフォント種として以下の論理フォントが用意されている。これらを指定すると、実際には該当する具体的フォントの一つが選ばれることになる。

論理フォント名定数説明
Serif
(セリフ)
Font.SERIFセリフ (文字の線の端につけられる飾り) のある書体の総称。欧字 (ラテン文字) ではローマン体。一般に日本語では明朝体になる
SansSerif
(サンセリフ)
Font.SANS_SERIFセリフ (文字の線の端につけられる飾り) のない書体の総称。一般に日本語ではゴシック体になる
MonospacedFont.MONOSPACED等幅フォント
DialogFont.DIALOGダイアログ (メッセージ喚起・選択・各種設定で使われるウインドウ) での表示に使われる
DialogInputFont.DIALOG_INPUTダイアログでの入力で使われる

今回のアプリケーションでは、グラフのパラメータ設定を表すという意味で、すべて Dialog 論理フォントを使用する。サイズを適切に設定し、また必要に応じて太字 (Font.BOLD を指定) とする。


JLabel である lblM, lblN, lblDelta は、text プロパティとして、それぞれ

m
n
δ

を設定し、フォントを適切に設定する。

グラフ用パネルは、JPanel として配置し、名前を graphPanel と変更し、インスタンス変数 (フィールド) とする。
なお、デザインの際、サイズを縦横同じとして正方形のパネルにしておくこと。

スライダ JSlider (javax.swing.JSlider クラス) では目盛りや数字を表示させることができ、値調整用の部品として使用が推奨されている。

JSlider である sliderM, sliderN は、orientation プロパティを

VERTICAL

として、適切な大きさとする。また、以下のようにプロパティを設定する。

minimum は 0 のまま、maximum は 30、value は 0、
minorTickSpacing は 2、majorTickSpacing は 10
paintLabels は true、paintTicks は true

フォントも目盛りサイズがちょうど良くなるような大きさに設定する。
もう一つの sliderDelta は、orientation は

HORIZONTAL

のまま、適切な大きさとする。また、以下のようにプロパティを設定する。

minimum は -180、maximum は 180、value は 0、
minorTickSpacing は 20、majorTickSpacing は 60
paintLabels は true、paintTicks は true

フォントも目盛りサイズがちょうど良くなるような大きさに設定する。

次に、GraphPanel.java を開き、GraphPanel クラスの中味を入力する。
ソース冒頭の package 指定、import の部分はそのまま残し、クラス本体はおおよそ以下のようなものとする。
ただし、すでに入っているSwingデザイナーが自動生成したコード (下コードの青色部分) は保持する形で入力する。クラス GraphPanel とコンストラクタ GraphPanel() は public のままでよいが、無指定でも構わない。

※ Graphics, Graphics2D, Color, Font 等で出るimport不足のエラーについては、エラーを示す赤い波線にマウスカーソルを持っていくとクイックフィックスで候補が示されるので適宜importを補うこと。Color など複数のimport候補がある場合、java.awt パッケージのもののimportを選ぶこと。


GraphPanel クラスは、インスタンス変数として、グラフ用のパラメータ、色、および、リサジュー図形のパラメータを持っている。リサジュー図形のパラメータは下表の変数となる。

リサジュー図形のパラメータの変数
変数名内容
intcoeffMリサジュー図形のパラメータ m
intcoeffNリサジュー図形のパラメータ n
intphaseDiffリサジュー図形のパラメータ δ (ただし度単位)

changeFormula メソッドは、Lissajous クラスのGUI部品のイベントハンドラから呼び出され、リサジュー図形のパラメータを設定し、repaint を呼び出すことによりパネルの再描画をGUIシステムに要求する。

GUIシステムから呼び出される paintComponent メソッドは、パネル内面の描画を担当する。
背景色でパネルを埋めた後、グリッド線を描く。
次に、パラメータ t を変化させ、リサジューの式に従って x, y を設定し線を引いていく。三角関数のパラメータはラジアン単位にして与えなければならないことに注意する。 for構文の中で定義されている変数 deg は度単位の角度であるため、ラジアン単位に変換して t に代入して使う。度からラジアンへの変換は π180 を乗じればよい。また、Math.toRadians メソッド (Mathクラスのstaticメソッド toRadians) を使用してもよい。
また、コサインは Math.cos メソッドで求められる。
係数 m, n は、プログラム内では、それぞれ coeffM, coeffN に対応している。
y の式で使う位相差 δ については、for 文に入る前に、度単位の phaseDiff をラジアン単位に変換した値を持つ変数 phaseDiffRad を定義しているので、これを使えばよい。


今回は、Graphics クラスよりも高機能な Graphics2D (抽象クラス java.awt.Graphics2D) を使用している。メソッド paintComponent(Graphics g) の最初の辺りで

Graphics2D g2 = (Graphics2D) g;

のように、メソッドのパラメータである Graphics 型の変数 g を Graphics2D 型に キャスト し変数 g2 にセットし、以降は g の代りに g2 を使用すればよい。
※ 今回は、線の太さを指定する際 ( setStroke メソッド ) などに使用している。


GraphPanel クラスでは、グラフ用のパラメータとして、

X座標の最小値・最大値    xMin, xMax
Y座標の最小値・最大値    yMin, yMax
X, Y方向のグリッドの分割数    xGridDiv, yGridDiv

を先頭で定義してある。これらを変化させれば、いろいろなグラフに対応できる。



次に、Lissajous.java に戻り、デザインモードでイベントハンドラを設定していく。

その前に準備として、JPanel として配置した graphPanel を GraphPanel 型に変更する。Lissajous.java のソースで、クラス Lissajous の先頭近くにある

private JPanel graphPanel;

とし、また、コンストラクタ内にある

graphPanel = new JPanel();

に変更すればよい。
ただし、この変更を行うとデザイン画面でパネルが見えなくなる場合がある。一通りデザインが終った段階で変更したほうがよい。Lissajous.java を開きなおすと表示が戻る。

もう一つの準備として、クラス Lissajous に、リサジュー図形のパラメータを記憶しておくインスタンス変数を追加し、さらに、図形パラメータの設定を行うための setFormula メソッドを追加する。
インスタンス変数は、クラス Lissajous の最初の辺りに、以下を追加する。

Lissajous-instance-vars

クラス GraphPanel にも、同じ名前のインスタンス変数があるが、クラスが違うので、全く別個の変数になる。下記で示すように、Lissajous クラスの setFormula メソッドおよび GraphPanel クラスの changeFormula メソッドによって、両クラスのリサジュー図形のパラメータが同一値に保たれるようにしている。

次に、Lissajous図形のパラメータを設定し式の表示を更新するためのメソッド setFormula を作る。

setFormula メソッド
Lissajous図形のパラメータを設定し、式を更新する
private void setFormula(int m, int n, int delta)
パラメータ名説明
intmLissajous図形の式の m に設定する値
intnLissajous図形の式の n に設定する値
intdeltaLissajous図形の式の位相差 δ に設定する値。ただし、単位は度

以下のようなコードになる。これを Lissajous クラスに追加する。
コード内で位相差 δ の表示に関わる部分では度単位の値の文字列に続けて、度記号 ° (U+00B0) を付加していることに注意する。

setFormula-code

JLabel 等の内部に text プロパティ (文字列) を持つGUI部品では、

public String getText()

で、文字列の内容を String オブジェクトとして得ることができ、また、

public void setText(String t)

によって、パラメータ t の内容を内部の text プロパティに設定することができる。
ここでは、lblFormula で示されている式を、値に合うように文字列として合成し、setText メソッドで設定した後、GraphPanel の changeFormula メソッドを呼び出し、リサジュー図形のパラメータをクラス Lissajous とクラス GraphPanel の双方で同一にし、グラフの表示を更新している。

GraphPanel の changeFormula メソッドでは、リサジュー図形のパラメータを設定し、グラフを再描画する。changeFormula メソッドの中で repaint が呼び出されているので、Lissajous のイベントハンドラ本体では graphPanel の repaint メソッドを呼び出さなくてよい。

また、JLabel の setText メソッドで文字列が変更される際には repaint も呼び出されるので、lblFormula に対して repaint を明示的に呼び出す必要はない。

準備が終ったら、まずは、各スライダに対するイベントハンドラを設定する。
スライダのノブが動かされる際には、クラス ChangeEvent (javax.swing.event.ChangeEvent) によるイベントが発生するので、インタフェース ChangeListener (javax.swing.event.ChangeListener) というイベントリスナの stateChanged イベントハンドラ でイベントを処理すればよい。

JSliderのイベント
イベント内容イベントリスナイベントハンドラ
ChangeEvent
スライダの状態が変更された。
ノブが移動開始・移動中・移動終了など
ChangeListenervoid stateChanged(ChangeEvent e)

スライダの値は getValue メソッド で取得できる。

各スライダに対する stateChanged ハンドラでは、値を取得し、設定が覚えているパラメータ値と違っていたら、変更されたパラメータを変え、その他のパラメータは元のまま、 setFormula メソッドを呼び出せばよい。
例えば sliderM に対するイベントハンドラは、デザイン画面で sliderM を選択した状態で、Event の change から stateChanged を選択して

空白部分をダブルクリックすると、stateChanged イベントハンドラ部分のソース入力に変わるので、以下のようなコードで設定できる。

sliderM-stateChanged-code

スライダの値を変数に入れ、変わっていた場合、対になっているラベル部品の text プロパティを setText メソッドにより変更し、さらに、その値および覚えているもののままの他のパラメータとともに setFormula メソッドを呼び出す。
m の値については、スライダ sliderM の値を getValue メソッドで読み取り、覚えている値 coeffM と比べ、変わっていた場合、ラベル lblM の m の値表示を更新し、setFormula を、読み取った m の値および覚えている n の値を表す coeffN、δの値を表す phaseDiff を3つのパラメータとして渡し、式表示の更新およびグラフの更新を行わせる。

他のスライダに対するイベントハンドラにおいても、同様なコードとする。

パラメータ n については、スライダ sliderN の値を読み取り、coeffN と異なっていた場合に、ラベル lblN の文字列を適切に設定し、新しい n の値と、覚えている coeffM と phaseDiff とともに setFormulaメソッド を呼び出す。

パラメータ δ については、スライダ sliderDelta の値を読み取り、phaseDiff と異なっていた場合に、ラベル lblDelta の文字列を適切に設定 (最後に ° (度記号) をつける) し、新しい δ の値と、覚えている coeffM と coeffN とともに setFormula メソッドを呼び出す。

また、Lissajous.java の「デザイン」画面で「終了」ボタンをダブルクリックし、actionPerformed イベントハンドラ に、プログラムを終了させるコード (System.exit メソッドの呼び出し。終了コードは 0) を記述する。

実行画面は例えば以下のようになる。

Lissajous実行画面例

できるだけ多くのパラメータの組合せを調べた方がよいが、全部を網羅することは難しいので、以下を中心に調べること。

多く見られる間違い


Eclipse上で課題プログラムが正しく動いたのを確認した後、プログラムの実行可能jarファイル Lissajous.jar を作る。作成は「ファイル(F)」-「エクスポート(O)」で、

「起動構成(L)」に「Lissajous - Lissajous」が設定されていなければ、ドロップダウンリストで設定

した上で、適当な保存場所と名前 Lissajous.jar を指定しエクスポートすること。

jarファイルの実行方法 を参考にして、作ったjarファイル Lissajous.jar が正常に実行されることを確認する。

すべての動作に問題がないと確認できたら、プログラムの実行可能jarファイル Lissajous.jar を講義のWebページから提出。


課題8   バラ曲線   (任意提出・発展課題)

バラ曲線 (rose curve) と言われる図形があり、一般的には、極座標 (r, θ) を使った次の式で表される。

r = cos(kθ)    (k は定数)

これを基に、t を媒介変数、n, d を整数、x を水平方向、y を鉛直方向として

x = cos(n/d t)cos(t)
y = cos(n/d t)sin(t)

で示すような k = n/d とした式を使ってバラ曲線を表示させるプログラムを作りなさい。


上のLissajous図形描画アプリケーションを基にして作成する。

まず、Lissajousプロジェクトをコピーする。
Eclispe の左上端窓で、プロジェクト名部分をクリックした状態で

「編集(E)」 - 「コピー(C)」

に続いて、

「編集(E)」 - 「貼り付け(P)」

とすれば、コピーされたプロジェクトが出来るので、プロジェクト名を

Rose

としておく。以下では、元のプロジェクトを閉じ、Rose のみ開いた状態で作業を行う。最初に

パッケージ名を ap1.lissajous から ap1.rose に、
フレームクラス名を Lissajous から Rose に

それぞれ変更してから、実際の修正作業をすること。

以下に、実行画面例を示す。

起動画面n=1, d=1画面

n=2, d=1画面n=1, d=2画面

n または d の値が大きくなっても、曲線をできるだけ滑らかに、粗くならないように、表示させること。
下に n = 20, d = 1 および n = 1, d = 24 のときの表示例を示す。

n=20, d=1画面n=1, d=24画面

正しくバラ曲線を描くには、曲線の周期と変化の度合いに注意する必要がある。

周期については、p, q を互いに素な自然数として、

k = n / d = p / q

とする既約分数で k が表されるとき、r = cos(k θ) の周期は、ラジアン単位で

π q (p, q が共に奇数のとき)
2π q (上記以外のとき)

となる。なお、n と d の最大公約数を求め、n と d をその値で割ることで p と q が求められる。
※ 最大公約数は Euclid の互除法で求められる。例えば以下のメソッドを使えばよい。

    /**
     * 2つの正整数の最大公約数を求める.
     * @param m 正の整数1
     * @param n 正の整数2
     * @return 最大公約数
     */
    private static int gcd(int m, int n) {
        while (n != 0) {
            int t = n;
            n = m % n;
            m = t;
        }
        return m;
    }

変化の度合いについては、厳密には微分 (速度) を計算し評価すべきだが、大まかに言うと、 r = cos(k θ) の変化の度合いは、単位円 (速度の大きさは全範囲で 1 で一定) に比較して、

max( k, 1 )

倍程度の量になる。つまり、k が大きいときは細かくプロットする必要がある。


課題プログラムのすべての動作に問題がないと確認出来たら、下記の「プロジェクトのアーカイブ作成方法」を参考にして、プロジェクトのファイルをすべてまとめたzipファイルを作る。

プロジェクトのzipファイルを講義のWebページから提出。


※ プロジェクトのアーカイブ (zipファイル) 作成方法

Eclipseで、まとめファイルを作りたいプロジェクトを開いた状態で、

「ファイル(F)」
「エクスポート(O)」

で、

「一般」

の中にある

「アーカイブ・ファイル」

を選択し、

「次へ(N)」

をクリックする。

次の画面では、

該当のプロジェクトだけにチェックが入っているはず。
「宛先アーカイブ・ファイル(A)」の「参照(R)」ボタンをクリックし、保存場所と名前を指定する。
ファイル名は、例えば「プロジェクト名Pack.zip」のように、拡張子を .zip とする。
オプションは「zipフォーマットで保管(Z)」がセットされているはず。

と入力・設定した上で、

下部にある「完了(F)」ボタンをクリック

すれば、指定した場所にzipファイルが作成される。