Улучшаем интерфейс — листенеры, модель таблицы и потоки
Переделав наше приложения в плане работы с базой данных, вернемся к пользовательскому интерфейсу. Во-первых, мы не реализовали достаточно много функций, во-вторых — наш интерфейс в общем-то далек от совершенства.
Предварительно опишем (еще раз), что мы теперь хотим получить:
- В верхней части экрана у нас остается тот же самый спин. Но к нему надо приделать листенер, который будет вызывать обновление списка студентов.
- Список групп. Мы его оставим таким, как был. Только добавим листенер, который будет реагировать на изменение выделенной группы.
- Список студентов будет изменен. Во-первых будем показывать студентов из выделенной группы, а во-вторых будем показывать их не списком, а таблицей. Для таблицы мы создадим свою собственную модель. Особенности мы обсудим позже.
- Добавим три кнопки для редактирования студентов в нижнюю часть таблицы со списком студентов.
- Добавим две кнопки внизу списка групп — для удаления всех студентов из группы и для перевода всех студентов в другую группу. Хорошо наверно было бы сделать возможность выделять студентов и переводить только выделенных, но это мы оставим для самостоятельного изучения. Кроме этого мы сделаем наш список групп постоянным по ширине — иначе, если группы называются 1, 2, 3 и т.д., то наш список будет очень узким.
- При добавлении нового студента или редактировании существующего будем выводить диалоговое окно. При добавлении студента диалоговое окно не должно закрываться после добавления студента — пользователь может сразу начать вводить данные другого студента. При этом значения для полей ГРУППА и ГОД должны оставаться такими, как были, а все остальные поля очищаются. Конечно же группы выбираются не по ИД, а из списка.
- Добавим пункты меню «Отчеты», где мы сможем получать различные отчеты. На сегодня у нас только один отчет — список всех студентов.
Теперь давайте постепенно будем приближаться к нашей цели. Стоит отметить, что наш класс ManagementSystem делает все, что нам надо и то, что касается так называемого back-end (такое название относится к той части системы, которая отвечает за работу с базой данных) нас уже мало волнует — все для работы с базой у нас есть. Мы будем просто вызывать те методы, которые нам потребуются.
Итак, давайте сделаем первую версию нашего интерфейса. Мы добавим кнопки внизу и сделаем нашу панель для групп не сжимаемую по ширине. Кнопки можно увидеть в тексте — там ничего нет сложного. Для «несжимаемости» нам надо будет переопределить метод getPreferredSize(). Его можно увидеть в коде.
Мы пока будем смотреть на один файл — StudentsFrame.java. Все остальные не будут изменяться совсем. Как обычно в конце статьи будут приведены тексты для всех файлов.
StudentsFrame.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
package students.frame; import java.sql.SQLException; import java.util.Vector; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.BorderLayout; import java.awt.GridLayout; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JSpinner; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import students.logic.ManagementSystem; public class StudentsFrame extends JFrame { // Введем сразу имена для кнопок - потом будем их использовать в обработчиках private static final String MOVE_GR = "moveGroup"; private static final String CLEAR_GR = "clearGroup"; private static final String INSERT_ST = "insertStudent"; private static final String UPDATE_ST = "updateStudent"; private static final String DELETE_ST = "deleteStudent"; private static final String ALL_STUDENTS = "allStudent"; private ManagementSystem ms = null; private JList grpList; private JTable stdList; private JSpinner spYear; public StudentsFrame() throws Exception { // Устанавливаем layout для всей клиентской части формы getContentPane().setLayout(new BorderLayout()); // Создаем строку меню JMenuBar menuBar = new JMenuBar(); // Создаем выпадающее меню JMenu menu = new JMenu("Отчеты"); // Создаем пункт в выпадающем меню JMenuItem menuItem = new JMenuItem("Все студенты"); menuItem.setName(ALL_STUDENTS); // Вставляем пункт меню в выпадающее меню menu.add(menuItem); // Вставляем выпадающее меню в строку меню menuBar.add(menu); // Устанавливаем меню для формы setJMenuBar(menuBar); // Создаем верхнюю панель, где будет поле для ввода года JPanel top = new JPanel(); // Устанавливаем для нее layout top.setLayout(new FlowLayout(FlowLayout.LEFT)); // Вставляем пояснительную надпись top.add(new JLabel("Год обучения:")); // Делаем спин-поле // 1. Задаем модель поведения - только цифры // 2. Вставляем в панель SpinnerModel sm = new SpinnerNumberModel(2006, 1900, 2100, 1); spYear = new JSpinner(sm); top.add(spYear); // Создаем нижнюю панель и задаем ей layout JPanel bot = new JPanel(); bot.setLayout(new BorderLayout()); // Создаем левую панель для вывода списка групп // Она у нас GroupPanel left = new GroupPanel(); // Задаем layout и задаем "бордюр" вокруг панели left.setLayout(new BorderLayout()); left.setBorder(new BevelBorder(BevelBorder.LOWERED)); // Получаем коннект к базе и создаем объект ManagementSystem ms = ManagementSystem.getInstance(); // Получаем список групп Vector<Group> gr = new Vector<Group>(ms.getGroups()); // Создаем надпись left.add(new JLabel("Группы:"), BorderLayout.NORTH); // Создаем визуальный список и вставляем его в скроллируемую // панель, которую в свою очередь уже кладем на панель left grpList = new JList(gr); left.add(new JScrollPane(grpList), BorderLayout.CENTER); // Создаем кнопки для групп JButton btnMvGr = new JButton("Переместить"); btnMvGr.setName(MOVE_GR); JButton btnClGr = new JButton("Очистить"); btnClGr.setName(CLEAR_GR); // Создаем панель, на которую положим наши кнопки и кладем ее вниз JPanel pnlBtnGr = new JPanel(); pnlBtnGr.setLayout(new GridLayout(1, 2)); pnlBtnGr.add(btnMvGr); pnlBtnGr.add(btnClGr); left.add(pnlBtnGr, BorderLayout.SOUTH); // Создаем правую панель для вывода списка студентов JPanel right = new JPanel(); // Задаем layout и задаем "бордюр" вокруг панели right.setLayout(new BorderLayout()); right.setBorder(new BevelBorder(BevelBorder.LOWERED)); // Создаем надпись right.add(new JLabel("Студенты:"), BorderLayout.NORTH); // Создаем таблицу и вставляем ее в скроллируемую // панель, которую в свою очередь уже кладем на панель right // Наша таблица пока ничего не умеет - просто положим ее как заготовку // Сделаем в ней 4 колонки - Фамилия, Имя, Отчество, Дата рождения stdList = new JTable(1, 4); right.add(new JScrollPane(stdList), BorderLayout.CENTER); // Создаем кнопки для студентов JButton btnAddSt = new JButton("Добавить"); btnAddSt.setName(INSERT_ST); JButton btnUpdSt = new JButton("Исправить"); btnUpdSt.setName(UPDATE_ST); JButton btnDelSt = new JButton("Удалить"); btnDelSt.setName(DELETE_ST); // Создаем панель, на которую положим наши кнопки и кладем ее вниз JPanel pnlBtnSt = new JPanel(); pnlBtnSt.setLayout(new GridLayout(1, 3)); pnlBtnSt.add(btnAddSt); pnlBtnSt.add(btnUpdSt); pnlBtnSt.add(btnDelSt); right.add(pnlBtnSt, BorderLayout.SOUTH); // Вставляем панели со списками групп и студентов в нижнюю панель bot.add(left, BorderLayout.WEST); bot.add(right, BorderLayout.CENTER); // Вставляем верхнюю и нижнюю панели в форму getContentPane().add(top, BorderLayout.NORTH); getContentPane().add(bot, BorderLayout.CENTER); // Сразу выделяем первую группу grpList.setSelectedIndex(0); // Задаем границы формы setBounds(100, 100, 700, 500); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { try { // Мы сразу отменим продолжение работы, если не сможем получить // коннект к базе данных StudentsFrame sf = new StudentsFrame(); sf.setDefaultCloseOperation(EXIT_ON_CLOSE); sf.setVisible(true); } catch (Exception ex) { ex.printStackTrace(); } } }); } } // Наш внутренний класс - переопределенная панель. class GroupPanel extends JPanel { public Dimension getPreferredSize() { return new Dimension(250, 0); } } |
Теперь мы сделаем так, чтобы наш интерфейс «ожил». Сделаем реакции на кнопки,изменения групп и спинера. Будем вызывать методы, в которых будут стоять «заглушки» — сообщения о том, что метод вызван. У нас сообщения приходят от четырех видов компонентов — меню, кнопка, спинер, список.
Для меню и кнопок используется один вид листенера — ActionListener.
Для списка используется, как мы уже знаем, ListSelectionListener.
Остается только спинер — для него используется ChangeListener.
Давайте теперь расставим все наши листенеры и сделаем «заглушки». Можно запустить наше приложение и проверить все наши управляющие компоненты — по идее все должно работать, т.е. выдавать сообщения на каждое действие — меню, кнопки, перемещение по списку групп, изменение величины в спинере. Комментарии к коду смотрите прямо в тексте программы.
StudentsFrame.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
package students.frame; import java.util.Vector; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.BorderLayout; import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JSpinner; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import students.logic.Group; import students.logic.ManagementSystem; public class StudentsFrame extends JFrame implements ActionListener, ListSelectionListener, ChangeListener { // Введем сразу имена для кнопок - потом будем их использовать в обработчиках private static final String MOVE_GR = "moveGroup"; private static final String CLEAR_GR = "clearGroup"; private static final String INSERT_ST = "insertStudent"; private static final String UPDATE_ST = "updateStudent"; private static final String DELETE_ST = "deleteStudent"; private static final String ALL_STUDENTS = "allStudent"; private ManagementSystem ms = null; private JList grpList; private JTable stdList; private JSpinner spYear; public StudentsFrame() throws Exception { // Устанавливаем layout для всей клиентской части формы getContentPane().setLayout(new BorderLayout()); // Создаем строку меню JMenuBar menuBar = new JMenuBar(); // Создаем выпадающее меню JMenu menu = new JMenu("Отчеты"); // Создаем пункт в выпадающем меню JMenuItem menuItem = new JMenuItem("Все студенты"); menuItem.setName(ALL_STUDENTS); // Добавляем листенер menuItem.addActionListener(this); // Вставляем пункт меню в выпадающее меню menu.add(menuItem); // Вставляем выпадающее меню в строку меню menuBar.add(menu); // Устанавливаем меню для формы setJMenuBar(menuBar); // Создаем верхнюю панель, где будет поле для ввода года JPanel top = new JPanel(); // Устанавливаем для нее layout top.setLayout(new FlowLayout(FlowLayout.LEFT)); // Вставляем пояснительную надпись top.add(new JLabel("Год обучения:")); // Делаем спин-поле // 1. Задаем модель поведения - только цифры // 2. Вставляем в панель SpinnerModel sm = new SpinnerNumberModel(2006, 1900, 2100, 1); spYear = new JSpinner(sm); // Добавляем листенер spYear.addChangeListener(this); top.add(spYear); // Создаем нижнюю панель и задаем ей layout JPanel bot = new JPanel(); bot.setLayout(new BorderLayout()); // Создаем левую панель для вывода списка групп // Она у нас GroupPanel left = new GroupPanel(); // Задаем layout и задаем "бордюр" вокруг панели left.setLayout(new BorderLayout()); left.setBorder(new BevelBorder(BevelBorder.LOWERED)); // Получаем коннект к базе и создаем объект ManagementSystem ms = ManagementSystem.getInstance(); // Получаем список групп Vector<Group> gr = new Vector<Group>(ms.getGroups()); // Создаем надпись left.add(new JLabel("Группы:"), BorderLayout.NORTH); // Создаем визуальный список и вставляем его в скроллируемую // панель, которую в свою очередь уже кладем на панель left grpList = new JList(gr); // Добавляем листенер grpList.addListSelectionListener(this); // Сразу выделяем первую группу grpList.setSelectedIndex(0); left.add(new JScrollPane(grpList), BorderLayout.CENTER); // Создаем кнопки для групп JButton btnMvGr = new JButton("Переместить"); btnMvGr.setName(MOVE_GR); JButton btnClGr = new JButton("Очистить"); btnClGr.setName(CLEAR_GR); // Добавляем листенер btnMvGr.addActionListener(this); btnClGr.addActionListener(this); // Создаем панель, на которую положим наши кнопки и кладем ее вниз JPanel pnlBtnGr = new JPanel(); pnlBtnGr.setLayout(new GridLayout(1, 2)); pnlBtnGr.add(btnMvGr); pnlBtnGr.add(btnClGr); left.add(pnlBtnGr, BorderLayout.SOUTH); // Создаем правую панель для вывода списка студентов JPanel right = new JPanel(); // Задаем layout и задаем "бордюр" вокруг панели right.setLayout(new BorderLayout()); right.setBorder(new BevelBorder(BevelBorder.LOWERED)); // Создаем надпись right.add(new JLabel("Студенты:"), BorderLayout.NORTH); // Создаем таблицу и вставляем ее в скроллируемую // панель, которую в свою очередь уже кладем на панель right // Наша таблица пока ничего не умеет - просто положим ее как заготовку // Сделаем в ней 4 колонки - Фамилия, Имя, Отчество, Дата рождения stdList = new JTable(1, 4); right.add(new JScrollPane(stdList), BorderLayout.CENTER); // Создаем кнопки для студентов JButton btnAddSt = new JButton("Добавить"); btnAddSt.setName(INSERT_ST); btnAddSt.addActionListener(this); JButton btnUpdSt = new JButton("Исправить"); btnUpdSt.setName(UPDATE_ST); btnUpdSt.addActionListener(this); JButton btnDelSt = new JButton("Удалить"); btnDelSt.setName(DELETE_ST); btnDelSt.addActionListener(this); // Создаем панель, на которую положим наши кнопки и кладем ее вниз JPanel pnlBtnSt = new JPanel(); pnlBtnSt.setLayout(new GridLayout(1, 3)); pnlBtnSt.add(btnAddSt); pnlBtnSt.add(btnUpdSt); pnlBtnSt.add(btnDelSt); right.add(pnlBtnSt, BorderLayout.SOUTH); // Вставляем панели со списками групп и студентов в нижнюю панель bot.add(left, BorderLayout.WEST); bot.add(right, BorderLayout.CENTER); // Вставляем верхнюю и нижнюю панели в форму getContentPane().add(top, BorderLayout.NORTH); getContentPane().add(bot, BorderLayout.CENTER); // Задаем границы формы setBounds(100, 100, 700, 500); } // Метод для обеспечения интерфейса ActionListener public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component) { Component c = (Component) e.getSource(); if (c.getName().equals(MOVE_GR)) { moveGroup(); } if (c.getName().equals(CLEAR_GR)) { clearGroup(); } if (c.getName().equals(ALL_STUDENTS)) { showAllStudents(); } if (c.getName().equals(INSERT_ST)) { insertStudent(); } if (c.getName().equals(UPDATE_ST)) { updateStudent(); } if (c.getName().equals(DELETE_ST)) { deleteStudent(); } } } // Метод для обеспечения интерфейса ListSelectionListener public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { reloadStudents(); } } // Метод для обеспечения интерфейса ChangeListener public void stateChanged(ChangeEvent e) { reloadStudents(); } // метод для обновления списка студентов для определенной группы private void reloadStudents() { JOptionPane.showMessageDialog(this, "reloadStudents"); } // метод для переноса группы private void moveGroup() { JOptionPane.showMessageDialog(this, "moveGroup"); } // метод для очистки группы private void clearGroup() { JOptionPane.showMessageDialog(this, "clearGroup"); } // метод для добавления студента private void insertStudent() { JOptionPane.showMessageDialog(this, "insertStudent"); } // метод для редактирования студента private void updateStudent() { JOptionPane.showMessageDialog(this, "updateStudent"); } // метод для удаления студента private void deleteStudent() { JOptionPane.showMessageDialog(this, "deleteStudent"); } // метод для показа всех студентов private void showAllStudents() { JOptionPane.showMessageDialog(this, "showAllStudents"); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { try { // Мы сразу отменим продолжение работы, если не сможем получить // коннект к базе данных StudentsFrame sf = new StudentsFrame(); sf.setDefaultCloseOperation(EXIT_ON_CLOSE); sf.setVisible(true); } catch (Exception ex) { ex.printStackTrace(); } } }); } } // Наш внутренний класс - переопределенная панель. class GroupPanel extends JPanel { public Dimension getPreferredSize() { return new Dimension(250, 0); } } |
Модель для таблицы
Теперь давайте реализуем первую команду — перегрузка списка студентов. Как уже упоминалось выше, нам предстоит определить свою модель. Реализация полной модели, т.е. интерфейса TableModel — задача сложная. В большинстве случаев нам необходимо только реализовать несколько методов. Понимая это разработчики Java создали класс AbstractTableModel, который реализует большинство необходимых методов. Для создания своей модели достаточно переопределить всего 3 метода:
- public int getRowCount();
- public int getColumnCount();
- public Object getValueAt(int row, int column);
Если мы запишем список студентов в вектор и определим порядок столбцов, то реализация будет выглядеть достаточно несложно. Конечно, существует возможность использовать таблицу через стандартные вызовы, отдавая ей векторы. Но могу вас уверить — это будет выглядеть сложнее, чем написать свою модель. Тем более, что бОльшая часть работы уже сделана за нас — классом AbstractTableModel.
StudentTableModel.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
package students.frame; import java.text.DateFormat; import java.util.Vector; import javax.swing.table.AbstractTableModel; import students.logic.Student; public class StudentTableModel extends AbstractTableModel { // Сделаем хранилище для нашего списка студентов private Vector students; // Модель при создании получает список студентов public StudentTableModel(Vector students) { this.students = students; } // Количество строк равно числу записей public int getRowCount() { if (students != null) { return students.size(); } return 0; } // Количество столбцов - 4. Фамилия, Имя, Отчество, Дата рождения public int getColumnCount() { return 4; } // Вернем наименование колонки public String getColumnName(int column) { String[] colNames = {"Фамилия", "Имя", "Отчество", "Дата"}; return colNames[column]; } // Возвращаем данные для определенной строки и столбца public Object getValueAt(int rowIndex, int columnIndex) { if (students != null) { // Получаем из вектора студента Student st = (Student) students.get(rowIndex); // В зависимости от колонки возвращаем имя, фамилия и т.д. switch (columnIndex) { case 0: return st.getSurName(); case 1: return st.getFirstName(); case 2: return st.getPatronymic(); case 3: return DateFormat.getDateInstance(DateFormat.SHORT).format( st.getDateOfBirth()); } } return null; } // Добавим метод, который возвращает студента по номеру строки // Это нам пригодится чуть позже public Student getStudent(int rowIndex) { if (students != null) { if (rowIndex < students.size() && rowIndex >= 0) { return (Student) students.get(rowIndex); } } return null; } } |
И теперь можно привести код для обновленного StudentsFrame — там сделано важное изменение — реализован метод reloadStudents(), который загружает список студентов для выделенной группы и года.
StudentTableModel.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
package students.frame; import java.sql.SQLException; import java.util.Vector; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.BorderLayout; import java.awt.Component; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Collection; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JSpinner; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import students.logic.Group; import students.logic.ManagementSystem; import students.logic.Student; public class StudentsFrame extends JFrame implements ActionListener, ListSelectionListener, ChangeListener { // Введем сразу имена для кнопок - потом будем их использовать в обработчиках private static final String MOVE_GR = "moveGroup"; private static final String CLEAR_GR = "clearGroup"; private static final String INSERT_ST = "insertStudent"; private static final String UPDATE_ST = "updateStudent"; private static final String DELETE_ST = "deleteStudent"; private static final String ALL_STUDENTS = "allStudent"; private ManagementSystem ms = null; private JList grpList; private JTable stdList; private JSpinner spYear; public StudentsFrame() throws Exception { // Устанавливаем layout для всей клиентской части формы getContentPane().setLayout(new BorderLayout()); // Создаем строку меню JMenuBar menuBar = new JMenuBar(); // Создаем выпадающее меню JMenu menu = new JMenu("Отчеты"); // Создаем пункт в выпадающем меню JMenuItem menuItem = new JMenuItem("Все студенты"); menuItem.setName(ALL_STUDENTS); // Добавляем листенер menuItem.addActionListener(this); // Вставляем пункт меню в выпадающее меню menu.add(menuItem); // Вставляем выпадающее меню в строку меню menuBar.add(menu); // Устанавливаем меню для формы setJMenuBar(menuBar); // Создаем верхнюю панель, где будет поле для ввода года JPanel top = new JPanel(); // Устанавливаем для нее layout top.setLayout(new FlowLayout(FlowLayout.LEFT)); // Вставляем пояснительную надпись top.add(new JLabel("Год обучения:")); // Делаем спин-поле // 1. Задаем модель поведения - только цифры // 2. Вставляем в панель SpinnerModel sm = new SpinnerNumberModel(2006, 1900, 2100, 1); spYear = new JSpinner(sm); // Добавляем листенер spYear.addChangeListener(this); top.add(spYear); // Создаем нижнюю панель и задаем ей layout JPanel bot = new JPanel(); bot.setLayout(new BorderLayout()); // Создаем левую панель для вывода списка групп // Она у нас GroupPanel left = new GroupPanel(); // Задаем layout и задаем "бордюр" вокруг панели left.setLayout(new BorderLayout()); left.setBorder(new BevelBorder(BevelBorder.LOWERED)); // Получаем коннект к базе и создаем объект ManagementSystem ms = ManagementSystem.getInstance(); // Получаем список групп Vector<Group> gr = new Vector<Group>(ms.getGroups()); // Создаем надпись left.add(new JLabel("Группы:"), BorderLayout.NORTH); // Создаем визуальный список и вставляем его в скроллируемую // панель, которую в свою очередь уже кладем на панель left grpList = new JList(gr); // Добавляем листенер grpList.addListSelectionListener(this); // Сразу выделяем первую группу grpList.setSelectedIndex(0); left.add(new JScrollPane(grpList), BorderLayout.CENTER); // Создаем кнопки для групп JButton btnMvGr = new JButton("Переместить"); btnMvGr.setName(MOVE_GR); JButton btnClGr = new JButton("Очистить"); btnClGr.setName(CLEAR_GR); // Добавляем листенер btnMvGr.addActionListener(this); btnClGr.addActionListener(this); // Создаем панель, на которую положим наши кнопки и кладем ее вниз JPanel pnlBtnGr = new JPanel(); pnlBtnGr.setLayout(new GridLayout(1, 2)); pnlBtnGr.add(btnMvGr); pnlBtnGr.add(btnClGr); left.add(pnlBtnGr, BorderLayout.SOUTH); // Создаем правую панель для вывода списка студентов JPanel right = new JPanel(); // Задаем layout и задаем "бордюр" вокруг панели right.setLayout(new BorderLayout()); right.setBorder(new BevelBorder(BevelBorder.LOWERED)); // Создаем надпись right.add(new JLabel("Студенты:"), BorderLayout.NORTH); // Создаем таблицу и вставляем ее в скроллируемую // панель, которую в свою очередь уже кладем на панель right // Наша таблица пока ничего не умеет - просто положим ее как заготовку // Сделаем в ней 4 колонки - Фамилия, Имя, Отчество, Дата рождения stdList = new JTable(1, 4); right.add(new JScrollPane(stdList), BorderLayout.CENTER); // Создаем кнопки для студентов JButton btnAddSt = new JButton("Добавить"); btnAddSt.setName(INSERT_ST); btnAddSt.addActionListener(this); JButton btnUpdSt = new JButton("Исправить"); btnUpdSt.setName(UPDATE_ST); btnUpdSt.addActionListener(this); JButton btnDelSt = new JButton("Удалить"); btnDelSt.setName(DELETE_ST); btnDelSt.addActionListener(this); // Создаем панель, на которую положим наши кнопки и кладем ее вниз JPanel pnlBtnSt = new JPanel(); pnlBtnSt.setLayout(new GridLayout(1, 3)); pnlBtnSt.add(btnAddSt); pnlBtnSt.add(btnUpdSt); pnlBtnSt.add(btnDelSt); right.add(pnlBtnSt, BorderLayout.SOUTH); // Вставляем панели со списками групп и студентов в нижнюю панель bot.add(left, BorderLayout.WEST); bot.add(right, BorderLayout.CENTER); // Вставляем верхнюю и нижнюю панели в форму getContentPane().add(top, BorderLayout.NORTH); getContentPane().add(bot, BorderLayout.CENTER); // Задаем границы формы setBounds(100, 100, 700, 500); } // Метод для обеспечения интерфейса ActionListener public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component) { Component c = (Component) e.getSource(); if (c.getName().equals(MOVE_GR)) { moveGroup(); } if (c.getName().equals(CLEAR_GR)) { clearGroup(); } if (c.getName().equals(ALL_STUDENTS)) { showAllStudents(); } if (c.getName().equals(INSERT_ST)) { insertStudent(); } if (c.getName().equals(UPDATE_ST)) { updateStudent(); } if (c.getName().equals(DELETE_ST)) { deleteStudent(); } } } // Метод для обеспечения интерфейса ListSelectionListener public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { reloadStudents(); } } // Метод для обеспечения интерфейса ChangeListener public void stateChanged(ChangeEvent e) { reloadStudents(); } // метод для обновления списка студентов для определенной группы private void reloadStudents() { if (stdList != null) { // Получаем выделенную группу Group g = (Group) grpList.getSelectedValue(); // Получаем число из спинера int y = ((SpinnerNumberModel) spYear.getModel()).getNumber().intValue(); try { // Получаем список студентов Collection<Student> s = ms.getStudentsFromGroup(g, y); // И устанавливаем модель для таблицы с новыми данными stdList.setModel(new StudentTableModel(new Vector<Student>(s))); } catch (SQLException e) { JOptionPane.showMessageDialog(StudentsFrame.this, e.getMessage()); } } } // метод для переноса группы private void moveGroup() { JOptionPane.showMessageDialog(this, "moveGroup"); } // метод для очистки группы private void clearGroup() { JOptionPane.showMessageDialog(this, "clearGroup"); } // метод для добавления студента private void insertStudent() { JOptionPane.showMessageDialog(this, "insertStudent"); } // метод для редактирования студента private void updateStudent() { JOptionPane.showMessageDialog(this, "updateStudent"); } // метод для удаления студента private void deleteStudent() { JOptionPane.showMessageDialog(this, "deleteStudent"); } // метод для показа всех студентов private void showAllStudents() { JOptionPane.showMessageDialog(this, "showAllStudents"); } public static void main(String args[]) { SwingUtilities.invokeLater(new Runnable() { public void run() { try { // Мы сразу отменим продолжение работы, если не сможем получить // коннект к базе данных StudentsFrame sf = new StudentsFrame(); sf.setDefaultCloseOperation(EXIT_ON_CLOSE); sf.setVisible(true); // Перегрузка списка нам нужна в этом треде // т.к. при создании формы списка студентов еще нет sf.reloadStudents(); } catch (Exception ex) { ex.printStackTrace(); } } }); } } // Наш внутренний класс - переопределенная панель. class GroupPanel extends JPanel { public Dimension getPreferredSize() { return new Dimension(250, 0); } } |
Кажется, что все замечательно — наша программа научилась обновлять список студентов и все смотрится красиво. Но есть одна неприятность. Представьте себе, что список студентов по какой-либо причине не может быть обновлен быстро. Проведите эксперимент — замените метод reloadStudents() на вот такой код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// метод для обновления списка студентов для определенной группы private void reloadStudents() { if (stdList != null) { // Получаем выделенную группу Group g = (Group) grpList.getSelectedValue(); // Получаем число из спинера int y = ((SpinnerNumberModel) spYear.getModel()).getNumber().intValue(); try { // Получаем список студентов Collection s = ms.getStudentsFromGroup(g, y); // И устанавливаем модель для таблицы с новыми данными stdList.setModel(new StudentTableModel(new Vector(s))); } catch (SQLException e) { JOptionPane.showMessageDialog(StudentsFrame.this, e.getMessage()); } } // Вводим искусственную задержку на 3 секунды try { Thread.sleep(3000); } catch (Exception e) { } } |
После этого запустите пример (подождите 3 секунды — форма появиться не сразу) и попробуйте нажать на стрелочку спинера. Как Вам эффект ? Спинер завис, все приложение не реагирует — крайне неприятная ситуация. Причем можно заметить, что несмотря на то, что мы уже обновили модель, изменения не видны. Мы вынуждены ждать.
Если погрузиться чуть глубже в систему рисования Swing, то вы узнаете, что прорисовка компонентов идет в отдельном потоке (треде — EDT, мы уже упоминали его) и, что самое неприятное, именно в этом же треде вызываются методы листенеров. Т.е. мы исправили модель, но т.к. наш метод еще не завершился, обновление экрана не произошло. И, как вы сами понимаете, зависание какого-либо метода внутри обработки выглядит ужасно. Поэтому очень хорошим выходом из данной ситуации может служить многопотоковость. О потоках вы можете прочитать в документации, либо посмотреть очень приличную статью в FAQ Vingrad Многопоточное программирование.
Если определить в двух словах, то поток это отдельный подпроцесс. Любое приложение имеет как минимум один поток, где все выполняется. Если вы создаете еще один поток, то выполнение их происходит как бы параллельно — выполняется то один, то другой. Потоков можно создать очень много и каждый будет выполнять свою работу самостоятельно. Программа будет переключаться с выполнения одного потока на выполнение другого и будет создаваться впечатление, что потоки выполняются одновременно.
Реализуем наш метод в виде потока. Отметим еще одну особенность — Java позволяет вам создавать «анонимные» классы. Т.е. Вы создаете класс прямо в коде, в нем же переопределяя нужные вам методы. Посмотрите код нашего метода
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
private void reloadStudents() { // Создаем анонимный класс для потока Thread t = new Thread() { // Переопределяем в нем метод run public void run() { if (stdList != null) { // Получаем выделенную группу Group g = (Group) grpList.getSelectedValue(); // Получаем число из спинера int y = ((SpinnerNumberModel) spYear.getModel()).getNumber().intValue(); try { // Получаем список студентов Collection<Student> s = ms.getStudentsFromGroup(g, y); // И устанавливаем модель для таблицы с новыми данными stdList.setModel(new StudentTableModel(new Vector<Student>(s))); } catch (SQLException e) { JOptionPane.showMessageDialog(StudentsFrame.this, e.getMessage()); } } // Вводим искусственную задержку на 3 секунды try { Thread.sleep(3000); } catch (Exception e) { } } // Окончание нашего метода run }; // Окончание определения анонимного класса // И теперь мы запускаем наш поток t.start(); } |
Теперь даже несмотря на то, что наш метод ждет после обновления модели 3 секунды, само обновление экрана происходит сразу. Что несомненно приятно. Команды «очистить группу», «удалить студента» не требуют каких-либо сложных действий. Желательно просто спросить у пользователя подтверждения и все. Приведем код для методов deleteStudent() и clearGroup()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// метод для удаления студента private void deleteStudent() { Thread t = new Thread() { public void run() { if (stdList != null) { StudentTableModel stm = (StudentTableModel) stdList.getModel(); // Проверяем - выделен ли хоть какой-нибудь студент if (stdList.getSelectedRow() >= 0) { if (JOptionPane.showConfirmDialog(StudentsFrame.this, "Вы хотите удалить студента?", "Удаление студента", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { // Вот где нам пригодился метод getStudent(int rowIndex) Student s = stm.getStudent(stdList.getSelectedRow()); try { ms.deleteStudent(s); reloadStudents(); } catch (SQLException e) { JOptionPane.showMessageDialog(StudentsFrame.this, e.getMessage()); } } } // Если студент не выделен - сообщаем пользователю, что это необходимо else { JOptionPane.showMessageDialog(StudentsFrame.this, "Необходимо выделить студента в списке"); } } } }; t.start(); } // метод для очистки группы private void clearGroup() { Thread t = new Thread() { public void run() { // Проверяем - выделена ли группа if (grpList.getSelectedValue() != null) { if (JOptionPane.showConfirmDialog(StudentsFrame.this, "Вы хотите удалить студентов из группы?", "Удаление студентов", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { // Получаем выделенную группу Group g = (Group) grpList.getSelectedValue(); // Получаем число из спинера int y = ((SpinnerNumberModel) spYear.getModel()).getNumber().intValue(); try { // Удалили студентов из группы ms.removeStudentsFromGroup(g, y); // перегрузили список студентов reloadStudents(); } catch (SQLException e) { JOptionPane.showMessageDialog(StudentsFrame.this, e.getMessage()); } } } } }; t.start(); } |
Для сборки нам потребуется команда
javac students/frame/*.java students/logic/*.java
Для сборки примера
javac -encoding UTF-8 students/frame/*.java students/logic/*.java
Для запуска нам надо указать в CLASSPATH файл mysqlJDBC-3.1.13-bin.jar
java -cp .;mysql-connector-java-3.1.13-bin.jar students.frame.StudentsFrame
Теперь мы подошли вплотную к завершающей стадии. Нам осталось только реализовать команды для добавления и редактирования студентов, переноса студентов из одной группы в другую и отчет по всем студентам. Это мы сделаем в следующей части нашего проекта — Часть 6 — GUI — заключительные классы
Архив с исходными кодами: Исходный код