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

Добавлено : 27 Mar 2009, 18:59

Совсем недавно 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.

Рисунок 1. Автогенерируемое меню приложения

Это меню автоматически генерируется при запуске приложения. В первой статье мы установили runtime-свойства, специально чтобы расположить это меню вверху экрана, а также, чтобы отобразить имя приложения в заголовке меню и в элементах About, Quit и Hide.

Все элементы меню исправно выполняют отведенные им функции. Это касается и элемента About. Вам совершенно не обязательно писать какой-то дополнительный код, если вы хотите использовать окно About принятое по умолчанию для всех Java-приложений. Но это не есть информативно и не привлекательно для конечного пользователя, поэтому все же следовало бы несколько преобразить это диалоговое окно.

Рис. 2 Обычное окно About

Чтобы при выборе пользователем этого пункта меню появлялось окно 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.ApplicationAdapter;
import com.apple.eawt.ApplicationEvent;
import com.apple.eawt.Application;
import javax.swing.JFrame;

public class MacOSAboutHandler extends Application {

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

 
class AboutBoxHandler extends ApplicationAdapter {
   
public void handleAbout(ApplicationEvent 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:

Рис. 3 Диалоговое окно JUnit

Human Interface (HI)

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

Первый не совсем очевиден. Запустите JUnit и обратите внимание на иерархию тестов. Вы увидите примерно следующее:

Рис. 4 Несоответствие Java-кода и директив

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

Рис 5 Скрывающиеся полосы прокрутки

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

Рис. 6 Более благоприятный вид для пользователя

Такой вид будет более благоприятным и удобным для пользователя. Изменить же приложение, чтобы добиться такого результата крайне просто. Для начала нужно решить важно ли оставить невидимыми эти панели для других платформ (не 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, вы должны увидеть примерно следующее:

Рис. 7 Директивы HI от Apple не позволяют менять цвет

Это окно содержим всю необходимую информацию, но отображена она не совсем корректно. Когда 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 getStatusColor() {
   
if (fError)
     
return Color.red;
   
return Color.green;
 
}

 
public void reset() {
   
fError = false;
    setForeground
(getStatusColor());
    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
(getStatusColor());
   
}
  }
}

Теперь возьмем два вызова метода 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 updateBarColor() {
   
setForeground(getStatusColor());
 
}
}

Создадим подкласс класса ProgressBar и назовем его MacProgressBar, который будет изменять цвет строки состояния, но не будет менять цвет индикатора выполнения.

package JUnit.swingui;

import javax.swing.JTextField;

public class MacProgressBar extends ProgressBar {

 
private JTextField component;

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

 
protected void updateBarColor() {
   
component.setBackground(getStatusColor());
 
}
}

Мы пришли к строке состояния, как аргументу конструктора. В то время как метод updateBarColor() может обновлять цвет индикатора выполнения.

Еще раз посмотрим на код класса ProgressBar. Появляется еще две опорные для нас точки:

//...

class ProgressBar extends JProgressBar {

 
boolean fError= false;


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

 
protected Color getStatusColor() {
   
if (fError)
   
return Color.red;
   
return Color.green;
 
}
 
// ...

Во первых, уровень доступа к методу getStatusColor() необходимо изменить с private на, хотя бы, protected, поскольку он используется подклассом MacProgressBar. Во вторых, мы не можем вызывать метод updateBarColor() в месте, где метод 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 = createStatusLine()// перенесено

if (System.getProperty("mrj.version") == null) {
 
fProgressIndicator = new ProgressBar();
else {
 
fProgressIndicator = new MacProgressBar(fStatusLine);
}

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

Рис. 8 Расширение индикатора выполнения

Если же хотя бы один из тестов не был успешно пройден, то вы должны будете увидеть что-то похожее:

Рис. 9 Расширение индикатора выполнения

Резюме

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

Теги: MacOS swing