Данная статья задумана для того, чтобы объяснить на начальном уровне идею менеджеров разметки (layout manager), дать примеры использования основных менеджеров и показать вариант, как можно написать свой собственный менеджер разметки. Все замечания автор с удовольствием принимает.
Итак – зачем нужен Layout Manager?
Если мы вспомним, что язык JAVA изначально создавался как язык, предназначенный для разработки приложений, которые способны работать на различных платформах и, что в данном случае более важно, устройствах, то необходимость такого рода интерфейса становится очевидна. Необходим механизм, который позволит одному и тому же приложению работать при различных разрешениях и при различных формах экрана.
Кто уже создавал различные приложения GUI, тот наверняка сталкивался с проблемами типа: как сделать так, чтобы при изменении размера формы расположенные на ней компоненты каким-либо образом вели себя. Либо пропорционально уменьшались (увеличивались) в размерах, либо оставались такого же размера, проявлялись полосы скроллинга, компоненты как-нибудь перемещались по экрану и так далее. И каждый раз приходилось придумывать некие программные механизмы, которые, по сути, занимались тем, что обрабатывали событие типа «окно изменило свои размеры». А дальше происходило следующее: на основе некого алгоритма программист вынужден менять размеры кнопок, панелей, списков и прочая. Автор сам это делал не один раз и особого восторга от этого не испытывал. Само собой у каждого со временем накапливались некоторые уже стандартные наработки.
Так вот основная идея LayoutManager состоит в том, что программисту предоставляется уже готовые варианты размещения компонентов на экране (это и есть стандартные Layout managers, о которых мы поговорим немного позже), а также дается возможность самому описать механизм (алгоритм) который будет корректно обрабатывать такую ситуацию. Любой контейнер (Container и его подклассы) имеет метод setLayout(LayoutManager mgr). Задавая новый layout manager Вы говорите контейнеру, какой алгоритм он должен использовать при размещении компонентов.
Какие бывают layout manager’ы?
Всего в Java SE (Standart Ediotion) API описано порядка 20 различных layout manager’ов. Мы рассмотрим на взгляд автора наиболее распространенные – задавая различные комбинации менеджеров для контейнеров на Вашей форме, Вы можете добиться практически любого нужного Вам результата. Если Вы считаете, что какой-то неупомянутый менеджер должен быть упомянут – пишите. Этот layout manager будет добавлен.
Прежде чем рассматривать конкретные менеджеры необходимо остановится на следующем моменте – все контейнеры для размещения своих компонент используют характеристики этих компонентов. А фактически — размеры. На основании этих данных и вычисляется расположение компонентов. К методам, которые отвечают за это, относятся:
getMaximumSize() – возвращает максимально допустимый размер компонента.
getMinimumSize() – возвращает минимально допустимый размер компонента.
getPrefferedSize() – возвращает наиболее предпочтительный размер компонента. Для понимания – кнопка вряд ли требует размера гораздо большего, чем надпись.
Если, например, Вам необходимо, чтобы Ваша панель слева имела постоянную ширину – просто переопределите нужные методы у панели, создав свой производный класс, и возвращайте нужную Вам величину. Установленный LayoutManager будет учитывать Ваши пожелания. В примере, где мы создадим свой собственный layout Вы сможете поэкспериментировать с этими методами, удалив и добавив комментарии в указанных местах.
Итак, мы рассмотрим такие менеджеры: FlowLayout, BoxLayout, BorderLayout, GridLayout, CardLayout, GridBagLayout.
FlowLayout – самый простой layout manager. Работает крайне примитивно – просто рисует в строку все компоненты в том порядке, в котором они были помещены в контейнер. Если места в строке уже не хватает, то переносит оставшиеся компоненты на другую строку. Чем-то это напоминает написание текста в текстовом редакторе. Посмотрите пример, в котором на форму кладется просто много кнопок. Если Вы попробуйте поменять размер формы (особенно хорошо видно если менять только ширину) ширину, то увидите, что расположение кнопок будет меняться. Данный layout вряд ли может быть использован при составлении сложных графических интерфейсов. Но он может быть использован для несложных задач выравнивания – данный layout позволяет задать вариант выравнивания (слева, по центру, справа) а также расстояние между компонентами.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import java.awt.*; import javax.swing.*; class FlowLayoutTest extends JFrame { public FlowLayoutTest() { getContentPane().setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); for(int k=0; k<20; k++) { getContentPane().add(new JButton(""+k)); } setBounds(100,100,400,300); } public static void main(String[] args) { FlowLayoutTest flt = new FlowLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
BoxLayout – тоже достаточно простой layout. Позволяет располагать компоненты вдоль одной из осей – вертикально или горизонтально. В конструкторе указывается, какой вариант предпочитает использовать программист. Сам по себе данный layout используется крайне редко, но использовать его в комплексе бывает достаточно удобно. Например, на какой-нибудь панели со скроллингом удобно расположить компоненты вертикально.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import java.awt.*; import javax.swing.*; class BoxLayoutTest extends JFrame { public BoxLayoutTest() { getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); for(int k=0; k<12; k++) { getContentPane().add(new JButton(""+k)); } setBounds(100,100,400,300); } public static void main(String[] args) { BoxLayoutTest flt = new BoxLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
BorderLayout – данный layout уже более сложен. Компоненты в нем располагаются в пяти областях: центр, север, юг, запад, восток. В момент расчета расположения преимущество имеют компоненты на севере и юге. После того, как эти компоненты размещены, layout определяет размещение компонентов на востоке и западе. И уже в последнюю очередь размещается компонент по центру, который занимает все оставшееся пространство. Попробуйте изменять размеры формы до такой степени, чтобы увидеть, кто будет иметь преимущество. Если уменьшить размер очень сильно, то можно увидеть, что в первую очередь «отнимается» пространство у центрального компонента, потом у левого и правого. И в последнюю очередь у нижнего компонента и верхнего. Данный layout достаточно точно отражает поведение многих программ – верхний toolbar, нижняя строка состояния, левая и правая панель навигации и инструментов и наконец центральная рабочая область.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import java.awt.*; import javax.swing.*; class BorderLayoutTest extends JFrame { public BorderLayoutTest() { getContentPane().setLayout(new BorderLayout()); getContentPane().add(new JButton("North"), BorderLayout.NORTH); getContentPane().add(new JButton("South"), BorderLayout.SOUTH); getContentPane().add(new JButton("West"), BorderLayout.WEST); getContentPane().add(new JButton("East"), BorderLayout.EAST); getContentPane().add(new JButton("Center"), BorderLayout.CENTER); setBounds(100,100,400,300); } public static void main(String[] args) { BorderLayoutTest flt = new BorderLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
GridLayout – думаю, что по названию многие могли догадаться, что данный layout располагает компоненты в гриде (таблице). Для примера я расположил двенадцать кнопок в таком вот гриде. Кроме количества ячеек Вы можете задавать отступы компонентов внутри ячейки. Задание таких границ продемонстрировано в примере.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import java.awt.*; import javax.swing.*; class GridLayoutTest extends JFrame { public GridLayoutTest() { getContentPane().setLayout(new GridLayout(4,3, 10, 10)); for(int k=0; k<12; k++) { getContentPane().add(new JButton(""+k)); } setBounds(100,100,400,300); } public static void main(String[] args) { GridLayoutTest flt = new GridLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
CardLayout – размещает все компоненты как карты в колоде. В этом случае только один компонент, который в данный момент является верхним, будет показан на экране. Используя методы этого layout’а – first(), last(), next(), previous() и show() – программист имеет возможность переключаться между компонентами. Данный пример иллюстрирует эти возможности. Достаточно эффективно можно использовать CardLayout совместно с BorderLayout. CardLayout используется для центральной рабочей области, где может быть множество открытых документов, а BorderLayout создает «окружение».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import java.awt.*; import java.awt.event.*; import javax.swing.*; class CardLayoutTest extends JFrame implements ActionListener { JPanel cardPanel = new JPanel(); JButton first = new JButton("First"); JButton last = new JButton("Last"); JButton next = new JButton("Next"); JButton prev = new JButton("Prev"); JButton show = new JButton("Show"); public CardLayoutTest() { cardPanel.setLayout(new CardLayout(10, 10)); for(int k=0; k<12; k++) { cardPanel.add(""+k, new JLabel(""+k, JLabel.CENTER)); } JPanel buttons = new JPanel(); buttons.setLayout(new FlowLayout(FlowLayout.CENTER)); buttons.add(first); first.addActionListener(this); buttons.add(last); last.addActionListener(this); buttons.add(next); next.addActionListener(this); buttons.add(prev); prev.addActionListener(this); buttons.add(show); show.addActionListener(this); getContentPane().add("Center", cardPanel); getContentPane().add("South", buttons); setBounds(100,100,400,300); } public static void main(String[] args) { CardLayoutTest flt = new CardLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } public void actionPerformed(ActionEvent e) { if(e.getSource()==first) { ((CardLayout)cardPanel.getLayout()).first(cardPanel); } if(e.getSource()==last) { ((CardLayout)cardPanel.getLayout()).last(cardPanel); } if(e.getSource()==next) { ((CardLayout)cardPanel.getLayout()).next(cardPanel); } if(e.getSource()==prev) { ((CardLayout)cardPanel.getLayout()).previous(cardPanel); } if(e.getSource()==show) { String answer = JOptionPane.showInputDialog("Input number: 0-11"); if(answer!=null) { ((CardLayout)cardPanel.getLayout()).show(cardPanel, answer); } } } } |
GridBagLayout – вариант достаточно сложного управления размещением компонентов. В данном менеджере используются специальные правила, которые задаются при добавлении каждого компонента. При прорисовке именно эти правила определяют, как будет вести себя тот или иной компонент. Поэтому хотелось бы предварительно описать этот важный класс. Его название – GridBagConstraints. Рассмотрим подробно важные поля этого класса, а потом приведем пример использования:
gridx, gridy – как Вы уже догадались из названия данного менеджера, он фактически оперирует тоже с набором ячеек подобно GridLayout, только более гибко. Эти два поля определяют фактически ячейку, куда будет помещен лидирующий угол компонента. Как можно будет увидеть чуть ниже, компонент может занимать несколько ячеек, как по горизонтали, так и по вертикали. Лидирующий угол зависит от того, какую ориентацию имеет компонент. Ориентация зависит от языковых установок. Русский язык предусматривает написание слева направо. А арабский – справа налево. Для ориентации слева направо лидирующий угол – верхний левый. Для ориентации справа налево – верхний правый. Как Вы наверно догадались, сами ячейки также будут иметь разный порядок нумерации – справа налево и вниз или слева направо и вниз.
gridwidth, gridheight – эти поля определяют, сколько ячеек будет занимать компонент (по горизонтали и по вертикали соответственно) в ячейках.
fill – компонент может занимать как все отведенное ему пространство, так и не все — это зависит от его размера. Данное поле как раз предназначено для того, чтобы определить, как компонент будет занимать все отведенные для него ячейки. Всего определено четыре константы: NONE – компонент не будет покрывать все пространство, HORIZONTAL – компонент будет растянут на всю ширину, VERTICAL – компонент будет растянут на всю высоту, BOTH – компонент будет растянут на всю ширину и всю высоту.
ipadx, ipady – данные поля фактически определяют, сколько пикселей будет прибавлено к минимальному размеру компонента. Т.е. фактически компонент становится больше на 2*ipadx пикселей по горизонтали и на 2*ipady пикселей по вертикали.
insets – это поле выполняет функцию подобно ipadx и ipady, только оно определяет ширину границы между краем компонента и краем области, которую он будет занимать. Интересно отметить, что данное поле может иметь и отрицательное значение. В этом случае компонент будет как бы «вылезать» за границы своей области.
anchor – в том случае, если компонент несколько меньше области, в которой он располагается, он может фактически «прилипать» к разным сторонам или быть в центре. Для такого поведения определено достаточно много констант, и их значения можно посмотреть в описании класса GridBagConstraints. Константы описывают, как абсолютное поведение компонента в своей области, так и относительное, которое зависит от предыдущих компонентов и положения самого компонента на форме.
weightx, weighty – в принципе название отражает суть. Это «вес» компонента, который показывает, как будут распределяться компоненты. Чем выше вес у компонента, тем больше места он пытается занять при увеличении размера контейнера. Т.е. если один компонент имеет «вес» 0, а второй 1, то второй при увеличении формы будет занимать все доступное новое пространство, а первый останется прежнего размера. Если все компоненты имеют «вес» равный нулю (так оно и есть по умолчанию), то будет просто появляться дополнительное пространство между ними.
Я не стал сильно мудрить и привел пример из Java API – пример очень наглядный и Вы можете просто попробовать поиграть с разными значениями (я кое-где поставил комментарии для этого)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
import java.awt.*; import javax.swing.*; class GridBagLayoutTest extends JFrame { public GridBagLayoutTest() { GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints c = new GridBagConstraints(); getContentPane().setLayout(gridbag); c.fill = GridBagConstraints.BOTH; // Попробуйте убрать эту строку c.weightx = 1.0; makebutton("Button1", gridbag, c); makebutton("Button2", gridbag, c); makebutton("Button3", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //конец строки makebutton("Button4", gridbag, c); c.weightx = 0.0; // вернуть к значению по умолчанию makebutton("Button5", gridbag, c); //другая строка // Попробуйте закрыть эту строку и открыть следующюю за ней c.gridwidth = GridBagConstraints.RELATIVE; //предпоследний элемент //c.gridwidth = 1; makebutton("Button6", gridbag, c); c.gridwidth = GridBagConstraints.REMAINDER; //конец строки makebutton("Button7", gridbag, c); c.gridwidth = 1; //установить значение по умолчанию c.gridheight = 2; // Попробуйте убрать эту строку c.weighty = 1.0; makebutton("Button8", gridbag, c); // Попробуйте убрать эту строку c.weighty = 0.0; //установить значение по умолчанию c.gridwidth = GridBagConstraints.REMAINDER; //конец строки c.gridheight = 1; //установить значение по умолчанию makebutton("Button9", gridbag, c); makebutton("Button10", gridbag, c); setBounds(100,100,600,400); } protected void makebutton(String name, GridBagLayout gridbag, GridBagConstraints c) { Button button = new Button(name); gridbag.setConstraints(button, c); getContentPane().add(button); } public static void main(String[] args) { GridBagLayoutTest flt = new GridBagLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
Также хотелось бы упомянуть возможность задания абсолютных координат без каких-либо layout вообще. Насколько этот вариант подходит – решать Вам. В примере можно видеть, что кнопки будут находиться на своих местах независимо от того, как меняются размеры окна.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import java.awt.*; import javax.swing.*; class NullLayoutTest extends JFrame { public NullLayoutTest() { getContentPane().setLayout(null); for(int k=0; k<12; k++) { JButton tmp = new JButton(""+k); tmp.setBounds(10, k*30, 50, 25); getContentPane().add(tmp); } setBounds(100,100,400,300); } public static void main(String[] args) { NullLayoutTest flt = new NullLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
А теперь давайте рассмотрим вариант создания нестандартного layout. Любой layout должен просто реализовать необходимые методы. Вот они:
— public void addLayoutComponent(String name, Component comp)
— public void removeLayoutComponent(Component comp)
— public Dimension minimumLayoutSize(Container parent)
— public Dimension preferredLayoutSize(Container parent)
— public void layoutContainer(Container parent)
Наиболее важным является последний метод. Он как раз и занимается тем, что «расставляет» все элементы в том порядке, в котором нам надо.
Я написал в качестве примера простой layout, который располагает свои элементы вдоль диагонали.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import java.awt.*; import javax.swing.*; class DiagLayoutTest extends JFrame { public DiagLayoutTest() { getContentPane().setLayout(new DiagLayout()); for(int k=0; k<5; k++) { getContentPane().add(new JButton(""+k)); } for(int k=0; k<5; k++) { getContentPane().add(new JLabel(""+k, JLabel.CENTER)); } setBounds(100,100,600,400); } public static void main(String[] args) { DiagLayoutTest flt = new DiagLayoutTest(); flt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); flt.setVisible(true); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
// Вот наш собственный layout import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.LayoutManager; import java.awt.Rectangle; class DiagLayout implements LayoutManager { // Эти методы нас не интересуют и их можно сделать пустыми public void addLayoutComponent(String name, Component comp) {} public void removeLayoutComponent(Component comp) {} public Dimension minimumLayoutSize(Container parent) { return computeLayoutSize(parent); } public Dimension preferredLayoutSize(Container parent) { return computeLayoutSize(parent); } private Dimension computeLayoutSize(Container parent) { int prefWidth = 0; int prefHeight = 0; Component[] components = parent.getComponents(); for(int k=0; k<components.length; k++) { prefWidth += components[k].getWidth(); prefHeight += components[k].getHeight(); } return new Dimension(prefWidth, prefHeight); } // Вот наш фактически самый главный метод. Здесь мы располагаем компоненты // по диагонали public void layoutContainer(Container parent) { // Получаем список компонентов Component[] components = parent.getComponents(); int row = 0; int col = 0; // Эти две строки можно закрыть комментариями (см. замечание ниже) int width = parent.getWidth()/components.length; int height = parent.getHeight()/components.length; for(int k=0; k<components.length; k++) { // Вы можете снять комментарии здесь и поставить их двумя строками // выше и увидите разницу //int width = (int)(components[k].getPreferredSize().getWidth()); //int height = (int)(components[k].getPreferredSize().getHeight()); // Определяем местоположение компонента и его размеры Rectangle r = new Rectangle(col, row, width, height); // Устанавливаем его components[k].setBounds(r); // Заготавливаем координаты следующего компонента col += width; row += height; } } } |