Затачиваем свое Java-приложение под Mac OS X. Часть вторая

Затачиваем свое Java-приложение под Mac OS X.
Часть вторая


Совсем недавно Apple выпустила Java 2 Standard Edition (J2SE) 1.4.1 для своей операционной системы Mac OS X. Изначально, при написании первой статьи из этой серии, Mac OS X поставлялась с предустановленной J2SE версии 1.3.1. Сейчас все обладатели Jaguar (Mac OS X версии 10.2) могут беспрепятственно скачать и использовать J2SE 1.4.1 с раздела сайта Apple, посвященного Java (http://www.apple.com/java). Во время портирования J2SE 1.4.1 на Mac OS X много времени было уделено переносу GUI-элементов с Carbon-фреймуорка на Cocoa-фреймуорк. Это значит, что теперь в ваших Java-приложенях гораздо проще воспользоваться специфичными для этой операционной системы особенностями, что даст возможность сделать внешний вид ваших Java-приложений еще более похожим на вид обычных приложений Mac OS X.

В этой серии статей мы сфокусируемся на настройке кроссплатформенных Java-приложений таким образом, чтобы сделать их максимально похожими на обычные приложения платформы Mac OS X, при этом их внешний вид под другими платформами остается прежним. В этих статьях представлены, в основном, небольшие изолированные друг от друга примеры, которые позволяют улучшить производительность ваших Java-приложений на платформе Mac и которые вы можете применить самостоятельно. В результате ваше приложение будет совмещать в себе все эти изменения, причем вы будете вставлять код, который определяет текущую платформу и, если это Mac OS X, делает что-то одно, иначе — что-то другое.
В этой статье мы продолжаем использовать в качестве примера open-source-приложение, предназначенное для проведения всякого рода тестов, — JUnit. В прошлый раз мы сделали JUnit более похожим на Mac-приложение, изменяя различные runtime-свойства. На этот раз мы внесем некоторые поправки непосредственно в исходный код этого приложения. Для начала распакуйте архив JUnit.zip, который вы можете совершенно бесплатно скачать с домашней страницы JUnit (http://www.junit.org/). После этого распакуйте файл-архив src.jar. Помимо этого нам понадобится пакет com.apple.eawt, который включен в поставку J2SE 1.4.1 от Apple и находится в файле ui.jar, который вы сможете найти в директории Classes. Вы также можете получать свежие Java-релизы от Apple с сайта Apple Developer Connection (http://connect.apple.com/), предварительно пройдя бесплатную регистрацию.

Изменение меню JUnit

Меню JUnit содержит всего два элемента: About и Exit. Однако пользователи Mac привыкли именно к Quit, а не к Exit. Разница несущественна, однако играет важную роль для того, чтобы представить Java-приложение как написанное специально под Mac-платформу. В принципе, вы можете справиться с этой задачей, используя java.util.ResourceBundle, с помощью которого можно сохранить различные имена элементов меню таким же образом, как вы сохраняли бы локализационные (locale) данные. Вы также можете параллельно использовать два варианта структур меню для нескольких платформ. В нашем частном случае меню приложения уже включает как элемент About, так и Quit.
Это меню автоматически генерируется при запуске приложения. В первой статье мы установили runtime-свойства специально чтобы расположить это меню вверху экрана, а также чтобы отобразить имя приложения в заголовке меню и в элементах About, Quit и Hide.
Все элементы меню исправно выполняют отведенные им функции. Это касается и элемента About. Вам совершенно необязательно писать какой-то дополнительный код, если вы хотите использовать окно About, принятое по умолчанию для всех Java-приложений. Но это не есть информативно и, к тому же, непривлекательно для конечного пользователя, поэтому все же следовало бы несколько преобразить это диалоговое окно.

Чтобы при выборе пользователем этого пункта меню появлялось окно About, включенное в программу JUnit, необходимо внести некоторые изменения в исходный код этого приложения. В коде JUnit при создании меню достаточно просто внести изменения, с помощью которых мы сможем отследить, запущено это приложение на платформе Mac либо на какой-либо другой. Если это не Mac, то мы будем выводить меню так, как положено, без каких-либо изменений. Если же JUnit стартует под Mac OS X, мы изменим меню приложения, чтобы оно отображало окно About приложения JUnit.
Найдите метод createMenus() в классе JUnit.swingui.TestRunner. Его код приведен ниже:
protected void createMenus(JMenuBar mb) {
mb.add(createJUnitMenu());
}
Для того чтобы проверить, работает ли в данный момент наше приложение под Mac, нам нужно просто посмотреть содержимое системного свойства mrj.version. Нас не интересует его значение: главное — проверить, не равняется ли оно null. Все виртуальные Java-машины (JVM) от Mac определяют это свойство. Поэтому, если вы попытались проверить значение этого системного свойства, и вам было возвращено значение null, тогда вы можете быть уверены, что программа запущена на платформе Mac. Новая версия метода createMenus(), которая определяет текущую операционную систему и отображает свое меню только в том случае, когда приложение запущено не на Mac, будет выглядеть следующим образом:
protected void createMenus(JMenuBar mb) {
if (System.getProperty("mrj.version") == null) {
mb.add(createJUnitMenu());
} else {
// здесь мы должны писать код, специфичный для Mac
}
}

Настраиваем элемент меню About
В программе JUnit есть свое диалоговое окно About, которое вызывается из меню этого приложения. Мы собираемся сделать аналогичный функциональный элемент меню в меню приложения (Application menu) посредством переопределения метода handleAbout() таким образом, чтобы он вызывал класс AboutDialog, поставляемый вместе с JUnit. Этот метод выглядит следующим образом:
public void handleAbout(ApplicationEvent event) {
new AboutDialog(new JFrame()).show();
}
Метод handleAbout() определен в интерфейсе ApplicationListener. Мы расширим класс ApplicatonAdapter, который включает в себя пустые реализации всех методов интерфейса ApplicationListener. Единственное, что мы сделаем, — переопределим метод handleAbout(). Следующий код представляет внутренний класс, который мы определили:
class AboutBoxHandler extends ApplicationAdapter {
public void handleAbout(ApplicationEvent event) {
new AboutDialog(new JFrame()).show();
}
}
Для того чтобы отделить код, специфичный для Mac OS X, от базового кода приложения JUnit, создадим класс MacOSAboutHandler, принадлежащий пакету JUnit.swingui. Класс MacOSAboutHandler расширяет класс com.apple.eawt.Application. Как и любой другой обработчик событий, с которыми вы знакомы по библиотеке Swing, мы должны зарегистрировать AboutBoxHandler как "слушателя" (обработчика) событий нашего приложения. Из приведенного ниже кода вы можете видеть, что регистрация происходит в теле конструктора. В принципе, вы могли бы сделать то же самое, создав внутренний класс, вместо того чтобы выносить его в какие-либо другие пакеты и классы.
package JUnit.swingui;

import com.apple.eawt.Applica-tionAdapter;
import com.apple.eawt.Applica-tionEvent;
import com.apple.eawt.Applica-tion;
import javax.swing.JFrame;

public class MacOSAboutHandler extends Application {

public MacOSAboutHandler() {
addApplicationListener(new AboutBoxHandler());
}

class AboutBoxHandler extends ApplicationAdapter {
public void handleAbout(Ap-plicationEvent event) {
new AboutDialog(new JFrame()).show();
}
}
}
Теперь вернемся к коду класса TestRunner и создадим экземпляр класса MacOSAboutHandler внутри метода createMenus().
protected void createMenus (JMenuBar mb) {
if (System.getProperty ("mrj.version") == null) {
mb.add(createJUnitMenu());
} else {
new MacOSAboutHandler();
}
}
Теперь попробуйте запустить новую версию этого приложения на платформе Windows и убедитесь, что все осталось как прежде, как будто бы мы ничего не меняли. Теперь попробуйте запустить эту же версию под Mac OS X. Теперь, когда пользователь выбирает пункт меню приложения About JUnit, он видит диалоговое окно About программы JUnit:

Human Interface (HI)
Apple потратили достаточно много времени на определение директив Human (свойственный человеку) Interface для Mac OS X. Поскольку Apple отобразили Swing- и AWT-компоненты на Cocoa-фреймуорк, все стандартные виджеты этого фреймуорка удовлетворяют многим директивам HI. Однако в приложении JUnit мы можем видеть как минимум два примера несоответствия Java-кода и директив HI, определенных Apple.
Первый не совсем очевиден. Запустите JUnit и обратите внимание на иерархию тестов. Вы увидите примерно следующее:

Развернув один из узлов, вы должны увидеть следующую картину:

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

Такой вид будет более благоприятным и удобным для пользователя. Изменить же приложение, чтобы добиться такого результата, крайне просто. Для начала нужно решить, важно ли оставить невидимыми эти панели для других платформ (не Mac). Если это важно, тогда нам нужно будет сначала убедиться в том, что программа запущена и работает на платформе Mac. Это можно сделать точно так же, как и в предыдущем рассмотренном нами примере. Если обратить внимание на закладку Failures, то сразу станет ясно, что это изменение не так уж и важно для авторов JUnit. Директивы HI, опираясь на опыт и пожелания пользователей, также способствуют тому, чтобы это было так.
Чтобы решить эту задачу, нам нужно внести некоторые изменения в исходный код нашего приложения. Для начала обратим внимание на одну строчку конструктора класса JUnit.swingui.TestSuitePanel:
fScrollTree= new JScrollPane (fTree);
Ее нам нужно заменить следующей:
fScrollTree = new JScrollPane (fTree, JScrollPane.VERTICAL_ SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
Этот пример с полосой прокрутки иллюстрирует тот факт, что область применения директив HI от Apple не ограничена рамками реализации Java от Apple.

Цвет строки состояния
Существует одна фундаментальная проблема в работе JUnit на Mac OS X, которую мы забыли рассмотреть. Как только вы запустили все тесты приложения JUnit, вы должны увидеть примерно следующее:

Это окно содержит всю необходимую информацию, но отображена она не совсем корректно. Когда JUnit запускается на других платформах, индикатор выполнения меняет свой цвет: зеленый — если все тесты были успешно пройдены, красный — если отдельные тесты провалились. Это является одним из достоинств приложения Junit, и самоочевидно, что, если все тесты были пройдены, индикатор имеет зеленый цвет. Такой подход конфликтует с директивами HI от Apple, следуя которым нельзя изменять цвет кнопок, индикаторов состояния и прочих виджетов.
Однако есть много способов это осуществить:
1. Создать свой собственный виджет, используя JPanel, который представляет собой простой прямоугольник зеленого цвета — эдакий самодельный индикатор выполнения. Конечно, это не будет настоящим объемным элементом Aqua, но вы получите реальную возможность менять цвет этого своего индикатора.
2. Можно использовать громоздкий виджет на основе AWT-компонентов — например, JUnit.awtui. ProgressBar. В этом случае вы также не добьетесь такого же внешнего вида, как и у Aqua-компонентов, но самое главное — у вас возникнут проблемы с наследованием, поскольку вы будете смешивать громоздкие и небольшие компоненты.
3. Согласиться с Apple и изменить поведение индикаторов состояния, которые представлены классом JProgressBar. Однако даже в этом случае ваше Java-приложение не будет выглядеть, как native-приложение платформы Mac.
4. Можно отражать цвета в других компонентах. Самое простое здесь — внести небольшие изменения в то, как JUnit представлен на платформе Mac, а для других платформ ничего не менять.
В духе "экстремального" программирования (eXtreme Programming) мы предпочтем последний вариант всем остальным, поскольку он самый простой и вполне рабочий. Мы оставим индикатор таким, какой он есть. На других платформах (отличных от Mac) мы будем отображать оба состояния индикатора при успешном выполнении всех тестов и при не полностью успешном их прохождении. На Mac индикатор будет использоваться только в своих обычных целях и не будет менять свой цвет. Вместо этого менять свой фоновый цвет будет строка состояния. Приводя в жизнь эту идеологию нового поведения строки состояния, мы постараемся внести как можно меньше небольших изменений в код приложения JUnit.

Расширение индикатора выполнения (Progress Bar)
Для начала взглянем на исходный код класса JUnit.swingui.ProgressBar:
import java.awt.Color;
import javax.swing.*;

/**
* A progress bar showing the green/red status
*/
class ProgressBar extends JProgressBar {
boolean fError= false;

public ProgressBar() {
super();
setForeground(getStatusColor());
}

private Color getStatus Color() {
if (fError)
return Color.red;
return Color.green;
}

public void reset() {
fError= false;
setForeground(getStatus Color());
setValue(0);
}

public void start(int total) {
setMaximum(total);
reset();
}

public void step(int value, boolean successful) {
setValue(value);
if (!fError && !successful) {
fError= true;
setForeground(getStatus Color());
}
}
}
Теперь возьмем два вызова метода setForeground(), которые находятся вне конструктора, и объединим их в методе с именем updateBarColor().
//...
class ProgressBar extends JProgressBar { //...

public void reset() {
fError= false;
updateBarColor();
setValue(0);
}

public void start(int total) {
setMaximum(total);
reset();
}

public void step(int value, boolean successful) {
setValue(value);
if (!fError && !successful) {
fError= true;
updateBarColor());
}
}

protected void updateBarCo-lor(){
setForeground(getSta-tusColor());
}
}
Создадим подкласс класса ProgressBar и назовем его Mac-ProgressBar, который будет изменять цвет строки состояния, но не будет менять цвет индикатора выполнения.
package JUnit.swingui;

import javax.swing.JTextField;

public class MacProgressBar extends ProgressBar{
private JTextField component;

public MacProgressBar(JText Field component) {
super();
this.component=component;
}

protected void updateBarCo-lor() {
component.setBackground(getStatusColor());
}
}

Мы пришли к строке состояния как аргументу конструктора, в то время как метод updateBarColor() может обновлять цвет индикатора выполнения.
Еще раз посмотрим на код класса ProgressBar. Появляются еще две опорные для нас точки:
//...
class ProgressBar extends JProgressBar {
boolean fError= false;

public ProgressBar() {
super();
setForeground(getStatus Color());
}

protected Color getStatus Color() {
if (fError)
return Color.red;
return Color.green;
}
//...
Во-первых, уровень доступа к методу getStatusColor() необходимо изменить с private на, хотя бы, protected, поскольку он используется подклассом MacProgressBar. Во-вторых, мы не можем вызывать метод update-BarColor() в месте, где метод setForeground() вызывается в конструкторе. Это приведет к исключению NullPointerException, поскольку метод updateBarColor() вызывается раньше, чем был создан экземпляр класса MacProgressBar, которому он принадлежит. Если мы оставим этот вызов метода setForeground(), то самому индикатору состояния это не повредит, поскольку класс MacProgressBar отвечает лишь за изменение цвета индикатора в Mac OS X.

Обновление класса TestRunner
Обратимся к методу createUI() класса JUnit.swingui.TestRunner. В нем содержится более пятидесяти строк. Поскольку мы поставили перед собой цель внести в исходный код настолько меньше изменений, насколько это возможно, нам придется выполнить некоторый рефакторинг исходного кода, разбив метод createUI() на несколько небольших. Мы изберем наиболее направленный подход и оставим эти изменения вам как упражнение.
Итак, обратите внимание на следующую строчку в методе createUI():
fProgressIndicator = new ProgressBar();
Мы изменим ее следующим образом:
— проверим, запущено приложение на платформе Mac или на какой-то другой;
— если это не Mac, тогда fProgressIndicator — это обычный ProgressBar;
— если же это Mac, тогда fProgressIndicator — это уже MacProgressBar с переданным его конструктору экземпляром строки состояния fStatusLine;
— создание строки состояния выносим за рамки этого кода.
Вот что у нас получилось:
fStatusLine = createStatus Line(); // перенесено
if (System.getProperty ("mrj.version") == null) {
fProgressIndicator = new ProgressBar();
} else {
fProgressIndicator = new MacProgressBar(fStatusLine);
}

Теперь, когда вы запустите JUnit и выполните тесты, каждый из которых будет успешно пройден, то увидите следующее:
Если же хотя бы один из тестов не был успешно пройден, то вы должны будете увидеть что-то похожее на:

Резюме
Ваше Java-приложение будет работать на Mac OS X в любом случае, даже если вы не выполняли каких-либо дополнительных изменений.
Из этих двух статей вы успели узнать лишь о небольшой части тех изменений, которые можно внести, чтобы ваше приложение больше обычного походило на native-приложение Mac OS X.
В следующий раз мы обратим внимание на различные параметры, позволяющие улучшить производительность вашего приложения на платформе Mac, а также затронем очень важную тему упаковки и разворачивания ваших приложений на этой платформе.

По материалам Daniel H. Steinberg

Подготовил Алексей Литвинюк,
http://www.litvinuke.hut.ru



Компьютерная газета. Статья была опубликована в номере 21 за 2003 год в рубрике программирование :: java

©1997-2024 Компьютерная газета