Фундаментальный принцип дизайна - “делать
простые вещи легкими, а трудные - возможными”.[61]
Основной целью дизайна библиотеки графического интерфейса пользователя (GUI) в Java 1.0 было позволить программисту построить GUI, который хорошо выглядит на всех платформах. Эта цель не была достигнута. Вместо этого Абстрактный Оконный Инструментарий Java 1.0 ( Аbstract Window Toolkit - AWT) вводил GUI, который выглядел достаточно заурядно на всех платформах. Кроме того, он был ограничен: вы могли использовать только четыре шрифта и вы не могли получить доступ к любому более сложному и тонкому GUI элементу, имеющемуся в вашей операционной системе. Модель программирования Java 1.0 AWT также была слабая и не объектно-ориентированная. Студент на одном из моих семинаров (который был в Sun во время создания Java) объяснил почему: начальная версия AWT была концептуализирована, разработана и реализована за месяц. Конечно - это чудо продуктивности, а также является предметом объяснения, почему дизайн так важен.
Ситуация улучшилась с появлением модели событий с Java 1.1 AWT, которая стала намного понятней, использовала объектно-ориентированный подход, наряду с добавлением JavaBeans, имела модель компонентного программирования, которая ориентируется на легкое создание среды визуального программирования. Java 2 завершила переход от старого Java 1.0 AWT, тщательно заменяя все, начиная с Фундаментальных Классов Java (Java Foundation Classes - JFC), часть GUI, которая теперь называется “Swing”. Теперь есть множество легких в использовании и понимании JavaBeans, которые могут быть перетянуты и брошены (наряду с программированием в ручную) для создания GUI, которым вы можете (наконец) быть удовлетворены. Правила “третьей ревизии” программной индустрии (продукт не считается хорошим до третьей ревизии) выглядит истинным для языков программирования.
Эта глава не охватывает ничего наиболее современного - библиотеку Java 2 Swing, а разумно считает, что Swing - это финальная стадия GUI библиотеки для Java. Если по некоторым причинам вам необходимо использовать изначальную “старую” библиотеку AWT (потому что вы поддерживаете старый код или у вас есть ограничения со стороны броузера), вы можете найти это описание в первой редакции этой книги, доступной на www.BruceEckel.com (также включенной в CD-ROM, прилагаемый к этой книге).
Далее в этой главе вы увидите, как отличаются вещи, когда вы хотите создать апплет и когда вы хотите создать обычное приложение с использование Swing, и как создать программу, являющуюся и приложением и апплетом, так чтобы она могла запускаться в броузере или из командной строки. Почти все GUI примеры в этой книге могут быть исполнены либо как апплет, либо как приложение.
Пожалуйста, запомните, что это не полный список всех компонентов Swing или всех методов для описанных классов. То, что вы увидите здесь, будет простым. Библиотека Swing обширна, и цель этой главы только ввести вас, познакомив с сутью и прелестью концепции. Если вам нужно больше, то, вероятно, Swing даст вам то, что вы хотите, если вы захотите заняться исследованием.
Здесь я принимаю во внимание, что вы имеете закаченную и установленную (бесплатную) документацию по библиотеке Java в формате HTML, имеющуюся на java.sun.com и буду рассматривать классы javax.swing этой документации, чтобы увидеть все детали и методы библиотеки Swing. Из-за простоты дизайна Swing здесь вы найдете достаточно информации для решения вашей проблемы. Есть много (более толстых) книг, посвященных исключительно Swing, и вы можете перейти к ним, если вам необходима большая глубина охвата, или если вы хотите изменить родное поведение Swing.
Когда вы выучите Swing, вы обнаружите:
Swing содержит все компоненты, которые вы ожидаете увидеть в современном интерфейсе пользователя, все, начиная от кнопок, содержащих рисунки, заканчивая деревьями и таблицами. Это большая библиотека, но она разработана так, чтобы иметь определенную сложность для имеющихся под рукой задач — если что-то просто, вы не пишите много кода, но если вы пытаетесь создать более сложную вещь, ваш код, вероятно, становится более сложным. Это значит легкость в подходе, но вы получите мощь, если она вам нужна.
Все, что вы захотите от Swing, может быть названо “ортогональностью использования”. То есть, как только вы схватите главные идеи библиотеки, вы можете применять их везде. Главным образом, из-за стандартного соглашения об именах, большую часть времени, что я писал эти примеры, я мог догадаться об именах методов, и был прав без дополнительного поиска. Это, конечно, отличительный признак хорошего дизайна библиотеки. Кроме того, вы, как правило, можете включать компоненты в другие компоненты, и вещи будут работать правильно.
Для скорости все компоненты являются “легковесными”, и Swing целиком написана на Java для портативности.
Клавиатура используется автоматически — вы можете запускать Swing приложения без использования мыши, и это не требует дополнительного программирования. Поддержка скроллинга не требует усилий — вы просто оборачиваете ваш компонент с помощью JScrollPane, когда вы добавляете его в вашу форму. Такие особенности, как инструмент подсказок, обычно требует одну строку кода для использования.
Swing также поддерживает радикальные особенности, называемые “настраиваемы look and feel”, который означает, что UI может динамически меняться в соответствии с ожиданием пользователя для разных платформ и разных операционных систем. Даже возможно (хотя трудно) выдумать ваш собственный вид.
Одна из целей разработки Java - это создание апплетов, которые являются маленькими программами, запускаемыми внутри Web броузера. Поскольку они должны быть безопасны, апплеты ограничены в своих возможностях. Однако апплеты являются мощным инструментом для поддержки программирования на стороне клиента - главной способности для Web.
Программирование апплетов настолько ограничено, что часто рассматривается как пребывание “внутри песочницы”, так как вы всегда есть кто-то — то есть, система безопасности Java времени выполнения — наблюдающий за вами.
Однако вы можете выйти из песочницы и писать обычные приложения, а не апплеты, в этом случае вы можете получить доступ к другим возможностям вашей OS. Мы писали обычные приложения на протяжении всей книги, но они были консольными приложениями без каких-то графических компонентов. Swing также можно использовать для построения GUI обычных приложений.
Обычно вы можете ответить на вопрос, что позволено делать апплету, взглянув на то, для чего он предназначен: расширить функциональность Web страницы в броузере. Так как, как тот, кто бродит по Internet, вы никогда реально не знаете, расположена ли Web страница дружественно к вам или нет, вам нужен код, запуск которого безопасен. Так что вы, вероятно, заметите огромные ограничения:
Если вы можете жить внутри ограничений, апплеты имеют определенные преимущества, особенно при построении клиент/серверных или сетевых приложений:
Так как апплеты автоматически интегрируются в HTML, вы имеете встроенную, платформо-независимую систему поддержки апплетов. Это интересный поворот, так как мы привыкли иметь часть документации программы, а не наоборот.
Библиотеки часто группируются в зависимости от их функциональности. Некоторые библиотеки, например, используются как есть. Классы String и ArrayList являются примерами стандартной библиотеки Java. Другие библиотеки разрабатывались специально как строительные кирпичики для создания других классов. Определенная категория библиотеки представляет рабочее пространство приложения, чьей целью является помощь вам в построении приложения. Она обеспечивает классы или набор классов, которые производят основу поведения, которая вам необходима в каждом приложении определенного типа. Затем, для настройки поведения согласно вашим требованиям, вы наследуете от класса приложения и перегружаете интересующие методы. Рабочее пространство приложения по умолчанию является механизмом управления, вызывающим ваши перегруженные методы в определенное время. Рабочее пространство приложения - это хороший пример “отделения тех вещей, которые меняются, от тех, которые остаются теми же”, так как оно пробует локализовать все уникальные части программы в перегружаемых методах [62].
Апплеты строятся с использованием рабочего пространства приложения. Вы наследуете от класса JApplet и перегружаете соответствующие методы. Есть несколько методов, которые управляют созданием и выполнением апплета на Web странице:
Метод | Операция |
---|---|
init( ) | Автоматически вызывается для выполнения начальной инициализации апплета, включая компоновку компонент. Вы всегда перегружаете этот метод. |
start( ) | Вызывается каждый раз, когда апплет переносится в поле зрения Web броузера, чтобы позволить апплету начать нормальные операции (особенно те, которые останавливаются в методе stop( )). Также вызывается после init( ). |
stop( ) | Вызывается каждый раз, когда апплет выходит из поля зрения Web броузера, чтобы позволить апплету завершить дорогостоящие операции. Также вызывается перед destroy( ). |
destroy( ) | Вызывается тогда, когда апплет начинает выгружаться со страницы для выполнения финального освобождения ресурсов, когда апплет более не используется. |
С этой информацией вы готовы создать простой апплет:
//: c13:Applet1.java // Очень простой апплет. import javax.swing.*; import java.awt.*; public class Applet1 extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~
Обратите внимание, что апплету не нужен main( ). Это то, что тянется из рабочего пространства приложения; вы помещаете код запуска в init( ).
В этой программе есть только одно действие: помещение текстовой метки в апплет с помощью класса JLabel (в старом AWT есть соответствующее имя Label, точно так же как и для других имен компонент, так что вы часто будете видеть, что для Swing используется лидирующая “J”). Конструктор для этого класса принимает String и использует его для создания метки. В приведенной выше программе эта метка помещается на форму.
Метод init( ) отвечает за помещение всех компонент на форму, используя метод add( ). Вы можете подумать, что вы способны просто вызвать add( ) сам по себе, и, фактически, этот способ использовался в старой библиотеке AWT. Однако Swing требует от вас, чтобы все компоненты добавлялись в “панель содержания” формы, так что вы должны вызывать getContentPane( ), как часть процесса add( ).
Для запуска этой программы вы должны поместить его внутрь Web страницы и просмотреть эту страницу внутри вашего Web броузера, поддерживающего Java. Для помещения апплета внутрь Web страницы, вы помещаете специальный ярлык в HTML источник этой Web страницы [63], чтобы сказать странице, как загрузить и запустить апплет.
Этот процесс был очень простым, когда сам язык Java был очень прост, и каждый оказывается в одном и том же положении и имел одинаковую поддержку Java в своем Web броузере. Таким образом, вы могли обойтись очень простым кусочком HTML внутри вашей Web странице, как здесь:
<applet code=Applet1 width=100 height=50> </applet>
Затем, с началом войн броузеров и языков, мы (программисты и одиночные конечные пользователи) понесли потери. Спустя некоторое время JavaSoft понял, что мы более не можем ожидать, что броузер поддерживает правильную версию Java, и было только одно решение: обеспечить некоторый род дополнения, которое будет предупреждать механизм расширения броузера. При использовании механизма расширения (который разработчик броузера не может отключить — в попытке получить наибольшую прибыль — без нарушения работы всех расширений сторонних разработчиков), JavaSoft гарантирует, что Java не может быть выброшена из Web броузера враждебным продавцом.
В Internet Explorer механизм расширения - это управление ActiveX, а в Netscape - это встраиваемый модуль. В вашем HTML коде вы должны вставить ярлыки для поддержки обоих. Вот как будет выглядеть HTML кода того простого примера для Applet1:[64]
//:! c13:Applet1.html <html><head><title>Applet1</title></head><hr> <OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" width="100" height="50" align="baseline" codebase="http://java.sun.com/products/plugin/1.2.2/jinstall-1_2_2-win.cab#Version=1,2,2,0"> <PARAM NAME="code" VALUE="Applet1.class"> <PARAM NAME="codebase" VALUE="."> <PARAM NAME="type" VALUE="application/x-java-applet;version=1.2.2"> <COMMENT> <EMBED type= "application/x-java-applet;version=1.2.2" width="200" height="200" align="baseline" code="Applet1.class" codebase="." pluginspage="http://java.sun.com/products/plugin/1.2/plugin-install.html"> <NOEMBED> </COMMENT> No Java 2 support for APPLET!! </NOEMBED> </EMBED> </OBJECT> <hr></body></html> ///:~
Некоторые из этих строк слишком длинные и разбиты на несколько для того, чтобы поместится на странице. Код в этой книге (на CD ROM, прилагаемом к книге, и доступный на www.BruceEckel.com) будет работать, и вам не нужно беспокоится об исправлении перенесенных строк.
Значение code задает имя .class файла, в котором расположен апплет. width и height указывают начальный размер апплета (в пикселях, как и раньше). Есть другие элементы, которые вы можете поместить в ярлык апплета: место, где искать другие файлы .class в Internet (codebase), информация о выравнивании (align), специальные идентификаторы, которые делают возможным общение апплета со всем остальным (name), а также параметры апплета, обеспечивающие информацию, которую апплет может найти. Параметры задаются следующей формой:
<param name="identifier" value = "information">
и их может быть столько, сколько вам нужно.
Пакет исходного кода для этой книги содержит HTML страницы для каждого апплета в этой книге и, таким образом, много примеров для ярлыка апплета. Вы можете найти полное и конкретное описание о деталях помещения апплетов на Web страницы на java.sun.com.
JDK от SUN (бесплатно доступен на java.sun.com) имеет инструмент, называемый Appletviewer, который выбирает ярлык <applet> из HTML файла и запускает апплет без отображения окружающего HTML текста. Из-за того, что Appletviewer игнорирует все, кроме ярлыка APPLET, вы можете поместить эти ярлыки в исходный код Java как комментарий:
// <applet code=MyApplet width=200 height=100> // </applet>
Этим способом вы можете запустить “appletviewer MyApplet.java” и вам не нужно будет создавать маленький HTML файл для запуска теста. Например, вы можете добавить закомментированный HTML ярлык в Applet1.java:
//: c13:Applet1b.java // Встроенный ярлык апплета для Appletviewer. // <applet code=Applet1b width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; public class Applet1b extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~
Теперь вы можете вызвать апплет командой
appletviewer Applet1b.java
В этой книге эта форма будет использоваться для простого тестирования апплетов. Вскоре вы увидите другой способ кодирования, который позволит вам выполнять апплеты из командной строки без Appletviewer.
Вы можете выполнить простой тест без каких-либо сетевых соединений при запуске Web броузера и открытии HTML файлов, содержащих ярлык апплета. Как только HTML файл будет загружен, броузер обнаружит ярлык апплета и пойдет охотиться за .class файлом, указанным в значении code. Конечно, он просматривает CLASSPATH для того, чтобы определить где охотится, и если ваш .class файл не найден по CLASSPATH, то он выведет сообщение об ошибке в строке состояния броузера о том, что он не смог найти этот .class файл.
Когда вы захотите проверить это на своем Web сайте, эти вещи становятся немного более сложными. Прежде всего, вы должны иметь Web сайт, который для большинства людей означает провайдеров третьей стороны (Internet Service Provider - ISP) в удаленном месте. Так как апплеты - это просто файлы или набор файлов, ISP не должен обеспечивать какую-то особую поддержку для Java. Вы также должны иметь способ переместить HTML файлы и .class файлы вашего сайта в правильную директорию машины провайдера. Обычно это выполняется с помощью программы, использующей протокол передачи файлов (FTP), которых имеется великое множество как бесплатных, таки условно-бесплатных. Так что на первый взгляд, все, что вам нужно сделать - это переметить файлы на машину провайдера с помощью FTP, затем соединиться с сайтом и HTML файлом, используя свой броузер; если апплет получен и работает, то все проверено. Верно?
Здесь вы можете быть одурачены. Если броузер на клиентской машине не может найти .class на сервере, он охотится за ним, просматривая CLASSPATH на вашей локальной машине. Таким образом, апплет может не загрузиться правильно с сервера, но для вас это будет выглядеть нормально в процессе тестирования, потому что броузер найдет апплет на вашей машине. Однако кода кто-то другой соединится, его или ее броузер не сможет найти его. Так что при тестировании убедитесь, что вы стерли соответствующий .class файл (или .jar файл) на своей локальной машине, чтобы проверить, что он правильно расположен на сервере.
Одно из коварных мест, в которое угодил я, когда поместил апплет внутри package. После загрузки HTML файла и апплета оказалось, что путь на сервере к апплету был перепутан с именем пакета. Однако мой броузер нашел его по локальному CLASSPATH. Таким образом, я был единственным, кто мог правильно загрузить апплет. Одновременно это позволило обнаружить, что инструкция package была всему виной. В общем, вы не должны включать инструкцию package в апплет.
Будет время, когда вы захотите сделать программу, которая выполняла что-то иное, чем просто сидела на Web странице. Возможно, вам также нравится делать какие-то вещи, которое может делать “обычное” приложение, но все-таки иметь хваленую моментальную мобильность, обеспечиваемую Java. В предыдущих главах этой книги мы делали приложения для командной строки, но в некоторых средах (например, для Макинтош) не существует командной строки. Так что по многим причинам вы захотите построить оконную программу, не являющуюся апплетом, используя Java. Это весьма резонное желание.
Библиотека Swing позволяет вам создавать приложения, сохраняющие внешний облик для среды операционной системы. Если вы хотите построить оконное приложение, имеет смысл делать так, [65] если вы может использовать самую последнюю версию Java и соответствующие элементы, чтобы вы могли выпустить приложение, которое не будет смущать ваших пользователей. Если по каким-то причинам вы вынуждены использовать старую версию Java, хорошо подумайте, прежде чем перейдете к построению значительного оконного приложения.
Часто у вас будет желание создать класс, который может быть вызван либо как окно, либо как апплет. Это особенно удобно, когда вы проверяете апплет, так как обычно намного проще и легче запустить результирующее приложение-апплет из командной строки, чем запускать его в Web броузере или с помощью Appletviewer.
Для создания апплета, который может быть запущен из командной строки консоли, вы просто добавляете main( ) в ваш апплет, который создает экземпляр апплета внутри JFrame.[66] В качестве простого примера давайте взглянем на измененный Applet1b.java, который теперь может работать и как приложение, и как апплет:
//: c13:Applet1c.java // Приложение и апплет. // <applet code=Applet1c width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Applet1c extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } // main() для приложения: public static void main(String[] args) { JApplet applet = new Applet1c(); JFrame frame = new JFrame("Applet1c"); // Для закрытия приложения: Console.setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(100,50); applet.init(); applet.start(); frame.setVisible(true); } } ///:~
main( ) - это просто элемент, добавляющийся к апплету, а оставшаяся часть апплета остается нетронутой. Апплет создается и добавляется в JFrame так, что он может быть отображен.
Строка:
Console.setupClosing(frame);
Является причиной правильного закрытия окна. Console пришло из com.bruceeckel.swing и будет объясняться позднее.
Вы можете видеть, что в main( ) апплет явно инициализируется и стартует, так как в этом случае броузер не выполняет это за вас. Конечно так вы не получите все возможности броузера, который также вызывает stop( ) и destroy( ), но для большинства ситуаций это приемлемо. Если это проблема, вы можете выполнить вызовы сами.[67]
Обратите внимание на последнюю строку:
frame.setVisible(true);
Без этого вы не увидите ничего на экране.
Хотя тот код, который, который делает программу запускаемой и как апплет, и как приложение, предоставляет ценные результаты, если его использовать везде, он сбивает с толку и впустую тратит бумагу. Вместо этого приведенное ниже отображение рабочего пространства будет использоваться для примеров Swing в оставшейся части книги:
//: com:bruceeckel:swing:Console.java // Инструмент для запуска демонстрации Swing // из консоли и доя апплета, и для JFrames. package com.bruceeckel.swing; import javax.swing.*; import java.awt.event.*; public class Console { // Создание строки заголовка из имени класса: public static String title(Object o) { String t = o.getClass().toString(); // Удаление слова "class": if(t.indexOf("class") != -1) t = t.substring(6); return t; } public static void setupClosing(JFrame frame) { // Решение JDK 1.2 - это // анонимный внутренний класс: frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); // улучшенное решение в JDK 1.3: // frame.setDefaultCloseOperation( // EXIT_ON_CLOSE); } public static void run(JFrame frame, int width, int height) { setupClosing(frame); frame.setSize(width, height); frame.setVisible(true); } public static void run(JApplet applet, int width, int height) { JFrame frame = new JFrame(title(applet)); setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(width, height); applet.init(); applet.start(); frame.setVisible(true); } public static void run(JPanel panel, int width, int height) { JFrame frame = new JFrame(title(panel)); setupClosing(frame); frame.getContentPane().add(panel); frame.setSize(width, height); frame.setVisible(true); } } ///:~
Этот инструмент вы можете использовать сами, так как он помещен в библиотеке com.bruceeckel.swing. Класс Console полностью состоит из статических методов. Первый используется для получения имени класса (используя RTTI) из любого объекта и удаления слова “class”, которое обычно присоединяется спереди методом getClass( ). Здесь используется метод indexOf( ) из String для определения присутствия слова “class” и substring( ) для получения новой строки без приставки “class” или заключающих пробелов. Это имя используется для метки окна, которая отображается в методах run( ).
setupClosing( ) используется для упрятывания кода, являющегося причиной выхода из программы при закрытии JFrame. По умолчанию при этом ничего не делается, так что если вы не вызовите setupClosing( ) или не напишите аналогичный код для своего JFrame, приложение не закроется. Причина упрятывания этого кода, а не помещения его прямо в последовательность метода run( ), частично в том, что это позволяет вам использовать этот метод сам по себе, когда вы захотите сделать что-то более сложное по сравнению с тем, что обеспечивает run( ). Однако это изолирует фактор изменения: Java 2 имеет два пути для закрытия некоторых видов окон. В JDK 1.2 решение состоит в создании нового класса WindowAdapter и реализации windowClosing( ), как показано выше (значение этого будет полностью объяснено позже в этой главе). Однако во время создания JDK 1.3 разработчики библиотеки заметили, что вам обычно нужно закрывать окна в любом случае, если вы создаете не апплет, и поэтому они добавили setDefaultCloseOperation( ) в JFrame и JDialog. С точки зрения написания кода, новый метод более приятный в использовании, но эта книга была написана в то время, когда еще не было реализации JDK 1.3. для Linux и других платформ, поэтому в интересах совместимости версий изменения были изолированы в методе setupClosing( ).
Методы run( ) перегружены для работы с JApplet, JPanel и JFrame. Обратите внимание, что только для JApplet вызывается init( ) и start( ).
Теперь любой апплет может быть запущен из консоли путем создания main( ), содержащей строку, подобную этой:
Console.run(new MyClass(), 500, 300);
в которой последние два аргумента показывают ширину и высоту. Здесь приведена Applet1c.java измененная для использования Console:
//: c13:Applet1d.java // Console запускает апплет из командной строки. // <applet code=Applet1d width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Applet1d extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } public static void main(String[] args) { Console.run(new Applet1d(), 100, 50); } } ///:~
Это позволяет уменьшить количество повторяющегося кода, одновременно обеспечивая великолепную гибкость в запуске примеров.
Если вы используете Windows, вы можете упростить процесс запуска Java программ из командной строки путем конфигурирования Windows Explorer — файл-менеджера в Windows, не Internet Explorer — так что теперь вы можете просто дважды щелкнуть мышкой на файле .class для его выполнения. Для этого нужно выполнить несколько шагов.
Первое, загрузить и установить язык программирования Perl с ww.Perl.org. Вы найдете инструкцию и документацию языка на этом сайте.
Далее, создать следующий сценарий без первой и последней строки (этот сценарий является частью пакета исходного кода книги):
//:! c13:RunJava.bat @rem = '--*-Perl-*-- @echo off perl -x -S "%0" %1 %2 %3 %4 %5 %6 %7 %8 %9 goto endofperl @rem '; #!perl $file = $ARGV[0]; $file =~ s/(.*)\..*/\1/; $file =~ s/(.*\\)*(.*)/$+/; ´java $file´; __END__ :endofperl ///:~
Теперь откройте Windows Explorer, выберете “Вид (View)”, “Параметры (Folder Options)”, затем щелкните на закладке “Типы файлов (File Types)”. Нажмите кнопку “Новый тип (New Type)”. В качестве “Описания (Description of Type)” введите “Java class file”. В качестве “Стандартного расширения (Associated Extension)” введите “class”. Под пунктом “Действия (Actions)” нажмите кнопку “Создать (New)”. В пункте “Действие (Action)” введите “Open”, а в поле “Приложение, запускающее действие (Application used to perform action)” введите строку, как показано здесь:
"c:\aaa\Perl\RunJava.bat" "%L"
Вы должны настроить путь перед “RunJava.bat”, чтобы он соответствовал месту, в которое вы поместили пакетный файл.
Как только вы выполните эту установку, вы можете запускать любую программу Java, просто выполнив двойной щелчок на .class файле, содержащем main( ).
Создание кнопок достаточно просто: вы просто вызываете конструктор JButton с меткой, которую хотите поместить на кнопке. Позже вы увидите, что вы можете делать фантастические вещи, такие как помещение графической картинки на кнопку.
Обычно вам будет нужно создавать поле для кнопки внутри вашего класса, чтобы вы могли обратиться к ней позже.
JButton - это компонент — своего рода маленькое окно — который автоматически перерисовывается как часть обновления. Это означает, что вам не нужно явно вызывать перерисовку кнопки или для любого управляющего элемента; вы просто помещаете его на форму, и позволяете ему автоматически заботиться о своей перерисовке. Чтобы поместить кнопку на форму, вы должны выполнить это внутри init( ):
//: c13:Button1.java // Помещение кнопки в апплете. // <applet code=Button1 width=200 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Button1 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); } public static void main(String[] args) { Console.run(new Button1(), 200, 50); } } ///:~
Здесь было добавлено кое-что новое: перед помещением любого элемента в содержащую панель, создается новый “менеджер компоновки” типа FlowLayout. Менеджер компоновки - это способ, которым панель решает, где поместить управляющий элемент на форме. Нормальным для апплета считается использование BorderLayout, но это не работает здесь потому (как вы выучите позднее в этой главе, где управляете компоновкой формы исследовано более подробно), что по умолчанию он перекрывает каждый новый управляющий элемент при добавлении. Однако FlowLayout является причиной того, что управляющие элементы равномерно плавают в форме, слева направо и сверху вниз.
Вы заметите, что если вы откомпилируете программу и запустите приведенный выше апплет, ничего не произойдет при нажатии кнопки. Это потому, что вы должны пойти и написать определенный код для определения того, что случилось. Основа для событийного программирования, которое включает многое из того, что включает GUI, это привязка кода, который отвечает на эти события.
Способ, которым это совершается в Swing, это ясно отделенный интерфейс (графические компоненты) и реализация (код, который вы хотите запустить при возникновении события от компоненты). Каждый компонент Swing может посылать все сообщения, которые могут в нем случатся, и он может посылать события каждого вида индивидуально. Так что если вам, например, не интересно было ли перемещение мыши над кнопкой, вы не регистрируете это событие. Это очень простой и элегантный способ обработки в событийном программировании, и как только вы поймете основы концепции, вы сможете легко использовать компоненты Swing, которые вы до этого не видели — фактически, эта модель простирается на все, что может быть классифицировано как JavaBean (который вы выучите позднее в этой главе).
Сначала мы сфокусируем внимание на основном, интересующем нас событии используемого компонента. В случае JButton, этим “интересующем событием” является нажатие кнопки. Для регистрации своей заинтересованности в нажатии кнопки вы вызываете метод addActionListener( ) класса JButton. Этот метод ожидает аргумент, являющийся объектом, реализующим интерфейс ActionListener, который содержит единственный метод, называемый actionPerformed( ). Таким образом, все, что вам нужно сделать для присоединения кода к JButton, это реализовать интерфейс ActionListener в классе и зарегистрировать объект этого класса в JButton через addActionListener( ). Метод будет вызван при нажатии кнопки (это обычно называется обратным вызовом).
Но что должно быть результатом нажатия кнопки? Нам хотелось бы увидеть какие-то изменения на экране, так что введем новый компонент Swing: JTextField. Это то место, где может быть напечатан текст или, в нашем случае, текст может быть изменен программой. Хотя есть несколько способов создания JTextField, самым простым является сообщение конструктору нужной вам ширину текстового поля. Как только JTextField помещается на форму, вы можете изменять содержимое, используя метод setText( ) (есть много других методов в JTextField, но вы должны посмотреть их в HTML документации для JDK на java.sun.com). Вот как это выглядит:
//: c13:Button2.java // Ответ на нажатие кнопки. // <applet code=Button2 width=200 height=75> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Button2 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); JTextField txt = new JTextField(10); class BL implements ActionListener { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(name); } } BL al = new BL(); public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2(), 200, 75); } } ///:~
Создание JTextField и помещение его на канву - это шаги, необходимые для и для JButton или любого компонента Swing. Отличия приведенной выше программы в создании вышеупомянутого класса BL, являющегося ActionListener. Аргумент для actionPerformed( ) имеет тип ActionEvent, который содержит всю информацию о событии и откуда оно исходит. В этом случае я хочу описать кнопку, которая была нажата: getSource( ) производит объект, явившийся источником события, и я полагаю, что это JButton. getText( ) возвращает текст, который есть на кнопке, а он помещается в JTextField для демонстрации, что код действительно был вызван при нажатии кнопки.
В init( ) используется addActionListener( ) для регистрации объекта BL в обеих кнопках.
Часто более последовательно кодировать ActionListener как анонимный внутренний класс, особенно потому, что вы склонны использовать единственный интерфейс для каждого следящего класса. Button2.java может быть изменена для использования анонимного внутреннего класса следующим образом:
//: c13:Button2b.java // Использование анонимного внутреннего класса. // <applet code=Button2b width=200 height=75> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Button2b extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); JTextField txt = new JTextField(10); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(name); } }; public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2b(), 200, 75); } } ///:~
Подход с использованием анонимного внутреннего класса будет наиболее предпочтительным (когда это возможно) для примеров из этой книги.
JTextArea - это как JTextField, за исключением того, что он может иметь множество строк и имеет большую функциональность. Особенно полезным методом является append( ); с ним вы можете легко сливать вывод в JTextArea, что делает программу, использующую Swing, удобнее (так как вы можете проскроллировать назад) по сравнению с тем, что использовалось в программах командой строки, печатающих в стандартный вывод. В качестве примера приведена программа заполнения JTextArea значениями, получающимися из генератора geography из Главы 9:
//: c13:TextArea.java // Использование управляющего элемента JTextArea. // <applet code=TextArea width=475 height=425> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import java.util.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*; public class TextArea extends JApplet { JButton b = new JButton("Add Data"), c = new JButton("Clear Data"); JTextArea t = new JTextArea(20, 40); Map m = new HashMap(); public void init() { // Использование всех данных: Collections2.fill(m, Collections2.geography, CountryCapitals.pairs.length); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(Iterator it= m.entrySet().iterator(); it.hasNext();){ Map.Entry me = (Map.Entry)(it.next()); t.append(me.getKey() + ": " + me.getValue() + "\n"); } } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText(""); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(b); cp.add(c); } public static void main(String[] args) { Console.run(new TextArea(), 475, 425); } } ///:~
В init( ) Map заполняется всеми странами и их столицами. Обратите внимание, что для обеих кнопок создается ActionListener и добавляется без определения промежуточной переменной, так как вам не нужно будет снова обращаться к следящему классу в программе. Кнопка Add Data” форматирует и добавляет все данные, а кнопка “Clear Data” использует setText( ) для удаления всего текста из JTextArea.
Когда JTextArea добавляется в апплет, он оборачивается в JScrollPane, для управления скроллингом, когда слишком много текста помещается на экран. Это все, что вы должны сделать для поддержки возможности скроллинга. Пробуя выяснить, как делать аналогичные вещи в других средах программирования GUI, я был поражен простотой и хорошим дизайном компонент, подобных JScrollPane.
Способ, которым вы помещаете компоненты на форму в Java, вероятно, отличается от всех других используемых вами GUI систем. Во-первых, это все код; здесь нет “ресурсов”, которые управляют помещением компонентов. Во-вторых, способ, которым компоненты помещаются на форму, управляется не абсолютным позиционированием, а с помощью “менеджера компоновки”, который решает, как располагать компонент, основываясь на порядке, в котором вы добавляете (add( )) их. Размер, образ и расположение компонентов будет значительно отличаться при использовании разных компоновщиков. Кроме того, менеджер компоновки адаптирует размеры вашего апплета или окна приложения, так что если размеры окна меняются, размер, образ и расположение компонентов может соответственно измениться.
JApplet, JFrame, JWindow и JDialog все могут производить Container с помощью getContentPane( ), который может содержать и отображать Component. В Container есть метод, называемый setLayout( ), который позволяет вам выбрать между различными менеджерами компоновки. Другие классы, такие как JPanel, содержат и отображают компоненты непосредственно, и вы так же можете установить менеджер компоновки непосредственно, без использования панели содержания.
В этом разделе мы исследуем различные менеджеры компоновки, помещая кнопки (так как это самое простое, что можно сделать). Здесь не будет никакого захвата событий, так как эти примеры предназначены только для показа, как расположатся кнопки.
Апплет по умолчанию использует схему компоновки по умолчанию: BorderLayout (несколько предыдущих примеров меняли менеджер компоновки на FlowLayout). Без каких-то дополнительных инструкций он принимает все, что вы добавляете (add( )) и помещает это в центр, растягивая объект во все стороны до края.
Однако BorderLayout может больше. Этот менеджер компоновки имеет концепцию четырех граничных областей и центральной области. Когда вы добавляете что-то в панель, которая использует BorderLayout, вы можете использовать перегруженный метод add( ), принимающий константу в качестве своего первого аргумента. Это значение может быть любым из следующих:
BorderLayout.NORTH (верх)
BorderLayout.SOUTH (низ)
BorderLayout.EAST (справа)
BorderLayout.WEST (слева)
BorderLayout.CENTER (заполнить середину до других
компонент или до краев)
Если вы не указываете область для помещения объекта, по умолчанию выбирается CENTER.
Вот пример. Используется компоновка по умолчанию, так как для JApplet по умолчанию используется BorderLayout:
//: c13:BorderLayout1.java // Демонстрация BorderLayout. // <applet code=BorderLayout1 // width=300 height=250> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class BorderLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.add(BorderLayout.NORTH, new JButton("North")); cp.add(BorderLayout.SOUTH, new JButton("South")); cp.add(BorderLayout.EAST, new JButton("East")); cp.add(BorderLayout.WEST, new JButton("West")); cp.add(BorderLayout.CENTER, new JButton("Center")); } public static void main(String[] args) { Console.run(new BorderLayout1(), 300, 250); } } ///:~
Для всех место, кроме CENTER, элемент, который вы добавляете, сжимается, чтобы занимать наименьшее пространство по одному измерению, а по другому измерению он растягивается. Однако для CENTER, подстройка идет в обоих направлениях, чтобы занять середину.
При этом компоненты просто “вливаются” в форму слева направо, пока не закончится место сверху, затем происходит переход на нижнюю строку и продолжается заливка.
Вот пример, который устанавливает менеджер компоновки FlowLayout. Вы заметите, что с FlowLayout компоненты принимают свои “естественные” размеры. Например, JButton, будет равна размеру своей строки.
//: c13:FlowLayout1.java // Демонстрация FlowLayout. // <applet code=FlowLayout1 // width=300 height=250> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class FlowLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < 20; i++) cp.add(new JButton("Button " + i)); } public static void main(String[] args) { Console.run(new FlowLayout1(), 300, 250); } } ///:~
Все компоненты будут компактными, занимая наименьший из возможных размеров, при использовании FlowLayout, так что вы можете быть немного удивлены поведением. Например, потому что размер JLabel будет определяться его строкой, попытка использовать выравнивание текста по правому краю оставит отображение неизменным, когда вы используете FlowLayout.
GridLayout позволяет вам построить таблицу компонент, и когда вы добавляете их, они помещаются слева - направо и сверху - вниз в сетке. В конструкторе вы определяете число строк и столбцов, сколько вам необходимо и они будут расположены в равной пропорции.
//: c13:GridLayout1.java // Демонстрация GridLayout. // <applet code=GridLayout1 // width=300 height=250> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class GridLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(7,3)); for(int i = 0; i < 20; i++) cp.add(new JButton("Button " + i)); } public static void main(String[] args) { Console.run(new GridLayout1(), 300, 250); } } ///:~
В этом случае есть 21 ячейка, но только 20 кнопок. Последний слот остается пустым, не происходит “балансировки” при использовании GridLayout.
GridBagLayout обеспечивает вас потрясающим инструментом для точного решения, как области вашего окна будут располагаться, и как они будут переформатироваться при изменении размеров окна. Однако это и наиболее сложный менеджер компоновки и достаточно трудный для понимания. Он предназначен, в основном, для автоматического генерирования кода построителем GUI (хорошие построители GUI будут использовать GridBagLayout вместо абсолютного размещения). Если ваш дизайн достаточно сложен, и вы чувствуете необходимость использовать GridBagLayout, то вы должны использовать инструмент построителя GUI для генерации вашего дизайна. Если вы чувствуете, что должны знать запутанные детали, я отошлю вас к книге Core Java 2 by Horstmann & Cornell (Prentice-Hall, 1999), или к любой книге, посвященной Swing, для начального знакомства.
Также возможно установить абсолютное позиционирование графической компоненты таким способом:
Некоторые построители GUI широко используют этот подход, но это обычно не лучший способ генерации кода. Более полезные построители GUI используют вместо него GridBagLayout.
Потому, что люди имеют много трудностей при работе с GridBagLayout, Swing также включает BoxLayout, который предоставляет вам много полезного, что умеет GridBagLayout без той сложности, так что вы можете часто использовать его, когда вам нужно выполнить ручное кодирование (опят таки, если ваш дизайн станет через чур сложным, используйте построитель GUI, который генерирует для вас GridBagLayout). BoxLayout позволяет вам управлять вам размещением компонент либо вертикально, либо горизонтально, и управлять пространством между компонентами, используя что-то, называемое “подпорки и склейки”. Сначала, позвольте показать, как использовать BoxLayout непосредственно, тем же способом, как были продемонстрированы другие менеджеры компоновки:
//: c13:BoxLayout1.java // Вертикальный и горизонтальный BoxLayouts. // <applet code=BoxLayout1 // width=450 height=200> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class BoxLayout1 extends JApplet { public void init() { JPanel jpv = new JPanel(); jpv.setLayout( new BoxLayout(jpv, BoxLayout.Y_AXIS)); for(int i = 0; i < 5; i++) jpv.add(new JButton("" + i)); JPanel jph = new JPanel(); jph.setLayout( new BoxLayout(jph, BoxLayout.X_AXIS)); for(int i = 0; i < 5; i++) jph.add(new JButton("" + i)); Container cp = getContentPane(); cp.add(BorderLayout.EAST, jpv); cp.add(BorderLayout.SOUTH, jph); } public static void main(String[] args) { Console.run(new BoxLayout1(), 450, 200); } } ///:~
Конструктор для BoxLayout немного отличается от других менеджеров компоновки — вы обеспечиваете Container, который будет управляться BoxLayout, в качестве первого аргумента, и направление компоновки в качестве второго аргумента.
Для упрощения дела, есть специальный контейнер, называемый Box, который использует BoxLayout, как свой родной менеджер. Следующий пример располагает компоненты горизонтально и вертикально, используя Box, который имеет два статических метода для создания боксов с вертикальным и горизонтальным выравниванием:
//: c13:Box1.java // Вертикальный и горизонтальный BoxLayouts. // <applet code=Box1 // width=450 height=200> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box1 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); for(int i = 0; i < 5; i++) bv.add(new JButton("" + i)); Box bh = Box.createHorizontalBox(); for(int i = 0; i < 5; i++) bh.add(new JButton("" + i)); Container cp = getContentPane(); cp.add(BorderLayout.EAST, bv); cp.add(BorderLayout.SOUTH, bh); } public static void main(String[] args) { Console.run(new Box1(), 450, 200); } } ///:~
Как только вы получаете Box, вы передаете его в качестве второго аргумента при добавлении компонента в панель содержания.
Распорки между компонентами измеряется в пикселях. Для использования распорок, вы просто добавляете их между вставкой компонент:
//: c13:Box2.java // Добавление разделителей. // <applet code=Box2 // width=450 height=300> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box2 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); for(int i = 0; i < 5; i++) { bv.add(new JButton("" + i)); bv.add(Box.createVerticalStrut(i*10)); } Box bh = Box.createHorizontalBox(); for(int i = 0; i < 5; i++) { bh.add(new JButton("" + i)); bh.add(Box.createHorizontalStrut(i*10)); } Container cp = getContentPane(); cp.add(BorderLayout.EAST, bv); cp.add(BorderLayout.SOUTH, bh); } public static void main(String[] args) { Console.run(new Box2(), 450, 300); } } ///:~
Распорки разделяют компоненты на фиксированную величину, а склейки наоборот: они разделят компоненты настолько, насколько это возможно. Так что это, скорее “пружина”, чем “клей” (а дизайн, на котором это базируется должен называться “пружины и распорки”, так что выбор терминов немного непонятен).
//: c13:Box3.java // Использование Glue (клея). // <applet code=Box3 // width=450 height=300> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box3 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); bv.add(new JLabel("Hello")); bv.add(Box.createVerticalGlue()); bv.add(new JLabel("Applet")); bv.add(Box.createVerticalGlue()); bv.add(new JLabel("World")); Box bh = Box.createHorizontalBox(); bh.add(new JLabel("Hello")); bh.add(Box.createHorizontalGlue()); bh.add(new JLabel("Applet")); bh.add(Box.createHorizontalGlue()); bh.add(new JLabel("World")); bv.add(Box.createVerticalGlue()); bv.add(bh); bv.add(Box.createVerticalGlue()); getContentPane().add(bv); } public static void main(String[] args) { Console.run(new Box3(), 450, 300); } } ///:~
Распорки работают в одном направлении, но закрепленное место фиксирует пространство между компонентами в обоих направлениях:
//: c13:Box4.java // Закрепленное Место(Rigid Areas) - это как пара распорок. // <applet code=Box4 // width=450 height=300> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box4 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); bv.add(new JButton("Top")); bv.add(Box.createRigidArea( new Dimension(120, 90))); bv.add(new JButton("Bottom")); Box bh = Box.createHorizontalBox(); bh.add(new JButton("Left")); bh.add(Box.createRigidArea( new Dimension(160, 80))); bh.add(new JButton("Right")); bv.add(bh); getContentPane().add(bv); } public static void main(String[] args) { Console.run(new Box4(), 450, 300); } } ///:~
Вы должны знать, что закрепленные области несколько спорны. Так как они использую абсолютное значение, некоторые люди чувствуют, что они причиняют больше неприятностей, чем они стоят.
Swing достаточно мощный; он может дать очень много при использовании всего нескольких строк. Примеры, показанные в этой книге, достаточно просты, и с целью обучения есть смысл писать их руками. Вы на самом деле можете выбрать комбинацию простых компоновок. Однако, с некоторой точки зрения, это теряет смысл для ручного проектирования GUI форм — это становится слишком сложным, и вы теряете время при программировании. Разработчики Java и Swing ориентировали язык и библиотеку на использование инструментов поддержки построителей GUI, которые были созданы с целью ускорения создания и приобретения опыта программирования. Как только вы поймете, что происходит с компоновкой и как работать с событиями (описано далее), то не особенно важно, что вы на самом деле знаете детали того, как располагать компоненты в ручную — позвольте соответствующему инструменту сделать это за вас (Java, помимо всего, предназначена для увеличения продуктивности программиста).
В модели событий Swing компоненты могут инициировать (“возбуждать”) события. Каждый тип события представляется разными классами. Когда событие возбуждается событие, оно принимается одним или несколькими “слушателями”, которые реагируют на это событие. Таким образом, источник событий и место, где событие обрабатывается, могут быть разделены. Так как вы обычно используете компоненты Swing, как они есть, то необходимо писать код, вызывающийся в том случае, когда компонент принимает событие, это хороший пример разделения интерфейса и реализации.
Каждый слушатель события - это объект класса, который реализует определенный тип интерфейса слушателя. Как программист, все, что вы делаете - это создаете объект слушателя и регистрируете его в компоненте, который возбуждает событие. Эта регистрация выполняется вызовом метода addXXXListener( ) для компонента, возбуждающего событие, в котором “XXX” представляет тип слушателя события. Вы можете легко узнать, какой тип события может быть обработан, просмотрев имена методов “addListener”, и если вы попробуете слушать неверные события, вы обнаружите ошибку времени компиляции. Позже в этой главе вы увидите, что JavaBeans также использует имена методов “addListener” для определения того, какое событие может обработать Bean.
Поэтому, вся ваша событийная логика переходит в класс слушателя. Когда вы создаете класс-слушатель, главное ограничение в том, что он должен реализовывать подходящий интерфейс. Вы можете создать класс глобального слушателя. Но это та ситуация, в которой внутренние классы более полезны, не только потому, что они обеспечивают логическое группирование ваших классов - слушателей внутри UI или логических бизнес - классов, которые они обслуживают, но и потому (как вы увидите позже), что фактически, объект внутреннего класса хранит ссылку на родительский объект, обеспечивая лучший способ для перекрестного вызова класса и подсистемных границ.
Все дальнейшие примеры этой главы используют модель событий Swing, а оставшаяся часть этого раздела опишет детали этой модели.
Все компоненты Swing включают методы addXXXListener( ) и removeXXXListener( ), так что подходящий тип слушателя может быть добавлен и удален для каждого компонента. Вы заметите, что “XXX” в каждом случае также представляет аргумент метода, например: addMyListener(MyListener m). Приведенная ниже таблица включает основные ассоциированные события, слушатели и методы, наряду с основными компонентами, которые поддерживают эти определенные события, обеспечивая методы addXXXListener( ) и removeXXXListener( ). Вы должны иметь в виду, что модель событий разработана для расширения, так что вы можете насчитать другие события и типы слушателей, не попавшие в эту таблицу.
Событие, интерфейс слушателя и методы добавления, удаления | Компоненты, поддерживающие это событие |
---|---|
ActionEvent ActionListener addActionListener( ) removeActionListener( ) |
JButton, JList, JTextField, JMenuItem и наследованные от них, включая JCheckBoxMenuItem, JMenu и JpopupMenu. |
AdjustmentEvent AdjustmentListener addAdjustmentListener( ) removeAdjustmentListener( ) |
JScrollbar и все, что вы создаете, реализуя Adjustable interface. |
ComponentEvent ComponentListener addComponentListener( ) removeComponentListener( ) |
*Component и наследованные от него, включая JButton, JCanvas, JCheckBox, JComboBox, Container, JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar, JTextArea и JTextField. |
ContainerEvent ContainerListener addContainerListener( ) removeContainerListener( ) |
Container и наследованные от него, включая JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog и JFrame. |
FocusEvent FocusListener addFocusListener( ) removeFocusListener( ) |
Component и унаследованные*. |
KeyEvent KeyListener addKeyListener( ) removeKeyListener( ) |
Component и унаследованные*. |
MouseEvent (для кликов и перемещений) MouseListener addMouseListener( ) removeMouseListener( ) |
Component и унаследованные*. |
MouseEvent[68]
(для кликов и перемещений) MouseMotionListener addMouseMotionListener( ) removeMouseMotionListener( ) |
Component и унаследованные*. |
WindowEvent WindowListener addWindowListener( ) removeWindowListener( ) |
Window и унаследованные от него, включая JDialog, JFileDialog и JFrame. |
ItemEvent ItemListener addItemListener( ) removeItemListener( ) |
JCheckBox, JCheckBoxMenuItem, JComboBox, JList и все, что реализует ItemSelectable interface. |
TextEvent TextListener addTextListener( ) removeTextListener( ) |
Все, что унаследовано от JTextComponent, включая JTextArea и JTextField. |
Вы видите, что каждый тип компонент поддерживает только определенные типы событий. Оказывается, довольно трудно просмотреть все события, поддерживаемые компонентом. Простой подход - это изменение программы ShowMethodsClean.java из Главы 12, чтобы отобразить все слушатели событий, поддерживаемые компонентами Swing, которые вы вводите.
В Главе 12 была введена рефлексия, которая использовалась для поиска методов определенного класса — или всего списка методов или подмножества методов, имена которых содержат передаваемое вами ключевое слово. Магия этого в том, что так автоматически можно показать все методы класса без прохождения по иерархии наследования, проверяя классы на всех уровнях. Таким образом, это обеспечивает сохранение драгоценного времени при программировании: потому что имена большинства методов Java сделаны очень многозначительными и описательными, вы можете искать имена методов, содержащих определенное, интересующее вас слово. Когда вы найдете то, что вы искали, проверьте онлайн документацию.
Однако в Главе 12 не было Swing, поэтому инструментарий той главы был разработан как приложение для командной строки. Здесь более полезная GUI версия, специализирующаяся на поиске методов “addListener” в компонентах Swing:
//: c13:ShowAddListeners.java // Отображение методов "addXXXListener" любого // класса Swing. // <applet code = ShowAddListeners // width=500 height=400></applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.lang.reflect.*; import java.io.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*; public class ShowAddListeners extends JApplet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; JTextField name = new JTextField(25); JTextArea results = new JTextArea(40, 65); class NameL implements ActionListener { public void actionPerformed(ActionEvent e) { String nm = name.getText().trim(); if(nm.length() == 0) { results.setText("No match"); n = new String[0]; return; } try { cl = Class.forName("javax.swing." + nm); } catch(ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); // Преобразование в массив Strings: n = new String[m.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); reDisplay(); } } void reDisplay() { // Создание результирующего множества: String[] rs = new String[n.length]; int j = 0; for (int i = 0; i < n.length; i++) if(n[i].indexOf("add") != -1 && n[i].indexOf("Listener") != -1) rs[j++] = n[i].substring(n[i].indexOf("add")); results.setText(""); for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); } public void init() { name.addActionListener(new NameL()); JPanel top = new JPanel(); top.add(new JLabel( "Swing class name (press ENTER):")); top.add(name); Container cp = getContentPane(); cp.add(BorderLayout.NORTH, top); cp.add(new JScrollPane(results)); } public static void main(String[] args) { Console.run(new ShowAddListeners(), 500,400); } } ///:~
Класс StripQualifiers, определенный в Главе 12 здесь повторно используется, импортируясь из библиотеки com.bruceeckel.util.
GUI содержит JTextField name, в котором вы можете вводить имя класса Swing, который вы хотите просмотреть. Результат отображается в JTextArea.
Вы увидите, что нет никаких кнопок или других компонент, чтобы указать, что можно начать поиск. Это потому, что за JTextField следит ActionListener. Когда вы сделаете изменения и нажмете ENTER, список немедленно обновится. Если текст не пустой, он используется внутри Class.forName( ), чтобы попытаться найти класс. Если имя неверное, Class.forName( ) завершится неудачей, в результате чего появится исключение. Оно будет поймано и в JTextArea появится “No match”. Но если вы напечатаете корректное имя (включая большие буквы), Class.forName( ) завершится успешно и getMethods( ) вернет массив объектов Method. Каждый объект массива включается в String через toString( ) (так получается полная сигнатура метода) и добавляется в n - массив String. Массив n - это член класса ShowAddListeners, он используется при обновлении отображения, когда вызывается reDisplay( ).
reDisplay( ) создает массив String, называемый rs (для “result set”). Результирующее множество условно копируется из String в n, который содержит “add” и “Listener”. Затем используются indexOf( ) и substring( ) для удаления квалификаторов, таких как public, static и т.п. В конце StripQualifiers.strip( ) удаляет дополнительные квалификаторы имени.
Эта программа - это удобный способ для исследования совместимости компонент Swing. Как только вы узнаете, какие события поддерживает определенный компонент, вам не нужно будет искать ничего, чтобы отреагировать на это событие. Вы просто:
Вот некоторые из интерфейсов слушателя:
Интерфейс
слушателя w/ adapter |
Методы интерфейса |
---|---|
ActionListener | actionPerformed(ActionEvent) |
AdjustmentListener | adjustmentValueChanged( AdjustmentEvent) |
ComponentListener ComponentAdapter |
componentHidden(ComponentEvent) componentShown(ComponentEvent) componentMoved(ComponentEvent) componentResized(ComponentEvent) |
ContainerListener ContainerAdapter |
componentAdded(ContainerEvent) componentRemoved(ContainerEvent) |
FocusListener FocusAdapter |
focusGained(FocusEvent) focusLost(FocusEvent) |
KeyListener KeyAdapter |
keyPressed(KeyEvent) keyReleased(KeyEvent) keyTyped(KeyEvent) |
MouseListener MouseAdapter |
mouseClicked(MouseEvent) mouseEntered(MouseEvent) mouseExited(MouseEvent) mousePressed(MouseEvent) mouseReleased(MouseEvent) |
MouseMotionListener MouseMotionAdapter |
mouseDragged(MouseEvent) mouseMoved(MouseEvent) |
WindowListener WindowAdapter |
windowOpened(WindowEvent) windowClosing(WindowEvent) windowClosed(WindowEvent) windowActivated(WindowEvent) windowDeactivated(WindowEvent) windowIconified(WindowEvent) windowDeiconified(WindowEvent) |
ItemListener | itemStateChanged(ItemEvent) |
Это не полный список, частично потому, что событийная модель позволяет вам создавать вам свои собственные типы событий и ассоциированные слушатели. Таким образом, вы регулярно будете просматривать библиотеки, чтобы унаследовать свое собственное событие, и знания, полученные в этой главе, позволят вам понять, как использовать это события.
В приведенной выше таблице вы можете видеть, что некоторые интерфейсы слушателей имеют только один метод. Они очень просты для реализации, так как вы реализуете его, только когда напишите этот определенный метод. Однако интерфейсы слушателей, имеющие несколько методов, менее приятны в использовании. Например, то, что вы должны всегда делать при создании приложения, это обеспечение WindowListener для JFrame, так что когда вы получаете событие windowClosing( ), вы могли бы вызвать System.exit( ) для выхода из приложения. Но так как WindowListener - это интерфейс, вы должны реализовать все другие методы, даже если они ничего не делают. Это может раздражать.
Для решения проблемы некоторые (но не все) из интерфейсов слушателей, которые имеют более одного метода, снабжаются адаптерами, имена которых вы можете видеть в приведенной выше таблице. Каждый адаптер обеспечивает по умолчанию пустые методы для каждого метода интерфейса. Поэтому все, что вам нужно сделать - это наследовать от адаптера и перекрыть только те методы, которые нужно изменить. Например, типичный WindowListener, который вы будете использовать, выглядит так (помните, что это было помещено внутрь класса Console в com.bruceeckel.swing):
class MyWindowListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } }
Основное назначение адаптеров состоит в облегчении создания слушающих классов.
Однако есть темная сторона адаптеров, из-за которой можно попасть в ловушку. Предположим, что вы написали WindowAdapter как показано выше:
class MyWindowListener extends WindowAdapter { public void WindowClosing(WindowEvent e) { System.exit(0); } }
Это не работает и это может свести вас с ума в попытке узнать почему, так как все прекрасно компилируется и запускается — за исключением того, что окно при закрытии окна не происходит выход из программы. Вы видите проблему? Она в имени метода WindowClosing( ) вместо windowClosing( ). Однако это не тот метод, который вызывается при закрытии окна, так что вы не получаете желаемый результат. Несмотря на неудобства, интерфейс гарантирует, что методы будут реализованы правильно.
Чтобы убедится, что эти события действительно возбуждаются, и в качестве эксперимента, стоит создать апплет, который отслеживает дополнительное поведение JButton (а не только следит за его нажатием). Этот пример также показывает вам, как наследовать вашу собственный объект кнопки, потому что она будет использоваться как мишень для всех интересующих нас событий. Чтобы сделать это, вы просто наследуете от JButton.[69]
Класс MyButton - это внутренний класс TrackEvent, так что MyButton может получить доступ в родительское окно и управлять его текстовыми полями, что необходимо для записи информации статуса в поля родителя. Конечно это ограниченная ситуация, так как myButton может использоваться только в соединении с TrackEvent. Код такого рода иногда называется “глубоко связанный”:
//: c13:TrackEvent.java // Показ возникающих событий. // <applet code=TrackEvent // width=700 height=500></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class TrackEvent extends JApplet { HashMap h = new HashMap(); String[] event = { "focusGained", "focusLost", "keyPressed", "keyReleased", "keyTyped", "mouseClicked", "mouseEntered", "mouseExited","mousePressed", "mouseReleased", "mouseDragged", "mouseMoved" }; MyButton b1 = new MyButton(Color.blue, "test1"), b2 = new MyButton(Color.red, "test2"); class MyButton extends JButton { void report(String field, String msg) { ((JTextField)h.get(field)).setText(msg); } FocusListener fl = new FocusListener() { public void focusGained(FocusEvent e) { report("focusGained", e.paramString()); } public void focusLost(FocusEvent e) { report("focusLost", e.paramString()); } }; KeyListener kl = new KeyListener() { public void keyPressed(KeyEvent e) { report("keyPressed", e.paramString()); } public void keyReleased(KeyEvent e) { report("keyReleased", e.paramString()); } public void keyTyped(KeyEvent e) { report("keyTyped", e.paramString()); } }; MouseListener ml = new MouseListener() { public void mouseClicked(MouseEvent e) { report("mouseClicked", e.paramString()); } public void mouseEntered(MouseEvent e) { report("mouseEntered", e.paramString()); } public void mouseExited(MouseEvent e) { report("mouseExited", e.paramString()); } public void mousePressed(MouseEvent e) { report("mousePressed", e.paramString()); } public void mouseReleased(MouseEvent e) { report("mouseReleased", e.paramString()); } }; MouseMotionListener mml = new MouseMotionListener() { public void mouseDragged(MouseEvent e) { report("mouseDragged", e.paramString()); } public void mouseMoved(MouseEvent e) { report("mouseMoved", e.paramString()); } }; public MyButton(Color color, String label) { super(label); setBackground(color); addFocusListener(fl); addKeyListener(kl); addMouseListener(ml); addMouseMotionListener(mml); } } public void init() { Container c = getContentPane(); c.setLayout(new GridLayout(event.length+1,2)); for(int i = 0; i < event.length; i++) { JTextField t = new JTextField(); t.setEditable(false); c.add(new JLabel(event[i], JLabel.RIGHT)); c.add(t); h.put(event[i], t); } c.add(b1); c.add(b2); } public static void main(String[] args) { Console.run(new TrackEvent(), 700, 500); } } ///:~
В конструкторе MyButton устанавливается цвет вызовом SetBackground( ). Все слушатели устанавливаются простым вызовом метода.
Класс TrackEvent содержит HashMap для хранения строк, представляющих тип события, и поля JTextField, которые содержат информацию о событиях. Конечно, это должно создаваться статически перед помещением в HashMap, но я думаю, что вы согласитесь, что это гораздо легче использовать и изменять. Обычно, если вам нужно добавить или удалить новый тип события в TrackEvent, вы просто добавляете или удаляете строку в массиве event — все остальное происходит автоматически.
Когда вызывается report( ) он дает имя события и строку параметров события. Далее используется HashMap h из внешнего класса для поиска реального JTextField, ассоциированного с этим именем события, и происходит помещение строки параметров в это поле.
С этот примером забавно поиграть, так как вы на самом деле видите то, что происходит с событиями в вашей программе.
Теперь, когда вы понимаете менеджеры компоновки и модель событий, вы готовы посмотреть как использовать компоненты Swing. Этот раздел не является исчерпывающим описанием компонент Swing и их свойств, которые вы, вероятно, будите использовать большую часть времени. Каждый пример намеренно сделан очень маленьким, чтобы вы могли разобрать код и использовать его в ваших программах.
Вы легко увидите, как выглядит каждый из этих примеров при запуске при просмотре HTML страниц в скаченном исходном коде для этой главы.
Имейте в виду:
Swing включает несколько разных типов кнопок. Все кнопки, checkBox-элементы, радио кнопки и даже элементы меню наследованы от AbstractButton (который, так как сюда включен элемент меню, вероятно должен называться “AbstractChooser” или аналогичным образом). Вы скоро увидите использование элементов меню, но следующий пример показывает различные поддерживаемые типы кнопок:
//: c13:Buttons.java // Различные кнопки Swing. // <applet code=Buttons // width=350 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.plaf.basic.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class Buttons extends JApplet { JButton jb = new JButton("JButton"); BasicArrowButton up = new BasicArrowButton( BasicArrowButton.NORTH), down = new BasicArrowButton( BasicArrowButton.SOUTH), right = new BasicArrowButton( BasicArrowButton.EAST), left = new BasicArrowButton( BasicArrowButton.WEST); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(jb); cp.add(new JToggleButton("JToggleButton")); cp.add(new JCheckBox("JCheckBox")); cp.add(new JRadioButton("JRadioButton")); JPanel jp = new JPanel(); jp.setBorder(new TitledBorder("Directions")); jp.add(up); jp.add(down); jp.add(left); jp.add(right); cp.add(jp); } public static void main(String[] args) { Console.run(new Buttons(), 350, 100); } } ///:~
Пример начинается с BasicArrowButton из javax.swing.plaf.basic, затем вводятся различные специфичные типы кнопок. Когда вы запустите пример, вы увидите, что переключающаяся кнопка запоминает свое последнее состояние, нажатая или нет. Checkbox-элемент и радио кнопка имеют идентичное поведение, нужно просто кликнуть на нее для включения или выключения (они унаследованы от JToggleButton).
Если вам нужны радио кнопки для получения поведения, вида “исключающего или”, вы должны добавить их в “группу кнопок”. Но, как показывает приведенный ниже пример, любая AbstractButton может быть добавлена в ButtonGroup.
Для предотвращения повтора большого количества кода этот пример использует рефлексию для генерации различных типов кнопок. Это происходит в makeBPanel( ), которая создает группу кнопок и JPanel. Второй аргумент для makeBPanel( ) - это массив String. Для каждого String, в JPanel добавляется кнопка класса, соответствующего первому аргументу:
//: c13:ButtonGroups.java // Использование рефлексии для создания групп // различных типов AbstractButton. // <applet code=ButtonGroups // width=500 height=300></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import java.lang.reflect.*; import com.bruceeckel.swing.*; public class ButtonGroups extends JApplet { static String[] ids = { "June", "Ward", "Beaver", "Wally", "Eddie", "Lumpy", }; static JPanel makeBPanel(Class bClass, String[] ids) { ButtonGroup bg = new ButtonGroup(); JPanel jp = new JPanel(); String title = bClass.getName(); title = title.substring( title.lastIndexOf('.') + 1); jp.setBorder(new TitledBorder(title)); for(int i = 0; i < ids.length; i++) { AbstractButton ab = new JButton("failed"); try { // Получение динамического метода конструктора, // который принимает аргумент String: Constructor ctor = bClass.getConstructor( new Class[] { String.class }); // Создание нового объекта: ab = (AbstractButton)ctor.newInstance( new Object[]{ids[i]}); } catch(Exception ex) { System.err.println("can't create " + bClass); } bg.add(ab); jp.add(ab); } return jp; } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(makeBPanel(JButton.class, ids)); cp.add(makeBPanel(JToggleButton.class, ids)); cp.add(makeBPanel(JCheckBox.class, ids)); cp.add(makeBPanel(JRadioButton.class, ids)); } public static void main(String[] args) { Console.run(new ButtonGroups(), 500, 300); } } ///:~
Заголовок для бордюра берется из имени класса, от которого отсекается вся информация о пути. AbstractButton инициализируется с помощью JButton, которая имеет метку “Failed”, так что если вы игнорируете сообщение исключения, вы видите проблему на экране. Метод getConstructor( ) производит объект Constructor, который принимает массив аргументов типов в массиве Class, переданном getConstructor( ). Затем, все, что вам нужно сделать, это вызвать newInstance( ), передав этот массив элементов Object, содержащий ваши реальные аргументы — в этом случае просто String из массива ids.
Здесь немного усложнен простой процесс. Для получения поведения кнопок, вида “исключающее или”, вы создаете группу кнопок и добавляете каждую кнопку, для которой вам нужно поведение в группе. Когда вы запустите программу, вы увидите, что все кнопки, за исключением JButton, показывают это поведение, вида “исключающее или”.
Вы можете использовать Icon внутри JLabel или всего, что унаследовано от AbstractButton (включая JButton, JCheckBox, JRadioButton и разного рода JMenuItem). Использование Icon с JLabel достаточно ясное (вы увидите пример позже). Приведенный ниже пример исследует все дополнительные способы, которыми вы можете использовать Icon с кнопками и их потомками.
Вы можете использовать любой gif файл, который хотите, и один из них, использующийся в этом примере, является частью кода этой книги, доступной на www.BruceEckel.com. Для открытия файла и получения изображения, просто создайте ImageIcon и передайте ему имя файла. После этого вы можете использовать полученную Icon в вашей программе.
Обратите внимание, что информация о пути жестко встроена в этот пример; вы должны изменить путь на соответствующий положению файла изображения на вашей машине.
//: c13:Faces.java // Поведение Icon в Jbuttons. // <applet code=Faces // width=250 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Faces extends JApplet { // Следующая информация о пути необходима // для запуска апплета непосредственно с диска: static String path = "C:/aaa-TIJ2-distribution/code/c13/"; static Icon[] faces = { new ImageIcon(path + "face0.gif"), new ImageIcon(path + "face1.gif"), new ImageIcon(path + "face2.gif"), new ImageIcon(path + "face3.gif"), new ImageIcon(path + "face4.gif"), }; JButton jb = new JButton("JButton", faces[3]), jb2 = new JButton("Disable"); boolean mad = false; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); jb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(mad) { jb.setIcon(faces[3]); mad = false; } else { jb.setIcon(faces[0]); mad = true; } jb.setVerticalAlignment(JButton.TOP); jb.setHorizontalAlignment(JButton.LEFT); } }); jb.setRolloverEnabled(true); jb.setRolloverIcon(faces[1]); jb.setPressedIcon(faces[2]); jb.setDisabledIcon(faces[4]); jb.setToolTipText("Yow!"); cp.add(jb); jb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(jb.isEnabled()) { jb.setEnabled(false); jb2.setText("Enable"); } else { jb.setEnabled(true); jb2.setText("Disable"); } } }); cp.add(jb2); } public static void main(String[] args) { Console.run(new Faces(), 400, 200); } } ///:~
Icon может быть использована во многих конструкторах, но вы можете также использовать setIcon( ) для добавления или изменения Icon. Этот пример также показывает как JButton (или любая AbstractButton) может устанавливать различные сорта иконок, которые появляются при возникновении каких-то событий с этой кнопкой: когда она нажата, отключена или “перекрыта” (мышь перемещается над ней без кликов). Вы увидите, что это дает кнопке прекрасную анимацию.
Предыдущий пример добавляет “инструмент подсказки” к кнопке. Почти все классы, которые вы будите использовать для создания интерфейса пользователя, наследуются от JComponent, который содержит метод, называемый setToolTipText(String). Поэтому, фактически, для всего, что вы помещаете на форму, все, что вам нужно сделать, это сказать (для объекта jc любого класса, унаследованного от JComponent):
jc.setToolTipText("My tip");
и когда мышь задержится над этим JComponent на предопределенное время, возле мыши всплывет крошечный прямоугольник, содержащий ваш текст.
Этот пример показывает дополнительные возможности, имеющиеся в JTextField:
//: c13:TextFields.java // Текстовые поля и события Java. // <applet code=TextFields width=375 // height=125></applet> import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class TextFields extends JApplet { JButton b1 = new JButton("Get Text"), b2 = new JButton("Set Text"); JTextField t1 = new JTextField(30), t2 = new JTextField(30), t3 = new JTextField(30); String s = new String(); UpperCaseDocument ucd = new UpperCaseDocument(); public void init() { t1.setDocument(ucd); ucd.addDocumentListener(new T1()); b1.addActionListener(new B1()); b2.addActionListener(new B2()); DocumentListener dl = new T1(); t1.addActionListener(new T1A()); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(t1); cp.add(t2); cp.add(t3); } class T1 implements DocumentListener { public void changedUpdate(DocumentEvent e){} public void insertUpdate(DocumentEvent e){ t2.setText(t1.getText()); t3.setText("Text: "+ t1.getText()); } public void removeUpdate(DocumentEvent e){ t2.setText(t1.getText()); } } class T1A implements ActionListener { private int count = 0; public void actionPerformed(ActionEvent e) { t3.setText("t1 Action Event " + count++); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { if(t1.getSelectedText() == null) s = t1.getText(); else s = t1.getSelectedText(); t1.setEditable(true); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { ucd.setUpperCase(false); t1.setText("Inserted by Button 2: " + s); ucd.setUpperCase(true); t1.setEditable(false); } } public static void main(String[] args) { Console.run(new TextFields(), 375, 125); } } class UpperCaseDocument extends PlainDocument { boolean upperCase = true; public void setUpperCase(boolean flag) { upperCase = flag; } public void insertString(int offset, String string, AttributeSet attributeSet) throws BadLocationException { if(upperCase) string = string.toUpperCase(); super.insertString(offset, string, attributeSet); } } ///:~
JTextField t3 включено как место для отчета при возбуждении слушателя действия JTextField t1. Вы увидите, что слушатель действия для JTextField возбуждается, только когда вы нажмете кнопку “enter”.
JTextField t1 имеет несколько присоединенных слушателей. Слушатель T1 - это DocumentListener, который отвечает на любые изменения в “документе” (в этом случае - это содержимое JTextField). Он автоматически копирует весь текст из t1 в t2. Кроме того, документ в t1 устанавливается на класс, унаследованный от PlainDocument, называемый UpperCaseDocument, который переводит все символы в верхний регистр. Он автоматически определяет пробелы и выполняет удаление, регулирование каретки и обработку всего, как вы можете ожидать.
JComponent содержит метод, называемый setBorder( ), который позволяет вам поместить разные интересные бордюры на любой видимый компонент. Следующий пример демонстрирует несколько различных поддерживаемых бордюров, используя метод, называемый showBorder( ), который создает JPanel и помещает бордюр в каждом случае. Также он использует RTTI для нахождения имени бордюра, который вы используете (отсекая информацию о пути), затем помещает это имя в JLabel, находящуюся в середине панели:
//: c13:Borders.java // Различные бордюры Swing. // <applet code=Borders // width=500 height=300></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class Borders extends JApplet { static JPanel showBorder(Border b) { JPanel jp = new JPanel(); jp.setLayout(new BorderLayout()); String nm = b.getClass().toString(); nm = nm.substring(nm.lastIndexOf('.') + 1); jp.add(new JLabel(nm, JLabel.CENTER), BorderLayout.CENTER); jp.setBorder(b); return jp; } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.setLayout(new GridLayout(2,4)); cp.add(showBorder(new TitledBorder("Title"))); cp.add(showBorder(new EtchedBorder())); cp.add(showBorder(new LineBorder(Color.blue))); cp.add(showBorder( new MatteBorder(5,5,30,30,Color.green))); cp.add(showBorder( new BevelBorder(BevelBorder.RAISED))); cp.add(showBorder( new SoftBevelBorder(BevelBorder.LOWERED))); cp.add(showBorder(new CompoundBorder( new EtchedBorder(), new LineBorder(Color.red)))); } public static void main(String[] args) { Console.run(new Borders(), 500, 300); } } ///:~
Вы также можете создать свой собственный бордюр и поместить его внутри кнопок, меток и т.п. — всего, что унаследовано от JComponent.
Большую часть времени вам будет нужно позволять JScrollPane делать его работу, но вы можете также управлять, какая полоса прокрутки доступна — вертикальная, горизонтальная, обе или ни одной:
//: c13:JScrollPanes.java //Управление полосами прокрутки в JScrollPane. // <applet code=JScrollPanes width=300 height=725> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class JScrollPanes extends JApplet { JButton b1 = new JButton("Text Area 1"), b2 = new JButton("Text Area 2"), b3 = new JButton("Replace Text"), b4 = new JButton("Insert Text"); JTextArea t1 = new JTextArea("t1", 1, 20), t2 = new JTextArea("t2", 4, 20), t3 = new JTextArea("t3", 1, 20), t4 = new JTextArea("t4", 10, 10), t5 = new JTextArea("t5", 4, 20), t6 = new JTextArea("t6", 10, 10); JScrollPane sp3 = new JScrollPane(t3, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), sp4 = new JScrollPane(t4, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), sp5 = new JScrollPane(t5, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS), sp6 = new JScrollPane(t6, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); class B1L implements ActionListener { public void actionPerformed(ActionEvent e) { t5.append(t1.getText() + "\n"); } } class B2L implements ActionListener { public void actionPerformed(ActionEvent e) { t2.setText("Inserted by Button 2"); t2.append(": " + t1.getText()); t5.append(t2.getText() + "\n"); } } class B3L implements ActionListener { public void actionPerformed(ActionEvent e) { String s = " Replacement "; t2.replaceRange(s, 3, 3 + s.length()); } } class B4L implements ActionListener { public void actionPerformed(ActionEvent e) { t2.insert(" Inserted ", 10); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); // Создание бордюра для компонент: Border brd = BorderFactory.createMatteBorder( 1, 1, 1, 1, Color.black); t1.setBorder(brd); t2.setBorder(brd); sp3.setBorder(brd); sp4.setBorder(brd); sp5.setBorder(brd); sp6.setBorder(brd); // Инициализация слушателей и добавление компонент: b1.addActionListener(new B1L()); cp.add(b1); cp.add(t1); b2.addActionListener(new B2L()); cp.add(b2); cp.add(t2); b3.addActionListener(new B3L()); cp.add(b3); b4.addActionListener(new B4L()); cp.add(b4); cp.add(sp3); cp.add(sp4); cp.add(sp5); cp.add(sp6); } public static void main(String[] args) { Console.run(new JScrollPanes(), 300, 725); } } ///:~
При использовании различных аргументов, в конструкторе JScrollPane происходит управление доступностью полос прокрутки. Этот пример также немного красивее при использовании бордюров.
Управляющий элемент JTextPane великолепно подходит для редактирования, без больших усилий. Следующий пример делает очень простое использование этого, игнорирую большую часть функциональности класса:
//: c13:TextPane.java // JTextPane - это маленький редактор. // <applet code=TextPane width=475 height=425> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*; public class TextPane extends JApplet { JButton b = new JButton("Add Text"); JTextPane tp = new JTextPane(); static Generator sg = new Arrays2.RandStringGenerator(7); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(int i = 1; i < 10; i++) tp.setText(tp.getText() + sg.next() + "\n"); } }); Container cp = getContentPane(); cp.add(new JScrollPane(tp)); cp.add(BorderLayout.SOUTH, b); } public static void main(String[] args) { Console.run(new TextPane(), 475, 425); } } ///:~
Кнопка просто добавляет случайно сгенерированный текст. Смысл JTextPane состоит в том, что она позволяет редактировать текст на месте, так что вы увидите и не нужен метод append( ). В этом случае (вероятно, недостаточное использование возможностей JTextPane), текст должен захватываться, изменятся и помещаться назад в панель, используя setText( ).
Как упоминалось ранее, апплет по умолчанию использует компоновку BorderLayout. Если вы добавите что-то в панель без указания детализации, оно просто заполнит центр панели до краев. Однако если вы укажите один из окружающих регионов (NORTH, SOUTH, EAST или WEST), как сделано здесь, компонент поместит себя в этот регион — в этом случае кнопка вмонтирована внизу экрана.
Обратите внимание на встроенные особенности JTextPane, такие как автоматическое разбиение строк. Есть много других особенностей, которые вы можете просмотреть, используя документацию JDK.
CheckBox-элемент обеспечивает способ создания единственного выбора включения/выключения; он состоит из небольшого прямоугольника и метки. Прямоугольник обычно содержит небольшой “x” (или какой-то другой индикатор того, что он установлен) или остается пустым, в зависимости от того, был ли он выбран.
Обычно вы будете создавать JCheckBox, используя конструктор, который получает метку в качестве аргумента. Вы можете установить и получить состояние и установить метку, если хотите прочесть или изменить ее после создания JCheckBox.
Независимо от того, где JCheckBox установлен или создан, происходят события, которые вы можете собирать тем же способом, что и для кнопки, используя ActionListener. Следующий пример использует JTextArea, чтобы убедится, что на всех checkBox-элементах произведен щелчок мышкой:
//: c13:CheckBoxes.java // Использование JCheckBoxes. // <applet code=CheckBoxes width=200 height=200> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class CheckBoxes extends JApplet { JTextArea t = new JTextArea(6, 15); JCheckBox cb1 = new JCheckBox("Check Box 1"), cb2 = new JCheckBox("Check Box 2"), cb3 = new JCheckBox("Check Box 3"); public void init() { cb1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("1", cb1); } }); cb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("2", cb2); } }); cb3.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("3", cb3); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(cb1); cp.add(cb2); cp.add(cb3); } void trace(String b, JCheckBox cb) { if(cb.isSelected()) t.append("Box " + b + " Set\n"); else t.append("Box " + b + " Cleared\n"); } public static void main(String[] args) { Console.run(new CheckBoxes(), 200, 200); } } ///:~
Метод trace( ) посылает имя выделенного JCheckBox и его текущего состояния в JTextArea, используя append( ), так что вы увидите совокупный список checkbox-элементов и их состояния.
Концепция радио кнопок в программировании GUI пришла из до электронного радио для автомобиля с механическими кнопками: когда вы нажимаете одну из них, все остальные, которые были нажаты, отжимаются. Таким образом, это позволяет вам навязывать единственный выбор из многих.
Все, что вам нужно сделать, это установить ассоциированную группу JRadioButton, добавив их в ButtonGroup (вы можете иметь любое число ButtonGroup на форме). Одна из кнопок может быть (не обязательно) выбрана в стартовом положении и для нее устанавливается true (используется второй аргумент конструктора). Если вы попробуете установить более чем одну радио кнопку в true, только последняя установка сохранит значение true.
Здесь приведен пример использования радио кнопок. Обратите внимание, что вы захватываете события радио кнопок, как и все остальные:
//: c13:RadioButtons.java // Использование JRadioButton. // <applet code=RadioButtons // width=200 height=100> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class RadioButtons extends JApplet { JTextField t = new JTextField(15); ButtonGroup g = new ButtonGroup(); JRadioButton rb1 = new JRadioButton("one", false), rb2 = new JRadioButton("two", false), rb3 = new JRadioButton("three", false); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { t.setText("Radio button " + ((JRadioButton)e.getSource()).getText()); } }; public void init() { rb1.addActionListener(al); rb2.addActionListener(al); rb3.addActionListener(al); g.add(rb1); g.add(rb2); g.add(rb3); t.setEditable(false); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(rb1); cp.add(rb2); cp.add(rb3); } public static void main(String[] args) { Console.run(new RadioButtons(), 200, 100); } } ///:~
Для отображения состояния используется текстовое поле. Это поле устанавливается, как не редактируемое, потому что оно используется только для отображения данных, а не для сбора их. Таким образом - это альтернатива использованию JLabel.
Как и группа радио кнопок, выпадающий список - это способ заставить пользователя выбрать только один элемент из группы возможных. Однако это более компактный способ выполнить это и он более легкий с точки зрения смены элементов списка, который не удивит пользователя. (Вы можете изменить радио кнопки динамически, но при этом произойдут визуальные вибрации).
JComboBox из Java отличается от аналогичного элемента в Windows, который позволяет вам выбрать из списка или напечатать в нем свое собственное значение. С помощью JComboBox вы выбираете один и только один элемент из списка. В следующем примере JComboBox в начале заполняется некоторым числом элементов, а затем добавляются новые элементы при нажатии кнопки.
//: c13:ComboBoxes.java // Использование выпадающих списков. // <applet code=ComboBoxes // width=200 height=100> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class ComboBoxes extends JApplet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; JTextField t = new JTextField(15); JComboBox c = new JComboBox(); JButton b = new JButton("Add items"); int count = 0; public void init() { for(int i = 0; i < 4; i++) c.addItem(description[count++]); t.setEditable(false); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(count < description.length) c.addItem(description[count++]); } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText("index: "+ c.getSelectedIndex() + " " + ((JComboBox)e.getSource()) .getSelectedItem()); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(c); cp.add(b); } public static void main(String[] args) { Console.run(new ComboBoxes(), 200, 100); } } ///:~
JTextField отображает “выбранный индекс”, являющийся последовательностью номеров элементов, являющихся выбранными, точно так же как и метки для радио кнопок.
Список значительно отличается от JComboBox и не только по внешнему виду. В то время как JComboBox выпадает вниз при активации, JList занимает определенное фиксированное число строк на экране все время и не изменяется. Если вы хотите видеть элементы в списке, вы просто вызываете getSelectedValues( ), который производи массив String из выбранных элементов.
JList позволяет множественный выбор: если вы используете кнопку CTRL при щелчке мышью на более чем одном элементе (удерживайте кнопку “control” при выполнении дополнительных щелчков мышью) начальный элемент остается подсвеченным, и вы можете выбрать столько элементов, сколько хотите. Если вы выбрали элемент, а затем щелкнули на другом, удерживая кнопку SHIFT, выберутся все элементы в пространстве между этими двумя. Для удаления элемента из группы вы можете выполнить щелчок с нажатой кнопкой CTRL.
//: c13:List.java // <applet code=List width=250 // height=375> </applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class List extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; DefaultListModel lItems=new DefaultListModel(); JList lst = new JList(lItems); JTextArea t = new JTextArea(flavors.length,20); JButton b = new JButton("Add Item"); ActionListener bl = new ActionListener() { public void actionPerformed(ActionEvent e) { if(count < flavors.length) { lItems.add(0, flavors[count++]); } else { // Отключено, так как не осталось больше // вкусов для добавления в список b.setEnabled(false); } } }; ListSelectionListener ll = new ListSelectionListener() { public void valueChanged( ListSelectionEvent e) { t.setText(""); Object[] items=lst.getSelectedValues(); for(int i = 0; i < items.length; i++) t.append(items[i] + "\n"); } }; int count = 0; public void init() { Container cp = getContentPane(); t.setEditable(false); cp.setLayout(new FlowLayout()); // Создание бордюра для компонента: Border brd = BorderFactory.createMatteBorder( 1, 1, 2, 2, Color.black); lst.setBorder(brd); t.setBorder(brd); // Добавление первых четырех элементов в список for(int i = 0; i < 4; i++) lItems.addElement(flavors[count++]); // Добавление элементов в Панель Содержания для отображения cp.add(t); cp.add(lst); cp.add(b); // Регистрация слушателей событий lst.addListSelectionListener(ll); b.addActionListener(bl); } public static void main(String[] args) { Console.run(new List(), 250, 375); } } ///:~
Когда вы нажимаете кнопку, происходит добавление элементов в верх списка (потому что в addItem( ) второй аргумент равен 0).
Вы можете видеть, что бордюр также добавляется в списки.
Если вы хотите поместить массив String в JList, есть достаточно простое решение: вы передаете массив в конструктор JList, а он строит список автоматически. Есть только одно объяснение для использования “модели списка” в приведенном выше примере - это то, что список может быть изменен во время выполнения программы.
JList не поддерживает напрямую автоматическое скроллирование. Конечно, все, что вам нужно сделать, это "обернуть" JList в JScrollPane, а все остальной автоматически будет сделано за вас.
JTabbedPane позволяет создать вам “диалог с закладками”, который имеет закладки наподобие файлов, расположенные с одной стороны, и позволяющий вам нажимать на закладку для отображения различных диалогов.
//: c13:TabbedPane1.java // Демонстрация Tabbed Pane. // <applet code=TabbedPane1 // width=350 height=200> </applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class TabbedPane1 extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTabbedPane tabs = new JTabbedPane(); JTextField txt = new JTextField(20); public void init() { for(int i = 0; i < flavors.length; i++) tabs.addTab(flavors[i], new JButton("Tabbed pane " + i)); tabs.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { txt.setText("Tab selected: " + tabs.getSelectedIndex()); } }); Container cp = getContentPane(); cp.add(BorderLayout.SOUTH, txt); cp.add(tabs); } public static void main(String[] args) { Console.run(new TabbedPane1(), 350, 200); } } ///:~
Java использование механизма “панели с закладками” достаточно важно, поскольку в программировании апплетов использование всплывающих диалогов обескураживает автоматическим добавлением небольшого предупреждения к любому диалогу, который всплывает из апплета.
Когда вы запустите программу, вы увидите, что JTabbedPane автоматически размещает закладки, если их слишком много для размещения в один ряд. Вы можете заметить это при изменении окна после запуска программы и командной строки консоли.
Оконная среда часто содержит стандартный набор окон сообщений, которые позволяют вам быстро посылать сообщения пользователю или получать информацию от пользователя. В Swing эти окна сообщений содержаться в JOptionPane. Вы имеете много различных возможностей (некоторые из них достаточно изощренные), но, наверное, одна из наиболее часто использующихся, это окно сообщения и диалог подтверждения, вызывающийся при использовании JOptionPane.showMessageDialog( ) и JOptionPane. showConfirmDialog( ). Следующий пример показывает подмножество окон сообщения, поддерживаемых JOptionPane:
//: c13:MessageBoxes.java // Демонстрация JoptionPane. // <applet code=MessageBoxes // width=200 height=150> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class MessageBoxes extends JApplet { JButton[] b = { new JButton("Alert"), new JButton("Yes/No"), new JButton("Color"), new JButton("Input"), new JButton("3 Vals") }; JTextField txt = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String id = ((JButton)e.getSource()).getText(); if(id.equals("Alert")) JOptionPane.showMessageDialog(null, "There's a bug on you!", "Hey!", JOptionPane.ERROR_MESSAGE); else if(id.equals("Yes/No")) JOptionPane.showConfirmDialog(null, "or no", "choose yes", JOptionPane.YES_NO_OPTION); else if(id.equals("Color")) { Object[] options = { "Red", "Green" }; int sel = JOptionPane.showOptionDialog( null, "Choose a Color!", "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if(sel != JOptionPane.CLOSED_OPTION) txt.setText( "Color Selected: " + options[sel]); } else if(id.equals("Input")) { String val = JOptionPane.showInputDialog( "How many fingers do you see?"); txt.setText(val); } else if(id.equals("3 Vals")) { Object[] selections = { "First", "Second", "Third" }; Object val = JOptionPane.showInputDialog( null, "Choose one", "Input", JOptionPane.INFORMATION_MESSAGE, null, selections, selections[0]); if(val != null) txt.setText( val.toString()); } } }; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < b.length; i++) { b[i].addActionListener(al); cp.add(b[i]); } cp.add(txt); } public static void main(String[] args) { Console.run(new MessageBoxes(), 200, 200); } } ///:~
Чтобы быть способным написать единственный ActionListener, я использую несколько рискованный подход проверки String метки кнопки. Проблема при этом в том, что легко получить слегка искаженную кнопку, обычно с большой буквы, и эта ошибка может быть трудна для обнаружения.
Обратите внимание, что showOptionDialog( ) и showInputDialog( ) обеспечивают возврат объекта, который содержит введенное пользователем значение.
Каждый компонент способен содержать меню, включая JApplet, JFrame, JDialog и их потомков, имеющих метод setJMenuBar( ), который принимает JMenuBar (вы можете иметь только один JMenuBar для определенного компонента). Вы добавляете JMenu в JMenuBar, и JMenuItem в JMenu. Каждый JMenuItem может иметь присоединенный к нему ActionListener для сигнализации, что элемент меню выбран.
В отличие от систем, использующих ресурсы, в Java и Swing вы должны в ручную собрать все меню в исходном коде. Вот очень простой пример меню:
//: c13:SimpleMenus.java // <applet code=SimpleMenus // width=200 height=75> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class SimpleMenus extends JApplet { JTextField t = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenu[] menus = { new JMenu("Winken"), new JMenu("Blinken"), new JMenu("Nod") }; JMenuItem[] items = { new JMenuItem("Fee"), new JMenuItem("Fi"), new JMenuItem("Fo"), new JMenuItem("Zip"), new JMenuItem("Zap"), new JMenuItem("Zot"), new JMenuItem("Olly"), new JMenuItem("Oxen"), new JMenuItem("Free") }; public void init() { for(int i = 0; i < items.length; i++) { items[i].addActionListener(al); menus[i%3].add(items[i]); } JMenuBar mb = new JMenuBar(); for(int i = 0; i < menus.length; i++) mb.add(menus[i]); setJMenuBar(mb); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); } public static void main(String[] args) { Console.run(new SimpleMenus(), 200, 75); } } ///:~
Использование оператора остатка от деления в выражении “i%3” распределяет элементы меню на три JMenu. Каждый JMenuItem должен иметь присоединенный к нему ActionListener; здесь один и тот же ActionListener используется везде, но вам обычно нужны индивидуальные слушатели для каждого JMenuItem.
JMenuItem наследует от AbstractButton, так что он имеет поведение, аналогичное кнопке. Сам по себе он обеспечивает элемент, который может быть помещен в выпадающее меню. Есть также три типа наследников от JMenuItem: JMenu для содержания других JMenuItem (так что вы можете иметь каскадированное меню), JCheckBoxMenuItem, которое производит отметки, указывающие, было ли выбрано меню, или нет, и JRadioButtonMenuItem, которое содержит радио кнопки.
В качестве более изощренного примере здесь снова используются вкусы мороженого, используемые для создания меню. Этот пример также показывает каскадируемое меню, мнемонические обозначение клавиш, JCheckBoxMenuItem, и способ, которым вы можете динамически менять меню:
//: c13:Menus.java // Подменю, checkbox-элементы в меню, перестановка меню, // мнемоники (горячие клавиши) и команды реакции. // <applet code=Menus width=300 // height=100> </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Menus extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTextField t = new JTextField("No flavor", 30); JMenuBar mb1 = new JMenuBar(); JMenu f = new JMenu("File"), m = new JMenu("Flavors"), s = new JMenu("Safety"); // Альтернативный подход: JCheckBoxMenuItem[] safety = { new JCheckBoxMenuItem("Guard"), new JCheckBoxMenuItem("Hide") }; JMenuItem[] file = { new JMenuItem("Open"), }; // Вторая полоса меню меняется на: JMenuBar mb2 = new JMenuBar(); JMenu fooBar = new JMenu("fooBar"); JMenuItem[] other = { // Добавление горячих клавиш в меню (мнемоник) // очень просто, но их могут иметь только JMenuItems // в своем конструкторе: new JMenuItem("Foo", KeyEvent.VK_F), new JMenuItem("Bar", KeyEvent.VK_A), // Нет горячей клавиши: new JMenuItem("Baz"), }; JButton b = new JButton("Swap Menus"); class BL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuBar m = getJMenuBar(); setJMenuBar(m == mb1 ? mb2 : mb1); validate(); // Обновление фрейма } } class ML implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Open")) { String s = t.getText(); boolean chosen = false; for(int i = 0; i < flavors.length; i++) if(s.equals(flavors[i])) chosen = true; if(!chosen) t.setText("Choose a flavor first!"); else t.setText("Opening "+ s +". Mmm, mm!"); } } } class FL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); t.setText(target.getText()); } } // Другой способ: вы можете создать другой класс // для каждого MenuItem. Затем вам // не нужно следить, кто есть кто: class FooL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Foo selected"); } } class BarL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Bar selected"); } } class BazL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Baz selected"); } } class CMIL implements ItemListener { public void itemStateChanged(ItemEvent e) { JCheckBoxMenuItem target = (JCheckBoxMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Guard")) t.setText("Guard the Ice Cream! " + "Guarding is " + target.getState()); else if(actionCommand.equals("Hide")) t.setText("Hide the Ice Cream! " + "Is it cold? " + target.getState()); } } public void init() { ML ml = new ML(); CMIL cmil = new CMIL(); safety[0].setActionCommand("Guard"); safety[0].setMnemonic(KeyEvent.VK_G); safety[0].addItemListener(cmil); safety[1].setActionCommand("Hide"); safety[0].setMnemonic(KeyEvent.VK_H); safety[1].addItemListener(cmil); other[0].addActionListener(new FooL()); other[1].addActionListener(new BarL()); other[2].addActionListener(new BazL()); FL fl = new FL(); for(int i = 0; i < flavors.length; i++) { JMenuItem mi = new JMenuItem(flavors[i]); mi.addActionListener(fl); m.add(mi); // Добавляем разделитель: if((i+1) % 3 == 0) m.addSeparator(); } for(int i = 0; i < safety.length; i++) s.add(safety[i]); s.setMnemonic(KeyEvent.VK_A); f.add(s); f.setMnemonic(KeyEvent.VK_F); for(int i = 0; i < file.length; i++) { file[i].addActionListener(fl); f.add(file[i]); } mb1.add(f); mb1.add(m); setJMenuBar(mb1); t.setEditable(false); Container cp = getContentPane(); cp.add(t, BorderLayout.CENTER); // Готовим систему к замене меню: b.addActionListener(new BL()); b.setMnemonic(KeyEvent.VK_S); cp.add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); fooBar.setMnemonic(KeyEvent.VK_B); mb2.add(fooBar); } public static void main(String[] args) { Console.run(new Menus(), 300, 100); } } ///:~
В этой программе я поместил элементы меню в массивы, а затем прошелся по всем массивам, вызывая add( ) для каждого JMenuItem. Это делает добавление или удаление пункта меню менее утомительным.
Эта программа создает не один, а два JMenuBar для демонстрации, что меню может быть заменено во время работы программы. Вы можете посмотреть, как создан JMenuBar из JMenu, и как каждый JMenu создан из JMenuItem, JCheckBoxMenuItem, или даже JMenu (который производит подменю). Когда JMenuBar собран, он может быть установлен в текущей программе с помощью метода setJMenuBar( ). Обратите внимание, что когда нажата кнопка, выполняется проверка какое меню сейчас установлено с помощью вызова метода getJMenuBar( ), а затем помещает другое меню на его место.
Когда проверяете “Open”, обратите внимание, что пробелы и большие буквы - критичны, но Java не сигнализирует об ошибках, если нет совпадений со словом “Open”. Такой род сравнения строк приводи к ошибкам программы.
Пометки и снятие пометки на элементах меню происходит автоматически. Код обработки JCheckBoxMenuItem показывает два разных способа определения, что было помечено: сравнение строк (которое, как упоминалось ранее, является не очень безопасным подходом, хотя вы видите его использование) и сравнение объектов - источников события. Как показано, метод getState( ) может использоваться для проверки состояния. Вы можете также менять состояние JCheckBoxMenuItem с помощью setState( ).
События для меню немного противоречивы и могут запутать: JMenuItem использует ActionListener, а JCheckboxMenuItem использует ItemListener. Объект JMenu может также поддерживать ActionListener, но обычно это бесполезно. В общем случае вы будете присоединять слушатели к каждому JMenuItem, JCheckBoxMenuItem или JRadioButtonMenuItem, но пример показывает ItemListener и ActionListener, присоединенные к различным компонентам меню.
Swing поддерживает мнемоники, или “горячие клавиши”, так что вы можете выбрать все что угодно, унаследованное от AbstractButton (кнопки, элементы меню и т.п.), используя клавиатуру вместо мыши. Это достаточно просто: для JMenuItem вы можете использовать перегруженный конструктор, который принимает второй аргумент, идентифицирующий клавишу. Однако большинство AbstractButton не имеют конструкторов, подобных этому, поэтому есть более общий способ решения проблемы - это использование метода setMnemonic( ). Приведенный выше пример добавляет мнемонику к кнопке и к некоторым из элементов меню; индикатор горячей клавиши в компоненте появляется автоматически.
Вы также можете видеть использование команды setActionCommand( ). Она выглядит немного странной, потому что в каждом случае “команда реакции” точно такая же, как и метка компонента меню. Почему просто не использовать метку вместо этой альтернативной строки? Проблема заключена в интернационализации. Если вы перенесете эту программу на другой язык, вы захотите изменить только метки в меню, и не изменять код (чтобы не вносить новые ошибки). Чтобы выполнить это легче при кодировании, выполняется проверка ассоциированной строки с этим компонентом меню, “команда реакции” может быть неизменной, в то время как метка меню может измениться. Весь код работает с “командой реакции”, так что на него не влияют никакие изменения меток меню. Обратите внимание, что в этой программе не у всех компонент меню проверяется их команда реакции, так что эти элементы не имеют своих установленных команд реакции.
Основная работа происходит в слушателях. BL выполняет обмен JMenuBar. В ML “cмотрят, кто звонил”, при этом подходе берется источник ActionEvent и приводится в типу JMenuItem, затем получается строка команды реакции для передачи ее в каскадную инструкцию if.
Слушатель FL прост, несмотря на то, что он обрабатывает все различные вкусы в меню вкусов. Этот подход полезен, если вы имеете достаточно простую логику, но в общем случае вы будете использовать подход с использованием FooL, BarL и BazL, каждый из которых присоединяется только к одному компоненту меню, так чтобы не нужна было дополнительная логика определения, и вы точно знали, кто вызвал слушателя. Даже с избытком классов, созданных этим способом, внутренний код имеет тенденцию быть меньше, а процесс более понятным.
Вы можете видеть, что код меню быстро становится многозначным и беспорядочным. Это другой случай, где использование построителя GUI является подходящим решением. Хороший инструмент также берет на себя заботы о меню.
Наиболее прямой путь для реализации JPopupMenu состоит в создании внутреннего класса, который расширяет MouseAdapter, с последующим добавлением объектов в этот внутренний класс для каждой компоненты, для которой вы хотите встроить всплывающее меню:
//: c13:Popup.java // Создание всплывающего меню со Swing. // <applet code=Popup // width=300 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Popup extends JApplet { JPopupMenu popup = new JPopupMenu(); JTextField t = new JTextField(10); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenuItem m = new JMenuItem("Hither"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Yon"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Afar"); m.addActionListener(al); popup.add(m); popup.addSeparator(); m = new JMenuItem("Stay Here"); m.addActionListener(al); popup.add(m); PopupListener pl = new PopupListener(); addMouseListener(pl); t.addMouseListener(pl); } class PopupListener extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if(e.isPopupTrigger()) { popup.show( e.getComponent(), e.getX(), e.getY()); } } } public static void main(String[] args) { Console.run(new Popup(), 300, 200); } } ///:~
Один и тот же ActionListener добавляется в каждый JMenuItem, так что он получает текст из метки меню и вставляет его в JTextField.
В хороших рабочих средах GUI рисунок должен быть разумно прост, и это есть в библиотеке Swing. Проблема с любым примером рисования в том, что расчеты, определяющие, где располагать вещи, обычно более сложные, чем вызов процедур рисования, а эти вычисления часто перемешиваются вместе с вызовами рисования, так что может показаться, что интерфейс более сложен, чем есть на самом деле.
Для упрощения проблемы представления данных на экране, здесь данные будут предоставляться встроенным методом Math.sin( ), который является математической функцией синуса. Чтобы сделать задачу более интересной, и для будущей демонстрации как легко использовать компоненты Swing, будет помещен слайдер внизу формы для динамического контроля числа отображаемых волн синуса. Кроме того, если вы измените размер окна, вы увидите, что волны синуса изменят свой размер в соответствии с размером окна.
Хотя любой JComponent может быть отрисован и, поэтому, использоваться в качестве канвы, если вы хотите просто рисовать на поверхности, вы обычно будете наследовать от JPanel. Только один метод вы должны перекрыть - это paintComponent( ), который вызывается всякий раз, когда компонент должен быть перерисован (вам обычно не нужно беспокоится об этом, это делает Swing). Когда он вызывается, Swing передает объект Graphics в этот метод, и вы затем можете использовать этот объект для рисования на поверхности.
В следующем примере вся информация относительно рисования находится в классе SineDraw; класс SineWave просто конфигурирует программу и слайдер. Внутри SineDraw метод setCycles( ) обеспечивает способ, позволяющий другому объекту — в этом случае слайдеру — регулировать число циклов.
//: c13:SineWave.java // Рисование с помощью Swing, используя JSlider. // <applet code=SineWave // width=700 height=400></applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*; class SineDraw extends JPanel { static final int SCALEFACTOR = 200; int cycles; int points; double[] sines; int[] pts; SineDraw() { setCycles(5); } public void setCycles(int newCycles) { cycles = newCycles; points = SCALEFACTOR * cycles * 2; sines = new double[points]; pts = new int[points]; for(int i = 0; i < points; i++) { double radians = (Math.PI/SCALEFACTOR) * i; sines[i] = Math.sin(radians); } repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); int maxWidth = getWidth(); double hstep = (double)maxWidth/(double)points; int maxHeight = getHeight(); for(int i = 0; i < points; i++) pts[i] = (int)(sines[i] * maxHeight/2 * .95 + maxHeight/2); g.setColor(Color.red); for(int i = 1; i < points; i++) { int x1 = (int)((i - 1) * hstep); int x2 = (int)(i * hstep); int y1 = pts[i-1]; int y2 = pts[i]; g.drawLine(x1, y1, x2, y2); } } } public class SineWave extends JApplet { SineDraw sines = new SineDraw(); JSlider cycles = new JSlider(1, 30, 5); public void init() { Container cp = getContentPane(); cp.add(sines); cycles.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { sines.setCycles( ((JSlider)e.getSource()).getValue()); } }); cp.add(BorderLayout.SOUTH, cycles); } public static void main(String[] args) { Console.run(new SineWave(), 700, 400); } } ///:~
Все члены - данные и массивы используются в расчетах точек волны синуса: cycles указывает нужное число полных волн синуса, points содержит полное число точек, которые будут построены, sines содержит значения функции синуса, а pts содержит y-координату точек, которые будут нарисованы на JPanel. Метод setCycles( ) создает массивы, размер которых равен числу необходимых точек и заполняет массив sines значениями. При вызове repaint( ), setCycles( ) становится причиной вызова paintComponent( ), так что происходит оставшаяся часть вычислений и перерисовки.
Первое, что вы должны сделать, когда перекрываете paintComponent( ), это вызвать версию метода базового класса. Затем вы свободны делать все, что захотите; обычно, это означает использование методов Graphics, которые вы можете найти в документации для java.awt.Graphics (в HTML документации на java.sun.com) для рисования и раскраски пикселей в JPanel. Здесь вы можете видеть, что почти весь код задействован в выполнении вычислений; только два метода реально управляют экраном, это setColor( ) и drawLine( ). Вы, вероятно, имели схожий опыт, когда создавали свою собственную программу, которая отображала графические данные — большую часть времени вы будете тратить на понимание того, что вы будете рисовать, но сам процесс рисования будет достаточно простым.
Когда я создавал эту программу, большую часть времени я потратил на получение волны синуса для отображения. Как только я сделал это, я подумал, что было бы неплохо иметь возможность динамически изменять число периодов. Из-за моего опыта программирования, когда я пробовал делать подобные вещи в других языках программирования, я неохотно взялся за эту попытку, но это была самая легкая часть проекта. Я создал JSlider (аргументами являются самое левое значение для JSlider, самое правое значение и начальное значение, соответственно, но также есть и другие конструкторы) и бросил его в JApplet. Затем я взглянул в HTML документацию, и заметил, что есть только слушатель addChangeListener, который запускается всегда, когда меняется слайдер для производства нового значения. Для этого был метод с очевидным названием stateChanged( ), которые имеет объект ChangeEvent, так что я мог снова посмотреть на источник изменений и найти новое значение. При вызове метода setCycles( ) объекта sines передается новое значение и JPanel перерисовывается.
В общем, вы найдете, что большинство ваших проблем в Swing могут быть решены при использовании аналогичного процесса, и вы найдете, что в общем случае это достаточно просто, даже если вы прежде не использовали некоторые компоненты.
Если ваши проблемы более сложные, есть более изощренные альтернативные способы рисования, включая JavaBeans компоненты сторонних производителей и Java 2D API. Эти решения не входят в число вопросов, рассматриваемых в этой книге, но вы должны просмотреть их, если ваш код рисования становится слишком обременительным.
Окна диалогов - это окна, которые всплывают из других окон. Их назначение состоит в решении специфических проблем без изменения деталей оригинального окна. Окна диалогов часто используются в оконной среде программ, и менее часто используются в апплетах.
Для создания диалога вы наследуете от JDialog, который является просто видом окна, как и JFrame. JDialog имеет менеджер компоновки (по умолчанию это BorderLayout) и вы добавляете слушатель событий для работы с событиями. Одно значительное отличие возникает при вызове windowClosing( ). Оно состоит в том, что вам не нужно завершать приложение. Вместо этого вы освобождаете ресурсы, используемые окном диалога, вызывая dispose( ). Вот простой пример:
//: c13:Dialogs.java // Создание и использование диалогов. // <applet code=Dialogs width=125 height=75> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; class MyDialog extends JDialog { public MyDialog(JFrame parent) { super(parent, "My dialog", true); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JLabel("Here is my dialog")); JButton ok = new JButton("OK"); ok.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ dispose(); // Закрытие диалога. } }); cp.add(ok); setSize(150,125); } } public class Dialogs extends JApplet { JButton b1 = new JButton("Dialog Box"); MyDialog dlg = new MyDialog(null); public void init() { b1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ dlg.show(); } }); getContentPane().add(b1); } public static void main(String[] args) { Console.run(new Dialogs(), 125, 75); } } ///:~
Как только JDialog создан, должен быть вызван метод show( ) для его отображения и активации. Для закрытия диалога должен быть вызван метод dispose( ).
Вы увидите, что ко всему, что всплывает из апплета, включая диалоги, нет доверия. То есть, вы получите всплывающее окно предупреждения. Это происходит потому, что, теоретически, есть возможность одурачить пользователя и заставить его думать, что он имеет дело с обычным местным приложением и дать ему напечатать номер его кредитной карточки, который будет передан через Web. Апплет всегда присоединяется в Web странице и его видно внутри вашего Web броузера, а диалог не присоединен, поэтому, теоретически, это становится возможным. В результате, не часто можно увидеть апплеты, использующие окна диалога.
Следующий пример сложнее; окно диалога сделано в виде решетки (используется GridLayout) кнопок специального рода, которые определены здесь, как класс ToeButton. Эти кнопки рисуют рамку вокруг себя и, в зависимости от состояния, остаются пустыми, рисуют “x” или “o” в середине. Изначально они пустые, а затем, в зависимости от того, кто включен, меняются на “x” или “o”. Однако также есть обратная и прямая связь между “x” и “o”, когда вы нажимаете кнопку. (Здесь воссоздается концепция tic-tac-toe, только немного более надоедливая, чем существующая.) Кроме того, диалог может быть установлен на любое число колонок и строк, путем изменения чисел в главном окне приложения.
//: c13:TicTacToe.java // Демонстрация диалогов // и создание ваших собственных компонент. // <applet code=TicTacToe // width=200 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class TicTacToe extends JApplet { JTextField rows = new JTextField("3"), cols = new JTextField("3"); static final int BLANK = 0, XX = 1, OO = 2; class ToeDialog extends JDialog { int turn = XX; // Начинается с включения x // w = число ячеек в ширину // h = число ячеек в высоту public ToeDialog(int w, int h) { setTitle("The game itself"); Container cp = getContentPane(); cp.setLayout(new GridLayout(w, h)); for(int i = 0; i < w * h; i++) cp.add(new ToeButton()); setSize(w * 50, h * 50); // JDK 1.3 закрытие диалога: //#setDefaultCloseOperation( //# DISPOSE_ON_CLOSE); // JDK 1.2 закрытие диалога: addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ dispose(); } }); } class ToeButton extends JPanel { int state = BLANK; public ToeButton() { addMouseListener(new ML()); } public void paintComponent(Graphics g) { super.paintComponent(g); int x1 = 0; int y1 = 0; int x2 = getSize().width - 1; int y2 = getSize().height - 1; g.drawRect(x1, y1, x2, y2); x1 = x2/4; y1 = y2/4; int wide = x2/2; int high = y2/2; if(state == XX) { g.drawLine(x1, y1, x1 + wide, y1 + high); g.drawLine(x1, y1 + high, x1 + wide, y1); } if(state == OO) { g.drawOval(x1, y1, x1 + wide/2, y1 + high/2); } } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { if(state == BLANK) { state = turn; turn = (turn == XX ? OO : XX); } else state = (state == XX ? OO : XX); repaint(); } } } } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { JDialog d = new ToeDialog( Integer.parseInt(rows.getText()), Integer.parseInt(cols.getText())); d.setVisible(true); } } public void init() { JPanel p = new JPanel(); p.setLayout(new GridLayout(2,2)); p.add(new JLabel("Rows", JLabel.CENTER)); p.add(rows); p.add(new JLabel("Columns", JLabel.CENTER)); p.add(cols); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); JButton b = new JButton("go"); b.addActionListener(new BL()); cp.add(b, BorderLayout.SOUTH); } public static void main(String[] args) { Console.run(new TicTacToe(), 200, 100); } } ///:~
Поскольку static может быть только на внешнем уровне класса, внутренний класс не может иметь статических данный или статических внутренних классов.
Метод paintComponent( ) рисует прямоугольник вокруг панели и “x” или “o”. Здесь выполняются достаточно скучные, но понятные вычисления.
Щелчок мыши захватывает MouseListener, который сначала проверяет, написано ли что-нибудь на панели. Если нет, опрашивается родительское окно, чтобы определить, что нужно включить, и это используется для установки состояния ToeButton. Через механизм внутреннего класса ToeButton обращается назад к своему родителю и меняется. Если кнопка в данный момент отображает “x” или “o”, то знак меняется. Вы можете видеть в расчетах последовательное использование тернарного оператора if-else, описанного в Главе 3. После смены состояния происходит перерисовка ToeButton.
Конструктор ToeDialog достаточно прост: он добавляет в GridLayout столько кнопок, сколько вы запросили, затем изменяет их размер до 50 пикселей на сторону для каждой кнопки.
TicTacToe устанавливает все приложение, создавая JTextField (для ввода числа строк и колонок кнопок в сетке) и кнопку “go” с присоединенным ActionListener. Когда нажимается кнопка, извлекаются данные из JTextField, и, так как они имеют форму String, они переводятся в int, используя статический метод Integer.parseInt( ).
Некоторые операционные системы имеют несколько специальных встроенных диалогов для обработки выбора таких вещей, как фонт, цвет, принтер и т.п. Фактически все графические операционные системы поддерживают открытие и сохранение файлов, точно так же и JFileChooser из Java инкапсулирует это для легкого использования.
Следующее приложение использует две формы диалогов JFileChooser, одну для открытия, а другую для записи. Большинство кода будет вам знакомо, а все интересующие нас действия происходят в слушателе событий для двух разных кнопок:
//: c13:FileChooserTest.java // Демонстрация файловых диалогов. import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class FileChooserTest extends JFrame { JTextField filename = new JTextField(), dir = new JTextField(); JButton open = new JButton("Open"), save = new JButton("Save"); public FileChooserTest() { JPanel p = new JPanel(); open.addActionListener(new OpenL()); p.add(open); save.addActionListener(new SaveL()); p.add(save); Container cp = getContentPane(); cp.add(p, BorderLayout.SOUTH); dir.setEditable(false); filename.setEditable(false); p = new JPanel(); p.setLayout(new GridLayout(2,1)); p.add(filename); p.add(dir); cp.add(p, BorderLayout.NORTH); } class OpenL implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser c = new JFileChooser(); // Демонстрируется диалог "Open": int rVal = c.showOpenDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { filename.setText( c.getSelectedFile().getName()); dir.setText( c.getCurrentDirectory().toString()); } if(rVal == JFileChooser.CANCEL_OPTION) { filename.setText("You pressed cancel"); dir.setText(""); } } } class SaveL implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser c = new JFileChooser(); // демонстрация диалога "Save": int rVal = c.showSaveDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { filename.setText( c.getSelectedFile().getName()); dir.setText( c.getCurrentDirectory().toString()); } if(rVal == JFileChooser.CANCEL_OPTION) { filename.setText("You pressed cancel"); dir.setText(""); } } } public static void main(String[] args) { Console.run(new FileChooserTest(), 250, 110); } } ///:~
Обратите внимание, что есть много вариаций, который вы можете применить к JFileChooser, включая фильтры для выделения имен файлов, которые вы хотите сделать доступными.
Для диалога “открытия файла” вы вызываете showOpenDialog( ), а для диалога “записи файла” вы вызываете showSaveDialog( ). Возврат из этих команд не происходит до закрытия диалога. Объект JFileChooser все еще существует, поэтому вы можете прочесть данные из него. Методы getSelectedFile( ) и getCurrentDirectory( ) - это два способа, которыми вы можете спросить результат операции. Если возвращен null, это значит, что пользователь аннулировал диалог.
Любая компонента, которая может принимать текст, также может принимать HTML текст, который будет переформатироваться в соответствии с правилами HTML. Это означает, что вы можете очень легко добавить разукрашенный текст в компонент Swing. Например:
//: c13:HTMLButton.java // Помещение HTML текста в компоненту Swing. // <applet code=HTMLButton width=200 height=500> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class HTMLButton extends JApplet { JButton b = new JButton("<html><b><font size=+2>" + "<center>Hello!<br><i>Press me now!"); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ getContentPane().add(new JLabel("<html>"+ "<i><font size=+4>Kapow!")); // Производим перекомпоновку для // включения новой метки: validate(); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b); } public static void main(String[] args) { Console.run(new HTMLButton(), 200, 500); } } ///:~
Вы должны начать текст с “<html>”, а затем вы можете использовать обычные ярлыки HTML. Обратите внимание, что вы не обязаны включать закрывающие ярлыки.
ActionListener добавляет новую JLabel в форму, которая так же содержит HTML текст. Однако эта метка не добавляется во время init( ), так что вы должны вызвать метод validate( ) для контейнера, который заставит перекомпоноваться компоненты (и после этого появится новая метка).
Вы также можете использовать HTML текст для JTabbedPane, JMenuItem, JToolTip, JRadioButton и JCheckBox.
Слайдер (который уже использовался в примере синусовой волны) позволяет пользователю вводить данные, перемещая точку вперед и назад, что интуитивно понятно в некоторых ситуациях (например, регулятор громкости). Индикатор выполнения отображает данные в относительной форме от “заполненного” до “пустого”, так что пользователь видит перспективу. Мой любимый пример этого - это простая связь слайдера м индикатора выполнения, так что когда вы перемещаете слайдер, индикатор выполнения меняется соответственно:
//: c13:Progress.java // Использование индикатора выполнения и слайдера. // <applet code=Progress // width=300 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class Progress extends JApplet { JProgressBar pb = new JProgressBar(); JSlider sb = new JSlider(JSlider.HORIZONTAL, 0, 100, 60); public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(2,1)); cp.add(pb); sb.setValue(0); sb.setPaintTicks(true); sb.setMajorTickSpacing(20); sb.setMinorTickSpacing(5); sb.setBorder(new TitledBorder("Slide Me")); pb.setModel(sb.getModel()); // Распределенная модель cp.add(sb); } public static void main(String[] args) { Console.run(new Progress(), 300, 200); } } ///:~
Ключевое место сцепления двух компонент вместе заключается в распределении их модели, в строке:
pb.setModel(sb.getModel());
Конечно, вы также можете управлять с использованием двух слушателей, но этот способ больше подходит для простой ситуации.
JProgressBar довольно понятен, а JSlider имеет массу опций, таких как ориентация, главные и второстепенные маркеры. Обратите внимание, как просто добавляется бордюр с заголовком.
Использование JTree может быть также просто, как об этом сказано:
add(new JTree( new Object[] {"this", "that", "other"}));
Так отображается примитивное дерево. Однако API для деревьев достаточно обширно — несомненно, одно из самых больших в Swing. Это означает, что вы можете делать с деревьями все, что угодно, но более изощренные задачи могут требовать немного исследований и экспериментов.
К счастью, библиотека обеспечивает ядро: компонент дерева “по умолчанию” обычно делает то, что вам надо. Так что большую часть времени вы будете использовать эти компоненты, и только в специальных случаях вам нужно будет исследовать и понимать деревья более глубоко.
Следующий пример использует компонент дерева “по умолчанию” для отображения дерева в апплете. Когда вы нажмете кнопку, добавится новое поддерево в текущей выделенной ноде (если нода не выбрана, используется корневая нода):
//: c13:Trees.java // Простой пример Swing дерева. Деревья могут // быть сделаны намного более сложными, чем здесь. // <applet code=Trees // width=250 height=250></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.tree.*; import com.bruceeckel.swing.*; // Берется массив Strings и создается первый // элемент ноды, а оставшиеся оставляются: class Branch { DefaultMutableTreeNode r; public Branch(String[] data) { r = new DefaultMutableTreeNode(data[0]); for(int i = 1; i < data.length; i++) r.add(new DefaultMutableTreeNode(data[i])); } public DefaultMutableTreeNode node() { return r; } } public class Trees extends JApplet { String[][] data = { { "Colors", "Red", "Blue", "Green" }, { "Flavors", "Tart", "Sweet", "Bland" }, { "Length", "Short", "Medium", "Long" }, { "Volume", "High", "Medium", "Low" }, { "Temperature", "High", "Medium", "Low" }, { "Intensity", "High", "Medium", "Low" }, }; static int i = 0; DefaultMutableTreeNode root, child, chosen; JTree tree; DefaultTreeModel model; public void init() { Container cp = getContentPane(); root = new DefaultMutableTreeNode("root"); tree = new JTree(root); // Это добавляется для заботы о скроллировании: cp.add(new JScrollPane(tree), BorderLayout.CENTER); // Получение модели дерева: model =(DefaultTreeModel)tree.getModel(); JButton test = new JButton("Press me"); test.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(i < data.length) { child = new Branch(data[i++]).node(); // Что было последним, на чем вы щелкнули? chosen = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); if(chosen == null) chosen = root; // Модель будет создавать // соответствующие события. В ответ // дерево будет обновлять себя: model.insertNodeInto(child, chosen, 0); // Здесь помещается новая нода // в текущую выбранную ноду. } } }); // Изменение цвета кнопки: test.setBackground(Color.blue); test.setForeground(Color.white); JPanel p = new JPanel(); p.add(test); cp.add(p, BorderLayout.SOUTH); } public static void main(String[] args) { Console.run(new Trees(), 250, 250); } } ///:~
Первый класс, Branch, это инструмент для получения массива String и построения DefaultMutableTreeNode с первым элементом String в качестве корня, а другие элементы не трогаются. Затем может быть вызван node( ) для производства корня этого “branch”.
Класс Trees содержит двумерный массив из String, из которого могут быть сделаны Branch (ветви), и static int i для подсчета в массиве. Объект DefaultMutableTreeNode содержит ноды, а физическое представление на экране управляется JTree и ассоциированной с ним моделью - DefaultTreeModel. Обратите внимание, что когда JTree добавляется в апплет, он оборачивается в JScrollPane — это все, что нужно сделать для автоматического скроллинга.
JTree управляется своей моделью. Когда вы делаете изменения в модели, модель генерирует событие, которое является причиной того, что JTree выполняет необходимые обновления для отображения представления дерева. В init( ) модель захватывается вызовом getModel( ). Когда нажимается кнопка, создается новая “ветвь(branch)”. Затем находится текущий выделенный компонент (или используется корень, если ничего не выбрано) и метод модели insertNodeInto( ) выполняет всю работу по изменению дерева и является причиной его обновления.
Пример, подобный приведенному мной выше, может дать вам то, что вы хотите от дерева. Однако деревья имеют мощь для выполнения всего, что вы можете вообразить — везде, где вы видите слова “по умолчанию” в приведенном выше примере, вы можете заменить своим собственным классом для получения другого поведения. Но остерегитесь: почти все классы имеют большие интерфейсы, так что вы можете потратить много времени на борьбу с пониманием интерфейсов деревьев. Несмотря на это, в этом заключается хороший дизайн, а альтернативы обычно хуже.
Как и деревья, таблицы в Swing всеобъемлющи и мощны. Они в первую очередь предназначены быть популярным интерфейсом, под названием “решетка (grid)” для баз данных через Java Database Connectivity (JDBC, обсуждаемой в Главе 15) и поэтому они имеют потрясающую гибкость, за которую вы платите сложностью. То, что здесь описано, это только основы, а полное описание могло бы занять целую книгу. Однако также возможно создать относительно простую JTable, если вы понимаете основы.
JTable управляет отображением данных, а TableModel оправляет самими данными. Так что для создания JTable вы обычно будете создавать сначала TableModel. Вы можете полностью реализовывать интерфейс TableModel, но обычно проще унаследовать его от вспомогательного класса AbstractTableModel:
//: c13:Table.java // Простая демонстрация JTable. // <applet code=Table // width=350 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.table.*; import javax.swing.event.*; import com.bruceeckel.swing.*; public class Table extends JApplet { JTextArea txt = new JTextArea(4, 20); // TableModel управляет всеми данными: class DataModel extends AbstractTableModel { Object[][] data = { {"one", "two", "three", "four"}, {"five", "six", "seven", "eight"}, {"nine", "ten", "eleven", "twelve"}, }; // Печатает данные при изменении таблицы: class TML implements TableModelListener { public void tableChanged(TableModelEvent e){ txt.setText(""); // Очистка for(int i = 0; i < data.length; i++) { for(int j = 0; j < data[0].length; j++) txt.append(data[i][j] + " "); txt.append("\n"); } } } public DataModel() { addTableModelListener(new TML()); } public int getColumnCount() { return data[0].length; } public int getRowCount() { return data.length; } public Object getValueAt(int row, int col) { return data[row][col]; } public void setValueAt(Object val, int row, int col) { data[row][col] = val; // Указывает на появление изменений: fireTableDataChanged(); } public boolean isCellEditable(int row, int col) { return true; } } public void init() { Container cp = getContentPane(); JTable table = new JTable(new DataModel()); cp.add(new JScrollPane(table)); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new Table(), 350, 200); } } ///:~
DataModel содержит массив данных, но вы можете также получить данные из какого-либо другого источника, такого, как база данных. Конструктор добавляет TableModelListener, который печатает массив всякий раз, когда меняется таблица. Оставшиеся методы следуют Главной концепции об именах, и используются JTable, когда она хочет представить информацию в DataModel. AbstractTableModel по умолчанию обеспечивает методы setValueAt( ) и isCellEditable( ), которые предотвращают изменение данных, так что если вы хотите иметь возможность редактирования данных, вы должны перекрыть эти методы.
Как только вы получите TableModel, все, что вам нужно - это передать ее в конструктор JTable. Обо всех деталях отображения, редактирования и обновления она будет заботиться вместо вас. Этот пример также помещает JTable в JScrollPane.
Один из самых интересных аспектов Swing заключен во встраиваемом внешнем виде (Look & Feel). Это позволяет вам эмулировать внешний вид различных операционных сред. Вы можете даже делать такие фантастические вещи, как динамическая смена внешнего вида при выполнении программы. Однако обычно вам будет нужно делать одну из двух вещей: либо выбирать “кросс платформенный” внешний вид (это Swing “metal”), либо выбирать внешний вид для используемой системы, так, чтобы ваша Java программа выглядела как созданная специально для этой системы. Код выбора любого из видов достаточно прост — но вы должны выполнить его прежде, чем вы создадите любой визуальный компонент, потому что компоненты будут сделаны с использованием текущего вида и не изменятся только из-за того, что произошла смена внешнего вида в середине выполнения программы (этот процесс более сложный и не общий, поэтому я отошлю вас к книгам, специфицирующимся на Swing).
На самом деле, если вы хотите использовать кросс платформенный (“metal”) внешний вид в качестве характерного для Swing программ, вам не нужно делать ничего — он используется по умолчанию. Но если вы хотите вместо этого использовать внешний вид текущего операционного окружения, вы просто вставляете следующий код, обычно в начале вашего метода main( ) и перед вставкой любых компонент:
try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) {}
Вам ничего не нужно делать в предложении catch, потому что UIManager будет по умолчанию использовать кросс платформенный внешний вид, если попытка установить любой другой провалится. Однако во время отладки исключение может быть достаточно полезным, так как вы можете пожелать распечатать формулировку в предложении catch.
Вот программа, которая принимает аргумент командной строки для выбора внешнего вида, и показывает, как выглядят несколько разных компонент при выбранном внешнем виде:
//: c13:LookAndFeel.java // Выбор разных looks & feels. import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class LookAndFeel extends JFrame { String[] choices = { "eeny", "meeny", "minie", "moe", "toe", "you" }; Component[] samples = { new JButton("JButton"), new JTextField("JTextField"), new JLabel("JLabel"), new JCheckBox("JCheckBox"), new JRadioButton("Radio"), new JComboBox(choices), new JList(choices), }; public LookAndFeel() { super("Look And Feel"); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < samples.length; i++) cp.add(samples[i]); } private static void usageError() { System.out.println( "Usage:LookAndFeel [cross|system|motif]"); System.exit(1); } public static void main(String[] args) { if(args.length == 0) usageError(); if(args[0].equals("cross")) { try { UIManager.setLookAndFeel(UIManager. getCrossPlatformLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(System.err); } } else if(args[0].equals("system")) { try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch(Exception e) { e.printStackTrace(System.err); } } else if(args[0].equals("motif")) { try { UIManager.setLookAndFeel("com.sun.java."+ "swing.plaf.motif.MotifLookAndFeel"); } catch(Exception e) { e.printStackTrace(System.err); } } else usageError(); // Обратите внимание, что look & feel должен // быть установлен перед созданием компонент. Console.run(new LookAndFeel(), 300, 200); } } ///:~
Вы можете видеть, что одна опция явно указывает строку для внешнего вида, как видно в MotifLookAndFeel. Однако этот внешний вид и внешний вид по умолчанию могут легально использоваться на любой платформе; даже если использовать строки для внешнего вида Windows и Macintosh, они могут быть использованы только на соответствующей платформе (это производится при вызове getSystemLookAndFeelClassName( ), а вы находитесь под определенной платформой).
Так же возможно создать собственный внешний вид, например, если вы строите рабочую среду компании, которая хочет иметь характерный внешний вид. Это большая работа и эта тема выходит далеко за пределы этой книги (фактически, вы обнаружите, что она выходит за пределы обсуждения многих книг о Swing!).
JFC поддерживает ограниченное число операций с системным буфером обмена (в пакете java.awt.datatransfer). Вы можете скопировать объект String в буфер обмена как текст, и вы можете вставить текст из буфера обмена в объект String. Конечно, буфер обмена предназначен для хранения данных любого типа, но как представить эти данные в буфере обмена, если программа выполняет вырезание и вставку. Java API для буфера обмена обеспечивает расширяющуюся концепцию “особенностей”. Когда данные приходят из буфера обмена, они имеют множество особенностей, которыми они могут быть представлены (например, графика может быть представлена как строка чисел или как изображение) и вы можете видеть, поддерживает ли определенный буфер интересующие вас особенности.
Следующая программа просто демонстрирует вырезание, копирование и вставку данных String в JTextArea. Одно вы должны заметить, это то, что последовательность кнопок, которую вы обычно используете для вырезания, копирования и вставки так же работает. Но если вы посмотрите на JTextField или JTextArea в любой другой программе, вы обнаружите, что они тоже автоматически поддерживают последовательность клавиш для буфера обмена. Этот пример просто добавляет программное управление буфером обмена, и вы можете использовать эту технику, если хотите захватывать текст из буфера обмена и вставлять во что-то другое, чем JTextComponent.
//: c13:CutAndPaste.java // Использование буфера обмена. import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import com.bruceeckel.swing.*; public class CutAndPaste extends JFrame { JMenuBar mb = new JMenuBar(); JMenu edit = new JMenu("Edit"); JMenuItem cut = new JMenuItem("Cut"), copy = new JMenuItem("Copy"), paste = new JMenuItem("Paste"); JTextArea text = new JTextArea(20, 20); Clipboard clipbd = getToolkit().getSystemClipboard(); public CutAndPaste() { cut.addActionListener(new CutL()); copy.addActionListener(new CopyL()); paste.addActionListener(new PasteL()); edit.add(cut); edit.add(copy); edit.add(paste); mb.add(edit); setJMenuBar(mb); getContentPane().add(text); } class CopyL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); if (selection == null) return; StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString,clipString); } } class CutL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); if (selection == null) return; StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString, clipString); text.replaceRange("", text.getSelectionStart(), text.getSelectionEnd()); } } class PasteL implements ActionListener { public void actionPerformed(ActionEvent e) { Transferable clipData = clipbd.getContents(CutAndPaste.this); try { String clipString = (String)clipData. getTransferData( DataFlavor.stringFlavor); text.replaceRange(clipString, text.getSelectionStart(), text.getSelectionEnd()); } catch(Exception ex) { System.err.println("Not String flavor"); } } } public static void main(String[] args) { Console.run(new CutAndPaste(), 300, 200); } } ///:~
Создание и добавление меню и JTextArea должно теперь выглядеть прозаическим действием. Что отличается, так это создание поля Clipboard clipbd, которое выполняется через Toolkit.
Все действия происходят в слушателях. Слушатели CopyL и CutL одинаковы, за исключением того, что последняя строка в CutL стирает скопированную строку. Специальные две строки создания объекта StringSelection из String вызывают setContents( ) с этим StringSelection. Это все, что нужно для помещения String в буфер обмена.
В PasteL данные вытягиваются из буфера обмена, используя getContents( ). Что приходит - это совершенно анонимный объект Transferable, который возвращает массив объектов DataFlavor, указывает, какие особенности поддерживаются определенным объектом. Вы можете так же спросить его напрямую с помощью isDataFlavorSupported( ), передав особенность, которая вас интересует. Однако здесь выбран самонадеянный подход: вызывается getTransferData( ) в надежде, что содержимое поддерживает особенности String, а если это не так, проблема выявляется в обработчике исключения.
В будущем вы можете ожидать поддержку большего числа особенностей.
Важность использования утилиты JAR состоит в оптимизации загрузки апплета. В Java 1.0 люди склонялись к попытке впихнуть весь код в единственный класс апплета, чтобы клиенту было нужно единственное обращение к серверу для загрузки кода апплета. Это не только приводило к грязному, трудно читаемому коду (и сложному уходу за программой), но сгенерированный .class файл все еще был не компрессированный, поэтому загрузка происходила не так быстро, как это могло бы быть.
JAR файлы решили проблему компресси всех ваших .class файлов в единственный файл, который загружается броузером. Теперь вы можете создавать правильный дизайн без заботы о количестве генерируемых .class файлов, а пользователь потратит меньше времени на загрузку.
Относительно TicTacToe.java. Программа выглядит как единый класс, но, фактически, она содержит пять внутренних классов, так что их всего шесть. Как только вы скомпилируете программу, вы упакуете ее в JAR файл с помощью строки:
jar cf TicTacToe.jar *.class
Здесь участвуют только .class из текущей директории, то есть файлы для TicTacToe.java (в противном случае вы получите дополнительный багаж).
Теперь вы можете создать HTML страницу с новым ярлыком archive, указывающим имя JAR файла. Здесь ярлык использует старую форму ярлыка HTML, как показано ниже:
<head><title>TicTacToe Example Applet </title></head> <body> <applet code=TicTacToe.class archive=TicTacToe.jar width=200 height=100> </applet> </body>
Вам нужно поместить это в новую (грязну, сложную) форму, показанную в этой главе, чтобы это правильно работало.
Поскольку GUI программирование в Java имеет развивающуюся технология с некоторыми значительными изменениями, произошедшими при переходе от Java 1.0/1.1к библиотеке Swing в Java 2, то появилось несколько старых идиом программирования, которые просочились в примеры, данные для Swing. Кроме того, Swing позволяет вам программировать больше и лучше, чем это позволяла старые модели. В этом разделе будет введена демонстрация некоторые из этих подходов и произведена проверка идиом.
Одна из выгод модели событий Swing заключена в гибкости. Вы можете добавлять и удалять обработку события одним вызовом метода. Следующий пример демонстрирует это:
//: c13:DynamicEvents.java // Вы можете динамически изменить проявление события. // Также показывается различные акции для события. // <applet code=DynamicEvents // width=250 height=400></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class DynamicEvents extends JApplet { ArrayList v = new ArrayList(); int i = 0; JButton b1 = new JButton("Button1"), b2 = new JButton("Button2"); JTextArea txt = new JTextArea(); class B implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("A button was pressed\n"); } } class CountListener implements ActionListener { int index; public CountListener(int i) { index = i; } public void actionPerformed(ActionEvent e) { txt.append("Counted Listener "+index+"\n"); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("Button 1 pressed\n"); ActionListener a = new CountListener(i++); v.add(a); b2.addActionListener(a); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("Button2 pressed\n"); int end = v.size() - 1; if(end >= 0) { b2.removeActionListener( (ActionListener)v.get(end)); v.remove(end); } } } public void init() { Container cp = getContentPane(); b1.addActionListener(new B()); b1.addActionListener(new B1()); b2.addActionListener(new B()); b2.addActionListener(new B2()); JPanel p = new JPanel(); p.add(b1); p.add(b2); cp.add(BorderLayout.NORTH, p); cp.add(new JScrollPane(txt)); } public static void main(String[] args) { Console.run(new DynamicEvents(), 250, 400); } } ///:~
Новшества этого примера в том, что:
Гибкость такого рода обеспечивает большую мощь вашему программированию.
Вы должны заметить, что слушатели событий вызываются не гарантированно после того, как они добавлены (хотя большинство реализаций делает работу, фактически, этим способом).
В общем случае вы захотите разработать ваши классы так, чтобы каждый из них выполнял только одно. Это особенно важно, когда код интерфейса пользователя является связанным, так как легче связать “то, что вы делаете” с тем, “как вы это отображаете”. Такой род связывания мешает повторному использованию кода. Поэтому желательно разделить вашу “бизнес логику” и GUI. Этим способом вы сможете не только с легкостью повторно использовать бизнес логику, но и с легкостью повторно использовать GUI.
Другим подходом является многосвязные системы, где “бизнес объекты” располагаются полностью отдельной машине. Такое централизованное расположение бизнес правил позволяет изменениям происходить мгновенно для всех новых транзакций, и поэтому оно является лучшим способом построения системы. Однако такие бизнес объекты могут использоваться во многих различный приложениях и, поэтому, не должны привязываться к определенному режиму отображения. Они просто должны выполнять бизнес операции и ничего более.
Следующий пример показывает, как легко разделяется бизнес логика и GUI код:
//: c13:Separation.java // Разделение GUI логики и бизнес объектов. // <applet code=Separation // width=250 height=150> </applet> import javax.swing.*; import java.awt.*; import javax.swing.event.*; import java.awt.event.*; import java.applet.*; import com.bruceeckel.swing.*; class BusinessLogic { private int modifier; public BusinessLogic(int mod) { modifier = mod; } public void setModifier(int mod) { modifier = mod; } public int getModifier() { return modifier; } // Какие-то бизнес операции: public int calculation1(int arg) { return arg * modifier; } public int calculation2(int arg) { return arg + modifier; } } public class Separation extends JApplet { JTextField t = new JTextField(15), mod = new JTextField(15); BusinessLogic bl = new BusinessLogic(2); JButton calc1 = new JButton("Calculation 1"), calc2 = new JButton("Calculation 2"); static int getValue(JTextField tf) { try { return Integer.parseInt(tf.getText()); } catch(NumberFormatException e) { return 0; } } class Calc1L implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText(Integer.toString( bl.calculation1(getValue(t)))); } } class Calc2L implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText(Integer.toString( bl.calculation2(getValue(t)))); } } // Если вы хотите, чтобы что-то происходило при // изменении JTextField, добавьте слушатель: class ModL implements DocumentListener { public void changedUpdate(DocumentEvent e) {} public void insertUpdate(DocumentEvent e) { bl.setModifier(getValue(mod)); } public void removeUpdate(DocumentEvent e) { bl.setModifier(getValue(mod)); } } public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); calc1.addActionListener(new Calc1L()); calc2.addActionListener(new Calc2L()); JPanel p1 = new JPanel(); p1.add(calc1); p1.add(calc2); cp.add(p1); mod.getDocument(). addDocumentListener(new ModL()); JPanel p2 = new JPanel(); p2.add(new JLabel("Modifier:")); p2.add(mod); cp.add(p2); } public static void main(String[] args) { Console.run(new Separation(), 250, 100); } } ///:~
Вы можете видеть, что BusinessLogic является четко очерченным классом, который выполняет свои операции даже не подозревая, что может быть GUI окружение. Он просто делает свою работу.
Separation следит за всеми деталями UI, и общается с BusinessLogic только через публичный интерфейс. Все операции собираются вокруг обмена информацией между интерфейсом пользователя и объектом BusinessLogic. Таким образом, Separation, в итоге, просто делает свою работу. Так как Separation знает только то, что он общается с объектом BusinessLogic (то есть, они не сильно связаны), он может общаться с другими типами объектов без особых затруднений.
Думая в терминах разделения UI от бизнес логики, можно облегчить жизнь, когда вы адаптируете унаследованный код для работы с Java.
Внутренние классы, модель событий Swing и, фактически то, что старая модель событий все еще поддерживается наряду с новыми особенностями библиотеки, которые полагаются на программирование в старом стиле, внесли новый элемент путаницы в процесс разработки кода. Теперь есть больше способов для людей писать неприятный код.
Кроме случаев, объясняемых обстоятельствами, вы должны всегда использовать простейший и ясный подход: классы слушателей (обычно пишущиеся как внутренние классы) для решения ваших требований по обработке событий. Эта форма используется в большинстве примеров в этой главе.
Следуя этой модели вы должны быть способны уменьшить инструкции в вашей программе, который говорят: “Я удивлен, что произошло это событие”. Каждый кусочек кода не должен заниматься проверкой типа. Это лучший способ написания вашего кода; те только потому, что это легче концептуально, но и легче при чтении и уходе.
Далее в этой книге вы увидите, как ценен Java для создания кусочков кода для повторного использования. “Наиболее часто используемый” блок кода имеет класс, так как он включает связующий модуль характеристик (полей) и поведений (методов), которые могут быть повторно использованы либо напрямую через композицию, либо через наследование.
Наследование и полиморфизм - это главные части объектно-ориентированного программирования, но для большинства классов, когда вы помещаете их вместе в приложение, то, что вы хотите - это то, чтобы компоненты точно делали то, что вам нужно. Вы можете ввести эти части в вашу разработку, как инженер-электронщик помещает вместе микросхемы на плате. Кажется, что должен быть способ для ускорения такого стиля программирования “модульной сборки”.
Первый успешный опыт “визуального программирования” — очень успешный — был получен с Visual Basic (VB) фирмы Microsoft, далее, среда второго поколения - это Delphi от Borland (главный вдохновитель дизайна JavaBeans). С этими инструментами программирования компоненты представлялись визуально, как кнопки или текстовые поля. Визуальное представление, фактически, часто является точным способом показа компонента в работающей программе. Так что часть процесса визуального программирования затрагивает перетаскивание компонент из палитры и помещение их в вашу форму. Инструмент построения приложения пишет код за вас, и этот код является причиной создания компонент в работающей программе.
Простого помещения компонент в форму обычно не достаточно для компиляции программ. Часто вы должны изменить характеристики компонент, такие как цвет, внутренний текст, присоединенную базу данных и т.п. Характеристики, которые могут быть изменены во время дизайна, называются свойствами (properties). Вы можете манипулировать свойствами вашего компонента внутри построителя приложения, и когда вы создадите программу, эти конфигурационные данные будут сохранены, так что они могут быть обновлены при запуске программы.
Теперь вы, вероятно, используете идею, что объект - это больше, чем характеристики; это также набор поведений. Во время дизайна, поведение визуальных компонент частично представлено событиями (events), означающих “Здесь то, что может случиться с компонентом”. Обычно вы решаете, что вам нужно при возникновении события, печатая код этого события.
Здесь заключена критическая часть: построитель приложения использует рефлексию для динамического исследования компоненты для нахождения свойств, чтобы позволить вам изменить их (записать состояние при построении программы), а также для отображения событий. В общем, вы делаете что-то типа двойного щелчка на событии, и построитель приложения создает тело кода и привязывает его к определенному событию. Все, что вы делаете в этом месте, это пишите код, выполняющийся при возникновении события.
Все это прибавляет работы, которую выполняет за вас построитель приложения. В результате вы можете сфокусировать внимание на том, как программа должна выглядеть, и для чего она предназначена, и положится на построитель приложения в управлении деталями. По этой причине инструменты визуального программирования так успешно применяются, они значительно ускоряют процесс построения приложения — в основном интерфейса пользователя, но часто и других частей приложения.
После того, как осядет пыль, компонент представляет собой блок кода, обычно, заключенного в класс. Ключевая способность построителя приложения состоит в обнаружении свойств и событий компонента. Для создания VB компонент программист должен написать довольно сложный кусок кода, следуя определенным соглашениям для выделения свойств и событий. Delphi был визуальным инструментом программирования второго поколения, и дизайн языка был построен исходя из простоты построения и использования визуальных компонент. Однако Java стал использовать создание визуальных компонент наиболее продвинутым способом с помощью JavaBeans, потому что компонент (Bean) - это просто класс. Вам не нужно писать дополнительный код или использовать специальное расширение языка для создания какого-нибудь компонента. Фактически, вам необходимо сделать только одну вещь: слегка модифицировать способ, которым вы создаете названия методов. То есть использовать имя метода, которое скажет построителю приложения, является ли он свойством, событием или просто обычным методом.
В документации по Java это соглашение об именах ошибочно называется “шаблон разработки (design pattern)”. Это не удачно, так как шаблоны разработки (смотрите Thinking in Patterns with Java, доступной на www.BruceEckel.com) и так оспариваются и без этой путаницы. Это не шаблоны разработки, это просто соглашение об именах и оно достаточно простое:
Пункт 1 отвечает на вопрос о том, что вы могли заметить, когда просматривали код в старом стиле и код в новом стиле: число имен методов стало меньше, и иметь явно осмысленный характер. Теперь вы видите, что большинство из этих изменений были сделаны для адаптации к соглашению об именах в отношении “get” и “set”, чтобы встроить определенный компонент в Bean.
Мы может использовать это руководство для создания простого компонента (Bean):
//: frogbean:Frog.java // Тривиальный JavaBean. package frogbean; import java.awt.*; import java.awt.event.*; class Spots {} public class Frog { private int jumps; private Color color; private Spots spots; private boolean jmpr; public int getJumps() { return jumps; } public void setJumps(int newJumps) { jumps = newJumps; } public Color getColor() { return color; } public void setColor(Color newColor) { color = newColor; } public Spots getSpots() { return spots; } public void setSpots(Spots newSpots) { spots = newSpots; } public boolean isJumper() { return jmpr; } public void setJumper(boolean j) { jmpr = j; } public void addActionListener( ActionListener l) { //... } public void removeActionListener( ActionListener l) { // ... } public void addKeyListener(KeyListener l) { // ... } public void removeKeyListener(KeyListener l) { // ... } // "Обычный" публичный метод: public void croak() { System.out.println("Ribbet!"); } } ///:~
Прежде всего, вы можете видеть, что это просто класс. Обычно все ваши поля будут private, и доступны только через методы. Следуя соглашению об именах, получим свойства jumps, color, spots и jumper (обратите внимание на регистр первой буквы имени свойства). Хотя имя внутреннего идентификатора такое же, как и имя свойства в первых трех случаях, в jumper вы можете видеть, что имя свойства не ограничивает вас в использовании определенного идентификатора для внутренней переменной (или, на самом деле, даже иметь любые внутренние переменные для этого свойства).
События, обрабатываемые этим компонентом, это ActionEvent и KeyEvent, основываются на наименовании методов “add” и “remove” для ассоциированного слушателя. И, наконец, вы можете видеть, что обычный метод croak( ) все еще является частью компонента, потому что это public метод, а не потому, что он удовлетворяет какой-то схеме названий.
Одна из наиболее критичных частей компонентной схемы возникает, когда вы перетаскиваете компонент (Bean) из палитры и бросаете его в форму. Построитель приложения должен быть способен создать компонент (Bean) (что выполняется с помощью конструктора по умолчанию), а затем, без доступа к исходному коду компонента (Bean), получить всю необходимую информацию для создания страничек свойств и обработчиков событий.
Часть решения ясно видна в конце Главы 12: рефлексия Java позволяет обнаружить все методы анонимных классов. Это совершенное решение проблемы компонента (Bean) без введения любых дополнительных ключевых слов, которые требуются в других визуальных языках программирования. Фактически, одна из главнейших причин добавления рефлексии в Java была в поддержке компонентов (Bean) (хотя рефлексия также поддерживает сериализацию объектов и удаление обращений к методам). Так что вы можете ожидать, что создатель построителя приложения будет рефлектировать каждый компонент (Bean) и охотится за его методами для нахождения свойств и событий для этого компонента (Bean).
Это, конечно, возможно, но разработчики Java хотели обеспечить стандартный инструмент, не только для упрощения использования компонент (Bean), но и для обеспечения стандартного подхода для создания более сложных компонент (Bean). Этим инструментом является класс Introspector, и наиболее важным методом этого класса является static getBeanInfo( ). Вы передаете ссылку на Class в этот метод, и он полностью опрашивает этот класс и возвращает объект BeanInfo, который вы можете затем раскрыть для нахождения свойств, методов и србытий.
Обычно вы не заботитесь об этом — вероятно, вы получите большинство ваших компонентов (Bean) от продавца, и вам не нужно будет знать всю магию, которая происходит внутри. Вы просто перетаскиваете ваш компонент (Bean) на вашу форму, затем конфигурируете его свойства и пишите обработчик для интересующих вас событий. Однако очень интересно и познавательно использовать Introspector для отображения информации о компоненте (bean), так что вот инструмент, который делает это:
//: c13:BeanDumper.java // Инспектирование Bean. // <applet code=BeanDumper width=600 height=500> // </applet> import java.beans.*; import java.lang.reflect.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class BeanDumper extends JApplet { JTextField query = new JTextField(20); JTextArea results = new JTextArea(); public void prt(String s) { results.append(s + "\n"); } public void dump(Class bean){ results.setText(""); BeanInfo bi = null; try { bi = Introspector.getBeanInfo( bean, java.lang.Object.class); } catch(IntrospectionException e) { prt("Couldn't introspect " + bean.getName()); return; } PropertyDescriptor[] properties = bi.getPropertyDescriptors(); for(int i = 0; i < properties.length; i++) { Class p = properties[i].getPropertyType(); prt("Property type:\n " + p.getName() + "Property name:\n " + properties[i].getName()); Method readMethod = properties[i].getReadMethod(); if(readMethod != null) prt("Read method:\n " + readMethod); Method writeMethod = properties[i].getWriteMethod(); if(writeMethod != null) prt("Write method:\n " + writeMethod); prt("===================="); } prt("Public methods:"); MethodDescriptor[] methods = bi.getMethodDescriptors(); for(int i = 0; i < methods.length; i++) prt(methods[i].getMethod().toString()); prt("======================"); prt("Event support:"); EventSetDescriptor[] events = bi.getEventSetDescriptors(); for(int i = 0; i < events.length; i++) { prt("Listener type:\n " + events[i].getListenerType().getName()); Method[] lm = events[i].getListenerMethods(); for(int j = 0; j < lm.length; j++) prt("Listener method:\n " + lm[j].getName()); MethodDescriptor[] lmd = events[i].getListenerMethodDescriptors(); for(int j = 0; j < lmd.length; j++) prt("Method descriptor:\n " + lmd[j].getMethod()); Method addListener = events[i].getAddListenerMethod(); prt("Add Listener Method:\n " + addListener); Method removeListener = events[i].getRemoveListenerMethod(); prt("Remove Listener Method:\n " + removeListener); prt("===================="); } } class Dumper implements ActionListener { public void actionPerformed(ActionEvent e) { String name = query.getText(); Class c = null; try { c = Class.forName(name); } catch(ClassNotFoundException ex) { results.setText("Couldn't find " + name); return; } dump(c); } } public void init() { Container cp = getContentPane(); JPanel p = new JPanel(); p.setLayout(new FlowLayout()); p.add(new JLabel("Qualified bean name:")); p.add(query); cp.add(BorderLayout.NORTH, p); cp.add(new JScrollPane(results)); Dumper dmpr = new Dumper(); query.addActionListener(dmpr); query.setText("frogbean.Frog"); // Навязываем определение dmpr.actionPerformed( new ActionEvent(dmpr, 0, "")); } public static void main(String[] args) { Console.run(new BeanDumper(), 600, 500); } } ///:~
BeanDumper.dump( ) - это метод, который делает всю работу. Сначала он пробует создать объект BeanInfo, и если это происходит успешно, вызывает метод BeanInfo, который производит информацию о свойствах, методах и событиях. В Introspector.getBeanInfo( ), вы увидите второй аргумент. Это говорит Introspector, где остановится в иерархии наследования. Здесь он остановится прежде, чем разберет все методы от Object, так как мы не интересуемся ими.
Для свойств: getPropertyDescriptors( ) возвращает массив из PropertyDescriptor. Для каждого PropertyDescriptor вы можете вызвать getPropertyType( ) для нахождения класса объекта, который передается и получается через методы свойства. Затем, для каждого свойства вы можете получить псевдоним (получается из имени метода) с помощью getName( ), метод для чтения с помощью getReadMethod( ), и метод для записи с помощью getWriteMethod( ). Последние два метода возвращают объект Method, который может на самом деле использоваться для вызова соответствующего метода объекта (это часть рефлексии).
Для public методов (включая методы свойств) getMethodDescriptors( ) возвращает массив MethodDescriptor. Для каждого их них вы можете получить ассоциированный объект Method и напечатать его имя.
Для событий getEventSetDescriptors( ) возвращает массив (как вы думаете, чего?) EventSetDescriptor. Каждый элемент массива может быть опрошен для нахождения класса слушателя, методов класса слушателя и методов добавления (add-) и удаления (remove-). Программа BeanDumper печатает всю эту информацию.
После запуска программа форсирует вычисления frogbean.Frog. То, что получается на выходе, после удаления дополнительных деталей, ненужных здесь, вы видите здесь:
class name: Frog Property type: Color Property name: color Read method: public Color getColor() Write method: public void setColor(Color) ==================== Property type: Spots Property name: spots Read method: public Spots getSpots() Write method: public void setSpots(Spots) ==================== Property type: boolean Property name: jumper Read method: public boolean isJumper() Write method: public void setJumper(boolean) ==================== Property type: int Property name: jumps Read method: public int getJumps() Write method: public void setJumps(int) ==================== Public methods: public void setJumps(int) public void croak() public void removeActionListener(ActionListener) public void addActionListener(ActionListener) public int getJumps() public void setColor(Color) public void setSpots(Spots) public void setJumper(boolean) public boolean isJumper() public void addKeyListener(KeyListener) public Color getColor() public void removeKeyListener(KeyListener) public Spots getSpots() ====================== Event support: Listener type: KeyListener Listener method: keyTyped Listener method: keyPressed Listener method: keyReleased Method descriptor: public void keyTyped(KeyEvent) Method descriptor: public void keyPressed(KeyEvent) Method descriptor: public void keyReleased(KeyEvent) Add Listener Method: public void addKeyListener(KeyListener) Remove Listener Method: public void removeKeyListener(KeyListener) ==================== Listener type: ActionListener Listener method: actionPerformed Method descriptor: public void actionPerformed(ActionEvent) Add Listener Method: public void addActionListener(ActionListener) Remove Listener Method: public void removeActionListener(ActionListener) ====================
Вот большая часть того, что видит и производит Introspector в качестве объекта BeanInfo для вашего компонента (Bean). Вы можете видеть, что тип свойств и их имена независимы. Обратите внимание на нижний регистр в имени свойства. (Это не случается, когда имя свойства начинается с более чем одной большой буквы в строке.) Запомните, что имена методов, которые вы видите здесь (такие как методы чтения и записи), на самом деле произведены объектом Method, который может быть использован для вызова ассоциированного метода объекта.
Список public методов включает методы, которые не связаны со свойствами или событиями, такие как croak( ). Здесь все методы, которые вы можете вызвать программно для компонента (Bean), и построитель приложения может выбрать список всех, когда вы выполняете вызов метода, для облегчения вашей задачи.
Наконец, вы можете видеть события, передающиеся в слушатели, методы слушателей, и методы добавления и удаления слушателей. В общем, так как вы имеете BeanInfo, вы можете найти все, что важно для компонента (Bean). Вы можете также вызвать методы для этого компонента (Bean), даже если у вас нет другой информации, за исключением объекта (опять с помощью рефлексии).
Этот следующий пример немного более изощренный, хотя фривольный. Есть JPanel, которая рисует маленькую окружность вокруг мыши, куда бы мышь не переместилась. Когда вы нажимаете кнопку, появляется “Bang!” в середине экрана, и возбуждается слушатель события.
Свойства, которые вы можете менять, это размер окружности, точно так же как и цвет, размер и текст слова, которое отображается при нажатии кнопки. BangBean также имеет свой собственный addActionListener( ) и removeActionListener( ), так что вы можете присоединять свой собственный слушатель, который будет обрабатывать щелчки мыши в BangBean. Вы должны быть способны распознать свойства и поддержку событий:
//: bangbean:BangBean.java // Графический Bean. package bangbean; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import com.bruceeckel.swing.*; public class BangBean extends JPanel implements Serializable { protected int xm, ym; protected int cSize = 20; // Размер окружности protected String text = "Bang!"; protected int fontSize = 48; protected Color tColor = Color.red; protected ActionListener actionListener; public BangBean() { addMouseListener(new ML()); addMouseMotionListener(new MML()); } public int getCircleSize() { return cSize; } public void setCircleSize(int newSize) { cSize = newSize; } public String getBangText() { return text; } public void setBangText(String newText) { text = newText; } public int getFontSize() { return fontSize; } public void setFontSize(int newSize) { fontSize = newSize; } public Color getTextColor() { return tColor; } public void setTextColor(Color newColor) { tColor = newColor; } public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // Это уникальный слушатель, который // является упрощенной формой управления слушателем: public void addActionListener ( ActionListener l) throws TooManyListenersException { if(actionListener != null) throw new TooManyListenersException(); actionListener = l; } public void removeActionListener( ActionListener l) { actionListener = null; } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); // Вызов метода слушателя: if(actionListener != null) actionListener.actionPerformed( new ActionEvent(BangBean.this, ActionEvent.ACTION_PERFORMED, null)); } } class MML extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public Dimension getPreferredSize() { return new Dimension(200, 200); } } ///:~
Первое, что вы заметите, это то, что BangBean реализует интерфейс Serializable. Это значит, что построитель приложения может “законсервировать” всю информацию для BangBean, используя сериализацию, после того, как дизайнер установит значения свойств. Когда компонент (Bean) создастся как часть работающего приложения, эти “законсервированные” свойства восстанавливаются, так что вы получаете точно то, что разрабатывали.
Вы можете видеть, что все поля являются private, это то, что вы обычно делаете с компонентами, позволяя получить доступ только через методы, обычно, используя схему “property”.
Когда вы взглянете на сигнатуру addActionListener( ), вы увидите, что он может выбрасывать TooManyListenersException. Это означает, что здесь индивидуальная (unicast) обработка, которая означает, что извещается только один слушатель о возникновении события. Обычно вы будете использовать групповые (multicast) события, так что много слушателей могут быть извещены о событии. Однако это вводит нас в область, которую вы не готовы воспринять до следующей главы, так что мы вернемся к этому (под заголовком “возврат к JavaBeans”). Индивидуальная обработка обходит проблему.
Когда вы щелкаете мышью, текст помещается в центр BangBean, а если поле actionListener не null, вызывается actionPerformed( ), в процессе создается новый объект ActionEvent. Куда бы мышь не переместилась, захватываются новые координаты, и происходит перерисовка канвы (стирание любого текста, расположенного на канве, как вы увидите).
Здесь приведен класс BangBeanTest, позволяющий вам проверить компонент либо как апплет, либо как приложение:
//: c13:BangBeanTest.java // <applet code=BangBeanTest // width=400 height=500></applet> import bangbean.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class BangBeanTest extends JApplet { JTextField txt = new JTextField(20); // Во время тестирования отчет о действиях: class BBL implements ActionListener { int count = 0; public void actionPerformed(ActionEvent e){ txt.setText("BangBean action "+ count++); } } public void init() { BangBean bb = new BangBean(); try { bb.addActionListener(new BBL()); } catch(TooManyListenersException e) { txt.setText("Too many listeners"); } Container cp = getContentPane(); cp.add(bb); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new BangBeanTest(), 400, 500); } } ///:~
Когда компонент (Bean) находится в среде разработки, этот класс не будет использоваться, но полезно обеспечить тестирование метода для каждого вашего компонента (Bean). BangBeanTest помещает BangBean в апплет, присоединяет простой ActionListener к BangBean для печати счетчика событий в JTextField, когда бы не возникло ActionEvent. Конечно, обычно построитель приложения создает большинство кода, который использует этот компонент (Bean).
Когда вы запустите BangBean через BeanDumper, или поместите BangBean внутрь среды разработки, поддерживающей компоненты, вы заметите, что есть гораздо больше свойств и действий, чем это видно в приведенном выше коде. Это происходит потому, что BangBean наследуется от JPanel, а JPanel - это тоже компонент (Bean), так что вы также видите его свойства и события.
Прежде, чем вы поместите компонент (Bean) в визуальный построитель, поддерживающий компоненты, он должен быть помещен в стандартный контейнер компонент (Bean), который является JAR файлом, включающим все классы компонент (Bean) наряду с файлом “манифеста”, который говорит: “Это компонент (Bean)”. Файл манифеста - это простой текстовый файл, который следует определенной форме. Для BangBean файл манифеста выглядит так (без первых и последних строчек):
//:! :BangBean.mf Manifest-Version: 1.0 Name: bangbean/BangBean.class Java-Bean: True ///:~
Первая строка указывает версию схемы манифеста, которая до особого уведомления от Sun, является 1.0. Вторая строка (пустые строки игнорируются) указывает имя файла BangBean.class, а третья говорит: “Это компонент”. Без третьей строки построитель программы не распознает класс, как компоненте (Bean).
Сложность состоит только в том, что вы должны убедиться, что вы получили правильный путь в поле “Name:”. Если вы снова взглянете на BangBean.java, вы увидите его в package bangbean (и поэтому поддиректорий, называемый bangbean” должен включаться в путь класса), а имя в файле манифеста должно включать эту информацию о пакете. Кроме того, вы должны поместить файл манифеста в директорию, перед корневым директорием пути вашего пакета, что в этом случае означает помещение файла в директорий, перед поддиректорием “bangbean”. Затем вы должны вызвать jar из той же директории, в которой находится файл манифеста, как показано ниже:
jar cfm BangBean.jar BangBean.mf bangbean
Здесь имеется в виду, что вы хотите в результате получить JAR файл с именем BangBean.jar и что вы хотите поместить файл манифеста, называемый BangBean.mf.
Вы можете удивиться: “Как насчет всех остальных классов, которые были сгенерированы, когда я компилировал BangBean.java?” Они все заключены в директории bangbean, и вы видите, что последний аргумент для приведенной выше команды jar - это директорий bangbean. Когда вы передаете jar имя поддиректории, он пакует весь поддиректорий в JAR файл (включая, в этом случае, оригинальный файл исходного кода BangBean.java — вы можете не включать исходный код вашего компонента). Кроме того, если вы в последствии распакуете JAR файл, который вы только что создали, вы обнаружите, что ваш манифест файл не находится внутри, а jar создал собственный манифест файл (частично основываясь на вашем), называемый MANIFEST.MF и помещенный в директории META-INF (для “meta-информации”). Если вы откроете этот файл манифеста, вы увидите цифровую подпись информации, добавленной jar для каждого файла, следующего вида:
Digest-Algorithms: SHA MD5 SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0= MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
В общем случае, вам не нужно беспокоится об этом, и если вы сделаете изменения, вы можете просто изменить ваш оригинальный файл манифеста и заново вызвать jar для создания нового JAR файла для вашего компонента (Bean). Вы можете также добавить другой компонент (Bean) в JAR файл, просто добавив информацию о нем в ваш файл манифеста.
Однако вы должны обратить внимание, что вы, вероятно, захотите поместить каждый компонент (Bean) в свой собственный директорий, так как когда вы создадите JAR файл, вы передадите утилите jar имя поддиректории, а она поместит все в этой директории в JAR файл. Вы можете видеть, что Frog и BangBean находятся в своих собственных директориях.
Как только вы получите ваш компонент правильно расположенным в JAR файле, вы можете ввести его в среду построителя программ, поддерживающую компоненты. Способ, которым вы можете сделать это, разнится для разных инструментов, но Sun распространяет бесплатную тестовую основу для JavaBeans в своем “Beans Development Kit” (BDK), называемом beanbox”. (BDK доступен на java.sun.com/beans.). Для помещения вашего компонента в beanbox, скопируйте JAR файл в поддиректорий “jars” из BDK прежде, чем вы запустите beanbox.
Вы можете видеть, как удивительно просто создать компонент (Bean). Но вы не ограничены тем, что вы видели здесь. Архитектура JavaBeans обеспечивает простой способ входа, но вы можете также распространить ее на более сложные ситуации. Эти ситуации выходят за пределы тем, рассматриваемых этой книгой, но они будут коротко обозначены здесь. Вы можете найти более подробный материал на java.sun.com/beans.
Одно из мест, где вы можете добавить изощренность - это свойства (properties). Приведенный выше пример показывает только единичные свойства, но также возможно представить различные свойства в массиве. Это называется индексированным свойством (indexed property). Вы просто обеспечиваете соответствующие методы (опять таки, следую соглашению об именах методов), а Introspector определяет их как индексированные свойства, так что ваш построитель приложения может отобразить их соответственно.
Свойства могут быть граничными, что означает, что они будут уведомлять другие объекты через PropertyChangeEvent. Другие объекты могут затем выбрать изменения себя, основываясь на изменении компонента (Bean).
Свойства могут быть ограничены, это значит, что другие объекты могут запрещать изменения этого свойства, если это недопустимо. Другие объекты уведомляются при помощи PropertyChangeEvent, и они могут выбросить исключение PropertyVetoException для предотвращения изменений и для восстановления старого значения.
Вы также можете изменить способ представления вашего компонента (Bean) в режиме дизайна:
Есть другой подход, который не будет указан здесь. Где бы вы не создавали компонент (Bean), вы должны ожидать, что он будет работать в многопоточной среде. Это означает, что вы должны понимать способности потоков, которые будут введены в Главе 14. Вы найдете раздел, называемый “возврат к JavaBeans”, в которой мы рассмотрим проблему и ее решение.
Есть несколько книг, посвященных JavaBeans; например, JavaBeans by Elliotte Rusty Harold (IDG, 1998).
Все библиотеки Java, а GUI библиотеки особенно, претерпели значительные изменения при переходе от Java 1.0 к Java 2. Java 1.0 AWT сильно критиковалась, как имеющих один из худших дизайнов, в то время как она позволяла вам создавать портативные программы, а результирующий GUI “эквивалентно выглядел на всех платформах”. Она была также ограничена, неуклюжа и неприятна в использовании по сравнению с родными инструментами разработки приложений, имеющихся для определенной платформы.
Когда Java 1.1 ввела новую модель событий и JavaBeans, на которые был сделан упор, стало возможным создавать GUI компоненты, которые легко могут быть перетащены и брошены в визуальном построителе приложений. Кроме того, дизайн модели событий и компонентов (Bean) ясно показывает, что большое внимание было уделено облегчению программирования и поддержки кода (то, что не было очевидно в 1.0 AWT). Но это было не так, пока не появились классы JFS/Swing, в которых эта работа была завершена. Со Swing компонентами кросс-платформенное программирование стало носить цивилизованный вид.
На самом деле, не хватало только одного - построителя приложений, и это было настоящей революцией. Microsoft Visual Basic и Visual C++ требуют построителя приложений от фирмы Microsoft, точно так же как и Borland Delphi и C++ Builder. Если вы хотите получить лучший построитель приложений, вы можете скрестить пальцы и надеяться, что продавец даст вам то, что вы хотите. Но Java - это открытая система, что позволяет не только состязаться средам разработки, но одобряет такое состязание. И для таких инструментов важна поддержка JavaBeans. Это означает уровневое поле игры: если вы находите лучший инструмент построителя приложений, вы не привязаны к тому, который вы используете — вы можете взять и перейти на другой, который повысит вашу производительность. Соперничество такого рода между средами построения GUI приложений ранее не встречалось, а в результате из-за продаж может быть получен позитивный рост производительности программистов.
Эта глава создавалась с целью дать вам начальное представление о силе Swing и показать вам, насколько относительно просто почувствовать вкус библиотеки. То, что вы видели, вероятно, достаточно для удовлетворения большей части вашего пользовательского интерфейса. Однако Swing может много больше — он предназначен, чтобы быть мощным инструментом разработки пользовательского интерфейса. Вероятно, есть способ совершить все, что вы можете придумать.
Если вы не увидели здесь то, что вам нужно, покопайтесь в онлайн документации от Sun и поищите в Web, и если этого не достаточно, то найдите книгу, посвященную Swing — неплохо начать с The JFC Swing Tutorial, by Walrath & Campione (Addison Wesley, 1999).
Решения для выбранных упражнений могут быть найдены в электронной документации The Thinking in Java Annotated Solution Guide, доступной за малую плату на www.BruceEckel.com.
[61] Вариации этого называются “принципом наименьшего удивления”, который особенно подчеркивает: “не удивляйте пользователя”.
[62] Это пример шаблона разработки, называемый метод шаблонов.
[63] Здесь принимается во внимание то, что читатель хорошо знаком с основами HTML. Это не трудно понять и есть много книг и ресурсов на эту тему.
[64] Эта страница —обычно, раздел ‘clsid’ — выглядит хорошо работающей и с JDK1.2.2, и с JDK1.3 rc-1. Однако вы можете обнаружить изменение ярлыка когда-нибудь в будущем. Более детально смотрите java.sun.com.
[65] По моему мнению. И после того, как вы выучите Swing, вы не захотите тратить ваше время на подготовительный материал.
[66] Как описано ранее, “Frame” был уже введен в AWT, так что Swing использует JFrame.
[67] Это будет иметь смысл после того, как вы прочтете продолжение этой главы. Прежде всего, сделайте ссылку на JApplet статическим членом класса member (вместо локальной переменной в main( )), а затем вызовите applet.stop( ) и applet.destroy( ) внутри WindowAdapter.windowClosing( ), прежде, чем вы вызовите System.exit( ).
[68] Не существует MouseMotionEvent, несмотря на то, что кажется, что оно должно быть. Щелчки и движение комбинируются в MouseEvent, так что это второе появление MouseEvent в таблице не является ошибкой.
[69] В Java 1.0/1.1 вы не могли выполнять полезное наследование от объекта кнопки. Это был один из многочисленных фундаментальных недостатков проекта.
[ Предыдущая глава ] [ Содержание ] [ Оглавление ] [ Индекс ] [ Следующая глава ]