Улучшаем интерфейс — листенеры, модель таблицы и потоки

Переделав наше приложения в плане работы с базой данных, вернемся к пользовательскому интерфейсу. Во-первых, мы не реализовали достаточно много функций, во-вторых — наш интерфейс в общем-то далек от совершенства.

Предварительно опишем (еще раз), что мы теперь хотим получить:

  • В верхней части экрана у нас остается тот же самый спин. Но к нему надо приделать листенер, который будет вызывать обновление списка студентов.
  • Список групп. Мы его оставим таким, как был. Только добавим листенер, который будет реагировать на изменение выделенной группы.
  • Список студентов будет изменен. Во-первых будем показывать студентов из выделенной группы, а во-вторых будем показывать их не списком, а таблицей. Для таблицы мы создадим свою собственную модель. Особенности мы обсудим позже.
  • Добавим три кнопки для редактирования студентов в нижнюю часть таблицы со списком студентов.
  • Добавим две кнопки внизу списка групп — для удаления всех студентов из группы и для перевода всех студентов в другую группу. Хорошо наверно было бы сделать возможность выделять студентов и переводить только выделенных, но это мы оставим для самостоятельного изучения. Кроме этого мы сделаем наш список групп постоянным по ширине — иначе, если группы называются 1, 2, 3 и т.д., то наш список будет очень узким.
  • При добавлении нового студента или редактировании существующего будем выводить диалоговое окно. При добавлении студента диалоговое окно не должно закрываться после добавления студента — пользователь может сразу начать вводить данные другого студента. При этом значения для полей ГРУППА и ГОД должны оставаться такими, как были, а все остальные поля очищаются. Конечно же группы выбираются не по ИД, а из списка.
  • Добавим пункты меню «Отчеты», где мы сможем получать различные отчеты. На сегодня у нас только один отчет — список всех студентов.

Теперь давайте постепенно будем приближаться к нашей цели. Стоит отметить, что наш класс ManagementSystem делает все, что нам надо и то, что касается так называемого back-end (такое название относится к той части системы, которая отвечает за работу с базой данных) нас уже мало волнует — все для работы с базой у нас есть. Мы будем просто вызывать те методы, которые нам потребуются.
Итак, давайте сделаем первую версию нашего интерфейса. Мы добавим кнопки внизу и сделаем нашу панель для групп не сжимаемую по ширине. Кнопки можно увидеть в тексте — там ничего нет сложного. Для «несжимаемости» нам надо будет переопределить метод getPreferredSize(). Его можно увидеть в коде.
Мы пока будем смотреть на один файл — StudentsFrame.java. Все остальные не будут изменяться совсем. Как обычно в конце статьи будут приведены тексты для всех файлов.

 

StudentsFrame.java

Теперь мы сделаем так, чтобы наш интерфейс «ожил». Сделаем реакции на кнопки,изменения групп и спинера. Будем вызывать методы, в которых будут стоять «заглушки» — сообщения о том, что метод вызван. У нас сообщения приходят от четырех видов компонентов — меню, кнопка, спинер, список.

Для меню и кнопок используется один вид листенера — ActionListener.
Для списка используется, как мы уже знаем, ListSelectionListener.
Остается только спинер — для него используется ChangeListener.

Давайте теперь расставим все наши листенеры и сделаем «заглушки». Можно запустить наше приложение и проверить все наши управляющие компоненты — по идее все должно работать, т.е. выдавать сообщения на каждое действие — меню, кнопки, перемещение по списку групп, изменение величины в спинере. Комментарии к коду смотрите прямо в тексте программы.

StudentsFrame.java

Модель для таблицы

Теперь давайте реализуем первую команду — перегрузка списка студентов. Как уже упоминалось выше, нам предстоит определить свою модель. Реализация полной модели, т.е. интерфейса TableModel — задача сложная. В большинстве случаев нам необходимо только реализовать несколько методов. Понимая это разработчики Java создали класс AbstractTableModel, который реализует большинство необходимых методов. Для создания своей модели достаточно переопределить всего 3 метода:

  1. public int getRowCount();
  2. public int getColumnCount();
  3. public Object getValueAt(int row, int column);

Если мы запишем список студентов в вектор и определим порядок столбцов, то реализация будет выглядеть достаточно несложно. Конечно, существует возможность использовать таблицу через стандартные вызовы, отдавая ей векторы. Но могу вас уверить — это будет выглядеть сложнее, чем написать свою модель. Тем более, что бОльшая часть работы уже сделана за нас — классом AbstractTableModel.

 

StudentTableModel.java

И теперь можно привести код для обновленного StudentsFrame — там сделано важное изменение — реализован метод reloadStudents(), который загружает список студентов для выделенной группы и года.

StudentTableModel.java

Кажется, что все замечательно — наша программа научилась обновлять список студентов и все смотрится красиво. Но есть одна неприятность. Представьте себе, что список студентов по какой-либо причине не может быть обновлен быстро. Проведите эксперимент — замените метод reloadStudents() на вот такой код:

После этого запустите пример (подождите 3 секунды — форма появиться не сразу) и попробуйте нажать на стрелочку спинера. Как Вам эффект ? Спинер завис, все приложение не реагирует — крайне неприятная ситуация. Причем можно заметить, что несмотря на то, что мы уже обновили модель, изменения не видны. Мы вынуждены ждать.

Если погрузиться чуть глубже в систему рисования Swing, то вы узнаете, что прорисовка компонентов идет в отдельном потоке (треде — EDT, мы уже упоминали его) и, что самое неприятное, именно в этом же треде вызываются методы листенеров. Т.е. мы исправили модель, но т.к. наш метод еще не завершился, обновление экрана не произошло. И, как вы сами понимаете, зависание какого-либо метода внутри обработки выглядит ужасно. Поэтому очень хорошим выходом из данной ситуации может служить многопотоковость. О потоках вы можете прочитать в документации, либо посмотреть очень приличную статью в FAQ Vingrad Многопоточное программирование.
Если определить в двух словах, то поток это отдельный подпроцесс. Любое приложение имеет как минимум один поток, где все выполняется. Если вы создаете еще один поток, то выполнение их происходит как бы параллельно — выполняется то один, то другой. Потоков можно создать очень много и каждый будет выполнять свою работу самостоятельно. Программа будет переключаться с выполнения одного потока на выполнение другого и будет создаваться впечатление, что потоки выполняются одновременно.
Реализуем наш метод в виде потока. Отметим еще одну особенность — Java позволяет вам создавать «анонимные» классы. Т.е. Вы создаете класс прямо в коде, в нем же переопределяя нужные вам методы. Посмотрите код нашего метода

Теперь даже несмотря на то, что наш метод ждет после обновления модели 3 секунды, само обновление экрана происходит сразу. Что несомненно приятно. Команды «очистить группу», «удалить студента» не требуют каких-либо сложных действий. Желательно просто спросить у пользователя подтверждения и все. Приведем код для методов deleteStudent() и clearGroup()

Для сборки нам потребуется команда

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 — заключительные классы

Архив с исходными кодами: Исходный код

8 comments to Пользовательский интерфейс, часть 2

  • Konstantin  says:

    Здравствуйте. На этапе этого урока программа выдает Exception при поочередной смене групп.
    Работаю в Eclipse. При чем не при каждой смене группы, а на раз восьмой, иногда пятнадцатый, в общем зависимости от чего либо, я не вижу. Программа не прекращает работу, а просто в консоль выдает следующее:
    Exception in thread «AWT-EventQueue-0» java.lang.ArrayIndexOutOfBoundsException: 2 >= 0
    at java.util.Vector.elementAt(Vector.java:474)
    at javax.swing.table.DefaultTableColumnModel.getColumn(DefaultTableColumnModel.java:294)
    at javax.swing.plaf.basic.BasicTableUI.paintCells(BasicTableUI.java:2012)
    at javax.swing.plaf.basic.BasicTableUI.paint(BasicTableUI.java:1812)
    at javax.swing.plaf.ComponentUI.update(ComponentUI.java:161)
    at javax.swing.JComponent.paintComponent(JComponent.java:780)
    at javax.swing.JComponent.paint(JComponent.java:1056)
    at javax.swing.JComponent.paintToOffscreen(JComponent.java:5219)
    at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:290)
    at javax.swing.RepaintManager.paint(RepaintManager.java:1265)
    at javax.swing.JComponent._paintImmediately(JComponent.java:5167)
    at javax.swing.JComponent.paintImmediately(JComponent.java:4978)
    at javax.swing.RepaintManager$4.run(RepaintManager.java:824)
    at javax.swing.RepaintManager$4.run(RepaintManager.java:807)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75)
    at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:807)
    at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:782)
    at javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:731)
    at javax.swing.RepaintManager.access$1300(RepaintManager.java:64)
    at javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1720)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
    at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
    at java.awt.EventQueue.access$500(EventQueue.java:97)
    at java.awt.EventQueue$3.run(EventQueue.java:709)
    at java.awt.EventQueue$3.run(EventQueue.java:703)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
    Это значение в первой строке каждый раз разное — «2 >= 0»
    Уже третий день ищу причину, пожалуйста подскажите в чем она.
    P.S. Спасибо за вашу работу, очень полезный пример и хорошо преподнесен.

    • Konstantin  says:

      Жаль, что вы не успели ответить, но возможно я нашел причину Excepion. По моему мнению причина кроется в том, что когда мы выбираем группу, вызывается метод — reloadStudents(), в котором мы, чтоб не тормозить программу, создали отдельный поток в котором и пытаемся отобразить данные из нашей базы установив новую модель для таблицы. Так вот, как я понял, любые действия с User Interface должны производится при помощи класса -javax.swing.SwingUtilities и его методов -invokeAndWait(Runnable) и invokeLater(Runnable).
      Переделал код и больше ни одного Exeption, плюс при загрузке программы стала отображаться таблица сразу, когда ранее приходилось делать выбор группы для отображения таблицы.
      В Общем прошу меня поправить если я в чем либо ошибаюсь либо подправить код и добавить комментарии касательно этого момента.
      В любом случае спасибо за полезный пример, ниже выкладываю переделанный метод reloadStudents() —
      private void reloadStudents(){
      SwingUtilities.invokeLater(new Runnable(){
      public void run(){
      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());
      }
      }
      }
      });
      }

      • Konstantin  says:

        А вот и нет))) при таком исправлении, если поставить sleep(3000), программа зависает и нет возможности нажать ни на одну кнопку. То есть вернулись к тому с чего начали(
        Admin HELP!

  • Konstantin  says:

    И вот наконец то Final Version:
    private void reloadStudents(){
    Thread t = new Thread(){
    public void run(){
    if(stdList != null){
    Group g = (Group) grpList.getSelectedValue();
    int y = ((SpinnerNumberModel) spYear.getModel()).getNumber().intValue();
    try{
    Collection s = ms.getStudentsFromGroup(g,y);
    SwingUtilities.invokeLater(new Runnable(){
    public void run(){
    stdList.setModel(new StudentTableModel(new Vector(s)));
    }
    });

    }catch(SQLException e){
    JOptionPane.showMessageDialog(StudentsFrame.this, e.getMessage());
    }
    }
    }
    };
    t.start();
    }

    вот так работает не тормозит и ни каких Exeption!!!

  • Konstantin  says:

    Пол года я подождал и никакого ответа на мой комментарий от «Июнь 28, 2015 at 19:07» !!!
    У меня уже кончается терпение!

    • admin  says:

      Подтвердить, что это хорошее (и более верное) решение ? Да, это более верное решение с точки зрения использования Swing. Я в общем мало занимался Swing и данные статьи были написаны просто для того, чтобы было не скучно дальше. Да и писалось это лет 10 назад. Я очень хочу переписать весь «Отдел кадров» — но после того, как закончу «Начала Java»

  • Павел  says:

    А как в grpList сразу загружается список групп? Почему именно названия групп, а не ID или любое другое поле? Где явно задается что там именно Select groupName ?

    • admin  says:

      Там список групп, и каждая группа имеет ID и имя. Посмотрите внимательно код

Leave a reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Лимит времени истёк. Пожалуйста, перезагрузите CAPTCHA.