• category: swing folder: CalendarTableWithEventBars title: JTableで作成したカレンダー上に複数日予定をJLayerで描画する title-en: Draw multi-day events using JLayer on a calendar created with JTable tags: [JTable, JLayer, Calendar] author: aterai pubdate: 2026-02-16T00:04:37+09:00 description: JTableで作成した月のグリッドカレンダーをJLayerでラップして複数日にわたる予定を日付セルを横断するカラーバーで表現します。 summary-jp: JTableで作成した月のグリッドカレンダーをJLayerでラップして複数日にわたる予定を日付セルを横断するカラーバーで表現します。 summary-en: Wrap the monthly grid calendar created with JTable in a JLayer to display multi-day events using color bars spanning across date cells. image: https://drive.google.com/uc?id=1J3jwfe6DOnB0gQ7b96A8XTS1rR-LZxpQ

Summary

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

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, Kotlin

Description

  • JTable6週×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番目のセルかで判断する
  • イベント名はカラーバーが十分長い場合のみ描画する

Reference

Comment