GUI-приложение на Swing с иконкой в трее — реальный пример проблемы с кроссплатформенностью в Java

7 июля 2014

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

Напишем простейшую программу, использующую Swing, которая создает пустой фрейм. Также программа создает иконку в системном трее с выпадающим меню. У этого меню есть единственный пункт «Exit», завершающий работу программы. Сворачивание в трей мне было влом реализовывать, но если интересно, рецепт можно найти на StackOverflow.

У меня получился такой код:

package me.eax.examples.tray_icon_example;

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, то все вполне себе чинно-блинно и работает в соответствии с ожиданиям:

GUI-приложение на Swing с иконкой в трее под Windows

А вот то же приложение, запущенное под Ubuntu Linux в дектоп-окружении Unity:

GUI-приложение на Swing с иконкой в трее под Ubuntu Linux

Налицо сразу множество проблем: (1) фон у иконки серый вместо прозрачного, (2) выпадающее меню выглядит не нативно, (3) а всплывающее сообщение выглядит, как говно. И это еще не самый худший вариант! Под Unity на другой машине всплывающее сообщение вовсе не появляется, а выпадающее меню рисуется как бы зачеркнутым, потому что поверх него рендерится кусок таскбара. Под Gnome не появляется иконка, а всплывающее сообщение появляется где-то чуть ниже нижней границы экрана, при условии, что таскбар расположен вверху. Под i3 с его мизерным таскбаром иконка превращается в желтый квадратик с черной точкой, а пункт меню невозможно прочитать, потому что поверх него отображается всплывающая подсказка нашей иконки.

В общем, иконки в трее под Linux абсолютно неюзабельны. Притом, похоже, что проблема известна уже не менее четырех лет, так что на скорое ее исправление надеяться не приходится.

Какими костылями можно это подпереть:

  • Заюзать JNI, отказавшись тем самым от одной из главных фичей Java;
  • Таскать за собой нативные приложения под N поддерживаемых платформ, которые будут рисовать нативные иконки и менюшки, взаимодействуя с ними через stdin/stdout или еще как-то;
  • Не использовать прозрачность и меню, а также при запуске под Linux показывать уведомления через notify-send, плюс предоставить пользователю возможность указать в настройках альтернативное приложение;
  • Под Linux по умолчанию ничего не делать с треем и спрятать соответствующую галочку поглубже в настройках приложения;
  • Никогда и ничего не делать с треем, там и без нас уже полно иконок;

Короче, с кроссплатформенностью в Java не все так здорово, как нас пытаются заверить. И это я только поигрываюсь с Java в свободное время. Более опытные Java-разработчики наверняка без труда назовут еще немало примеров. Невольно начинаешь задумываться, а не обман ли вся эта ваша JIT-компиляция? Мало того, что я, несчастный пользователь, должен устанавливать виртуальную машину, так мне еще приходится тратить процессорное время на то, чтобы скомпилировать программу. Выходит, разработчики прислали мне какой-то полуфабрикат? Самим скомпилировать лень было? Да и с точки зрения разработчиков уж не проще ли самостоятельно собирать нативное приложение под N целевых платформ и ни в чем себя не ограничивать?

Исходники к этой заметке лежат тут. Как можно сделать иконку для трея в Gimp рассказано здесь. А там чувак предлагает воркэраунд.

Как всегда, буду рад вашим комментариям.

Дополнение: Запустили jar-ник под маками, вроде там все норм.

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.