今月はもちろんのこと、過去、または、未来のある月のカレンダーを表示する CalendarFX を作成した。その中で、ComboBox と StringConverter を利用したので、それも含めて説明する。完成形は、次の通りである。
起動時には今月のカレンダーが表示される。過去、または、未来のある月のカレンダーを表示させるには、左上の元号コンボボックスから「西暦」、「明治」、「大正」、「昭和」、「平成」、「令和」を選び、その右の年コンボボックスから年を選び(例えば、「平成」ならば、1から31までのどれか)、月コンボボックスから表示したい月を選び、「表示」ボタンを押す。または、表示されているカレンダーの上下左右にある「前年」、「翌年」、「<」(前月)、「>」(翌月)ボタンを押す。コンボボックスの下に、表示される月が西暦表示で表示される。また、コンボボックスもその時に選ばれた元号になるべく合わして、表示されている月の情報に変更される。
すなわち、「表示」ボタンが押されたら、コンボボックスから表示すべき年月(の西暦表示)を求め、その年月のカレンダーを表示する。「前年」、「翌年」、「<」(前月)、「>」(翌月)ボタンが押されたら、表示すべき年月(の西暦表示)を求め、その年月のカレンダーを表示する。必要ならば、コンボボックスの元号、年、月を修正する。
Scene Builder での作業の結果は、次の通りである。この Eclipse プロジェクト CalenderFX の src の zip ファイルはこちら。
カレンダーを表示する部分が anchorPaneTable である。
まず、カレンダーに表示する西暦年と、各元号に関する、必要な情報を下記の表にまとめる。ここでは、西暦1850年から2052年までのカレンダーを表示する。各元号の開始年と終了年は西暦何年か?ある元号から次の元号へ何時変わるか?などである。
西暦 | 明治 | 大正 | 昭和 | 平成 | 令和 | |
---|---|---|---|---|---|---|
開始年(西暦) | 1850(*1) | 1868 | 1912 | 1926 | 1989 | 2019 |
終了年(西暦) | 2052(*1) | 1912 | 1926 | 1989 | 2019 | 今年(*2) |
開始年(各々) | 1850 | 1 | 1 | 1 | 1 | 1 |
開始日 | — | — | 7月30日 | 12月25日 | 1月8日 | 5月1日 |
終了年(各々) | 2052 | 45 | 15 | 64 | 31 | 2019-今年+1(*2) |
終了日 | — | 7月29日 | 12月24日 | 1月7日 | 4月30日 | — |
開始月 | — | — | 8 | 12 | 2 | 5 |
(*1)1850年から2052年までのカレンダーを表示対象とした。
(*2)令和はいつ終わるか不明なので、一応、今年12月末日までとした。
上の表から、次のことが分かる。
- 明治の終了年と大正の開始年、・・・、平成の終了年と令和の開始年、は一致する(例えば、昭和64年は平成元年でもある)。
- ある月のある日を境にして、それよりも前が古い元号、その日以降が新しい元号に属する。
今回作成するカレンダーは月を表示するので、(上の 2 と矛盾する部分もあるが)各月がどれか一つの元号に属していると見なす。また、各元号の開始月を決め、その開始月以降を新しい元号、その月よりも前を古い元号、に属する、と見なす。更に、(上の 1 より)前の元号と新しい元号は必ずある年を共有する、という所は尊重し、開始月は 2 から 12 までとした。上の表で、開始月の行がこの部分であり、昭和64年1月はわずか7日しかなく、平成元年1月は残り24日もあるが、1月は昭和に属すると見なした。この取り扱いにより、例えば、ユーザーがコンボボックスで、「昭和64年3月」を選び、「表示」ボタンを押すと、(平成元年3月のことなので)コンボボックスは「平成1年3月」と修正され、西暦1989年3月のカレンダーが表示される。
Scene Builder で作成した(VBox の派生クラスである、また、コントローラーでもある)CalendarFXMainVBox の説明に移る。(Main クラスに関しては、ソースコードを参照して下さい。)コンボボックス関連とカレンダー表示用のインスタンス変数は次の通りである。
private static final int startYearInWestern = 1850; private static final int endYearInWestern = 2052; // set some year in the future private static final int startYearOfMeiji = 1868; private static final int endYearOfMeiji = 1912; private static final int startYearOfTaisho = 1912; private static final int endYearOfTaisho = 1926; private static final int startYearOfShowa = 1926; private static final int endYearOfShowa = 1989; private static final int startYearOfHeisei = 1989; private static final int endYearOfHeisei = 2019; private static final int startYearOfReiwa = 2019; private static final int[] startYears = {startYearInWestern, startYearOfMeiji, startYearOfTaisho, startYearOfShowa, startYearOfHeisei, startYearOfReiwa}; private static final int[] startMonths = {-1, -1, 8, 12, 2, 5}; private int[] endYears = {endYearInWestern, endYearOfMeiji, endYearOfTaisho, endYearOfShowa, endYearOfHeisei, -1}; private static final int[] offsets = {0, startYearOfMeiji-1, startYearOfTaisho-1, startYearOfShowa-1, startYearOfHeisei-1, startYearOfReiwa-1}; private static final String[] gengos = {"西暦", "明治", "大正", "昭和", "平成", "令和"}; private int type = 0; // current gengo in comboBoxGengo, 0: 西暦, 1: 明治, 2: 大正, 3: 昭和, 4: 平成, 5: 令和 // type may not be the gengo in which the calendar shows private int realType = 0; // gengo in which the calendar shows private Calendar cal = Calendar.getInstance(); private Date date = new Date(); private int thisYear = 0; private int thisMonth = 0; private int showingYearInWestern = 0; // year in Western in which the calendar shows // showingYearInWester is modified only in showMonth() private int showingMonth = 0; // month which the calendar shows // showingMonth is modified only in showMonth()
これで、cal に、利用可能なカレンダーが、date に今日のデータ(今年の今月)が入力された。FXMLLoader の初期化が完了した時に呼ばれる initialize()(すなわち、初期化)は次の通りである。cal に今日のデータをセットし、今年(thisYear)と今月(thisMonth)を得る。ここでは、令和(type=5)の終了年を今年に決めたのでそのようにセットする。元号コンボボックスのデータをセットし、西暦(type=0)を選ぶ。年コンボボックスに Converter として nengoConverter をセットする。年と月のコンボボックスのデータをセットし、今年と今月を選ぶ。showMonth() で(今年の今月の)カレンダーを表示する。setListeners()、setHandlers() でイベントリスナーとイベントハンドラーを記述する。
@FXML private void initialize() { cal.setTime(date); thisYear = cal.get(Calendar.YEAR); thisMonth = cal.get(Calendar.MONTH); // 0 means January, ..., 11 means December endYears[5] = thisYear; comboBoxGengo.getItems().setAll(gengos); type = 0; comboBoxGengo.setValue(gengos[type]); comboBoxYear.setConverter(nengoConverter); setComboBoxYear(thisYear); comboBoxMonth.getItems().setAll(getAllMonths()); comboBoxMonth.setValue(thisMonth+1); // comboBoxMonth displays 1 for January, ..., 12 for December showMonth(); setListeners(); setHandlers(); }
ここで、getAllMonths() は次の通り、1から12までの Integer 型の配列を返す。
/** * * @return Integer[] which means Integer.valueOf(1), ..., Integer.valueOf(12) */ private Integer[] getAllMonths() { Integer[] retVal = new Integer[12]; for (int j = 0; j < 12; j++) { retVal[j] = Integer.valueOf(j+1); } return retVal; }
まず、comboBoxYear に設定した Converter に関して説明する。comboBoxYear に表示される内容は comboBoxGengo により選ばれている元号(type)により、異なる。例えば、昭和(type=3)では 1 から 64 までであり(対応する西暦年は 1926 から 1989 まで)、平成(type=4)では 1 から 32 までである(対応する西暦年は 1989 から 2019 まで)。表示される年をセットする comboBoxYear.setValue(aYear) の aYear を各元号による年ではなく、対応する(Integer 型に変換した)西暦年の方が、comboBoxYear を type によらない統一した方法で扱えるので望ましい。これを可能にするのが下記の NengoConverter である。行うことは文字列表現の yearInGengo を(Integer 型に変換された)その西暦年に変換する。また、Integer 型の西暦年 yearInWestern を type で与えられた元号で表すと何年になるかの文字列に変換する。以下で、例えば、offset[3] は 1925 である。
private NengoConverter nengoConverter = new NengoConverter(); private class NengoConverter extends StringConverter<Integer> { @Override public Integer fromString(String yearInGengo) { int val = Integer.valueOf(yearInGengo).intValue() + offsets[type]; return Integer.valueOf(val); } @Override public String toString(Integer yearInWestern) { if (yearInWestern == null) return null; // yearInWestern may be null! int year = yearInWestern.intValue() - offsets[type]; return "" + year; } }
showMonth() はカレンダーに表示するデータを作成する部分とそれを表示する部分に分かれる。データを作成する部分は、次のようである。まず、コンボボックスから、次の2つの情報を収集し、対応する変数に代入する。
- 年コンボボックスから表示したい西暦年(showingYearInWestern)
- 月コンボボックスから表示したい月(showingMonth)
コンボボックスの下の表示用ラベル(labelYearInWestern と labelMonth)にそれらの値をセットする。checkGengo() では、例えば、平成31年5月を令和元年5月に調整する、等の作業を行う。(checkGengo()、realType、checkPreviousFollowingButtons() に関しては後述する。)次に、表示したい年月の初日(一日)の曜日(start)とその月の日数(maxDay)を求めるために、cal に表示したい年月と(一日の)日付をセットする。(最終的に、2次元配列に変換するが、その前に、各行を最初の行の右側に並べた 1 次元配列)data に日曜日から土曜日の繰り返しを入力する。データの大きさを適切な 7 の倍数に決め、空文字で初期化する。表示したい年月が西暦2020年4月ならば、初日は水曜日から始まり、30日であるので、水曜日の所から Integer 型の 1、2、・・・、30 まで入れる。
private void showMonth() { // creating data for showing calendar showingYearInWestern = comboBoxYear.getSelectionModel().getSelectedItem().intValue(); showingMonth = comboBoxMonth.getSelectionModel().getSelectedItem().intValue(); checkGengo(); // adjust comboBoxGengo properly realType = comboBoxGengo.getSelectionModel().getSelectedIndex(); // save the current type as realType labelYearInWestern.setText("" + showingYearInWestern); labelMonth.setText("" + showingMonth); checkPreviousFollowingButtons(); // disable buttons for previous or following year or month properly cal.set(showingYearInWestern, showingMonth - 1, 1); // set the calendar cal to the first day of the month, showingMonth -1, in showingYearInWestern int start = cal.get(Calendar.DAY_OF_WEEK); // Sunday: 1, ..., Saturday: 7 int maxDay = cal.getActualMaximum(Calendar.DATE); int n = maxDay + start - 1; int numberOfRows = n/7 + (n % 7 > 0 ? 1 : 0); Object[] data = new Object[numberOfRows * 7]; for (int k = 0; k < data.length; k++) { data[k] = ""; // set default null string } for (int k = 1; k <= maxDay; k++) { data[k+start-2] = Integer.valueOf(k); // set Integer.valueOf(k) for the k-th day of month } // showing calendar for the data obtained above objectMatrixData = new Object[numberOfRows][7]; for (int i = 0; i < numberOfRows; i++) { for (int j = 0; j < 7; j++) { objectMatrixData[i][j] = data[7*i + j]; } } if (objectTableView == null) { objectTableView = new ObjectTableView( new ObjectMatrix(objectMatrixData), false, 40, 54, false, null, columnNames, null ); objectTableView.setSelectedCell(-1, -1); // no cell is selected objectTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); // align all column-widths equally while resizing table width anchorPaneTable.getChildren().add(objectTableView); // width of table changes according to that of anchorPaneTable AnchorPane.setLeftAnchor(objectTableView, 5.0); AnchorPane.setRightAnchor(objectTableView, 5.0); } else { objectTableView.updateTable(new ObjectMatrix(objectMatrixData)); } }
データを表示する部分では、TableView を利用して行列を扱った前回の方法を利用する(詳しくは、前回を参照して下さい。)。上記で作成した1次元配列 data を適切な行数と 7 個の列を持つ、objectMatrixData に代入し、これを利用し、まだ、一度も、objectTableView が作成されていなければ作成し、既に作成されていれば更新する。作成する時は、説明列は非表示(false)、編集は不可能(false)、列名は columnNames とする。更新する時は、変更がないので列名は不要である。ただし、これらのインスタンス変数は、下記のように定義されている。
private Object[][] objectMatrixData = null; private ObjectTableView objectTableView = null; private static String[] columnNames = {"日", "月", "火", "水", "木", "金", "土"};
この showMonth() はカレンダーの表示を一手に引き受けており、初期化時(initialize())以外にも、「表示」、「前年」、「翌年」、「<」(前月)、「>」(翌月)ボタンが押された時も(最終的に)呼び出される。また、カレンダーに表示したい西暦年月を求める時には、元号コンボボックスの情報は利用していない。checkGengo() の後で、元号コンボボックスの情報を realType として、保存し、元号コンボボックスの内容を表示されているカレンダーと一致させるために、showPreviousFollowingYearMonth(incYear, incMonth) と checkGengo() で利用している。詳しくはここを参照。
setListeners() では、comboBoxGengo のリスナーを次のように記述している。type に選択された元号を入れ(0: 西暦、1: 明治、2: 大正、3: 昭和、4: 平成、5: 令和)、setComboBoxYear(showingYearInWestern) を実行する。この時点で type と選ばれている元号コンボボックスの内容が一致する。
private void setListeners() { comboBoxGengo.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> ov, Number oldValue, Number newValue) { type = newValue.intValue(); // type and current gengo in comboBoxGengo have the same value setComboBoxYear(showingYearInWestern); } }); }
下記の通り、setComboBoxYear(showingYearInWestern) は選ばれた元号(type)に応じ、年として適切な表示範囲(例えば、2の大正ならば1から15)を設定し、現在、カレンダーに表示されている年(showingYearInWestern)に最も近い年を選ぶ。
/** * type is selected in comboBoxGengo * set suitable items and select a proper value in comboBoxYear * select the nearest one to year between startYears[type] and endYears[type] */ private void setComboBoxYear(int year) { comboBoxYear.getItems().setAll(getAllYears()); if (year >= startYears[type] && year <= endYears[type]) { comboBoxYear.setValue(Integer.valueOf(year)); } else if (year < startYears[type]) { comboBoxYear.setValue(Integer.valueOf(startYears[type])); } else { comboBoxYear.setValue(Integer.valueOf(endYears[type])); } } /** * * @return Integer[] which means Integer.valuOf(startYears[type]), ..., Integer.valueOf(endYears[type]) */ private Integer[] getAllYears() { int start = startYears[type]; int end = endYears[type]; Integer[] retVal = new Integer[end - start + 1]; int i = 0; for (int j = start; j <= end; j++) { retVal[i++] = Integer.valueOf(j); } return retVal; }
setHandlers() では、「表示」ボタン、「前年」、「翌年」、「<」(前月)、「>」(翌月)ボタンのハンドラーを記述している。buttonShow が押されたら、上述した showMonth() を呼び出す。他のボタンが押されたら、showPreviousFollowingYearMonth(incYear, incMonth) を呼び出す。
private void setHandlers() { buttonShow.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { showMonth(); } }); buttonPreviousYear.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { showPreviousFollowingYearMonth(-1, 0); } }); buttonFollowingYear.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { showPreviousFollowingYearMonth(1, 0); } }); buttonPreviousMonth.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { showPreviousFollowingYearMonth(0, -1); } }); buttonFollowingMonth.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { showPreviousFollowingYearMonth(0, 1); } }); }
ここで、realType の利用法を説明する。ある月のカレンダーを表示する「表示」ボタンを押した時の処理(これを処理S と呼んでおく)とそれ以外の(「前年」、「翌年」、「<」(前月)、「>」(翌月))ボタンを押した時の処理(これを処理PF と呼んでおく)の違いについて述べる。処理S は、showMonth() で行っている。処理PF も、最終的に showMonth() を呼び出す。処理S で表示するカレンダーは、その時点において、年コンボボックスで選ばれている西暦年(showingYearInWestern)の月コンボボックスで選ばれている月(showingMonth)であり、元号は元号コンボボックスで選ばれている元号(type)である。しかし、処理PF で表示するカレンダーの西暦年と月は、その時点において表示されているカレンダーの西暦年と月であり、その時点において選ばれている、年と月コンボボックスの値とは限らない。元号も、その時点において表示されているカレンダーが表示された時点での元号であり、その時点において選ばれている元号コンボボックスの値とは限らない。(例えば、元号、年、月コンボボックスで値を変更したが、「表示」ボタンを押さず、「翌年」ボタンを押した場合である。)従って、処理PF において、showMonth() を呼び出す前に、これから表示したいカレンダーの西暦年月にコンボボックスの値を変更しておく必要がある。また、元号コンボボックスの値も表示されているカレンダーが表示された時点での元号(これは、直前に処理S、または、処理PF を行った時点での元号の値であり、showMonth() の checkGengo() を呼び出した後で、realType としてこの元号を保存すれば良い)に戻す必要がある。
showPreviousFollowingYearMonth(incYear, incMonth) は最終的に showMonth() を呼ぶ前に、必要な前処理を行っている。前段で述べたように、この前処理は、次のように、type を realType に戻すこと。(実際に comboBoxGengo の値が type に設定され、両者が一致するのは showMonth() の中にある checkGengo() においてである。)incYear、incMonth の値により、対応する年、月コンボボックスの値を次または前にセットすること、である。
private void showPreviousFollowingYearMonth(int incYear, int incMonth) { if (incYear*incMonth != 0) return; type = realType; // this assignment takes effect at checkGengo() in showMonth() if (incYear == 1) { comboBoxYear.setValue(Integer.valueOf(showingYearInWestern+1)); showMonth(); } else if (incYear == -1) { comboBoxYear.setValue(Integer.valueOf(showingYearInWestern-1)); showMonth(); } else if (incMonth == 1) { if (showingMonth < 12) { comboBoxMonth.setValue(Integer.valueOf(showingMonth+1)); showMonth(); } else { comboBoxYear.setValue(Integer.valueOf(showingYearInWestern+1)); comboBoxMonth.setValue(Integer.valueOf(1)); showMonth(); } } else if (incMonth == -1) { if (showingMonth > 1) { comboBoxMonth.setValue(Integer.valueOf(showingMonth-1)); showMonth(); } else { comboBoxYear.setValue(Integer.valueOf(showingYearInWestern-1)); comboBoxMonth.setValue(Integer.valueOf(12)); showMonth(); } } else { return; } }
type を realType に戻すと、元号コンボボックスの値とtypeの値が一致しない(可能性がある)が、showMonth() 内の checkGengo() において、一致するようになる。
checkGengo() では、表示されるカレンダーに適切な元号を元号コンボボックスに表示する。checkGengo() は showMonth() の中でのみ呼ばれ、showMonth() は initialize() 内で、「表示」ボタンが押された時(処理S のため)、showPreviousFollowingYearMonth(incYear, incMonth) 内で(処理PF の一部として)、呼ばれる。次のコードの最後の行の comboBoxGengo.setValue(・・・) により、元号コンボボックスの値が変更され、その結果、年コンボボックスも適切に修正される。type の値により、想定される処理の例は次の通りである。
0(西暦)
直前の showMonth() 実行時に元号が西暦であり(realType = 0)、その後、元号コンボボックスが変更された可能性があるが「表示」ボタンは押されず、それ以外の「前年」、「翌年」、「<」(前月)、「>」(翌月)ボタンが押された。または、元号コンボボックスが西暦の時に、「表示」ボタンが押された。従って、元号コンボボックスは西暦のままである。
1(明治)
直前の showMonth() 実行時に元号が明治であり(realType = 1)、その後、元号コンボボックスが変更された可能性があるが「表示」ボタンは押されず、「前年」ボタンが押された。従って、元号、年コンボボックスを西暦1867年とする。
表示したいのが明治45年10月であるが、10月は大正に属するので、元号、年コンボボックスを大正元年10月にする。
2、3、4(大正、昭和、平成)
表示したいのが昭和元年11月であるが、11月は大正に属するので、元号、年コンボボックスを大正15年11月にする。
表示したいのが昭和64年3月であるが、3月は平成に属するので、元号、年コンボボックスを平成元年3月にする。
5(令和)
表示したいのが令和元年4月であるが、4月は平成に属するので、元号、年コンボボックスを平成31年4月にする。
表示していたのが令和2年であり、「翌年」ボタンを押したので、(令和は2年までと解釈しているので)元号、年コンボボックスを西暦2021年とする。
/** * showMonth() is about to display showingMonth in showingYearInWestern * type here is type (gengo) if buttonShow is pressed or realType if one if the others is pressed * select the right gengo in comboBoxGengo */ private void checkGengo() { int inc = 0; switch (type) { case 0: break; case 1: // 明治 if (showingYearInWestern == startYears[type] - 1) { // this case happens, for example, when buttonPreviousYear is pressed in the first year of Meiji inc--; // the right gengo is 0 } if (showingYearInWestern == endYears[type]) { if (showingMonth >= startMonths[type+1]) { // this case happens, for example, when showing September in the 45-th year of Meiji inc++; // the right gengo is 2 } } break; case 2: // 大正 case 3: // 昭和 case 4: // 平和 if (showingYearInWestern == startYears[type]) { if (showingMonth < startMonths[type]) { // this case happens, for example, when showing January in the first year of Heisei inc--; // the right gengo is type - 1 } } else if (showingYearInWestern == endYears[type]) { if (showingMonth >= startMonths[type+1]) { // this case happens, for example, when showing May in the 31-st year of Heisei inc++; // the right gengo is type + 1 } } break; case 5: // 令和 if (showingYearInWestern == startYears[type]) { if (showingMonth < startMonths[type]) { // this case happens, for example, when showing March in the first year of Reiwa inc--; // the right gengo is type - 1 } } if (showingYearInWestern == endYears[type] + 1) { // this case happens, for example, when buttonFollowingYear is pressed in the second year of Heisei inc++; // the right gengo is 0 } break; } comboBoxGengo.setValue(gengos[(type+inc) % 6]); // (type+inc) % 6 is the right gengo }
checkPreviousFollowingButtons() では、表示する西暦年月をチェックし、「前年」、「翌年」、「<」(前月)、「>」(翌月)ボタンを利用可能か不可能にする。
private void checkPreviousFollowingButtons() { buttonPreviousYear.setDisable(showingYearInWestern <= startYears[0]); buttonPreviousMonth.setDisable(showingYearInWestern <= startYears[0] && showingMonth <= 1); buttonFollowingYear.setDisable(showingYearInWestern >= endYears[0]); buttonFollowingMonth.setDisable(showingYearInWestern >= endYears[0] && showingMonth >= 12); }
カレンダーにおいて、日曜日(第0列)は赤(red)、土曜日(第6列)は青(blue)、文字のサイズは14で太くしたい。そのために、ObjectTableView で利用している TableColumn にクラスを設定し、それを利用する。前回に利用した ObjectTableView に追加した部分は、次の通りである。
/** * set column name, cellValueFactory, cellFactory, and setOnEditCommit for columnValues, and add these columnValues to this tableView */ private void setColumnValues() { /* ・・・ */ columnValues = new TableColumn[numberOfColumns]; for (int j = 0; j < numberOfColumns; j++) { final int jj = j; columnValues[j] = new TableColumn<ObjectRowVector, Object>(); if (columnValueNames != null) { columnValues[j].setText(columnValueNames[j]); } columnValues[j].getStyleClass().add("table-column-" + j); // added for CalendarFX /* ・・・ */ } }
また、前回利用した NonnegativeInteger 等を添付しなかったので、それらの部分をコメントアウトしておいた。
applicaton.css は次の通りである。
.table-column-0 { -fx-text-fill: red; -fx-font-size: 14; -fx-font-weight: bolder; } .table-column-1, .table-column-2, .table-column-3, .table-column-4, .table-column-5 { -fx-font-size: 14; -fx-font-weight: bolder; } .table-column-6 { -fx-text-fill: blue; -fx-font-size: 14; -fx-font-weight: bolder; }