GUI-приложение на Swing с иконкой в трее — реальный пример проблемы с кроссплатформенностью в Java
7 июля 2014
Считается, что код, один раз написанный на Java, безо всяких проблем одинаково хорошо работает на любой платформе, под которую есть виртуальная машина. В первом приближении, если не использовать всякие там JNI, это действительно так. Но вот я, можно сказать, лишь недавно начал играться с языком, а уже успел столкнуться с ситуацией, когда на самом деле это нефига не так.
Напишем простейшую программу, использующую Swing, которая создает пустой фрейм. Также программа создает иконку в системном трее с выпадающим меню. У этого меню есть единственный пункт «Exit», завершающий работу программы. Сворачивание в трей мне было влом реализовывать, но если интересно, рецепт можно найти на StackOverflow.
У меня получился такой код:
import java.awt.*;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.*;
public class TrayIconExample {
public static final String APPLICATION_NAME = "TrayIconExample";
public static final String ICON_STR = "/images/icon32x32.png";
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
createGUI();
}
});
}
private static void createGUI() {
JFrame frame = new JFrame(APPLICATION_NAME);
frame.setMinimumSize(new Dimension(300, 200));
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
setTrayIcon();
}
private static void setTrayIcon() {
if(! SystemTray.isSupported() ) {
return;
}
PopupMenu trayMenu = new PopupMenu();
MenuItem item = new MenuItem("Exit");
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
trayMenu.add(item);
URL imageURL = TrayIconExample.class.getResource(ICON_STR);
Image icon = Toolkit.getDefaultToolkit().getImage(imageURL);
TrayIcon trayIcon = new TrayIcon(icon, APPLICATION_NAME, trayMenu);
trayIcon.setImageAutoSize(true);
SystemTray tray = SystemTray.getSystemTray();
try {
tray.add(trayIcon);
} catch (AWTException e) {
e.printStackTrace();
}
trayIcon.displayMessage(APPLICATION_NAME, "Application started!",
TrayIcon.MessageType.INFO);
}
}
Если запустить эту программу под Windows, то все вполне себе чинно-блинно и работает в соответствии с ожиданиям:
А вот то же приложение, запущенное под Ubuntu Linux в дектоп-окружении Unity:
Налицо сразу множество проблем: (1) фон у иконки серый вместо прозрачного, (2) выпадающее меню выглядит не нативно, (3) а всплывающее сообщение выглядит, как говно. И это еще не самый худший вариант! Под Unity на другой машине всплывающее сообщение вовсе не появляется, а выпадающее меню рисуется как бы зачеркнутым, потому что поверх него рендерится кусок таскбара. Под Gnome не появляется иконка, а всплывающее сообщение появляется где-то чуть ниже нижней границы экрана, при условии, что таскбар расположен вверху. Под i3 с его мизерным таскбаром иконка превращается в желтый квадратик с черной точкой, а пункт меню невозможно прочитать, потому что поверх него отображается всплывающая подсказка нашей иконки.
В общем, иконки в трее под Linux абсолютно неюзабельны. Притом, похоже, что проблема известна уже не менее четырех лет, так что на скорое ее исправление надеяться не приходится.
Какими костылями можно это подпереть:
- Заюзать JNI, отказавшись тем самым от одной из главных фичей Java;
- Таскать за собой нативные приложения под N поддерживаемых платформ, которые будут рисовать нативные иконки и менюшки, взаимодействуя с ними через stdin/stdout или еще как-то;
- Не использовать прозрачность и меню, а также при запуске под Linux показывать уведомления через notify-send, плюс предоставить пользователю возможность указать в настройках альтернативное приложение;
- Под Linux по умолчанию ничего не делать с треем и спрятать соответствующую галочку поглубже в настройках приложения;
- Никогда и ничего не делать с треем, там и без нас уже полно иконок;
Короче, с кроссплатформенностью в Java не все так здорово, как нас пытаются заверить. И это я только поигрываюсь с Java в свободное время. Более опытные Java-разработчики наверняка без труда назовут еще немало примеров. Невольно начинаешь задумываться, а не обман ли вся эта ваша JIT-компиляция? Мало того, что я, несчастный пользователь, должен устанавливать виртуальную машину, так мне еще приходится тратить процессорное время на то, чтобы скомпилировать программу. Выходит, разработчики прислали мне какой-то полуфабрикат? Самим скомпилировать лень было? Да и с точки зрения разработчиков уж не проще ли самостоятельно собирать нативное приложение под N целевых платформ и ни в чем себя не ограничивать?
Исходники к этой заметке лежат тут. Как можно сделать иконку для трея в Gimp рассказано здесь. А там чувак предлагает воркэраунд.
Как всегда, буду рад вашим комментариям.
Дополнение: Запустили jar-ник под маками, вроде там все норм.
Метки: Java, Кроссплатформенность.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.