Про java swing - часть 2

January 11, 2008

Обработка событий



Любой элемент управления генерирует события при воздействии на него пользователя или при наступлении внутренних изменений. В обработке событий иделогия java прошла через три стадии развития. Если кратко представить первые две модели, то они подобны тем, которые приняты в библиотеке VCL. Т.е. мы создаваем класс-наследник от некоторого элемента управления, а затем перекрываем методы обработчики событий. Например, так в старой модели выглядело бы закрытие формы.

Самая первая модель


  1. public static void main(String[] args) {
  2.    new JFrame("Old Events Model") {
  3.       public boolean handleEvent(Event evt) {
  4.         if (evt.id == Event.WINDOW_DESTROY) {
  5.             System.exit(0);
  6.             return true;
  7.         } else
  8.             return super.handleEvent(evt);
  9.         }
  10.    }.setVisible(true);
  11. }

Вторая, но тоже устаревшая, модель


  1. new JFrame("Old Events Model") {
  2.       protected void processWindowEvent(WindowEvent evt) {
  3.          if (evt.getID() == Event.WINDOW_DESTROY) {
  4.              System.exit(0);
  5.          } 
  6.       }
  7.   }.setVisible(true);
Новая модель основана на концепции слушателей, для демонстарации ее приводится пример (не GUI). Где класс Summator предназначен для накопления суммы чисел, и как только данная сумма превосходит некоторый лимит, то извещает заинтересованные фрагменты кода в этом событии.
  1. // есть класс, который умеет складывать числа (накапливать их в какой-то внутренней переменной)
  2. class Summator {
  3.        ArrayList listeners = new ArrayList();
  4.        // некий массив, список, коллекция в которой хранится множество объектов, 
  5.        // поддерживающих интерфейс слушателя (надо было бы здесь использовать generics)
  6.        // метод добавляющий объект-слушатель
  7.        public void addSumEventListener (ISumReachedListener l){
  8.            // перед добавлением слушателя очень неплохо проверить, что такого объекта
  9.            // еще нет в списке добавленных слушателей
  10.            if (!listeners.contains(l))
  11.                listeners.add (l);
  12.        }
  13.        // метод удаляющий из очереди подписчиков объект-слушатель
  14.        public void removeSumEventListener (ISumReachedListener l){
  15.                listeners.remove (l);
  16.        }
  17.        // переменная нужная для работы сумматора. В ней накапливатся 
  18.        int current_sum = 0;
  19.        // получаем значение текущей суммы - в лучших традициях,
  20.        // доступ к внутренним полям класса должен быть закрыт
  21.        public int getCurrentSum() {
  22.            return current_sum;
  23.        }
  24.        // добавляем к сумме очередное число, и если лимит в 1000 был превышен, то генерируем событие
  25.        public void addNumber (int num){
  26.            current_sum+=num;
  27.            if (current_sum > 1000)
  28.                fireSumWasReachedEvent ();
  29.        }
  30.        // служебная функция выполняющая отправку всем находящимся в коллекции 
  31.        // объектам-подписчикам извещения об наступлении события
  32.        private void fireSumWasReachedEvent() {
  33.            for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
  34.                ISumReachedListener iSumReachedListener = (ISumReachedListener) iterator.next();
  35.                iSumReachedListener.onSumWasReached(this);
  36.            }
  37.        }
  38.    }
  39.    // интерфейс, который должны поддерживать все объекты, заинтересованные в получении извещений
  40.    interface ISumReachedListener {
  41.        void onSumWasReached (Summator sum);
  42.    }
  43.  
  44. // пример кода тестирующего класс сумматор
  45. public class TestF {
  46.     public static void main(String[] args) {
  47.     // я создаю анонимный класс реализующий интерфейс 'слушающего' ISumReachedListener
  48.        ISumReachedListener l1 = new ISumReachedListener() {
  49.            public void onSumWasReached(Summator sum) {
  50.                // когда наступает событие-достигнута сумма в 1000 $
  51.                System.out.println ("Sum Was Reached: Listener # 1: " + sum.getCurrentSum());
  52.            }
  53.        };
  54.         // количество классов подписчиков является неограниченным
  55.         ISumReachedListener l2 = new ISumReachedListener() {
  56.             public void onSumWasReached(Summator sum) {
  57.                 System.out.println ("Sum Was Reached: Listener # 2 "+ sum.getCurrentSum());
  58.             }
  59.         };
  60.         // создали класс сумматор, генерирующий события
  61.         Summator s = new Summator();
  62.         // привязали к нему два заинтересованных подписчика 
  63.         s.addSumEventListener(l1);
  64.         s.addSumEventListener(l2);
  65.         // один из подписчиков решил что ему более не нужны извещения об работе сумматора
  66.         s.removeSumEventListener(l1);
  67.         // тест на повторное добавление объекта-слушателя
  68.         s.addSumEventListener(l2);
  69.         // затем выполняется генерация в цикле случайных чисел и после этого 
  70.         Random random = new Random();
  71.         for (int i = 0; i < 100; i++) {
  72.             int num = Math.abs(random.nextInt()) % 100;
  73.             s.addNumber(num);
  74.         }
  75.     }
  76. }
Класс который хочет извещать другие классы (слушателей) о том, что происходит некоторое событие, должен внутри себя реализовать некоторый контейнер (массив, список, что угодно), в который будут помещаться и удаляться слушатели. Классы-слушатели обязательно должны поддерживать интерфейс IСобытиеПроизошло. Фактически мы не ограничиваем линию наследования классов слушателей и получаем гарантию, что в их составе будет специальный метод, который необходимо вызвать при наступлении данного события. Для добавления и удаления создаются два общедоступных (public) метода, addXXX, removeXXX. Для того, чтобы сгенерировать событие необходимо вызвать вспомогательный (и обычно private или protected) метод fireXXX. Который в цикле пробегает по списку слушателей и вызывает методы по очереди. Важно т.к. при вызове метода слушателя возможна ошибка (исключение), то желательно заключать такие вызовы в секции try … catch. В противном случае если при извещении одного из слушателей будет выброшено непойманное исключение, то остальные слушатели не будут пройдены и ничего об произошедшем событии не узнают. В примере выше это не сделано мною, сделайте это сами.

Теперь пример того как можно реагировать на события изменения размеров и закрытия окна.
  1. public class TWNow {
  2.     public static void main(String[] args) {
  3.         // создаем окно
  4.         JFrame jf = new JFrame("Nova Events Model");
  5.         Toolkit tk = jf.getToolkit();
  6.         // определяем поддерживает ли оконный менеджер (ну java типа работает под разными OС,
  7.         и типа разными оконными менеджерами)
  8.         if (!(tk.isFrameStateSupported(Frame.ICONIFIED))) {
  9.             System.out.println("Your window manager doesn't support ICONIFIED.");
  10.         }
  11.         if (!(tk.isFrameStateSupported(Frame.MAXIMIZED_VERT))) {
  12.             System.out.println("Your window manager doesn't support MAXIMIZED_VERT.");
  13.         }
  14.         if (!(tk.isFrameStateSupported(Frame.MAXIMIZED_HORIZ))) {
  15.             System.out.println("Your window manager doesn't support MAXIMIZED_HORIZ.");
  16.         }
  17.         if (!(tk.isFrameStateSupported(Frame.MAXIMIZED_BOTH))) {
  18.             System.out.println("Your window manager doesn't support MAXIMIZED_BOTH.");
  19.         } else {
  20.             System.out.println("Your window manager supports MAXIMIZED_BOTH.");
  21.         }
  22.         // добавляем обработчик события - класс который слушает интерфейс WindowStateListener 
  23.         // будет получать извещения о том, что окно было минимизировано, максимизировано, ...
  24.         // для этого вызывается функция windowStateChanged, во входной параметре которой передается вся
  25.         // дополнительная информация что же случилось
  26.         jf.addWindowStateListener(new WindowStateListener() {
  27.             String convertStateToString(int state) {
  28.                 if (state == Frame.NORMAL) {
  29.                     return "NORMAL";
  30.                 }
  31.                 if ((state & Frame.ICONIFIED) != 0) {
  32.                     return "ICONIFIED";
  33.                 }
  34.                 if ((state & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH) {
  35.                     return "MAXIMIZED_BOTH";
  36.                 }
  37.                 if ((state & Frame.MAXIMIZED_VERT) != 0) {
  38.                     return "MAXIMIZED_VERT";
  39.                 }
  40.                 if ((state & Frame.MAXIMIZED_HORIZ) != 0) {
  41.                     return "MAXIMIZED_HORIZ";
  42.                 }
  43.                 return "UNKNOWN";
  44.             }
  45.             public void windowStateChanged(WindowEvent e) {
  46.                 int state = e.getNewState();
  47.                 int oldState = e.getOldState();
  48.                 String msg = "New state: "
  49.                         + convertStateToString(state) + "\n"+
  50.                         "Old state: " + convertStateToString(oldState);
  51.                 System.out.println(msg);
  52.             }
  53.         });
  54.         jf.setVisible(true);// это конечно не самый лучший способ показать окно и далее я скажу почему
  55.     }
  56. }
Также вы можете реализовать реакцию на событие активации или деактивации и закрытия окна.
 <h3> Обработка событий изменения состояния окна </h3>
  1. jf.addWindowListener(
  2.   new WindowListener() {
  3.     public void windowOpened(WindowEvent e) {}
  4.     public void windowClosing(WindowEvent e) {}
  5.     public void windowClosed(WindowEvent e) {}
  6.     public void windowIconified(WindowEvent e) {}
  7.     public void windowDeiconified(WindowEvent e) {}
  8.     public void windowActivated(WindowEvent e) {}
  9.     public void windowDeactivated(WindowEvent e) {}
  10.  }
  11. );

Обработка события получения окном фокуса или его потери


  1. jf.addWindowFocusListener(
  2.   new WindowFocusListener() {
  3.      public void windowGainedFocus(WindowEvent e) {}
  4.      public void windowLostFocus(WindowEvent e) {}
  5.   }
  6. );
Примечание: в общем случае, количество методов входящих в состав интерфейса слушателя достаточно велико и требование релизовывать все эти методы - неудобно. Для таких ситаций были изобретены адаптеры. Класс считается адаптером, если он реализует интерфейс слушателя и все его методы, которые ничего не делают (имеют пустое тело). Затем при необходимости вы подаете на вход методу addXXX не класс поддерживающий интерфейс слушателя, а именно класс производный от адаптера. Но перекрываете внутри этого класса метод-пустышку нужным вам поведением.
  1. jf.addWindowListener(new WindowAdapter() {
  2.    public void windowClosing(WindowEvent e) {
  3.       System.out.println ("Close Frame: " + e);
  4.       System.exit (0);
  5.    }
  6. });
Следующим шагом будет реакция на события мыши. Для примера создается фрагмент кода, который рисует рамку в зависимости от текущего выделения мышью.
  1. public class BooRamka {
  2.     // создаем и запускам экземляр класса строящего графический интерфейс
  3.     public static void main(String[] args) {
  4.         new BooRamka().go ();
  5.     }
  6.     // признак того что сейчас рисуется рамка
  7.     boolean is_started = false;
  8.     // начальная точка (левый-верхний угол) рамки
  9.     Point start_point = new Point(0,0);
  10.     Point old_point = new Point();
  11.     // функия создающая окно и добавляющая к нему функцию рисования рамки и реакцию на перемещение мыши
  12.     private void go() {
  13.         final JFrame jf = new JFrame("Selection Example"){
  14.             public void paint(Graphics g) {
  15.                 super.paint(g);
  16.                 // рисование очень простое - рисуем прямоугольники с рамкой синего цвета
  17.                 g.setColor(Color.BLUE);
  18.                 for (int i = 0; i < 10; i++)
  19.                     g.drawRect(25*i , 25*i , 50 , 50);
  20.             }
  21.         };
  22.         // добавляем обработчик события для нажатия и освобождения кнопки мыши
  23.         jf.addMouseListener(
  24.                 new MouseAdapter() {
  25.                     // когда кнопку нажали, то очевидно, что начато рисование зеленого прямоугольника
  26.                     // рамки выделения
  27.                     public void mousePressed(MouseEvent e) {
  28.                         is_started = true;
  29.                         start_point = e.getPoint();
  30.                         old_point = start_point;
  31.                     }
  32.                     // рисование рамки завершено, тогда когда кнопка мыши была отпущена
  33.                     public void mouseReleased(MouseEvent e) {
  34.                         is_started = false;
  35.                     }
  36.                 }
  37.         );
  38.         // добавляем обработку событий для перемещения мыши
  39.         jf.addMouseMotionListener(
  40.                 new MouseMotionAdapter() {
  41.                     // Dragged - значит что указатель мыши перетаскивают, кнопка нажата и следовательно нужно выполнять рисование
  42.                     public void mouseDragged(MouseEvent e) {
  43.                         // откровенно говоря, показанный здесь способ рисования ужасен - рисовать следует внутри метода paint, 
  44.                         // а отсюда следовало бы выполнить отправку запроса на вызов этого метода - 
  45.                         // ни в коем случае нельзя метод paint вызывать явно
  46.                         Graphics g = jf.getGraphics();
  47.                         Point now = e.getPoint();
  48.                         g.setXORMode(Color.BLUE);
  49.                         g.setColor(Color.RED);
  50.                         g.drawRect((int)start_point.getX() , (int)start_point.getY(),
  51.                                 (int)(old_point.getX() - start_point.getX()),
  52.                                 (int)(old_point.getY() - start_point.getY())
  53.                                 );
  54.                         g.drawRect((int)start_point.getX() , (int)start_point.getY(),
  55.                                 (int)(now.getX() - start_point.getX()),
  56.                                 (int)(now.getY() - start_point.getY())
  57.                                 );
  58.                         old_point = e.getPoint();
  59.                     }
  60.                 }
  61.         );
  62.         // показываем окно для рисования
  63.         jf.setVisible(true);
  64.     }
  65. }


Откровенно говоря, реализован пример просто ужасно: при изменении размеров или при попытке поместить окно приложения за другим окном возникают артефакты изображения. Также в свое первое появление окно имеет минимальный размер. Далее я раскажу как решить эту проблему.

Врезка: проектирование интерфейса в java может быть проведено с использованием всевозможных мастеров. Какие мастера и как их использовать вы должны смотреть в составе конкретного средства разработки. Один из лидеров интерфейсостроения borland jbuilder, для таких сред как eclipse или idea есть плагины. В последней версии idea дизайнер форм - является встроенным(4.5). Собственный опыт: создавать интерфейс без мастеров и визуальных построителей проще и удобнее, да и контроля больше. Собственный опыт будет меняться во времени, с появлением новых средств проектирования или изменения взглядов на жизнь.

Как грамотно показать окно, спрятать, изменить надпись на кнопке и т.д.



Для того чтобы отобразить созданное окно мы применяли метод setVisible (true), соответственно для того чтобы окно спрятатать setVisible (false). Этот метод применим и для любых других элементов управления (кнопки, текстовые поля и т.д.). Существует рекомендация как из кода (выполняющего некоторый расчет) выполнить изменение UI.

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

Вы создали кнопку, создали обработчик события "click", и самой большой ошибкой будет написать внутри этого метода что-то вроде:
  1. public void actionPerformed(ActionEvent e) {
  2.   // Расчет числа PI. Длительный расчет. Очень длительный.
  3.   txt_result.setText (РезультатРасчета);
  4. }
Т.к. в UI только один поток занимается обработкой интерфейса (перерисовку элементов управления, каждый раз когда их кто-то перекрывает, сворачивание-разворачивание окна) и этот же поток вызывает функции-обработчики событий (когда пользователь нажимате на кнопку или выбирает что-то в падающем списке). Значит что длительные вычисления внутри функции-обработчика событий приведут к блокировке выполнения других задач обслуживания UI (баги отрисовки, интерфейс "замерзнет" и т.д.). Следовательно длительный расчет необходимо выполнять только внутри отдельного потока. Это еще не все. Есть требование чтобы код который получает доступ к элементам управления выполнялся внутри специального системного потока (обрабатывающего события, так называемый, EventDispatchThread). Следовательно мы не имеем права внутри запущенного нами потока выполняющего расчет изменять содержимое UI, ведь этот поток не EventDispatchThread.

Поэтому в состав класса javax.swing.SwingUtilities были введены два метода: invokeLater и invokeAndWait. Каждому из них в качестве параметра передается ссылка на объект Runnable. Эти методы откладывают выполнение кода заключенного в методе run, до тех пор пока не будут выполнены все остальные запланированные действия подсистемой UI. Только затем выполняется код помещенный внутрь run и выполняется он в потоке EventDispatchThread. Рекомендуется весь код, который выполняет изменения интерфейса (прячет или показывает кнопки, меняет текстовые поля), заключать внутрь invoke*. Различие между invokeLater и invokeAndWait в том, что второй метод блокирует выполнение вашего кода до тех пор пока не отработает код внутри run. Ха, и если вы вызовите этот метод из потока EventDispatchThread, то получится чушь, и будет выброшено исключение. Первый метод может быть вызван из любого потока (в том числе и EventDispatchThread). Если вы не в состоянии определить какой поток выполняет ваш код, то используйте в составе класса SwingUtilites следующий метод:
  1. public static boolean isEventDispatchThread()
Рассмотрим следующий пример:
  1. public static void main(String[] args) {
  2.     JFrame f = new JFrame("hack gui");
  3.     JButton jp = new JButton ("click me");
  4.     f.setSize(400, 400);
  5.     // элементы UI созданы, и с этого момента мы не должны обращаться к ним извне пределов EventDispatchThread
  6.     // фактически даже следующая строка является уже неправильной, т.к. выполняется в потоке main
  7.     f.setVisible(true);
  8. }
Последнюю строку (показ окна после его создания с помощью setSize) следует выполнить так:
  1. SwingUtilities.invokeLater(
  2.   new Runnable() {
  3.     public void run() {
  4.       jf.setVisible(true);                
  5.     }
  6.   }
  7. );
Есть ряд методов работающих с UI, которые можно смело вызывать из любого потока (ищите пометки в документации). Естественно, что это не обязательно и вы можете проигнорировать требования работы с UI - но разве вам нужны лишние проблемы?

Элементы управления



Начнем с простого: необходимо создать приложение наподобие калькулятора. Прежде всего, необходимо понимать, что все элементы управления делятся на две категории: элементы и контейнеры элементов. Контейнеры содержат список расположенных элементов и управляют их размещением, используя концепцию менеджеров раскладок. Менеджер раскладки – класс, который располагает элементы по правилам: например все друг за дружкой, а при нехватке места - переход на новую строку; или вся область контейнера разделяется на ячейки, в каждой из которых будет размещено по элементу управления. Возможно, эти клетки могут быть одинакового размера а возможно и нет. Разумно комбинируя контейнеры с разными менеджерами раскладок, можно создать практически любой интерфейс. Мы вернемся к менеджерам раскладки позже, пока же будем управлять размещением элементов вручную указывая для каждого координаты и размер.

Все контейнеры в мире awt (той самой древней библиотеке построения UI) производны от класса java.awt.Container. Впоследствии, все элементы управления в библиотеке swing были унаследованы от javax.swing.Jcomponent. Который, в свою очередь, является наследником от java.awt.Container. Следовательно, любой элемент управления может выступать в роли контейнера для других элементов. Правда при этом иногда приходится бороться с его желаниями. Но если вам заняться нечем - то можете попробовать. Класс окна Jframe является наследником от Frame, но является необычным контейнером. Прежде всего, на него нельзя напрямую помещать другие элементы управления, как было общепринято в библиотеке awt. Для доступа к контейнеру окна необходимо использовать вызов getContentPane. Другой вариант заключается в том, что вы создаете класс контейнера, например Jpanel. Затем на нее помещаете множество элементов управления и делаете вызов форма.setContentPane (панель).

Для демонстрации создается небольшое приложение состоящее из окна с кнопкой (за это отвечает компонент JButton), текстовой надписью (за это отвечает компонент JLabel). При нажатии на кнопку изменяется надпись на текстовой надписи, проходя через три стадии: одна надпись для количества нажатий от 0-5, 2-ая надпись – 6-10 нажатий, третья - более 10 нажатий. При наведении мыши на кнопку появляется всплывающая подсказка, в которой приводится таблица с перечислениями того, в какое время были выполнены эти "click"-и.
  1. public static void main(String[] args) {                                                                                           
  2.     JFrame jf = new JFrame("Mega Buttons Test");                                                                                   
  3.     Container con = jf.getContentPane();                                                                                           
  4.     // получаем ссылку на контейнер для окна. именно на этот контейнер можно добавлять какие-либо элементы управления              
  5.     con.setLayout(null); // отказываемся от использования менеджера раскладок,                                                     
  6.     // это значит что я должен буду выполнять позиционирование элементов и указание их размеров сам, без                           
  7.     // помощи менеджера раскладок                                                                                                  
  8.     // создаем кнопку, в качестве параметров конструктора указывается текстовая надпись для кнопки и, опционально,                 
  9.     // изображение иконки                                                                                                          
  10.     final JButton btn_next = new JButton("Click Me !", new ImageIcon("C:\\tmp\\end00000.jpg"));                                    
  11.     // добавляем на окно (JFrame) созданную кнопку                                                                                 
  12.     con.add(btn_next);                                                                                                             
  13.     // указываем координаты кнопки и ее размер, первые два числа для конструктора Rectangle - координаты                           
  14.     // левого верхнего укгла где будет размещена кнопка, остальные два числа - ее размеры                                          
  15.     btn_next.setBounds(new Rectangle(50, 50, 150, 50));                                                                            
  16.     // создается элемент управления - надпись, в качестве параметров конструктора следует                                          
  17.     // указать текстовую надпись для кнопки, горизонтальное выравнивание содержимого надписи, и иконка                             
  18.     final JLabel lab_status = new JLabel("Пока ничего не произошло",                                                               
  19.             new ImageIcon("C:\\tmp\\end00000.jpg"), JLabel.CENTER);                                                                
  20.     con.add(lab_status);                                                                                                           
  21.     // добавляем на окно (JFrame) созданную текстовую надпись                                                                      
  22.     lab_status.setBounds(new Rectangle(50, 120, 300, 50));                                                                         
  23.     // теперь выполним позиционирование созданной надписи                                                                          
  24.  
  25.     // создаем обработчик события для нажатия на кнопку                                                                            
  26.     btn_next.addActionListener(                                                                                                    
  27.             new ActionListener() {                                                                                                 
  28.                 // поле класса служит для подсчета того сколько раз                                                                
  29.                 // было выполнено нажатие на кнопку в течении срока работы программы                                               
  30.                 int count_was_pressed = 0;                                                                                         
  31.                 // а этот список служит для хранения тех дат-времени, когда именно было выполнено                                  
  32.                 ArrayList times = new ArrayList();                                                                                 
  33.  
  34.                 public void actionPerformed(ActionEvent e) {                                                                       
  35.                     // при нажатии на кнопку - увеличим переменную счетчик количества нажатия                                      
  36.                     count_was_pressed++;                                                                                           
  37.                     // также добавим в массив текущую дату-время                                                                   
  38.                     times.add(new java.util.Date());                                                                               
  39.                     // изменим текст надписи в завимимости от того сколько именно раз мы выполнили нажатие на кнопку               
  40.                     if (count_was_pressed < 5)                                                                                     
  41.                         lab_status.setText("Количество нажатий менее чем 5");                                                      
  42.                     else if (count_was_pressed < 10)                                                                               
  43.                         lab_status.setText("Количество нажатий в отрезке от 5 до 10");                                             
  44.                     else                                                                                                           
  45.                         lab_status.setText("Количество нажатий превзошло 10");                                                     
  46.                     // создается строка содержащая некоторый HTML-документ, обратите внимание на то,                               
  47.                     // что содержимое строки начинатся со слова <HTML>                                                             
  48.                     String mes = "<HTML>Количество нажатий: " + count_was_pressed + "<BR>";                                        
  49.                     mes += " <IMG SRC='C:\tmp\\end00000.jpg'>";                                                                    
  50.                     mes += "<TABLE BORDER='1'>";                                                                                   
  51.                     for (int i = 0; i < times.size(); i++) {                                                                       
  52.                         mes += "<TR>";                                                                                             
  53.                         mes += "<TD> <IMG SRC='C:\\tmp\\pic" + ((i) % 3 + 1) + ".jpg'> </TD>";                                     
  54.                         mes += "<TD>";                                                                                             
  55.                         java.util.Date date = (java.util.Date) times.get(i);                                                       
  56.                         java.text.SimpleDateFormat sdf = new SimpleDateFormat("MMMM-DD-yyyy hh:mm:ss");                            
  57.                         mes += sdf.format(date) + "</TD>";                                                                         
  58.                         mes += "</TR>";                                                                                            
  59.                     }                                                                                                              
  60.                     mes += "</TABLE>";                                                                                             
  61.                     // в html-тексте я использую таблицы, картинки                                                                 
  62.                     // теперь получившуюся строку я хочу установить как значение tooltip-а (всплывающей                            
  63.                     // подсказки, возникающей при наведении мыши на кнопку)                                                        
  64.                     btn_next.setToolTipText(mes);                                                                                  
  65.                 }                                                                                                                  
  66.             }                                                                                                                      
  67.     );                                                                                                                             
  68.     jf.setVisible(true);                                                                                                           
  69. }


Для обработки событий "нажатия на кнопку" я добавил обработчик события в виде анонимного класса, реализующий интерфейс ActionListener. При вызове метода actionPerformed, передается в качестве параметра объект ActionEvent. В составе этого объекта его нет ничего особенно ценнного, за исключением ссылки на объект, который сгенерировал данное событие (это неплохо в том случае, если вы хотите привязать один и тот же обработчик к множеству кнопок, а внутри функции actionPerformed выполнить проверку с помощью if, switch какая же именно кнопка была активирована и что-то сделать). Это единая методика обработки событий, будут меняться лишь названия интерфейсов которые вам следует реализовать, имена методов в составе этого интерфейса и, естественно, будет изменься входной параметр этих методов. Так производные классы MouseEvent, KeyEvent и другие, приобретают специфические параметры содержащие дополнительные сведения о том, что же произошло с нашим UI.



Для хранения истории времени, когда было выполнено нажатие на кнопку используется массив ArrayList. Важно: при задании всплывающей подсказки tooltip я сформировал строку содержащую теги HTML. Это один из столпов swing – тесная интеграция с html. Практически любой элемент управления, который способен отображать текст, может принимать в качестве параметра документы HTML. Критически важно: это не internet explorer, который игнорирует ошибки: поэтому пишите теги корректно. К сожалению, использовать возможности стилей не возможно.

Поправка, это можно делать, но с большими ограничениями, так что эксперементируйте.
  1. mes += "<TABLE BORDER='1' STYLE='color: green;' >";
В крайнем случае, если у вас возникнет необходимость использовать html+javascript+css+flash, то для java разработаны библиотеки компонентов с поддержкой вышеперечисленных технологий.

Для задания размеров и местоположения элементов используется метод setBounds. В качестве входного параметра методу setBounds задается объект прямоугольник (Rectangle). Параметры его конструктора - координаты левого верхнего угла и размеры: ширина и высота.

События и класссы слушателей, которые к ним привязаны


Событие, которое произошло Тип класса слушателя
Пользователь нажимает на кнопку или что-то от нее производное: radio, check. Нажимает enter в текстовом поле или выбирает пункт меню. ActionListener
Закрывает окно WindowListener
Нажимает кнопку мыши или отпускает ее MouseListener
Пермещает мышь над компонентом MouseMotionListener
Компонент становится видимым или нет ComponentListener
Компонент получает или теряет фокус FocusListener
Таблица или список изменяют выделение ListSelectionListener
Изменяется свойство компонента: например, текстовая надпись для Jlabel. PropertyChangeListener

Рамки



Для оформления и выделения созданных элементов управления удобно использовать рамки. Еще их удобно использовать для отладки: если вы создаете UI "ручками", и использете прием с чередование большого количества вложенных друг внутрь друга контейнеров, то неплохо каждому контейнеру назнаить свою, разноцветную, рамку. Например, так:
  1. btn_next.setBorderPainted(true);
  2. btn_next.setBorder(BorderFactory.createEtchedBorder(Color.BLUE , Color.GREEN));
В составе библиотеки swing заданы несколько рамок. В большинстве случаев для получения рамки вызывается специальный класс-фабрика BorderFactory. Рамки можно разделить на категории : простые рамки, сложные, комбинированные.

BevelBorder - это типовая для графического интерфейса рамка, имитирующая либо выпуклость, либо вогнутость панели. Чтобы получить рамку типа BevelBorder, нужно вызвать метод createBevelBorder класса BorderFactory.
  1. btn_next.setBorder(BorderFactory.createBevelBorder(
  2.    BevelBorder.LOWERED , Color.YELLOW , Color.GREEN , Color.WHITE , Color.MAGENTA
  3. ));
Обратите внимание на параметр createBevelBorder. С помощью которого задается тип рельефа рамки. Константой BevelBorder.RAISED создается выпуклая рамка, тогда как с помощью константы BevelBorder.LOWERED изготавливается углубленная рамка.

Достаточно похожа на BevelBorder по дизайну рамка SoftBevelBorder и EtchedBorder. Первая из них должна отображать рамку, похожую на BevelBorder, но со "смягченными" краями.
  1. btn_next.setBorder(new SoftBevelBorder (BevelBorder.LOWERED, Color.RED , Color.BLUE));
Рамка EtchedBorder имеет вид тонкого гравированного углубления или, наоборот, тонкого возвышения.

Сложная рамка - EmptyBorder. Эта прозрачная рамка, которая не отображается на экране, однако занимает место вокруг контейнера. Особое ее значение необходимо при точном позиционировании элементов, или для того, чтобы создать зазор между контейнером и элементами.

Для того чтобы логически объединить элементы в окне или в панели используйте рамку TitledBorder.

В примере далее создается набор радиокнопок, для выбора различных видов “еды“. Варианты выбора помещаются внутрь рамки с надписью, при выборе какого-либо из пунктов появляется сообщения о том, что произошло.
  1. JPanel jp_items = new JPanel();
  2. jp_items.setBorder(new TitledBorder ("Выберите предпочтимое блюдо из списка ниже:"));
  3. String foods [] = new String[]{"apple" , "orange" , "grapes" , "pizza"};
  4. String imgs [] = new String[]{"C:\\tmp\\pic1.jpg" , "C:\\tmp\\pic2.jpg" , "C:\\tmp\\pic3.jpg" , "C:\\tmp\\pic4.jpg"};
  5. String simgs [] = new String[]{"C:\\tmp\\spic1.jpg" , "C:\\tmp\\spic2.jpg" , "C:\\tmp\\spic3.jpg" , "C:\\tmp\\spic4.jpg"};
  6. final ButtonGroup group_food = new ButtonGroup();
  7. int []  keys = new int [] {KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4};
  8.  
  9. for (int i = 0; i < foods.length; i++) {
  10.   String food = foods[i];
  11.   final JRadioButton radio = new JRadioButton(food, new ImageIcon (imgs [i]));
  12.   radio.setBounds(new Rectangle(20 , 20 + 30*i , 200 , 25));
  13.   radio.setRolloverIcon(new ImageIcon (simgs [i]));
  14.   radio.setMnemonic(keys[i]);
  15.   radio.addActionListener(
  16.      new ActionListener() {
  17.         public void actionPerformed(ActionEvent e) {
  18.           System.out.println ("Was Selected : " + radio);
  19.         }
  20.      }
  21.   );
  22.   jp_items.add (radio);
  23.   group_food.add(radio);
  24. }
  25.  
  26. con.add (jp_items);
  27. jp_items.setBounds(new Rectangle(300 , 50 , 300 , 300)) ;
Примечание: при создании TitledBorder можно использовать более полную версию конструктора и задать множество параметров. Что дает возможность настраивать вид ограничительной линии рамки, текст и его местоположение различными способами. Для управления местоположения текста используйте константы: TitledBorder.BUTTOM и TitledBorder.RIGHT – нижний и правый угол рамки. Остальные константы выравнивают текст по другим краям.

Для каждой из радио-кнопок создается мнемоника – комбинация клавиш для ее активации. Устанавливаются две иконки: состояние обычное и когда мышь находится над элементом. Также вы еще можете задать иконку для состояния "выделено":
  1. radio.setSelectedIcon(new ImageIcon ("C:\\tmp\\end00000.jpg"));
Очевидно, что радио-кнопки должны быть объединены в группы. Для этого используется класс ButtonGroup. В примере все радиокнопки были добавлены в группу с помощью метода add.

Следующая рамка - MatteBorder. Она заполняет все отведенное ей пространство графическим изображением, которое берется из файла формата GIF или JPG. Если изображение слишком велико для окна, оно будет усечено. Если же окно больше изображения, то все отведенное под рамку пространство будет заполнено по принципу мозаики.
  1. jp_items.setBorder(new MatteBorder  (new Insets(10, 20 , 30, 40) , new ImageIcon ("C:\\tmp\\end00000.jpg")));
Здесь вы знакомитесь еще с одним классом: Insets. Это класс управляет отступами и всякий раз когда необходимо задать отступ (границы чего-то) используется данный класс. В примере между границей рамки и ее заполнением будут созданы интервалы размером 10, 20, 30, 40 пикселей соответственно по top, left, bottom , right.

Последний вид рамки - CompoundBorder. Она служит для объединения двух заданных пользователем рамок в одну. При этом сначала создается первая рамка, а уже внутри нее отображается вторая.
  1. JPanel jp_items = new JPanel();
  2.   MatteBorder mb = new MatteBorder  (new Insets(10, 20 , 30, 40) , new ImageIcon ("C:\\tmp\\end00000.jpg"));
  3.   TitledBorder tb = new TitledBorder("Выберите предпочтимое блюдо из списка ниже:");
  4.   CompoundBorder cm = new CompoundBorder(/*outside border*/tb , /*inside border*/mb);
  5.   jp_items.setBorder(cm);
Для работы с check-кнопками используйте класс JcheckBox. Общая схема идентична radio-кнопкам (и кроме того, вы даже можете не объединять их в группу). В примере далее создается набор check-кнопок для отметки свободных мест в театре, после выбора отметки изменяется подсказка панели, на которой расположены все эти check-кнопки.
  1. final JPanel jp_chks = new JPanel();
  2. jp_chks.setBorder(new TitledBorder("Доступные места для сеанса:"));
  3. jp_chks.setLayout(null);
  4. final ArrayList all_chks = new ArrayList();
  5. for (int i = 0; i < 5; i++)
  6.   for (int j = 0; j < 5; j++){
  7.      JCheckBox chk = new JCheckBox("Место : " + i + " Ряд :" + j);
  8.      jp_chks.add (chk);
  9.      all_chks.add (chk);
  10.      chk.setBorderPaintedFlat(true);
  11.      chk.setBounds(new Rectangle (20+i*70, 20+j*30, 65, 25));
  12.      chk.addActionListener(
  13.         new ActionListener() {
  14.            public void actionPerformed(ActionEvent e) {
  15.               int c = 0;
  16.               String mes = "<HTML>";
  17.               for (Iterator iterator = all_chks.iterator(); iterator.hasNext();) {
  18.                  JCheckBox jCheckBox = (JCheckBox) iterator.next();
  19.                  if (jCheckBox.isSelected()){
  20.                    mes +=  jCheckBox.getText() + ", ";
  21.                    c++;
  22.                    if (c % 5 == 0)
  23.                      mes += "<BR>";
  24.                  }
  25.               }
  26.               jp_chks.setToolTipText(mes); // обратите внимание на декларацию переменной - final
  27.            }
  28.          }
  29.      );
  30. }
  31.  
  32. con.add (jp_chks);
  33. jp_chks.setBounds(new Rectangle(300 , 50 , 300 , 300)) ;
Последний вид кнопок: toggle-кнопки. Этот вид кнопки похож на обычную Jbutton, но при этом может находиться в двух состояних: нажатое и отжатое. В чем-то это поведение подобно radio-кнопке или check-кнопке и бывает полезным, если вы хотите поменять внешний вид вашего приложения.
  1. JPanel what_i_must_do = new JPanel(null);
  2.  JToggleButton tog_nothing = new JToggleButton( "Ничего", new ImageIcon("C:\\tmp\\spic2.jpg"));
  3.  JToggleButton tog_rest = new JToggleButton( "Отдыхать", new ImageIcon("C:\\tmp\\spic3.jpg"));
  4.  JToggleButton tog_think = new JToggleButton( "Размышлять", new ImageIcon("C:\\tmp\\spic4.jpg"));
  5.  
  6.  what_i_must_do.add (tog_think);
  7.  what_i_must_do.add (tog_rest);
  8.  what_i_must_do.add (tog_nothing);
  9.  
  10.  tog_think.setBounds(new Rectangle(10 , 10 , 220 , 30));
  11.  tog_rest.setBounds(new Rectangle(10 , 50 , 220 , 30));
  12.  tog_nothing.setBounds(new Rectangle(10 , 90 , 220 , 30));
  13.  
  14.  con.add (what_i_must_do);
  15.  what_i_must_do.setBounds(new Rectangle(300 , 50 , 300 , 300)) ;
  16.  
  17.  tog_think.setCursor(new Cursor (Cursor.CROSSHAIR_CURSOR));
  18.  tog_rest.setCursor(new Cursor (Cursor.WAIT_CURSOR));
  19.  tog_nothing.setCursor(new Cursor (Cursor.NW_RESIZE_CURSOR));
На рисунке показаны кнопки находящиеся в двух состояниях: обычном и "вжатом".



Курсор



В примере выше была показана возможность установить для некоторого элемента управления особый вид курсора (наводим мышь на элемент и видим не привычную стрелку - а, например, песочные часы или что-то еще). У каждого UI элемента есть метод setCursor, который и служит для того чтобы установить внешний вид курсора.

В составе класса java.awt.Cursor определен ряд констант - кодов курсоров.
  1. JPanel pcurs = new JPanel();
  2. Cursor [] curs = new Cursor [] {
  3.    Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR),
  4.    Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR),
  5.    Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR),
  6.    Cursor.getPredefinedCursor(Cursor.HAND_CURSOR),
  7.    Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR),
  8.    Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR),
  9.    Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR),
  10.    Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR),
  11.    Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR),
  12.    Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR),
  13.    Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR),
  14.    Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR),
  15.    Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR),
  16.    Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)
  17. };
  18. for (int i = 0; i < curs.length; i++) {
  19.    Cursor cur = curs[i];
  20.    JButton comp = new JButton(cur.getName());
  21.    comp.setCursor(cur);
  22.    pcurs.add(comp);
  23. }
  24.  
  25. con.add(pcurs);


Есть возможность создавать свои собственные курсоры. Для этого в состав класса Toolkit введен метод создающий пользовательский курсор на основании указанного изображения, координат "горячей точки" и имени курсора.
  1. final JButton btn_cust = new JButton("Custom Cursor");
  2. con.add(btn_cust);
  3. Cursor customCursor = Toolkit.getDefaultToolkit().createCustomCursor(
  4.    new ImageIcon("C:\\tmp\\cursora.jpg").getImage(), new Point(5, 5), "foo_cursor"
  5. );
  6. btn_cust.setCursor(customCursor);


Categories: Java