С самого начала JDK пропагандировал модель событий, в которой программа (или аплет) с помощью методов с зарезервированными именами перехватывает события, проделывает с ними всяческие операции и далее решает, передавать ли события в суперкласс. Для каждого события в такой схеме существовал свой отдельный метод, который вызывался главным диспетчером событий - методом handleEvent(). И все бы ничего, если бы не громоздкость и не разрастание классов от лишних строк, которые программист на Java должен был "намолотить" специально для обработки событий. Да и это бы ничего, если бы в каждом обработчике того или иного события не требовалось узнать, что за источник событий (читай элемент управления) сгенерировал данное событие. Так что приходится признать, что первая модель управления была ох как далека от идеала.

Компания JavaSoft предложила новую модель передачи и обработки событий Delegation (делегирование). Согласно этой модели каждый класс, которому нужно получать сообщение, должен зарегистрировать методом add<тип события>Listener у элемента управления специальный интерфейс-слушатель (Listener). Этот класс унаследован от класса java.util.EventListener. Причем для каждой группы событий имеется свой слушатель. К примеру, MouseListener заведует событиями нажатия кнопок мыши, а KeyListener обрабатывает все нажатия и отпускания клавиш клавиатуры. В свою очередь элемент управления хранит список всех зарегистрированных у него слушателей и при подходящем событии вызывает каждый из них. Таким образом, все классы, реализовавшие интерфейс слушателя какой-либо группы событий, уведомляются об их наступлении.

Расширения коснулись и самих классов событий. Если в JDK 1.0 все события были классом java.awt.Event, то в JDK 1.1 представлена целая иерархия классов событий, наследуемых от java.util.EventObject.

Низкоуровневые события служат для отслеживания элементарных операций, тогда как семантические события выполняют более "интеллектуальную" работу по регистрации изменений в состоянии объектов.

Можно использовать в Java-программах и собственные события, наследуя их от классов java.util.EventObject или java.util.AWTEvent. Создавая свой собственный класс событий, нужно придерживаться двух правил:

  • Данные класса следует делать закрытыми для других классов, а для их чтения и записи предоставлять методы get<переменная>() и set<переменная>();
  • Событию следует присваивать уникальное число, которое должно быть больше определенной в Java константы java.awt. AWTEvent.RESERVED_ID_MAX.
  • Вообще-то сама компания JavaSoft в документации к JDK 1.1 предлагает использовать подход, отличный от установки слушателей. JavaSoft навязывает пользователям механизм расширения классов, от которого попахивает духом JDK версии 1.0. В этом случае пользователь создает класс-наследник от того элемента управления, который он хочет использовать в своем приложении. В конструкторе элемента нужно вызвать метод enableEvents(), задав ему в качестве параметров набор констант, описывающих те события элемента, которые необходимо обработать. И следом написать для каждого из этих событий свой обработчик с именем process<тип события>Event(), внутри которого размещаете логику обработки событий. Когда в элементе управления происходит событие, оно передается методу processEvent(), который в цикле switch-case проверяет, что это за событие, и, если оно разрешено вами, вызывает нужный обработчик process<тип события>Event(). Возьмем маленький пример из документации к JDK 1.1:

    public class TextCanvas extends Canvas { 
       boolean haveFocus = false; 
       public TextCanvas() { 
          enableEvents(AWTEvent.FOCUS_EVENT_MASK);
           ... 
          } 
       protected void processFocusEvent(FocusEvent e) { 
          switch(e.getID()) { 
             case FocusEvent.FOCUS_GAINED: 
                haveFocus = true; break; 
             case FocusEvent.FOCUS_LOST: 
                haveFocus = false; 
             }
          repaint(); 
          super.processFocusEvent(e); // let superclass dispatch to listeners 
       } 
       public void paint(Graphics g) { 
          if (haveFocus) { 
          ... 
          } 
       } 
       ... 
    }

    Данная программа достаточно примитивна и просто отслеживает получение и потерю фокуса ввода элементом управления, заставляя элемент перерисоваться по приходу данного события. Как видите, пришлось разрешить обработку события FocusEvent в конструкторе, написать обработчик этого события processFocusEvent(), где не только нужно выполнить действия по обработке события, но и передать обработанное событие суперклассу. Заметим, что такой способ обработки событий совершенно не отличается от того, что предлагалось в JDK 1.0, ну разве что управлением разрешения и запрещения прохождения событий. Элегантности в этом решении никакой, да и возни много. А если потребуется обрабатывать множество событий, то придется описать их все в enableEvents()! Ничего не скажешь, "дубовый" подход. Кроме того, логика, демонстрируемая примером, распространяется на все создаваемые экземпляры объектов класса TextCanvas, и, чтобы хоть чуть-чуть изменить поведение нашего элемента, придется вносить весьма серьезные поправки. Однако компания JavaSoft сама помогает решить проблему и приводит другой пример, уже с применением слушателей:

    public class TextCanvas extends Canvas implements FocusListener { 
       boolean haveFocus = false; 
       public TextCanvas() { 
          addFocusListener(this);
       } 
       public void focusGained(FocusEvent e) { 
          haveFocus = true; 
          repaint(); 
       } 
       public void focusLost(FocusEvent e) { 
          haveFocus = false;
          repaint(); 
       } 
       public void paint(Graphics g) { 
          if (haveFocus) { 
            ...
          } 
       } 
    ... 
    }

    В этом примере элемент устанавливает слушатель для события FocusEvent. Для этого класс TextCanvas реализует интерфейс FocusListener и два его метода - focusGained() и focusLost(), вызываемые при получении и потере фокуса ввода соответственно. Как видите, исходный текст упростился до предела. Правда, первое время трудно понять, каким образом класс устанавливает слушателя для самого себя. Но к этому можно привыкнуть.

    Осталось написать класс так, чтобы каждый новый экземпляр объекта мог сам изменять свое поведение. А теперь еще об одной новинке в JDK 1.1, хотя не связанной с событиями, но часто применяемой при их обработке. Это так называемые внутренние классы (inner classes).

    Внутренние классы давно применяются в языке Cи++, только там называются вложенными (nested). Коротко внутренние классы можно описать как классы, которые объявляются внутри других классов. При этом благодаря скрытой ссылке на вмещающий класс они имеют полный доступ к членам вмещающего класса. Что это означает, можно увидеть на следующем примере:

    import java.awt.*;
    import java.awt.event.*;
    import java.applet.*;
    
    public class MyApplet extends Applet {
       Button b;
       public void init() {
          b = new Button("New Button");
          // Создать и добавить слушатель
          b.addActionListener(new MyListener());
          add(b);
       }
       // Внутренний класс реализует слушатель
       class MyListener implements ActionListener {
          // Вызывается когда кнопка нажата
          public void actionPerformed(ActionEvent e) {
             getAppletContext().showStatus("New Button pressed!");
          }
       }
    }

    В этом примере слушатель события реализован как внутренний класс-наследник от интерфейса ActionListener. В методе init() экземпляр слушателя устанавливается для того, чтобы перехватывать сообщения нажатия кнопки и выводит "New Button pressed!" в строку состояния браузера или утилиты просмотра аплетов Appletvewer. Обратите внимание, как компактнен и понятен исходный текст. Этим замечательным преимуществом внутренних классов можно и нужно пользоваться.

    В Java предусмотрены 11 различных слушателей на все случаи жизни (см. таблицу.).

    Однако учтите, что каждый элемент реализует не все интерфейсы-слушатели. Например, классы Menu и MenuItem реализуют только интерфейс ActionListener. А другие слушатели им не нужны. Какие слушатели поддерживаются тем или иным элементом управления, подробно описано в документации к JDK 1.1.

    Как известно, любой класс, который реализует интерфейс, должен реализовать все его методы. А как быть, если из семи методов, имющихся в интерфейсе слушателя окна, нам нужен всего лишь один windowClosing()? Воспользуйтесь адаптерами. Это классы, реализующие все методы слушателя, но абсолютно пустые, как это показано на примере:

    public abstract class WindowAdapter implements WindowListener {
        public void windowOpened(WindowEvent e) {}
        public void windowClosing(WindowEvent e) {}
        public void windowClosed(WindowEvent e) {}
        public void windowIconified(WindowEvent e) {}
        public void windowDeiconified(WindowEvent e) {}
        public void windowActivated(WindowEvent e) {}
        public void windowDeactivated(WindowEvent e) {}
    }

    Оригинально и просто! Теперь вы просто наследуете свой класс-слушатель не от интерфейса, а от адаптера, реализуя единственный нужный метод windowClosing():

    class MyListener extends WindowAdapter {
       public void windowClosing(WindowEvent e) {
          System.exit(0); // Завершить выполнение программы
       }
    }

    Разумеется, не для всех интерфейсов-слушателей имеются адаптеры. Например, адаптер для ActionListener и не нужен, поскольку у слушателя всего один метод.

    Давайте рассмотрим небольшой хрестоматийный пример аплета с двумя кнопками:

    import java.awt.*;
    import java.awt.event.*;
    import java.applet.*;
    
    public class MyApplet extends Applet {
      Button b1, b2;
      TextField t = new TextField(20);
      public void init() {
        b1 = new Button("Button 1");
        b2 = new Button("Button 2");
        // добавить слушателей для кнопок
        b1.addActionListener(new Listener1());
        b2.addActionListener(new Listener2());
        add(b1);
        add(b2);
        add(t);
      }
      // Слушатель для первой кнопки
      class Listener1 implements ActionListener {
        public void actionPerformed(ActiomEvent e) {
          t.setText("Button 1 pressed");
        }
      }
      // Слушатель для второй кнопки
      class Listener2 implements ActionListener {
        public void actionPerformed(ActiomEvent e) {
          t.setText("Button 2 pressed");
        }
      }
        // Адаптер, следящий за закрытием окна
      static class MyAdapter extends WindowAdapter {
        public void windowClosing(WindowEvent e) {
          // Завершить приложение
          System.exit(0);
        }
      }
      // Точка входа в программу
      public static void main(String args[]) {
        // Создаем аплет
        MyApplet applet = new MyApplet();
        // Создаем окно
        Frame frame = new Frame("Test frame");
        // Вставляем аплет внутрь окна
        frame.add(applet. BorderLayout.CENTER);
        // Подключаем слушатель закрытия окна
        frame.addWindowListener(new MyAdapter());
        // Изменяем размер (бывший метод resize())
        frame.setSize(300, 200);
        applet.init();
        applet.start();
        // Показать окно (бывший метод show())
        frame.setVisible(true);
      }
    }

    Вряд ли к такой программе нужны какие-нибудь комментарии. Метод слушателей делает логику работы приложения очевидной, потому более привлекателен, чем метод разрешения событий и отдельных обработчиков. Тем более, как вы могли заметить, задавая разные слушатели для разных кнопок, мы создали две одинаковые с виду кнопки с разным поведением. При этом мы не употребили ни одного оператора if или switch-case и не делали никакой работы по разрешению обработки сообщений - все это делается без нас внутри библиотеки классов Java.

    В связи с тем, что в делегирующей модели сообщений передача событий от класса-наследника к классу-предку присходит автоматически, встает вопрос об управлении прохождением сообщений. Типичный пример этому - желание разработчика нестандартно обработать нажатие кнопки формы, но при этом запретить показывать нажатие визуально. Чтобы сделать это, нужно отменить передачу сообщения суперклассу. Модель событий JDK 1.0 просто не предполагает вызов метода класса-предка - и все. В JDK 1.1 для прекращения действия события нужно вызвать специальный метод под названием consume(), принадлежащий классу java.awt.event.InputEvent и, следовательно, действующий лишь для событий самого низкого уровня (нажатие и отпускание клавиши клавиатуры, перемещение мыши и нажатие кнопок мыши). Но беспокоиться не о чем, потому что прерывать поток прохождения других событий вам вряд-ли понадобится. Проверить, было ли обработано событие, можно, вызвав метод isConsumed(), который возвратит вам true, если событие уже прекращено.

    Модель обработки сообщений JDK 1.1 интересна еще и тем, что пользователю доступна очередь сообщений. Она (как и все в Java) представлена классом. Этот класс носит название java.awt.EventQueue и предоставляет в распоряжение программиста несколько полезных методов для манипуляции очередью.

  • public synchronized void postEvent(AWTEvent e) - добавляет сообщение в конец очереди;
  • public synchronized AWTEvent getNextEvent() - вынимает очередное сообщение и удаляет его из очереди;
  • public synchronized AWTEvent peekEvent() - вынимает очередное сообщение, но оставляет его в очереди;
  • public synchronized AWTEvent peekEvent(int eventID) - аналогичен предыдущему, но вынимает лишь то сообщение, которое имеет число, заданное параметром ID.
  • Добраться до системной очереди сообщений можно с помощью набора графических инструментов виртуальной машины (Toolkit). Для этого нужно взять ссылку на Toolkit, затем вызвать метод получения ссылки на очередь сообщений, который описан следующим образом:

    public final EventQueue getSystemEventQueue() 

    Однако этот метод защищен менеджером безопасности, поэтому не пытайтесь использовать его из Internet-аплетов. Примерный фрагмент "залезания" в системную очередь может выглядеть так:

    ...
    Toolkit toolkit = getToolkit();
    EventQueue queue = toolkit.getSystemEventQueue();
    queue.postEvent(event);
    ...

    Поскольку такой фрагмент работает лишь для локальных приложений, то любой аплет должен использовать dispatchEvent() вместо postEvent(), иначе будет возбуждено исключение защиты.

    * * *

    И в заключение хочу обратить ваше внимание вот на что: все, что связано с использованием событий в Java-приложениях, лучше всего постигать на практике, разрабатывая собственные компоненты и программы.


    Слушатели и их методы

    Слушатель

    Реализуемые методы

    Когда вызываются

    ActionListener actionPerformed Срабатывание предопределенного действия управляющего элемента
    AdjustmenListener adjustmenValueChanged() Изменение отслеживаемого значения
    ComponentListener componentHidden() componentShown() componentMoved() componentResized() Изменение состояния компонента
    ContainerListener componentAdded() componentRemoved() Добавление и удаление компонентов из контейнера
    FocusListener focusGained() focusLost() Получение и потеря фокуса ввода
    KeyListener keyPressed() keyReleased() keyTyped() Нажатия клавиши клавиатуры
    MouseListener mouseClicked() mouseEntered() mouseExited() mousePressed() mouseReleased() Слежение за состоянием мыши и ее место положение
    MouseMotionListener mouseDragged() mouseMoved() Слежение за перемещением мыши
    WindowsListener windowsOpened() windowsClosing() windowsClosed() windowsActivated() windowsDeactivated() windowsIconified() windowsDeiconified() Изменение состояния окна программы
    ItemListener itemStateChanged() Изменение состояния элементов
    TextListener textValueChanged() Текстовые изменения