Kalendarz miesięczny – komponent Swing
Napisanie komponentu ‘Kalendarz’ nie jest trudnym zadaniem, ale wymaga włożenia sporej ilości pracy.
Pierwszy problem to ustawienie zmieniających się położeń dni na nie zmieniającej się planszy i wyświetlenie tylko tych dni, które w danym miesiącu powinny być widoczne, w prawidłowym układzie
Drugi problem to zaznaczenie niedziel i dni świątecznych. Jest to łatwe dla świąt stałych, ale trudne dla Wielkanocy i Bożego Ciała, gdyż data ich zmienia się z roku na rok. trzeba więc daty tych świąt obliczyć. Dni świąteczne zaznaczyliśmy przez pogrubienie i wyświetlenie w kolorze czerwonym
Trzeci problem to pokazanie bieżącego dnia. Zrobiliśmy to przez wytłuszczenie kursywy w takim kolorze w jakim wyświetlony jest dzień. Prowadzi to do nieporozumień gdy bieżący dzień jest świąteczny i jest ‘podwójnie’ wytłuszczony. Chyba mądrzej byłoby zaznaczać dzień bieżący przez zmianę tła, albo rysowanie ramek wokół etykiety oznaczającej bieżący dzień. Zmiana jest kosmetyczna, ale pozostawiamy ją Czytelnikowi jako ćwiczenie.
Następnym problemem jest dodanie elementów typu Spinner do przestawiania daty. Trzeba oczywiście uwzględnić, że zakres spinera reprezentującego dni, w lata przestępne, gdy spiner miesięcy pokazuje luty, może pokazywać 29 dni jeśli spiner roku jest ustawiony na rok przestępny, a tylko 28 dni, gdy rok jest zwykły. Dotyczy to również stosownego ustawienia zakresu spinera dni na 30 i 31 dni w innych miesiącach niż luty.
Spiner daty musi informować o tym, że data się zmieniła. Wychwycenie tego zdarzenia pozwala na zmianę ułożenia etykiet w komponencie LabelComponent
.
Jeżeli przyjrzysz się uważniej komponentowi zauważysz dwie maleńkie wady, czy potrzebne usprawnienia komponentu. Każdy ze spinnerów działa niezależnie od drugiego. Jeśli jesteś na 31 stycznia, to aby przejść na 1 lutego musisz przewinąć spiner dni wstecz do nr 1 i spiner miesięcy do przodu o 1. Spinery powinny działać cyklicznie. Spiner dni powinien przejść na 1 i przerzucić miesiąc na luty. Dotyczy to również przejścia z miesięcy na rok. Przewijanie cykliczne powinno również działać wstecz. Drugą poprawką powinno być umożliwienie wpisywania roku. Przewijanie o wiele lat wstecz jest denerwujące.
Ten układ używa klas rozszerzających klasy SpinnerNumberModel
i SpinnerListMod
el, których użycie jest konieczne, żeby móc na bieżąco zmieniać dane wyświetlane przez spiner.
Klasy
Klasa Wielkanoc
była już pokazana we wpisie Jak obliczyć datę Wielkanocy i Bożego Ciała?. Tutaj po prostu powtarzam ją ponownie.
CalendarComponent.java
package calendar; import java.util.*; import javax.swing.*; public class CalendarComponent extends JComponent { private static final long serialVersionUID = -5573893051003388619L; private final SpinnerComponent spinComp; private final LabelComponent labComp; private int[] dmr; private int[] labs; private GregorianCalendar cal; private int[] len; private final int[] len1 = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; private final int[] len2 = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; private int day; private final ArrayList<Swieto> swieta; private int dif; public CalendarComponent() { setLayout(null); setSize(174, 174); spinComp = new SpinnerComponent(); labComp = new LabelComponent(); add(spinComp); add(labComp); dmr = new int[3]; labs = new int[42]; len = new int[12]; swieta = new ArrayList<>(); swieta.add(new Swieto("Nowy Rok", 1, 1)); swieta.add(new Swieto("Święto Pracy", 5, 1)); swieta.add(new Swieto("Święto Narodowe 3 Maja", 5, 3)); swieta.add(new Swieto("Wniebowzięcie Najświętszej Marii Panny", 8, 15)); swieta.add(new Swieto("Dzień Wszystkich Świętych", 11, 1)); swieta.add(new Swieto("Narodowe Święto Niepodległości", 11, 11)); swieta.add(new Swieto("Boże Narodzenie 1 dzień", 12, 25)); swieta.add(new Swieto("Boże Narodzenie 2 dzień", 12, 26)); swieta.add(new Swieto("Wielkanoc", 0, 0)); swieta.add(new Swieto("Boże Ciało", 0, 0)); changeLabels(); spinComp.addPropertyChangeListener(evt -> changeLabels()); } private void changeLabels() { dmr = spinComp.getDmr(); Wielkanoc wielkanoc = new Wielkanoc(dmr[2]); int miesiacWielk = wielkanoc.getMiesiacWielk(); int dzienWielk = wielkanoc.getDzienWielk(); setWielkanoc(miesiacWielk, dzienWielk); int miesiacBoze = wielkanoc.getMiesiacBoze(); int dzienBoze = wielkanoc.getDzienBoze(); setBozeCialo(miesiacBoze, dzienBoze); cal = new GregorianCalendar(dmr[2], dmr[1], dmr[0]); day = cal.get(Calendar.DAY_OF_MONTH); cal.set(Calendar.DAY_OF_MONTH, 1); SwingUtilities.invokeLater(() -> { int dzienTygodnia = cal.get(Calendar.DAY_OF_WEEK); if (dzienTygodnia == 1) { dzienTygodnia = 8; } dif = dzienTygodnia - Calendar.MONDAY; if (cal.isLeapYear(cal.get(Calendar.YEAR))) { len = len2; } else { len = len1; } int lead = len[cal.get(Calendar.MONTH)]; labs = rangeArray(dif, 1, 42 - dif, 42 - dif - lead); labComp.setTexts(labs); labComp.setToday(day + dif + 6); labComp.setSaintDays(saintDays(dmr[1] + 1)); }); } public static int[] rangeArray(int leading0, int n1, int n2, int final0) { int[] table = new int[n2 - n1 + 1 + leading0]; for (int i = 0; i < table.length; i++) { if (i < leading0) { table[i] = 0; } else { table[i] = n1 + i - leading0; } } for (int j = 0; j < final0; j++) { table[table.length - j - 1] = 0; } return table; } public int[] getLen() { return len; } public void setLen(int[] len) { this.len = len; } private int[] saintDays(int miesiac) { int count = 0; int[] days = null; for (Swieto value : swieta) { if (value.getMiesiac() == miesiac) { count++; } } if (count > 0) { days = new int[count]; int count1 = 0; for (Swieto swieto : swieta) { if (swieto.getMiesiac() == miesiac) { days[count1] = swieto.getDzien() + dif + 6; count1++; } } } return days; } public void setBozeCialo(int miesiac, int dzien) { for (int i = 0; i < swieta.size(); i++) { if (swieta.get(i).getNazwa().equals("Boże Ciało")) { swieta.set(i, new Swieto("Boże Ciało", miesiac, dzien)); break; } } } public void setWielkanoc(int miesiac, int dzien) { for (int i = 0; i < swieta.size(); i++) { if (swieta.get(i).getNazwa().equals("Wielkanoc")) { swieta.set(i, new Swieto("Wielkanoc", miesiac, dzien)); break; } } } }
LabelComponent.java
package calendar; import java.awt.*; import javax.swing.*; public class LabelComponent extends JComponent { private static final long serialVersionUID = 5284864733877452571L; private JLabel[] labels; private final Font labFont = new Font("SansSerif", Font.PLAIN, 12); private final Font labFont1 = new Font("Sanerif", Font.BOLD, 12); private final Font labFont2 = new Font("Sanserif", Font.BOLD | Font.ITALIC, 12); private int today; private int[] saintDays; public LabelComponent() { setLayout(new GridLayout(7, 7)); setBounds(1, 28, 173, 143); labels = new JLabel[7 * 7]; labels[0] = new JLabel("Pn"); labels[1] = new JLabel("Wt"); labels[2] = new JLabel("Śr"); labels[3] = new JLabel("Cz"); labels[4] = new JLabel("Pt"); labels[5] = new JLabel("So"); labels[6] = new JLabel("No"); int k = 0; for (int i = 0; i < 6; i++) { for (int j = 0; j < 7; j++) { JLabel lab = new JLabel(""); labels[k + 7] = lab; k++; } } } public JLabel[] getLabels() { return labels; } public void setLabels(JLabel[] labels) { this.labels = labels; } synchronized public void setTexts(int[] texts) { for (int i = 0; i < texts.length; i++) { String temp = String.valueOf(texts[i]); if (temp.equals("0")) { temp = ""; } labels[i + 7].setText(temp); } } @Override public void paintComponent(Graphics g) { super.paintComponent(g); for (int i = 0; i < labels.length; i++) { labels[i].setFont(labFont); labels[i].setForeground(Color.BLACK); labels[i].setHorizontalAlignment(SwingConstants.RIGHT); if ((i + 1) % 7 == 0) { labels[i].setFont(labFont); labels[i].setForeground(Color.RED); } if (saintDays != null) { for (int saintDay : saintDays) { if (i == saintDay) { labels[i].setFont(labFont1); labels[i].setForeground(Color.RED); } } } if (i == getToday()) { labels[i].setFont(labFont2); } add(labels[i]); } } public int getToday() { return today; } public void setToday(int today) { this.today = today; } public int[] getSaintDays() { return saintDays; } public void setSaintDays(int[] saintDays) { this.saintDays = saintDays; } }
Main.java
package calendar; import java.awt.*; import javax.swing.*; public class Main extends JFrame { private static final long serialVersionUID = 4253121351267368454L; private static final int W = 800; private static final int H = 800; private static final int screenW = Toolkit.getDefaultToolkit() .getScreenSize().width; private static final int screenH = Toolkit.getDefaultToolkit() .getScreenSize().height; public Main() { setLayout(null); setPreferredSize(new Dimension(W, H)); setBounds(screenW / 2 - W / 2, screenH / 2 - H / 2, W, H); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setTitle("Kalendarz"); CalendarComponent comp = new CalendarComponent(); comp.setLocation(0, 0); add(comp); setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new Main()); } }
SpinnerComponent.java
package calendar; import java.beans.*; import java.text.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; public class SpinnerComponent extends JComponent implements ChangeListener { private static final long serialVersionUID = -7156399121664518291L; private GregorianCalendar kalendarz; private int aktDzien, aktMies, aktRok; private String[] miesiace; private SpinnerListModel miesiacM; private SpinnerNumberModel dzienM, rokM; private JSpinner dzienS, miesiacS, rokS; private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); private int[] dmr; private final int[] len1 = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; private final int[] len2 = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; public SpinnerComponent() { setLayout(null); setBounds(0, 0, 173, 27); kalendarz = new GregorianCalendar(); aktDzien = kalendarz.get(Calendar.DAY_OF_MONTH); aktMies = kalendarz.get(Calendar.MONTH); aktRok = kalendarz.get(Calendar.YEAR); miesiace = pobierzMiesiace(); //- rokM = new SpinnerNumberModel(aktRok, aktRok - 100, aktRok + 100, 1); rokS = new JSpinner(rokM); rokS.setValue(aktRok); rokS.setBounds(122, 1, 50, 25); rokS.setEditor(new JSpinner.NumberEditor(rokS, "#")); ((JSpinner.DefaultEditor) rokS.getEditor()).getTextField().setEditable( false); rokS.addChangeListener(this); add(rokS); //- miesiacM = new SpinnerListModel(miesiace); miesiacS = new JSpinner(miesiacM); miesiacS.setValue(miesiace[aktMies]); miesiacS.setBounds(36, 1, 85, 25); ((JSpinner.DefaultEditor) miesiacS.getEditor()).getTextField() .setEditable(false); miesiacS.addChangeListener(this); add(miesiacS); //- int[] len; if (!kalendarz.isLeapYear(kalendarz.get(Calendar.YEAR))) { len = len1; } else { len = len2; } dzienM = new SpinnerNumberModel(aktDzien, 1, len[aktMies], 1); dzienS = new JSpinner(dzienM); dzienS.setValue(aktDzien); dzienS.setBounds(1, 1, 35, 25); ((JSpinner.DefaultEditor) dzienS.getEditor()).getTextField() .setEditable(false); dzienS.addChangeListener(this); add(dzienS); //- dmr = new int[3]; SpinnerModel d = dzienS.getModel(); SpinnerModel m = miesiacS.getModel(); String temp = (String) m.getValue(); SpinnerModel r = rokS.getModel(); dmr[0] = (Integer) d.getValue(); dmr[1] = getIndex(miesiace, temp); dmr[2] = (Integer) r.getValue(); } private String[] pobierzMiesiace() { String[] miesiace = new DateFormatSymbols().getMonths(); int miesLength = miesiace.length - 1; if ((miesiace[miesLength] == null) || miesiace[miesLength].length() <= 0) { String[] nazwyMies = new String[miesLength]; System.arraycopy(miesiace, 0, nazwyMies, 0, miesLength); return nazwyMies; } else { return miesiace; } } public GregorianCalendar getKalendarz() { return kalendarz; } public void setKalendarz(GregorianCalendar kalendarz) { this.kalendarz = kalendarz; } public int getAktDzien() { return aktDzien; } public void setAktDzien(int aktDzien) { this.aktDzien = aktDzien; } public int getAktMies() { return aktMies; } public void setAktMies(int aktMies) { this.aktMies = aktMies; } public int getAktRok() { return aktRok; } public void setAktRok(int aktRok) { this.aktRok = aktRok; } public String[] getMiesiace() { return miesiace; } public void setMiesiace(String[] miesiace) { this.miesiace = miesiace; } public SpinnerListModel getMiesiacM() { return miesiacM; } public void setMiesiacM(SpinnerListModel miesiacM) { this.miesiacM = miesiacM; } public SpinnerNumberModel getDzienM() { return dzienM; } public void setDzienM(SpinnerNumberModel dzienM) { this.dzienM = dzienM; } public SpinnerNumberModel getRokM() { return rokM; } public void setRokM(SpinnerNumberModel rokM) { this.rokM = rokM; } public JSpinner getDzienS() { return dzienS; } public void setDzienS(JSpinner dzienS) { this.dzienS = dzienS; } public JSpinner getMiesiacS() { return miesiacS; } public void setMiesiacS(JSpinner miesiacS) { this.miesiacS = miesiacS; } public JSpinner getRokS() { return rokS; } public void setRokS(JSpinner rokS) { this.rokS = rokS; } public static int getIndex(String[] strs, String str) { int index = -1; for (int i = 0; i < strs.length; i++) { if (strs[i].equals(str)) { index = i; return index; } } return index; } @Override public synchronized void addPropertyChangeListener( PropertyChangeListener list) { pcs.addPropertyChangeListener(list); } @Override public synchronized void removePropertyChangeListener( PropertyChangeListener list) { pcs.removePropertyChangeListener(list); } public int[] getDmr() { return dmr; } public void setDmr(int[] newValue) { int[] oldValue = dmr; dmr = newValue; pcs.firePropertyChange("zmiana danych", oldValue, newValue); } @Override public void stateChanged(ChangeEvent e) { int[] tt = new int[3]; //- SpinnerModel r = rokS.getModel(); int rr = (Integer) r.getValue(); kalendarz.set(Calendar.YEAR, rr); boolean leap = kalendarz.isLeapYear(kalendarz.get(Calendar.YEAR)); //- SpinnerModel m = miesiacS.getModel(); String temp = (String) m.getValue(); int mm = getIndex(miesiace, temp); kalendarz.set(Calendar.MONTH, mm); int monthLength; //- SpinnerModel d = dzienS.getModel(); int dd = (Integer) d.getValue(); //- if (leap) { if (mm == Calendar.FEBRUARY) { monthLength = len2[mm]; dzienM.setMaximum(monthLength); if (dd > 29) { dzienM.setValue(29); dd = (Integer) d.getValue(); } } else { monthLength = len1[mm]; dzienM.setMaximum(monthLength); if (dd > monthLength) { dzienM.setValue(monthLength); dd = (Integer) d.getValue(); } } } else { monthLength = len1[mm]; dzienM.setMaximum(monthLength); if (dd > monthLength) { dzienM.setValue(monthLength); dd = (Integer) d.getValue(); } } kalendarz.set(Calendar.DAY_OF_MONTH, dd); tt[0] = dd; tt[1] = mm; tt[2] = rr; setDmr(tt); } }
Swieto.java
package calendar; public class Swieto { private String nazwa; private int miesiac; private int dzien; public Swieto(String nazwa, int miesiac, int dzien) { this.nazwa = nazwa; this.miesiac = miesiac; this.dzien = dzien; } public String getNazwa() { return nazwa; } public void setNazwa(String nazwa) { this.nazwa = nazwa; } public int getMiesiac() { return miesiac; } public void setMiesiac(int miesiac) { this.miesiac = miesiac; } public int getDzien() { return dzien; } public void setDzien(int dzien) { this.dzien = dzien; } }
Wielkanoc.java
package calendar; /** * Oblicza date Wielkanocy od 1582 roku do 2499 r. * oraz date Bożego Ciała. Użyty jest tu tzw. algorytm Gaussa. */ class Wielkanoc { private int dzienWielk; private int miesiacWielk; private int rok; private int dzienBoze = 0; private int miesiacBoze = 0; public Wielkanoc(int rok) { this.rok = rok; int wskA = 0; int wskB = 0; if (rok <= 1582) { wskA = 15; wskB = 6; } else if ((rok >= 1583) && (rok <= 1699)) { wskA = 22; wskB = 2; } else if ((rok >= 1700) && (rok <= 1799)) { wskA = 23; wskB = 3; } else if ((rok >= 1800) && (rok <= 1899)) { wskA = 23; wskB = 4; } else if ((rok >= 1900) && (rok <= 2099)) { wskA = 24; wskB = 5; } else if ((rok >= 2100) && (rok <= 2199)) { wskA = 24; wskB = 6; } else if ((rok >= 2200) && (rok <= 2299)) { wskA = 25; wskB = 0; } else if ((rok >= 2300) && (rok <= 2399)) { wskA = 26; wskB = 1; } else if ((rok >= 2300) && (rok <= 2499)) { wskA = 25; wskB = 1; } int a = rok % 19; int b = rok % 4; int c = rok % 7; int d = (a * 19 + wskA) % 30; int e = (2 * b + 4 * c + 6 * d + wskB) % 7; int liczbaDni = 22 + d + e; if ((d == 29) && (e == 6)) { dzienWielk = 19; miesiacWielk = 4; } else if ((d == 28) & (e == 6)) { dzienWielk = 18; miesiacWielk = 4; } else { if (liczbaDni <= 31) { dzienWielk = liczbaDni; miesiacWielk = 3; } else { dzienWielk = liczbaDni - 31; miesiacWielk = 4; } } obliczBozeCialo(); } private void obliczBozeCialo() { int temp; if (miesiacWielk == 3) { temp = 60 - (31 - dzienWielk) - 30; } else { temp = 60 - (30 - dzienWielk); } if (temp <= 31) { dzienBoze = temp; miesiacBoze = 5; } else { dzienBoze = temp - 31; miesiacBoze = 6; } } public int getRok() { return rok; } public void setRok(int rok) { this.rok = rok; } public int getDzienWielk() { return dzienWielk; } public void setDzienWielk(int dzienWielk) { this.dzienWielk = dzienWielk; } public int getMiesiacWielk() { return miesiacWielk; } public void setMiesiacWielk(int miesiacWielk) { this.miesiacWielk = miesiacWielk; } public int getDzienBoze() { return dzienBoze; } public void setDzienBoze(int dzienBoze) { this.dzienBoze = dzienBoze; } public int getMiesiacBoze() { return miesiacBoze; } public void setMiesiacBoze(int miesiacBoze) { this.miesiacBoze = miesiacBoze; } @Override public String toString() { return "Data Wielkanocy: " + dzienWielk + "-" + miesiacWielk + "-" + rok + "\n" + "Data Bożego Ciała: " + dzienBoze + "-" + miesiacBoze + "-" + rok; } }
Wyniki
Aby zobaczyć komponent Kalendarz
z pokazanym obecnym miesiącem i zaznaczonym dzisiejszym dniem należy uruchomić klasę Main
.
Po złożeniu całości okazuje się, że komponent jest nieco przyciężki. Przyczyną jest użycie wielu etykiet ( JLabel
). Prawdopodobnie lepiej byłoby rysować daty bezpośrednio przy użyciu metody paintComponent()
.