JPopupMenu内にJListを配置してドロップダウン時刻ピッカーを作成する
Total: 182, Today: 9, Yesterday: 9
Posted by aterai at
Last-modified:
Summary
JPopupMenu内に配置した3つのJListで「hh:mm aa」形式の時刻を選択可能な時刻ピッカーを作成します。
Screenshot

Advertisement
Source Code Examples
class TimePickerPopup extends JPopupMenu {
private final TimePickerField owner;
private final JList<String> hourList;
private final JList<String> minList;
private final JList<String> ampmList;
private final List<String> hourModel;
private final List<String> minModel;
protected TimePickerPopup(TimePickerField owner) {
super();
this.owner = owner;
hourModel = IntStream.rangeClosed(1, 12)
.mapToObj(h -> String.format("%02d", h))
.collect(Collectors.toList());
minModel = IntStream.range(0, 60)
.mapToObj(m -> String.format("%02d", m))
.collect(Collectors.toList());
hourList = createList(hourModel.toArray(new String[0]));
minList = createList(minModel.toArray(new String[0]));
ampmList = createList(TimePickerField.getAmPmStrings());
JPanel listsPanel = new JPanel(new GridBagLayout());
listsPanel.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.BOTH;
c.weightx = 1d;
c.weighty = 1d;
c.insets = new Insets(0, 2, 0, 2);
c.gridx = GridBagConstraints.RELATIVE;
Locale loc = Locale.getDefault();
String hourLabel = ChronoField.HOUR_OF_DAY.getDisplayName(loc);
listsPanel.add(createColumn(hourLabel, hourList, true), c);
String minLabel = ChronoField.MINUTE_OF_HOUR.getDisplayName(loc);
listsPanel.add(createColumn(minLabel, minList, true), c);
String ampmLabel = getAmpmLabel(loc);
listsPanel.add(createColumn(ampmLabel, ampmList, false), c);
JPanel root = new JPanel(new BorderLayout(0, 0));
root.add(listsPanel, BorderLayout.CENTER);
root.add(createFooter(), BorderLayout.SOUTH);
setLayout(new BorderLayout());
add(root);
addPopupMenuListener(new PopupMenuListener() {
@Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
synchronizeFromField(owner.getTimeText());
Point p = popupMenuLocation();
if (p != null) {
setInvoker(owner);
setLocation(p.x, p.y);
}
}
@Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// No operation needed
}
@Override public void popupMenuCanceled(PopupMenuEvent e) {
// No operation needed
}
});
}
private Point popupMenuLocation() {
Component invoker = getInvoker();
if (invoker == null || !invoker.isShowing()) {
return null;
}
Point p = owner.getLocationOnScreen();
p.y += owner.getHeight();
return p;
}
@Override public void show(Component invoker, int x, int y) {
setInvoker(invoker);
Point p = popupMenuLocation();
if (p != null) {
setLocation(p.x, p.y);
setVisible(true);
} else {
super.show(invoker, x, y);
}
}
private static String getAmpmLabel(Locale loc) {
String ampmRaw = ChronoField.AMPM_OF_DAY.getDisplayName(loc);
String ampmLabel;
boolean b1 = !Objects.equals(ampmRaw, "AmPmOfDay");
boolean b2 = !Objects.equals(ampmRaw, "AMPM_OF_DAY");
if (ampmRaw != null && !ampmRaw.isEmpty() && b1 && b2) {
ampmLabel = ampmRaw;
} else {
String[] ap2 = DateFormatSymbols.getInstance(loc).getAmPmStrings();
ampmLabel = ap2[0] + "/" + ap2[1];
}
return ampmLabel;
}
@Override public Dimension getPreferredSize() {
Dimension d = super.getPreferredSize();
d.width = 220;
return d;
}
@Override public final void setLayout(LayoutManager mgr) {
super.setLayout(mgr);
}
@Override public final Component add(Component comp) {
return super.add(comp);
}
private JList<String> createList(String... model) {
JList<String> list = new JList<>(model);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.setFixedCellHeight(20);
list.setFocusable(true);
list.setCellRenderer(new DefaultListCellRenderer() {
@Override public Component getListCellRendererComponent(
JList<?> l, Object val, int idx, boolean sel, boolean focus) {
super.getListCellRendererComponent(l, val, idx, sel, focus);
setHorizontalAlignment(CENTER);
Border border = BorderFactory.createEmptyBorder(1, 4, 1, 4);
setBorder(border);
return this;
}
});
return list;
}
private JPanel createColumn(String label, JList<String> list, boolean alwaysScroll) {
JLabel lbl = new JLabel(label, SwingConstants.CENTER);
lbl.setBorder(BorderFactory.createEmptyBorder(0, 0, 2, 0));
Component sp;
if (alwaysScroll) {
sp = new TranslucentScrollPane(list);
} else {
sp = new JScrollPane(list) {
@Override public void updateUI() {
super.updateUI();
setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
}
};
}
JPanel col = new JPanel(new BorderLayout(0, 1));
col.setOpaque(false);
col.add(lbl, BorderLayout.NORTH);
col.add(sp);
return col;
}
private JPanel createFooter() {
JButton resetBtn = new JButton("Now");
resetBtn.addActionListener(e ->
synchronizeFromField(TimePickerField.getNowString()));
JButton okBtn = new JButton("OK");
okBtn.addActionListener(e -> applyAndClose());
JPanel footer = new JPanel(new FlowLayout(FlowLayout.TRAILING, 6, 1));
footer.add(resetBtn);
footer.add(okBtn);
return footer;
}
public void synchronizeFromField(String text) {
int[] t = TimePickerField.parseTime(text);
int hour = t[0]; // 1..12
int min = t[1]; // 0..59
int ampm = t[2]; // 0=AM, 1=PM
int hourIndex = Math.min(Math.max(hour - 1, 0), 11);
hourList.setSelectedIndex(hourIndex);
minList.setSelectedIndex(min);
ampmList.setSelectedIndex(ampm);
EventQueue.invokeLater(() -> {
scrollToSelected(hourList);
scrollToSelected(minList);
scrollToSelected(ampmList);
});
}
private void scrollToSelected(JList<?> list) {
int idx = list.getSelectedIndex();
if (idx >= 0) {
Rectangle cell = list.getCellBounds(idx, idx);
if (cell != null) {
Rectangle vis = list.getVisibleRect();
vis.y = Math.max(0, cell.y + cell.height / 2 - vis.height / 2);
list.scrollRectToVisible(vis);
}
}
}
private void applyAndClose() {
int hourIndex = hourList.getSelectedIndex();
int minuteIndex = minList.getSelectedIndex();
int ampmIndex = ampmList.getSelectedIndex();
String hour = hourIndex >= 0 ? hourModel.get(hourIndex) : "12";
String min = minuteIndex >= 0 ? minModel.get(minuteIndex) : "00";
String[] ampmStrings = TimePickerField.getAmPmStrings();
String ampm = ampmIndex == 1 ? ampmStrings[1] : ampmStrings[0];
owner.applyTime(hour + ":" + min + " " + ampm.toUpperCase(Locale.ENGLISH));
setVisible(false);
}
}
View in GitHub: Java, KotlinDescription
JFormattedTextFieldの右端にドロップダウン用JButtonを重ねて配置し、ロケール対応の3連JListを配置したJPopupMenuから時刻を選択できるピッカーを作成します。
JFormattedTextField:OverlayLayoutを親JPanelに設定してJFormattedTextFieldの右端の領域にドロップダウン起動用JButtonを重ねて配置MaskFormatterに「「##:## **」##:## UU」形式のマスクを設定して時刻の入力、表示を制限
JButton:JButton#setComponentPopupMenu(...)で設定され、WindowsLookAndFeelなどの環境でボタンが右クリックされた場合でも、マウスポインタの位置ではなく左クリックと同様に親JPanelの左下端を基準とした適正位置へ配置されるようJPopupMenu#show()メソッドやPopupMenuListener内で表示座標を補正- 可視化直前に
JPopup#synchronizeFromField()を呼び出し、JFormattedTextFieldの時刻をJPopupMenu内に配置したJListの表示する時刻と同期
JPopupMenu:JPopupMenuのメイン領域には、時(01..12)、分(00..59)、AM/PM(ロケール対応表示名)を個別に選択可能な3つのJListをGridBagLayoutで横一列に配置- 時・分の領域には垂直スクロールバーを常時表示
- ポップアップが表示される直前に、現在テキストフィールドに保持されている時刻文字列を解析し、各
JListの選択状態を同期 ChronoField、DateFormatSymbolsを使用して実行環境のロケール(日本語環境なら「午前/午後」、英語環境なら「AM/PM」など)に合致した表示ラベルやリスト要素へ切り替え可能に設定
JScrollPane(JList):- 時・分用の縦スクロールバーは常時表示、
AM/PM用は非表示なので3つの列幅の推奨サイズが不均等になるが、これを回避して等幅でレイアウトするため縦JScrollBarをJViewport(JList)の上に重ねて半透明描画するカスタムJScrollPaneを使用している - 時刻の同期後、選択された時・分要素が可能なかぎりスクロール領域の中央付近へ表示されるよう
JList#scrollRectToVisible(...)を用いてビューポート位置を調整している
- 時・分用の縦スクロールバーは常時表示、
Reference
- JFormattedTextFieldと増減用JButtonを組み合わせて時間選択コンポーネントを作成する
- OverlayLayoutの使用
- JScrollBarをJTable上に重ねて表示するJScrollPaneを作成する