Summary
JTableで作成した月のグリッドカレンダーをJLayerでラップして複数日にわたる予定を日付セルを横断するカラーバーで表現します。
Screenshot

Advertisement
Source Code Examples
class EventBarLayerUI extends LayerUI<JTable> {
private static final int BAR_HEIGHT = 10;
private static final int BAR_MARGIN = 2;
private final List<EventPeriod> events;
protected EventBarLayerUI(List<EventPeriod> events) {
super();
this.events = events;
}
@Override public void paint(Graphics g, JComponent c) {
super.paint(g, c);
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
JTable table = (JTable) ((JLayer<?>) c).getView();
// Assign tracks (lanes) to events
assignTracksToEvents();
// Draw a color bar for each event
for (EventPeriod ev : events) {
drawEventBars(g2, table, ev);
}
g2.dispose();
}
/**
* Assign track numbers to overlapping events.
*/
private void assignTracksToEvents() {
int[] tracks = new int[events.size()];
boolean[] usedTracks = new boolean[events.size()];
for (int i = 0; i < events.size(); i++) {
EventPeriod event = events.get(i);
Arrays.fill(usedTracks, false);
// Check for overlap with already processed events
for (int j = 0; j < i; j++) {
EventPeriod other = events.get(j);
if (isOverlapping(event, other)) {
usedTracks[tracks[j]] = true;
}
}
// Assign the smallest available track number
int track = 0;
while (track < usedTracks.length && usedTracks[track]) {
track++;
}
tracks[i] = track;
event.setTrack(track);
}
}
/**
* Check if two event periods overlap.
*/
private boolean isOverlapping(EventPeriod e1, EventPeriod e2) {
boolean b1 = e1.getEndDate().isBefore(e2.getStartDate());
boolean b2 = e2.getEndDate().isBefore(e1.getStartDate());
return !(b1 || b2);
}
private void drawEventBars(
Graphics2D g2, JTable table, EventPeriod event) {
LocalDate calendarStartDate = (LocalDate) table.getModel().getValueAt(0, 0);
int daysInTable = DayOfWeek.values().length * CalendarViewTableModel.WEEKS;
LocalDate current = event.getStartDate();
while (!current.isAfter(event.getEndDate())) {
long sinceStart = ChronoUnit.DAYS.between(calendarStartDate, current);
if (sinceStart >= 0 && sinceStart < daysInTable) {
int consecutiveDays = getConsecutiveDaysAndPaintBar(
g2, table, event, current);
current = current.plusDays(consecutiveDays);
} else {
current = current.plusDays(1);
}
}
}
private static void drawEventBar(
Graphics2D g2, EventPeriod event, Rectangle barRect) {
Color clr = event.getColor();
g2.setColor(clr);
g2.fillRoundRect(barRect.x, barRect.y, barRect.width, barRect.height, 5, 5);
g2.setColor(clr.darker());
g2.drawRoundRect(barRect.x, barRect.y, barRect.width, barRect.height, 5, 5);
boolean b = barRect.width > 60;
if (b) {
drawBarTitle(g2, event, barRect);
}
}
private static int getConsecutiveDaysAndPaintBar(
Graphics2D g2, JTable tbl, EventPeriod ev, LocalDate cur) {
LocalDate calendarStartDate = (LocalDate) tbl.getModel().getValueAt(0, 0);
long sinceStart = ChronoUnit.DAYS.between(calendarStartDate, cur);
int trackOffset = ev.getTrack() * (BAR_HEIGHT + BAR_MARGIN);
int headerHeight = tbl.getTableHeader().getHeight();
long daysInWeek = DayOfWeek.values().length;
int weekRow = (int) (sinceStart / daysInWeek);
int dayCol = (int) (sinceStart % daysInWeek);
int consecutiveDays = 1;
LocalDate nextDay = cur.plusDays(1);
boolean notEndOfWeek = dayCol != daysInWeek - 1;
while (!nextDay.isAfter(ev.getEndDate()) && notEndOfWeek) {
consecutiveDays++;
nextDay = nextDay.plusDays(1);
if (dayCol + consecutiveDays >= daysInWeek) {
break;
}
}
Rectangle firstRect = tbl.getCellRect(weekRow, dayCol, false);
Rectangle lastRect = tbl.getCellRect(weekRow, dayCol + consecutiveDays - 1, false);
int barX = firstRect.x + 5;
int barY = firstRect.y + trackOffset + headerHeight;
int barWidth = lastRect.x + lastRect.width - firstRect.x - 10;
drawEventBar(g2, ev, new Rectangle(barX, barY, barWidth, BAR_HEIGHT));
return consecutiveDays;
}
private static void drawBarTitle(Graphics2D g2, EventPeriod event, Rectangle rect) {
g2.setColor(Color.BLACK);
g2.setFont(g2.getFont().deriveFont(9f));
FontMetrics fm = g2.getFontMetrics();
String eventName = event.getName();
int textWidth = fm.stringWidth(eventName);
if (textWidth > rect.width - 6) {
eventName = eventName.substring(0, Math.min(eventName.length(), 5)) + "...";
}
int textX = rect.x + 3;
int textY = rect.y + rect.height / 2 + fm.getAscent() / 2 - 1;
g2.drawString(eventName, textX, textY);
}
}
View in GitHub: Java, KotlinDescription
JTableで6週×7曜日のカレンダーを作成し、これをJLayerでラップして日付セルを横断するイベント期間などのカラーバーを描画する- 描画処理済みのイベント期間と現在処理中のイベント期間が重複するかをチェックし、重なる場合は空いている最小のトラック番号を検索して割り当てる
- 現状のサンプルでは重複するイベント期間のトラックがセルの高さを超えてもセルの高さを調整する機能は実装していないので、その場合ははみ出してしまう
private void assignTracksToEvents() {
int[] tracks = new int[events.size()];
boolean[] usedTracks = new boolean[events.size()];
for (int i = 0; i < events.size(); i++) {
EventPeriod event = events.get(i);
Arrays.fill(usedTracks, false);
// Check for overlap with already processed events
for (int j = 0; j < i; j++) {
EventPeriod other = events.get(j);
if (isOverlapping(event, other)) {
usedTracks[tracks[j]] = true;
}
}
// Assign the smallest available track number
int track = 0;
while (track < usedTracks.length && usedTracks[track]) {
track++;
}
tracks[i] = track;
event.setTrack(track);
}
}
- カラーバーの高さは固定、幅はイベント開始日セルからイベント終了日セルのセル領域を
JTable#getCellRect(...)で取得して決定する- カラーバーはイベント期間が週をまたぐ場合は、週末セルで折り返して描画する
- 週末は国やロケールで異なるので、曜日ではなく
JTable上で6番目のセルかで判断する
- イベント名はカラーバーが十分長い場合のみ描画する