Мир ПК, #01/1999
Постоянный адрес статьи: http://www.osp.ru/pcworld/1999/01/137.htm

Библиотека Swing: многослойные панели и вложенные окна

Дмитрий Рамодин

16.01.1999

Многооконный (многодокументный) интерфейс знаком подавляющему большинству пользователей Windows. С помощью MDI (Multi Document Interface) выполнены такие известные программы, как Microsoft Word, CorelDRAW!, Adobe Photoshop, да мало ли еще что!

Технология Java изначально сторонилась применения MDI, но только до тех пор, пока не появилась библиотека Swing, где этот интерфейс представляют три класса: JLayeredPane, JDesktopPane и JInternalFrame. Однако в разработках чаще всего используются только два последних. Дело в том, что JDesktopPane является потомком JLayeredPane, поэтому все поля и методы этого класса наследуются, а его собственные расширяют функциональный потенциал. Например, JDesktopPane закрашивает фон, а JLayeredPane ≈ нет.

Внутри класса JInternalFrame есть еще один класс JdesktopIcon, представляющий на экране дочерние окна, свернутые в пиктограмму. Этот класс вызывает значительный интерес, но разработчики Swing пока рекомендуют не использовать его, так как в самое ближайшее время он будет радикально изменен. Впрочем, для того, чтобы освоить создание MDI-приложений на базе Swing, достаточно разобраться с JDesktopPane и JInternalFrame.

Класс JDesktopPane можно условно представить как окно, набранное из множества "стеклянных" слоев. Замечательно, что можно располагать дочерние окна между "стеклами", причем слоями, да еще так, как заблагорассудится: все на одном слое, все на разных слоях или по нескольку окон на каждом слое. Если расположить дочерние окна на одном слое, то каждое окно, на котором пользователь щелкает мышью, будет переноситься на передний план. Окна же, размещенные на разных слоях, никогда не перетасовываются. Лежащее на ближнем к пользователю слое, оно всегда будет самым ближним к пользователю, и единственный способ увидеть его на удаленном слое ≈ сдвинуть на участок экрана, не занятый самым верхним окном. Это замечательное свойство классов JDesktopPane и JLayeredPane позволяет с легкостью создавать "плавающие" окна≈палитры инструментов (которые никогда не перекрываются рабочими окнами), всплывающие модальные панели сообщений и прочие окна, если по роду своей деятельности они не должны быть ничем перекрыты.

Добавляя дочернее окно в экземпляр JDesktopPane (или JLayeredPane), программист задает номер слоя. Самый дальний слой имеет номер 0. Чем больше номер слоя, тем ближе он к пользователю. При этом не обязательно нумеровать слои начиная с 0 и не обязательно присваивать им номера, идущие подряд. Достаточно знать, что окно на слое с большим номером перекроет окно, помещенное на слой с более низким номером.

Существуют два способа задать слой для добавляемого дочернего окна. Можно установить номер слоя как второй параметр метода add класса JDesktopPane, а первым параметром будет ссылка на добавляемый объект. Кстати, совсем не обязательно, чтобы последним было окно класса JInternalFrame. Эту роль может выполнять любой компонент.

Другой способ задания номера слоя ≈ использование метода setLayer, первый аргумент которого является ссылкой на помещаемый объект, а второй ≈ это номер слоя. Маленькое замечание: применяя метод setLayer, задавайте номер слоя как число типа int.

Многослойные панели имеют несколько предопределенных констант, задающих номера часто используемых в разработке слоев:

DEFAULT_LAYER ≈ обычные компоненты и окна должны добавляться на этот слой;

PALETTE_LAYER ≈ на этом слое целесообразно размещать окна палитр инструментов и т. п.;

MODAL_LAYER ≈ этот слой выделен для показа модальных диалоговых панелей;

POPUP_LAYER ≈ различные всплывающие окна (раскрывающиеся списки, всплывающие подсказки и т. п.) лучше всего помещать сюда;

DRAG_LAYER ≈ самый верхний слой отдан для перетаскиваемых с места на место элементов.

Если вы используете метод add, номер слоя должен быть передан как экземпляр объекта Integer. Это связано с тем, что у базового класса Container нужный нам метод имеет следующий прототип:

public void add(Component comp, Object constraints)

Если же совершить ошибку и передать аргумент не Integer, а int, то получится вызов совсем другого метода:

public Component add(Component comp, int index)

В результате вместо номера слоя вы зададите порядковый номер окна внутри своего слоя. Это, кстати, тоже может быть сделано, если вы примените для добавления в панель класса JDesktopPane дочернего окна метод add с тремя аргументами. Первые два вы уже знаете, а третий аргумент метода ≈ порядковый номер в иерархии окон на том слое, в который добавляется окно. Присваивая номера позиций добавляемым компонентам, легко запутаться. Хотя в документации есть полезная информация, вы не найдете там правильного способа нумерации позиций. Так, больший порядковый номер компонента, в отличие от номера слоя, обозначает более удаленный от зрителя компонент, т. е. компонент с номером 1 располагается дальше компонента с номером 0. К примеру, вы добавляете четыре окна и хотите, чтобы они шли в обратном порядке. Поэтому вы задаете номера 3, 2, 1, 0. По идее все должно быть корректно. А вот и нет! Все добавляемые окна перемешаются. Оказывается, нумерацию каждого нового окна необходимо вести с учетом уже добавленных компонентов. Поэтому корректной будет последовательность 0, 0, 0, 0 (вспомните, что 0 ≈ это самая ближняя к зрителю позиция). И так далее.

Можно воспользоваться еще парой полезных методов: moveToFront и moveToBack. Первый из них переносит выбранный компонент в самую ближнюю к пользователю позицию на его слое, а второй, наоборот, прячет выбранный компонент за другими компонентами на его слое.

Рассмотрим небольшой пример, демонстрирующий создание приложений с интерфейсом MDI. Это будет окно с многослойной панелью (JDesktopPane) меню, через которое можно показать дочерние окна (JInternalFrame) на одном слое и на разных слоях. Должен быть предусмотрен пункт меню для удаления всех имеющихся дочерних окон. Создайте файл Sample.java и начинайте набирать исходный текст программы. Для удобства фрагменты листинга перемежаются комментариями.

Сначала необходимо импортировать нужные классы:

import com.sun.java.swing.*;
import java.awt.*;
import java.awt.event.*;

Теперь мы создаем класс-потомок от JInternalFrame. Это необходимо потому, что нам нужно изменить наполнение дочерних окон по умолчанию, а это можно сделать, создав свой собственный класс ChildWindow:

class ChildWindow extends JInternalFrame
{

Отличительной особенностью класса ChildWindow является его конструктор, который помимо параметра title, задающего заголовок создаваемого окна, имеет параметр type, определяющий тип окна и параметр iconFileName, с помощью которого можно задать пиктограмму для рамки создаваемого окна:

  
public ChildWindow(String title, int type, String iconFileName)
  {
    super(title);
    if((type & RESIZABLE) != 0) setResizable(true);
    if((type & MAXIMIZABLE) != 0) setMaximizable(true);
    if((type & ICONIFIABLE) != 0) setIconifiable(true);
    if((type & CLOSABLE) != 0) setClosable(true);
    if(iconFileName != null) setFrameIconnew ImageIcon(iconFileName));

Как видно из этого фрагмента, в конструкторе создается новое дочернее окно и в зависимости от флагов, переданных в параметре type, вызываются следующие стандартные методы класса JInternalFrame: setResizable, setMaximizable, setIconifiable и setClosable. Эти методы, получив в единственном параметре значение true, разрешают окну изменять размер, максимизироваться, минимизироваться, превращаясь в пиктограмму, или закрываться соответственно. При этом в заголовке окна появятся соответствующие кнопки. Обратите внимание, что если параметр конструктора iconFileName не пустой, то на рамке окна появится заданная пользователем пиктограмма, для чего задействуется метод setFrameIcon класса JInternalFrame.

Следующее, что делает конструктор, ≈ задает случайный цвет фона для окна, пользуясь для этого функцией random математического аппарата Java:

    getContentPane().setBackground(
        new Color((float) Math.random(),
          (float) Math.random(),
          (float) Math.random()));
  }

Для удобства задания флагов в параметре type конструктора ChildWindow в этом классе определены несколько полезных хорошо читаемых констант:

  public static final int REGULAR     = 0;
  public static final int CLOSABLE    = 1;
  public static final int ICONIFIABLE = 2;
  public static final int MAXIMIZABLE = 4;
  public static final int RESIZABLE   = 8;
}

А вот и сама программа. Помимо описания класса Sample мы создаем переменную≈экземпляр класса многослойной панели JdesktopPane:

public class Sample extends JFrame
{
  private JDesktopPane dp = new JDesktopPane();

После чего создаются и инициализируются элементы меню. Для корректного воспроизведения русскоязычных надписей все они выполнены в кодировке Unicode:

  private JMenuBar bar = new JMenuBar();
  private JMenu menu = new JMenu(■\u041e\u043a\u043d\u0430■);
  private JMenuItem difLayersItem = new JMenuItem(■\u043d\u0430 \u0440\u0430\u0437\u043d\u044b\u0445 ■ + ■\u0441\u043b\u043e\u044f\u0445■);
  private JMenuItem singleLayerItem = new JMenuItem(■\u043d\u0430 \u043e\u0434\u043d\u043e\u043c ■ + ■\u0441\u043b\u043e\u0435■);
  private JMenuItem resetItem = new JMenuItem(■\u0421\u0431\u0440\u043e\u0441!■);

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

  public Sample(String caption)
  {
    super(caption);
    addWindowListener(new WA());
    difLayersItem.addActionListener(new AL());
    singleLayerItem.addActionListener(new AL());
    resetItem.addActionListener(new AL());

Для красоты зададим внешний вид программы согласно предопределенной настройке MetalLookAndFeel. И не забудем добавить многослойную панель в окно:

    try
    {
      UIManager.setLookAndFeel(■com.sun.java.swing.plaf.metal.MetalLookAndFeel■);
    }
    catch(Exception e){e.printStackTrace();};
    getContentPane().add(dp);

В завершение работы конструктор собирает меню из отдельных элементов и подключает его к главному окну программы:

    menu.add(difLayersItem);
    menu.add(singleLayerItem);
    menu.addSeparator();
    menu.add(resetItem);
    bar.add(menu);
    setJMenuBar(bar);
  }

Большая часть работы нашего примера падает на "плечи" метода createChildWindows. Его единственный параметр служит семафором, определяющим, как создавать дочерние окна: на одном слое или на разных. Если параметр равен true, то генерируется массив layers объектов Integer со значениями 0. Если же параметр равен false, то в массив будут загружены объекты Integer с различными значениями:

  void createChildWindows(boolean onSameLayer)
  {
    Integer[] layers = new Integer[4];
    if(onSameLayer == true)
        for(int i = 0; i < 4; i++)
           layers[i] = new Integer(0);
    else for(int i = 0; i < 4; i++)
           layers[i] = new Integer(i * 2);

Значения этого массива в дальнейшем будут определять, на какой слой добавить новое дочернее окно. Обратите внимание, что при использовании различных слоев номера генерируются выражением i * 2. Это иллюстрирует утверждение, что номера слоев могут идти непоследовательно.

Теперь создаем окно, для которого разрешаем операцию изменения размера, ему будет присвоена собственная пиктограмма с изображением красного светодиода. Имя файла берется произвольно. Главное, чтобы файл пиктограммы, имя которого вы добавите в исходный текст, существовал:

    ChildWindow child = null;
    child = new ChildWindow(■\u042f - \u043e\u043a\u043d\u043e 1■,ChildWindow.REGULAR | ChildWindow.RESIZABLE,■led_red.gif■);
    child.setBounds(30,30,250,250);
    dp.add(child, layers[0], -1);

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

Следующее дочернее окно может быть закрыто по команде пользователя и ложится ближе к наблюдателю, чем предыдущее:

    child = new ChildWindow(■\u042f - \u043e\u043a\u043d\u043e 2■,ChildWindow.CLOSABLE, null);
    child.setBounds(80,80,250,250);
    dp.add(child, layers[1], 0);

Третье окно может сворачиваться в пиктограмму и занимает позицию 1 (т. е. следующее после предыдущего):

    child = new ChildWindow("\u042f - \u043e\u043a\u043d\u043e 3■,ChildWindow.ICONIFIABLE, null);
    child.setBounds(130,130,250,250);
    dp.add(child, layers[2], 1);

И наконец, четвертое, последнее окно может быть распахнуто, а его позиция 0 говорит о том, что оно становится самым ближним к зрителю, отодвинув назад три предыдущих окна.

Вот так хитро приходится размещать окна, чтобы добиться их корректного местоположения. В итоге окна встанут в следующем порядке: 4, 2, 3, 1 (если начинать с самого ближнего к пользователю):

    child = new ChildWindow(■\u042f - \u043e\u043a\u043d\u043e 4■,ChildWindow.MAXIMIZABLE, null);
    child.setBounds(180,180,250,250);
    dp.add(child, layers[3], 0);
  }

Конечно, это размещение по позициям имеет смысл, только если окна располагаются на одном и том же слое. Иначе значения позиций просто игнорируются.

Для удаления всех окон из многослойной панели имеется следующий метод:

  public void resetAll()
  {
    dp.removeAll();
    dp.repaint();
  }

Сначала он вызывает стандартный метод removeAll, удаляющий все компоненты на всех слоях панели, после чего производит перерисовку.

Два внутренних класса-слушателя отвечают за закрытие главного окна программы и работу меню:

  class WA extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      System.exit(0);
    }
  }
  class AL implements ActionListener
  {
    public void actionPerformed(ActionEvent e)
    {

Этот фрагмент листинга показывает разбор выбранного пункта меню и запуск метода createChildWindows со значением параметра false или true. Выбор ≈ создавать окна на одном слое или же на разных ≈ происходит так:

      JMenuItem item = (JMenuItem) e.getSource();
      if(item == resetItem) resetAll();
      else if(item == difLayersItem)createChildWindows(false);
      else if(item == singleLayerItem)createChildWindows(true);
    }
  }
  public static void main(String[] args)
  {
    Sample s = new Sample(■\u0411\u0438\u0431\u043b\u0438\u043e\u0442■ + ■\u0435\u043a\u0430 Swing■);
    s.setSize(500,500);
    s.setVisible(true);
  }
}

И наконец, вы видите стандартную точку входа main, задающую размер окна и его заголовок.

Наверное, вы уже заметили, что главное окно нашего примера наследуется от JFrame. В этом заключается маленькая хитрость. Дело в том, что окна-наследники класса JFrame уже имеют в своем составе экземпляр класса JLayeredPane! Это значит, что вам нет нужды создавать объект класса многослойной панели. Можно получить ссылку на уже имеющуюся, вызвав метод getLayeredPane. Мы же пошли по более сложному пути, чтобы показать технику программирования MDI-приложений.

Меню программы показано на рис. 1.
Если воспользоваться меню, то работающая программа будет выглядеть примерно так, как показано на рис. 2.
Попробуйте минимизировать окно и посмотрите, как выглядит пиктограмма для дочернего окна (рис. 3).

Чтобы разобраться в многослойной системе, поэкспериментируйте с нашим примером, щелкая на окнах и их пиктограммах. И естественно, у вас есть необъятное поле для создания своих MDI-примеров, может даже намного лучше того, что мы с вами создали.

Мир ПК, #01/1999
Постоянный адрес статьи: http://www.osp.ru/pcworld/1999/01/137.htm