?

Log in

Previous Entry | Next Entry

Недавно наткнулся в форуме на задачу, кооторая показалась мне любопытной. В смысле, сама по себе она практически тривиальна, но есть пара неочевидных моментов, на которые я потратил с полчаса.

Итак, поговорим о схлопывании окна в область системных задач. В Java6 появилась такая возможность, как добавление в эту область иконки. Со всеми сопутствующими приятными моментами - обработка на ней кликов, контекстные меню, всплывающие подсказки и т.п.

Реализуется это всё через классы java.awt.SystemTray и java.awt.TrayIcon. Сначала выполняется проверка, поддерживается ли вообще системный трей - SystemTray.isSupported(). Если да - создается иконка, к ней можно добавить обработчиков мыши, всплывающее меню, подсказку и т.п. Все используемые компоненты - из пакета AWT, а не SWING, но это в принципе логично - мы все-таки работаем не с окном приложения, а с системной частью.

Дальше тоже вроде просто. По клику на кнопку закрытия окна мы прячем форму, по двойному клику на иконке - показываем.

Интереснее становится, когда возникает желание прятать окно и при его минимизации. Вот тут пришлось немного подумать. Нет, с тем, чтобы спрятать, проблем нет. А вот при показе - окно появляется в панели задач, однако на экране - нет. Кликнешь - показалось. Никакие toFront(), requestFocus() и иже с ними ситуацию не спасают.

Что оказалось? У окна есть несколько состояний. В частности - минимизированое оно или нормальное. Поскольку события в UI носят уведомительный характер - слушатель windowIconified срабатывает уже ПОСЛЕ того, как окно схлопнулось. И именно в этом состоянии окно прячется. И, соответственно, в этом состоянии и показывается. Таким образом, чтобы действительно показать окно, необходимо установить нормальное состояние - setState(JFrame.NORMAL).

И вот тут возникает вторая тонкость. Окно может быть также и максимизировано перед тем, как его схлопнули. А если мы выставим нормальное состояние - оно вернется к первоначальным размерам. Что естественным образом нехорошо. К счастью, есть у Frame такая функция как setExtendedState (и, соответственно, getExtendedState для получения состояния). Причем состояния реализованы грамотно - в виде битовых флагов. Первый бит - ICONIFIED, второй - MAXIMIZED_HORIZ, третий - MAXIMIZED_VERT. Таким образом, когда мы схлопываем окно, просто устанавливается первый бит, а два оставшихся остаются неизменными. И если мы просто сбросим первый бит - состояние вернется точно к исходному. А сбросить его можно так: setExtendedState(getExtendedState() & (JFrame.ICONIFIED ^ 0xFFFF)).

Собственно, это все тонкости. Дальше всё прозрачно. Минимальный пример приложения, сворачивающегося в системный трей, приведен ниже. Для его запуска надо прописать имя иконки, которую вы будете использовать. Я использовал вот эту: s.png

package ru.skipy.tests.ui;

import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;

/**
 * FormToTrayMinimizeSample
 *
 * @author Eugene Matyushkin aka Skipy 
 */
public class FormToTrayMinimizeSample extends JFrame {

    /**
     * Constructs frame. Tray icon is constructed only if system tray is supported
     */
    public FormToTrayMinimizeSample() {
        super("Form-to-tray minimize sample");
        Image image = Toolkit.getDefaultToolkit().createImage("s.png");
        setIconImage(image);
        JLabel lbl;
        ActionListener exitAL = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        };
        if (SystemTray.isSupported()) {
            PopupMenu pm = new PopupMenu();
            MenuItem miExit = new MenuItem("Exit");
            miExit.addActionListener(exitAL);
            MenuItem miRestore = new MenuItem("Restore");
            miRestore.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    restoreWindow();
                }
            });
            pm.add(miRestore);
            pm.addSeparator();
            pm.add(miExit);
            lbl = new JLabel("<html><font color=\"blue\">System tray is supported</font></html>");
            SystemTray st = SystemTray.getSystemTray();
            TrayIcon ti = new TrayIcon(image, "Double click to restore window", pm);
            ti.addMouseListener(new TrayMouseListener());
            try {
                st.add(ti);
                addWindowListener(new WindowMinimizeListener());
            } catch (AWTException ex) {
                ex.printStackTrace();
            }
        } else {
            lbl = new JLabel("<html><font color=\"red\">System tray is NOT supported</font></html>");
        }
        lbl.setVerticalAlignment(JLabel.CENTER);
        lbl.setHorizontalAlignment(JLabel.CENTER);
        JButton btn = new JButton("Click to close application");
        btn.addActionListener(exitAL);
        getContentPane().setBackground(Color.white);
        getContentPane().add(lbl, BorderLayout.CENTER);
        getContentPane().add(btn, BorderLayout.SOUTH);
        setSize(300, 100);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
    }

    /**
     * Hides frame
     */
    private void hideWindow() {
        setVisible(false);
    }

    /**
     * Shows frame. Restores frame state (normal or maximized)
     */
    private void restoreWindow() {
        setVisible(true);
        setExtendedState(getExtendedState() & (JFrame.ICONIFIED ^ 0xFFFF));
        requestFocus();
    }

    /**
     * Mouse listener for tray icon. Restores frame on double click.
     */
    class TrayMouseListener extends MouseAdapter {
        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() == 2) {
                restoreWindow();
            }
        }
    }

    /**
     * Window event listener. Hides frame in iconfying and window closing events
     */
    class WindowMinimizeListener extends WindowAdapter {
        @Override
        public void windowClosing(WindowEvent e) {
            hideWindow();
        }
        @Override
        public void windowIconified(WindowEvent e) {
            hideWindow();
        }
    }

    /**
     * Runs test
     *
     * @param args test arguments
     */
    public static void main(String[] args) {
        new FormToTrayMinimizeSample().setVisible(true);
    }
}

Как видите, всё просто. Всем спасибо!

С уважением,
Евгений aka Skipy



Комментарии? Дополнения?

Comments

Лёха
Sep. 27th, 2012 09:31 am (UTC)
в openSUSE restoreWindow() отрабатывает некорректно

Edited at 2012-09-27 09:51 am (UTC)
skipy_ru
Sep. 27th, 2012 11:29 am (UTC)
Ну, я бы сказал, что это все-таки проблема openSUSE. Или JVM под нее. Кстати, "некорректно" - это как?
Лёха
Sep. 27th, 2012 12:44 pm (UTC)
А некорректно это, попытаюсь описать все эти чудеса... Если свернуть окно кнопкой, восстановить его двойным нажатием иконки в трее или выбором в PopupMenu - Restore не получается. В отладке видно, что сначала срабатывает restoreWindow(), затем... внимание - hideWindow(). При установке setExtendedState(JFrame.NORMAL) результат тот же. Но иногда окно все же восстанавливается свернутым в панель задач, тоже не то что ожидалось. В Windows XP все отрабатывает без вопросов. SUSE 11.4, позже испытаю в 12.2, вдруг SUSE...