Part III - Advanced Topics
In chapters 15 through 21 we discuss the most advanced Swing components and the classes and interfaces that support them. We start with
JLayeredPane in chapter 15, and implement our own MDI internal frame component from scratch. Chapter 16 is about JDesktopPane and JIntenalFrame, the MDI components that ship with Swing. This chapter culminates with the implementation of a multi-user networked desktop environment. Chapters 17 and 18 discuss the powerful and intricate tree and table components. Among other examples, we show how to build a directory browser using the tree component, and a sortable, JDBC-aware stocks application using the table component. Chapter 19 continues with text component coverage whe! re chapter 11 left off, and discusses them at a much lower level. Chapter 20 is the most advanced chapter in this book and presents a complete RTF word processor application using JTextPane and several powerful custom dialogs used to manage fonts, paragraph formatting, find and replace, and spell checking. Chapter 21 discusses the pluggable look-and-feel architecture in detail and presents the contstruction of our own custom LookAndFeel implementation. This chapter includes examples showing how to implement custom component support for existing and third party L&Fs, and custom L&F support for both existing and custom components.Chapter 15. Layered Panes and custom MDI
In this chapter:
15.1 JLayeredPane
class javax.swing.JLayeredPane
JLayeredPane
is one of the most powerful and robust components in the Swing package. It is a container with a practically infinite number of layers in which components can reside. Not only is there no limit to the number or type of components in each layer, but components can overlap one another.Components within each layer of a
JLayeredPane are organized by position. When overlapping is necessary those components with a higher valued position are displayed under those with a lower valued position. However, components in higher layers are displayed over all components residing in lower layers. It is important to get this overlapping hierarchy down early, as it can often be confusing.Position is numbered from -1 to the number of components in the layer minus one. If we have N components in a layer then the component at position 0 will overlap the component at position 1, and the component at position 1 will overlap the component at position 2, etc. The lowest position is N-1. Note that position -1 represents the same position as N-1. Figure 15.1 illustrates position within a layer.
Figure 15.1 Position of components within a layer.
<<file figure15-1.gif>>
The layer a component resides at is often referred to as its depth. (Note that heavyweight components cannot conform to this notion of depth--see chapter 1 for more about this.) Each layer is represented by an
Integer object (whereas the position of a component within each layer is represented by an int value). The JLayeredPane class defines six different Integer object constants representing, what are intended to be, commonly used layers:FRAME_CONTENT_LAYER
, DEFAULT_LAYER
, PALETTE_LAYER
, !
FONT>MODAL_LAYER
, POPUP_LAYER
, and DRAG_LAYER
.
Figure 15.2 illustrates the six standard layers and their overlap hierarchy.
Figure 15.2. The JLayeredPane standard layers
<<file figure15-2.gif>>
We have discussed a component’s layer and position within a layer. There is also another value associated with each component within a
JLayeredPane. This value is called the index. The index is the same as position if we were to ignore layers. That is, components are assigned indices by starting from position 0 in the highest layer and counting upward in position and downward in layer until all layers have been exhausted. The lowest component in the lowest layer will have index M-1, where M is the total number of components in the JLayeredPane. Similar to position, an index of -1 means the bottom-most component. (Note that the index is really a combination a component’s layer and position. As such there are no methods to directly change a component’s index within JLayeredPane. Although we can always query a component for its current index.)There are three ways to add a component to a
JLayeredPane. (Note that there is no add method defined within JLayeredPane itself.) Each method used to add a component to a JLayeredPane is defined within the Container class (see API docs).add(Component component)
method. This places the component in the layer represented by the Integer object with value 0, the DEFAULT_LAYER.
add(Component component, Object obj)
method. We pass this method our component and an Integer object representing the desired layer. For layer 10 we would pass it: new Integer(10)
. If we wanted to place it on one of the standard layers, for instance the POPUP_LAYER, we could instead pass it JLayeredPane.POPUP_LAYER
.
add(Component component, Object obj, int index)
method. Where the object is specified as above, and the int is the value representing the component’s position with the layer.
15.2 Using JLayeredPane to enhance interfaces
As we mentioned early in chapter 4,
JLayeredPane can sometimes come in handy when we want to manually position and size components. Because it’s layout is null, it is not prone to the effects of resizing. Thus, when its parent is resized, a layered pane’s children will stay in the same position and maintain the same size. (We saw a simple example of this in the beginning of chapter 6.) However, there are other ways to use JLayeredPane in typical interfaces. For instance, we can easily place a nice background image behind all of our components, giving life to an otherwise dull-looking panel.Figure 15.3. The JLayeredPane standard layers
<<file figure15-3.gif>>
The Code: TestFrame.java
see \Chapter15\1
import javax.swing.*; import java.awt.*; import java.awt.event.*; public class TestFrame extends JFrame { public TestFrame() { super("JLayeredPane Demo"); setSize(256,256); JPanel content = new JPanel(); content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS)); content.setOpaque(false); JLabel label1 = new JLabel("Username:"); label1.setForeground(Color.white); content.add(label1); JTextField field = new JTextField(15); content.add(field); JLabel label2 = new JLabel("Password:"); label2.setForeground(Color.white); content.add(label2); JPasswordField fieldPass = new JPasswordField(15); content.add(fieldPass); getContentPane().setLayout(new FlowLayout()); getContentPane().add(content); ((JPanel)getContentPane()).setOpaque(false); ImageIcon earth = new ImageIcon("earth.jpg"); JLabel backlabel = new JLabel(earth); getLayeredPane().add(backlabel, new Integer(Integer.MIN_VALUE)); backlabel.setBounds(0,0,earth.getIconWidth(), earth.getIconHeight()); WindowListener l = new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }; addWindowListener(l); setVisible(true); } public static void main(String[] args) { new TestFrame(); } }
Most of this code should look familiar. We extend
JFrame and create a new JPanel with a y-oriented BoxLayout. We make this panel non-opaque so our background image will show through, then we add four simple components: two JLabels, a JTextField, and a JPasswordField. We then set the layout of the contentPane to FlowLayout (remember that the contentPane has a BorderLayout by default), and add our panel to it. We also set the contentPane’s opaque property to false ensuring that our background will show through this panel as well. Finally we create a JLabel containing our background image, add it to our JFrame’s layeredPane, and set its bounds based on the background image’s size.15.3 Creating a custom MDI: part I - Dragging panels
JDesktopPane
, Swing’s version of a multiple document interface (MDI), is the prime example of a complicated layering environment derived from JLayeredPane. One of the best ways to fully leverage the power of JLayeredPane is through the construction of our own MDI environment from scratch. We will end up with a powerful desktop environment that we understand from the inside out and have complete control over. The central component we will be developing, InnerFrame, will ultimately immitate the functionality of JInternalFrame (discussed in the next chapter). In chapter 21, we will continue to develop ! this component by adding support for all the major look-and-feels, as well as our own custom look-and-feel. The next five sections are centered around this component’s construction and proceed in a stepwise fashion.
Note: Due to the complexity of these examples, we suggest you have some time on your hands before attempting to understand each part. Some fragments are too mathematically dense (although not rigorous by any means) to warrant exaustive explanation. In these cases we expect the reader to either trust that it does what we say it does, or bite the bullet and plunge through the code.
We start our construction of
InnerFrame by implementing movable, closeable, and iconifiable functionality. LayeredPaneDemo is a simple class we will use throughout each stage of development (with very few modifications) to load several InnerFrames and place them in a JLayeredPane.Figure 15.4. Custom MDI: part I
<<file figure15-4.gif>>
The Code: LayeredPaneDemo.java
see \Chapter15\2
import java.awt.*; import java.awt.event.*; import javax.swing.*; import mdi.*; public class LayeredPaneDemo extends JFrame { public LayeredPaneDemo() { super("Custom MDI: Part I"); setSize(570,400); getContentPane().setBackground(new Color(244,232,152)); getLayeredPane().setOpaque(true); InnerFrame[] frames = new InnerFrame[5]; for(int i = 0; i < 5; i++) { frames[i] = new InnerFrame("InnerFrame " + i); frames[i].setBounds(50+i*20, 50+i*20, 200, 200); getLayeredPane().add(frames[i]); } WindowListener l = new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }; Dimension dim = getToolkit().getScreenSize(); setLocation(dim.width/2-getWidth()/2, dim.height/2-getHeight()/2); ImageIcon image = new ImageIcon("spiral.gif"); setIconImage(image.getImage()); addWindowListener(l); setVisible(true); } public static void main(String[] args) { new LayeredPaneDemo(); } }
The Code: InnerFrame.java
see \Chapter15\2\mdi
package mdi; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.EmptyBorder; public class InnerFrame extends JPanel { private static String IMAGE_DIR = "mdi" + java.io.File.separator; private static ImageIcon ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"iconize.gif"); private static ImageIcon RESTORE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"restore.gif"); private static ImageIcon CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"close.gif"); private static ImageIcon PRESS_CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressclose.gif"); private static ImageIcon PRESS_RESTORE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressrestore.gif"); private static ImageIcon PRESS_ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressiconize.gif"); private static final int WIDTH = 200; private static final int HEIGHT = 200; private static final int TITLE_BAR_HEIGHT = 25; private static Color TITLE_BAR_BG_COLOR = new Color(108,190,116); private String m_title; private JLabel m_titleLabel; private boolean m_iconified; private JPanel m_titlePanel; private JPanel m_contentPanel; private JPanel m_buttonPanel; private JPanel m_buttonWrapperPanel; private InnerFrameButton m_iconize; private InnerFrameButton m_close; public InnerFrame(String title) { m_title = title; setLayout(new BorderLayout()); createTitleBar(); m_contentPanel = new JPanel(); add(m_titlePanel, BorderLayout.NORTH); add(m_contentPanel, BorderLayout.CENTER); } public void toFront() { if (getParent() instanceof JLayeredPane) ((JLayeredPane) getParent()).moveToFront(this); } public void close() { if (getParent() instanceof JLayeredPane) { JLayeredPane jlp = (JLayeredPane) getParent(); jlp.remove(InnerFrame.this); jlp.repaint(); } } public void setIconified(boolean b) { m_iconified = b; if (b) { setBounds(getX(), getY(), WIDTH, TITLE_BAR_HEIGHT); m_iconize.setIcon(RESTORE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_RESTORE_BUTTON_ICON); } else { setBounds(getX(), getY(), WIDTH, HEIGHT); m_iconize.setIcon(ICONIZE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON); revalidate(); } } public boolean isIconified() { return m_iconified; } //////////////////////////////////////////// //////////////// Title Bar ///////////////// //////////////////////////////////////////// // create the title bar m_titlePanel public void createTitleBar() { m_titlePanel = new JPanel() { public Dimension getPreferredSize() { return new Dimension(InnerFrame.WIDTH, InnerFrame.TITLE_BAR_HEIGHT); } }; m_titlePanel.setLayout(new BorderLayout()); m_titlePanel.setOpaque(true); m_titlePanel.setBackground(TITLE_BAR_BG_COLOR); m_titleLabel = new JLabel(m_title); m_titleLabel.setForeground(Color.black); m_close = new InnerFrameButton(CLOSE_BUTTON_ICON); m_close.setPressedIcon(PRESS_CLOSE_BUTTON_ICON); m_close.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.close(); } }); m_iconize = new InnerFrameButton(ICONIZE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON); m_iconize.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.setIconified(!InnerFrame.this.isIconified()); } }); m_buttonWrapperPanel = new JPanel(); m_buttonWrapperPanel.setOpaque(false); m_buttonPanel = new JPanel(new GridLayout(1,2)); m_buttonPanel.setOpaque(false); m_buttonPanel.add(m_iconize); m_buttonPanel.add(m_close); m_buttonPanel.setAlignmentX(0.5f); m_buttonPanel.setAlignmentY(0.5f); m_buttonWrapperPanel.add(m_buttonPanel); m_titlePanel.add(m_titleLabel, BorderLayout.CENTER); m_titlePanel.add(m_buttonWrapperPanel, BorderLayout.EAST); InnerFrameTitleBarMouseAdapter iftbma = new InnerFrameTitleBarMouseAdapter(this); m_titlePanel.addMouseListener(iftbma); m_titlePanel.addMouseMotionListener(iftbma); } // title bar mouse adapter for frame dragging class InnerFrameTitleBarMouseAdapter extends MouseInputAdapter { InnerFrame m_if; int m_XDifference, m_YDifference; boolean m_dragging; public InnerFrameTitleBarMouseAdapter(InnerFrame inf) { m_if = inf; } public void mouseDragged(MouseEvent e) { if (m_dragging) m_if.setLocation(e.getX()-m_XDifference + getX(), e.getY()-m_YDifference + getY()); } public void mousePressed(MouseEvent e) { m_if.toFront(); m_XDifference = e.getX(); m_YDifference = e.getY(); m_dragging = true; } public void mouseReleased(MouseEvent e) { m_dragging = false; } } // custom button class for title bar class InnerFrameButton extends JButton { Dimension m_dim; public InnerFrameButton(ImageIcon ii) { super(ii); m_dim = new Dimension(ii.getIconWidth(), ii.getIconHeight()); setOpaque(false); setContentAreaFilled(false); setBorder(null); } public Dimension getPreferredSize() { return m_dim; } public Dimension getMinimumSize() { return m_dim; } public Dimension getMaximumSize() { return m_dim; } } }
Understanding the Code:
Class LayeredPaneDemo
We are familiar with most of the code in this class. Note that the
mdi package is imported, which is the package InnerFrame resides in. The only purpose of LayeredPaneDemo is to provide a container for several InnerFrames. An array of five InnerFrames is created and they are placed in our JFrame’s layered pane, at the default layer, in a cascading fashion. Note that the layered pane’s opaque property has been explicity enabled (setOpaque(true)). This is to ensure smooth repainting when dragging our InnerFrames by guaranteeing that all layered pane pixels will be painted (see chapter 2 for more about opacity).Class mdi.InnerFrame
This is our custom internal frame we will be expanding on throughout the remainder of this chapter. It is defined within the
mdi package and extends JPanel.Class variables:
String IMAGE_DIR
: directory header string used to locate images in the mdi packageImageIcon ICONIZE_BUTTON_ICON
: icon used for the iconify button = [black interior]ImageIcon RESTORE_BUTTON_ICON
: icon used for the iconfy button when in the iconified state (representing de-iconify) = [black interior]ImageIcon CLOSE_BUTTON_ICON
: icon used for the close button = [black interior]ImageIcon PRESS_CLOSE_BUTTON_ICON
: icon used for the close button when it is in the pressed state = [dark green interior]ImageIcon PRESS_RESTORE_BUTTON_ICON
: icon used for the iconfy button in the iconified state (representing de-iconify) when also in the pressed state = [dark green interior]ImageIcon PRESS_ICONIZE_BUTTON_ICON
: icon used for the iconify button when in the pressed state = [dark green interior]Color C_BUTTONBACK
: the titlebar button background colorSeveral instance variables are also necessary:
int WIDTH
: InnerFrame’s width.int HEIGHT
: InnerFrame’s height.int TITLE_BAR_HEIGHT
: default height of the title bar.Color TITLE_BAR_BG_COLOR
: default Color the title bar.Instance variables
String m_title
: title bar String.JLabel m_titleLabel
: label contained in the title bar displaying the title string, m_title.boolean m_iconified
: true when in the iconified state.JPanel m_titlePanel
: panel representing the title bar.JPanel m_contentPanel
: the central InnerFrame container.JPanel m_buttonPanel
: small panel for frame buttons.JPanel m_buttonWrapperPanel
: panel to surround m_buttonPanel to allow correct alignment.InnerFrameButton m_iconize
: custom frame button for iconification/de-iconification.InnerFrameButton
m_close: custom frame button for closing.The
InnerFrame constructor takes a title String as its only parameter and sets the title variable accordingly. We use a BorderLayout and call our createTitleBar() method to initialize m_titlePanel, which represents our title bar (see below). We then initialize m_contentPanel which acts as our frame’s central container. We add these components to InnerFrame in the NORTH and CENTER regions respectively.The
toFront() method is responsible for moving InnerFrame to the frontmost position in its layer. This is called from m_titlePanel’s mouse adapter when the title bar is pressed, as we will see below. This will only function if InnerFrame is within a JLayeredPane (or JLayeredPane subclass--such as JDesktopPane).The
close() method is responsible for removing an InnerFrame from its parent container. This is called when the close button, m_close, is clicked within the title bar. We specifically only allow removal to work from within a JLayeredPane. If this component is added to any other container we are assuming that it is not meant for removal in this fashion. We also keep a reference to the parent layered pane so that it can be repainted after we have been removed.The
setIconified() method controls iconification and deiconification (often called restore) and reduces the height of InnerFrame to that of the title bar. When an iconification occurs we change the m_iconize button’s regular and pressed state icons to those representing de-iconification. Similarly, when a de-iconify occurs we restore the original height of InnerFrame and replace the icons with those representing iconification.The
createTitleBar() method is responsible for populating and laying out m_titlePanel, our title bar. It starts by initializing m_titlePanel with a preferred size according to our WIDTH and TITLE_BAR_HEIGHT constants. m_titlePanel uses a BorderLayout and its background is set to TITLE_BAR_BG_COLOR. (Note that these hard-coded values will be replaced by variables in future stages of developme! nt.) m_titleLabel is created with the m_title String that was passed to the InnerFrame constructor.The
m_close button is an instance of InnerFrameButton (see below) and uses custom ImageIcon’s for normal and pressed states. An ActionListener is added to simply invoke close() when this button is pressed. The m_iconize button is created in a similar fashion, except its ActionListener is constructed to invoke the setIconified() method. Once these buttons are constructed, we place them in m_buttonPanel useing a GridLayout, and place this panel within our m_buttonWrapperPanel which uses a FlowLayout. This allows for perfect button alignment, using JComponent’s setAlignmentX/Y() methods, while enforcing equal size among buttons no greater than their preferred sizes.Finally, the
createTitleBar() method ends by creating an InnerFrameTitleBarMouseAdapter and attaches it to m_titlePanel.Class mdi.InnerFrame.InnerFrameTitleBarMouseAdapter
This class extends javax.swing.event.
MouseInputAdapter and holds a reference to the InnerFrame it is associated with. The function of mousePressed() is to obtain the base coordinate offsets, m_XDifference and m_YDifference, for use in the mouseDragged method, and set the m_dragging flag to true (because a mouse press always precedes a mouse drag). The! mouseDragged() method is called whenever an InnerFrame is dragged. This method starts by testing whether the m_dragging flag is set to true. If it is InnerFrame is moved to a new location. To calculate the new position as it is dragged this method uses the current InnerFrame coordinates, getX() and getY(), plus the difference of the mouseDr! agged() event coordinate! s,! e.getX() and e.getY(), and the base offset coordinates, m_XDifference and m_YDifference. These base offset coordinates, initialized within the mousePressed() method, are necessary to keep mouse position with regard to the upper left-hand corner of InnerFrame remains constant. The mouseReleased() method’s only function is to set the m_dragging flag to false when the mouse button is released.Class mdi.InnerFrame.InnerFrameButton
The
InnerFrameButton class extends JButton and its constructor takes an ImageIcon parameter. It uses this icon to construct a Dimension instance which is in turn used for the button’s minimum, maximum, and preferred sizes. The button is then set to non-opaque, and we call setContentAreaFilled(false) to guarantee that its background will never be filled. We also remove its border to give it an embedded look.Running the Code
Figure 15.4 shows LayeredPaneDemo in action. Experiment with moving, iconifying, de-iconifying and closing our custom frames. Note that a frame can be selected even when its title bar lies behind the content of another frame. Mouse events pass right through our frame’s content panels. This is just one of the many flaws that need to be accounted for. Also notice that we can drag any frame completely outside of the layered pane. The most immediately noticeable functionality that InnerFrame lacks is resizability. This is the single most difficult feature to implement, and is the subject of the next section.
15.4 Creating a custom MDI: part II - Resizability
In this stage, four distinct components are added to
InnerFrame’s NORTH, SOUTH, EAST, and WEST regions. Each of these components, defined by the NorthResizeEdge, SouthResizeEdge, EastResizeEdge, and WestResizeEdge inner classes respectively, provide the functionality needed to all! ow proper resizing in all directions. The title bar and content panel of InnerFrame are now wrapped inside another panel that is placed in InnerFrame’s CENTER region. Additionaly, we add a label containing an icon to the WEST region of the title bar, and allow dynamic title bar height based on the size of the icon used.Figure 15.6. Custom MDI: part II with default icon
<<file figure15-6.gif>>
Figure 15.7. Custom MDI: part II with large icon
<<file figure15-7.gif>>
The Code: InnerFrame.java
see \Chapter15\3\mdi
package mdi; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.EmptyBorder; public class InnerFrame extends JPanel { private static String IMAGE_DIR = "mdi" + java.io.File.separator; private static ImageIcon ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"iconize.gif"); private static ImageIcon RESTORE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"restore.gif"); private static ImageIcon CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"close.gif"); private static ImageIcon PRESS_CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressclose.gif"); private static ImageIcon PRESS_RESTORE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressrestore.gif"); private static ImageIcon PRESS_ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressiconize.gif"); private static ImageIcon DEFAULT_FRAME_ICON = new ImageIcon(IMAGE_DIR+"default.gif"); private static int BORDER_THICKNESS = 4; private static int WIDTH = 200; private static int HEIGHT = 200; private static int TITLE_BAR_HEIGHT = 25; private static int FRAME_ICON_PADDING = 2; private static int ICONIZED_WIDTH = 150; private static Color TITLE_BAR_BG_COLOR = new Color(108,190,116); private static Color BORDER_COLOR = new Color(8,90,16); private int m_titleBarHeight = TITLE_BAR_HEIGHT; private int m_width = WIDTH; private int m_height = HEIGHT; private int m_iconizedWidth = ICONIZED_WIDTH; private String m_title; private JLabel m_titleLabel; private JLabel m_iconLabel; private boolean m_iconified; private boolean m_iconizeable; private boolean m_resizeable; private boolean m_closeable; // used to wrap title bar and contentPanel private JPanel m_frameContentPanel; private JPanel m_titlePanel; private JPanel m_contentPanel; private JPanel m_buttonPanel; private JPanel m_buttonWrapperPanel; private InnerFrameButton m_iconize; private InnerFrameButton m_close; private ImageIcon m_frameIcon = DEFAULT_FRAME_ICON; private NorthResizeEdge m_northResizer; private SouthResizeEdge m_southResizer; private EastResizeEdge m_eastResizer; private WestResizeEdge m_westResizer; public InnerFrame() { this(""); } public InnerFrame(String title) { this(title, null); } public InnerFrame(String title, ImageIcon frameIcon) { this(title, frameIcon, true, true, true); } public InnerFrame(String title, ImageIcon frameIcon, boolean resizeable, boolean iconizeable, boolean closeable) { super.setLayout(new BorderLayout()); attachNorthResizeEdge(); attachSouthResizeEdge(); attachEastResizeEdge(); attachWestResizeEdge(); populateInnerFrame(); setTitle(title); setResizeable(resizeable); setIconizeable(iconizeable); setCloseable(closeable); if (frameIcon != null) setFrameIcon(frameIcon); } protected void populateInnerFrame() { m_frameContentPanel = new JPanel(); m_frameContentPanel.setLayout(new BorderLayout()); createTitleBar(); m_contentPanel = new JPanel(); m_frameContentPanel.add(m_titlePanel, BorderLayout.NORTH); m_frameContentPanel.add(m_contentPanel, BorderLayout.CENTER); super.add(m_frameContentPanel, BorderLayout.CENTER); } public Component add(Component c) { return((m_contentPanel == null) ? null : m_contentPanel.add(c)); } public void setLayout(LayoutManager mgr) { if (m_contentPanel != null) m_contentPanel.setLayout(mgr); } public void toFront() { if (getParent() instanceof JLayeredPane) ((JLayeredPane) getParent()).moveToFront(this); } public void close() { if (getParent() instanceof JLayeredPane) { JLayeredPane jlp = (JLayeredPane) getParent(); jlp.remove(InnerFrame.this); jlp.repaint(); } } public boolean isIconizeable() { return m_iconizeable; } public void setIconizeable(boolean b) { m_iconizeable = b; m_iconize.setVisible(b); m_titlePanel.revalidate(); } public boolean isCloseable() { return m_closeable; } public void setCloseable(boolean b) { m_closeable = b; m_close.setVisible(b); m_titlePanel.revalidate(); } public boolean isIconified() { return m_iconified; } public void setIconified(boolean b) { m_iconified = b; if (b) { m_width = getWidth(); // remember width m_height = getHeight(); // remember height setBounds(getX(), getY(), ICONIZED_WIDTH, m_titleBarHeight + 2*BORDER_THICKNESS); m_iconize.setIcon(RESTORE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_RESTORE_BUTTON_ICON); setResizeable(false); } else { setBounds(getX(), getY(), m_width, m_height); m_iconize.setIcon(ICONIZE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON); setResizeable(true); } revalidate(); } //////////////////////////////////////////// //////////////// Title Bar ///////////////// //////////////////////////////////////////// public void setFrameIcon(ImageIcon fi) { m_frameIcon = fi; if (fi != null) { if (m_frameIcon.getIconHeight() > TITLE_BAR_HEIGHT) setTitleBarHeight(m_frameIcon.getIconHeight() + 2*FRAME_ICON_PADDING); m_iconLabel.setIcon(m_frameIcon); } else setTitleBarHeight(TITLE_BAR_HEIGHT); revalidate(); } public ImageIcon getFrameIcon() { return m_frameIcon; } public void setTitle(String s) { m_title = s; m_titleLabel.setText(s); m_titlePanel.repaint(); } public String getTitle() { return m_title; } public void setTitleBarHeight(int h) { m_titleBarHeight = h; } public int getTitleBarHeight() { return m_titleBarHeight; } // create the title bar: m_titlePanel protected void createTitleBar() { m_titlePanel = new JPanel() { public Dimension getPreferredSize() { return new Dimension(InnerFrame.this.getWidth(), m_titleBarHeight); } }; m_titlePanel.setLayout(new BorderLayout()); m_titlePanel.setOpaque(true); m_titlePanel.setBackground(TITLE_BAR_BG_COLOR); m_titleLabel = new JLabel(); m_titleLabel.setForeground(Color.black); m_close = new InnerFrameButton(CLOSE_BUTTON_ICON); m_close.setPressedIcon(PRESS_CLOSE_BUTTON_ICON); m_close.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.close(); } }); m_iconize = new InnerFrameButton(ICONIZE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON); m_iconize.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.setIconified(!InnerFrame.this.isIconified()); } }); m_buttonWrapperPanel = new JPanel(); m_buttonWrapperPanel.setOpaque(false); m_buttonPanel = new JPanel(new GridLayout(1,2)); m_buttonPanel.setOpaque(false); m_buttonPanel.add(m_iconize); m_buttonPanel.add(m_close); m_buttonPanel.setAlignmentX(0.5f); m_buttonPanel.setAlignmentY(0.5f); m_buttonWrapperPanel.add(m_buttonPanel); m_iconLabel = new JLabel(); m_iconLabel.setBorder(new EmptyBorder( FRAME_ICON_PADDING, FRAME_ICON_PADDING, FRAME_ICON_PADDING, FRAME_ICON_PADDING)); if (m_frameIcon != null) m_iconLabel.setIcon(m_frameIcon); m_titlePanel.add(m_titleLabel, BorderLayout.CENTER); m_titlePanel.add(m_buttonWrapperPanel, BorderLayout.EAST); m_titlePanel.add(m_iconLabel, BorderLayout.WEST); InnerFrameTitleBarMouseAdapter iftbma = new InnerFrameTitleBarMouseAdapter(this); m_titlePanel.addMouseListener(iftbma); m_titlePanel.addMouseMotionListener(iftbma); } // title bar mouse adapter for frame dragging class InnerFrameTitleBarMouseAdapter extends MouseInputAdapter { InnerFrame m_if; int m_XDifference, m_YDifference; boolean m_dragging; public InnerFrameTitleBarMouseAdapter(InnerFrame inf) { m_if = inf; } public void mouseDragged(MouseEvent e) { if (m_dragging) m_if.setLocation(e.getX()-m_XDifference + getX(), e.getY()-m_YDifference + getY()); } public void mousePressed(MouseEvent e) { m_if.toFront(); m_XDifference = e.getX(); m_YDifference = e.getY(); m_dragging = true; } public void mouseReleased(MouseEvent e) { m_dragging = false; } } // custom button class for title bar class InnerFrameButton extends JButton { Dimension m_dim; public InnerFrameButton(ImageIcon ii) { super(ii); m_dim = new Dimension(ii.getIconWidth(), ii.getIconHeight()); setOpaque(false); setContentAreaFilled(false); setBorder(null); } public Dimension getPreferredSize() { return m_dim; } public Dimension getMinimumSize() { return m_dim; } public Dimension getMaximumSize() { return m_dim; } } /////////////////////////////////////////////// //////////////// Resizability ///////////////// /////////////////////////////////////////////// public boolean isResizeable() { return m_resizeable; } public void setResizeable(boolean b) { if (!b && m_resizeable == true) { m_northResizer.removeMouseListener(m_northResizer); m_northResizer.removeMouseMotionListener(m_northResizer); m_southResizer.removeMouseListener(m_southResizer); m_southResizer.removeMouseMotionListener(m_southResizer); m_eastResizer.removeMouseListener(m_eastResizer); m_eastResizer.removeMouseMotionListener(m_eastResizer); m_westResizer.removeMouseListener(m_westResizer); m_westResizer.removeMouseMotionListener(m_westResizer); } else if (m_resizeable == false) { m_northResizer.addMouseListener(m_northResizer); m_northResizer.addMouseMotionListener(m_northResizer); m_southResizer.addMouseListener(m_southResizer); m_southResizer.addMouseMotionListener(m_southResizer); m_eastResizer.addMouseListener(m_eastResizer); m_eastResizer.addMouseMotionListener(m_eastResizer); m_westResizer.addMouseListener(m_westResizer); m_westResizer.addMouseMotionListener(m_westResizer); } m_resizeable = b; } protected void attachNorthResizeEdge() { m_northResizer = new NorthResizeEdge(this); super.add(m_northResizer, BorderLayout.NORTH); } protected void attachSouthResizeEdge() { m_southResizer = new SouthResizeEdge(this); super.add(m_southResizer, BorderLayout.SOUTH); } protected void attachEastResizeEdge() { m_eastResizer = new EastResizeEdge(this); super.add(m_eastResizer, BorderLayout.EAST); } protected void attachWestResizeEdge() { m_westResizer = new WestResizeEdge(this); super.add(m_westResizer, BorderLayout.WEST); } class EastResizeEdge extends JPanel implements MouseListener, MouseMotionListener { private int WIDTH = BORDER_THICKNESS; private int MIN_WIDTH = ICONIZED_WIDTH; private boolean m_dragging; private JComponent m_resizeComponent; protected EastResizeEdge(JComponent c) { m_resizeComponent = c; setOpaque(true); setBackground(BORDER_COLOR); } public Dimension getPreferredSize() { return new Dimension(WIDTH, m_resizeComponent.getHeight()); } public void mouseClicked(MouseEvent e) {} public void mouseMoved(MouseEvent e) {} public void mouseReleased(MouseEvent e) { m_dragging = false; } public void mouseDragged(MouseEvent e) { if (m_resizeComponent.getWidth() + e.getX() >= MIN_WIDTH) m_resizeComponent.setBounds(m_resizeComponent.getX(), m_resizeComponent.getY(), m_resizeComponent.getWidth() + e.getX(), m_resizeComponent.getHeight()); else m_resizeComponent.setBounds(m_resizeComponent.getX(), m_resizeComponent.getY(), MIN_WIDTH, m_resizeComponent.getHeight()); m_resizeComponent.validate(); } public void mouseEntered(MouseEvent e) { if (!m_dragging) setCursor(Cursor.getPredefinedCursor( Cursor.E_RESIZE_CURSOR)); } public void mouseExited(MouseEvent e) { if (!m_dragging) setCursor(Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR)); } public void mousePressed(MouseEvent e) { toFront(); m_dragging = true; } } class WestResizeEdge extends JPanel implements MouseListener, MouseMotionListener { private int WIDTH = BORDER_THICKNESS; private int MIN_WIDTH = ICONIZED_WIDTH; private int m_dragX, m_rightX; private boolean m_dragging; private JComponent m_resizeComponent; protected WestResizeEdge(JComponent c) { m_resizeComponent = c; setOpaque(true); setBackground(BORDER_COLOR); } public Dimension getPreferredSize() { return new Dimension(WIDTH, m_resizeComponent.getHeight()); } public void mouseClicked(MouseEvent e) {} public void mouseMoved(MouseEvent e) {} public void mouseReleased(MouseEvent e) { m_dragging = false; } public void mouseDragged(MouseEvent e) { if (m_resizeComponent.getWidth()- (e.getX()-m_dragX) >= MIN_WIDTH) m_resizeComponent.setBounds( m_resizeComponent.getX() + (e.getX()-m_dragX), m_resizeComponent.getY(), m_resizeComponent.getWidth()-(e.getX()-m_dragX), m_resizeComponent.getHeight()); else if (m_resizeComponent.getX() + MIN_WIDTH < m_rightX) m_resizeComponent.setBounds(m_rightX-MIN_WIDTH, m_resizeComponent.getY(), MIN_WIDTH, m_resizeComponent.getHeight()); else m_resizeComponent.setBounds(m_resizeComponent.getX(), m_resizeComponent.getY(), MIN_WIDTH, m_resizeComponent.getHeight()); m_resizeComponent.validate(); } public void mouseEntered(MouseEvent e) { if (!m_dragging) setCursor(Cursor.getPredefinedCursor( Cursor.W_RESIZE_CURSOR)); } public void mouseExited(MouseEvent e) { if (!m_dragging) setCursor(Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR)); } public void mousePressed(MouseEvent e) { toFront(); m_rightX = m_resizeComponent.getX() + m_resizeComponent.getWidth(); m_dragging = true; m_dragX = e.getX(); } } class NorthResizeEdge extends JPanel implements MouseListener, MouseMotionListener { private static final int NORTH = 0; private static final int NORTHEAST = 1; private static final int NORTHWEST = 2; private int CORNER = 10; private int HEIGHT = BORDER_THICKNESS; private int MIN_WIDTH = ICONIZED_WIDTH; private int MIN_HEIGHT = TITLE_BAR_HEIGHT+(2*HEIGHT); private int m_width, m_dragX, m_dragY, m_rightX, m_lowerY; private boolean m_dragging; private JComponent m_resizeComponent; private int m_mode; protected NorthResizeEdge(JComponent c) { m_resizeComponent = c; setOpaque(true); setBackground(BORDER_COLOR); } public Dimension getPreferredSize() { return new Dimension(m_resizeComponent.getWidth(), HEIGHT); } public void mouseClicked(MouseEvent e) {} public void mouseMoved(MouseEvent e) { if (!m_dragging) { if (e.getX() < CORNER) { setCursor(Cursor.getPredefinedCursor( Cursor.NW_RESIZE_CURSOR)); } else if(e.getX() > getWidth()-CORNER) { setCursor(Cursor.getPredefinedCursor( Cursor.NE_RESIZE_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor( Cursor.N_RESIZE_CURSOR)); } } } public void mouseReleased(MouseEvent e) { m_dragging = false; } public void mouseDragged(MouseEvent e) { int h = m_resizeComponent.getHeight(); int w = m_resizeComponent.getWidth(); int x = m_resizeComponent.getX(); int y = m_resizeComponent.getY(); int ex = e.getX(); int ey = e.getY(); switch (m_mode) { case NORTH: if (h-(ey-m_dragY) >= MIN_HEIGHT) m_resizeComponent.setBounds(x, y + (ey-m_dragY), w, h-(ey-m_dragY)); else m_resizeComponent.setBounds(x, m_lowerY-MIN_HEIGHT, w, MIN_HEIGHT); break; case NORTHEAST: if (h-(ey-m_dragY) >= MIN_HEIGHT && w + (ex-(getWidth()-CORNER)) >= MIN_WIDTH) m_resizeComponent.setBounds(x, y + (ey-m_dragY), w + (ex-(getWidth()-CORNER)), h-(ey-m_dragY)); else if (h-(ey-m_dragY) >= MIN_HEIGHT && !(w + (ex-(getWidth()-CORNER)) >= MIN_WIDTH)) m_resizeComponent.setBounds(x, y + (ey-m_dragY), MIN_WIDTH, h-(ey-m_dragY)); else if (!(h-(ey-m_dragY) >= MIN_HEIGHT) && w + (ex-(getWidth()-CORNER)) >= MIN_WIDTH) m_resizeComponent.setBounds(x, m_lowerY-MIN_HEIGHT, w + (ex-(getWidth()-CORNER)), MIN_HEIGHT); else m_resizeComponent.setBounds(x, m_lowerY-MIN_HEIGHT, MIN_WIDTH, MIN_HEIGHT); break; case NORTHWEST: if (h-(ey-m_dragY) >= MIN_HEIGHT && w-(ex-m_dragX) >= MIN_WIDTH) m_resizeComponent.setBounds(x + (ex-m_dragX), y + (ey-m_dragY), w-(ex-m_dragX), h-(ey-m_dragY)); else if (h-(ey-m_dragY) >= MIN_HEIGHT && !(w-(ex-m_dragX) >= MIN_WIDTH)) { if (x + MIN_WIDTH < m_rightX) m_resizeComponent.setBounds(m_rightX-MIN_WIDTH, y + (ey-m_dragY), MIN_WIDTH, h-(ey-m_dragY)); else m_resizeComponent.setBounds(x, y + (ey-m_dragY), w, h-(ey-m_dragY)); } else if (!(h-(ey-m_dragY) >= MIN_HEIGHT) && w-(ex-m_dragX) >= MIN_WIDTH) m_resizeComponent.setBounds(x + (ex-m_dragX), m_lowerY-MIN_HEIGHT, w-(ex-m_dragX), MIN_HEIGHT); else m_resizeComponent.setBounds(m_rightX-MIN_WIDTH, m_lowerY-MIN_HEIGHT, MIN_WIDTH, MIN_HEIGHT); break; } m_rightX = x + w; m_resizeComponent.validate(); } public void mouseEntered(MouseEvent e) { mouseMoved(e); } public void mouseExited(MouseEvent e) { if (!m_dragging) setCursor(Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR)); } public void mousePressed(MouseEvent e) { toFront(); m_dragging = true; m_dragX = e.getX(); m_dragY = e.getY(); m_lowerY = m_resizeComponent.getY() + m_resizeComponent.getHeight(); if (e.getX() < CORNER) { m_mode = NORTHWEST; } else if(e.getX() > getWidth()-CORNER) { m_mode = NORTHEAST; } else { m_mode = NORTH; } } } class SouthResizeEdge extends JPanel implements MouseListener, MouseMotionListener { private static final int SOUTH = 0; private static final int SOUTHEAST = 1; private static final int SOUTHWEST = 2; private int CORNER = 10; private int HEIGHT = BORDER_THICKNESS; private int MIN_WIDTH = ICONIZED_WIDTH; private int MIN_HEIGHT = TITLE_BAR_HEIGHT+(2*HEIGHT); private int m_width, m_dragX, m_dragY, m_rightX; private boolean m_dragging; private JComponent m_resizeComponent; private int m_mode; protected SouthResizeEdge(JComponent c) { m_resizeComponent = c; setOpaque(true); setBackground(BORDER_COLOR); } public Dimension getPreferredSize() { return new Dimension(m_resizeComponent.getWidth(), HEIGHT); } public void mouseClicked(MouseEvent e) {} public void mouseMoved(MouseEvent e) { if (!m_dragging) { if (e.getX() < CORNER) { setCursor(Cursor.getPredefinedCursor( Cursor.SW_RESIZE_CURSOR)); } else if(e.getX() > getWidth()-CORNER) { setCursor(Cursor.getPredefinedCursor( Cursor.SE_RESIZE_CURSOR)); } else { setCursor(Cursor.getPredefinedCursor( Cursor.S_RESIZE_CURSOR)); } } } public void mouseReleased(MouseEvent e) { m_dragging = false; } public void mouseDragged(MouseEvent e) { int h = m_resizeComponent.getHeight(); int w = m_resizeComponent.getWidth(); int x = m_resizeComponent.getX(); int y = m_resizeComponent.getY(); int ex = e.getX(); int ey = e.getY(); switch (m_mode) { case SOUTH: if (h+(ey-m_dragY) >= MIN_HEIGHT) m_resizeComponent.setBounds(x, y, w, h+(ey-m_dragY)); else m_resizeComponent.setBounds(x, y, w, MIN_HEIGHT); break; case SOUTHEAST: if (h+(ey-m_dragY) >= MIN_HEIGHT && w + (ex-(getWidth()-CORNER)) >= MIN_WIDTH) m_resizeComponent.setBounds(x, y, w + (ex-(getWidth()-CORNER)), h+(ey-m_dragY)); else if (h+(ey-m_dragY) >= MIN_HEIGHT && !(w + (ex-(getWidth()-CORNER)) >= MIN_WIDTH)) m_resizeComponent.setBounds(x, y, MIN_WIDTH, h+(ey-m_dragY)); else if (!(h+(ey-m_dragY) >= MIN_HEIGHT) && w + (ex-(getWidth()-CORNER)) >= MIN_WIDTH) m_resizeComponent.setBounds(x, y, w + (ex-(getWidth()-CORNER)), MIN_HEIGHT); else m_resizeComponent.setBounds(x, y, MIN_WIDTH, MIN_HEIGHT); break; case SOUTHWEST: if (h+(ey-m_dragY) >= MIN_HEIGHT && w-(ex-m_dragX) >= MIN_WIDTH) m_resizeComponent.setBounds(x + (ex-m_dragX), y, w-(ex-m_dragX), h+(ey-m_dragY)); else if (h+(ey-m_dragY) >= MIN_HEIGHT && !(w-(ex-m_dragX) >= MIN_WIDTH)) { if (x + MIN_WIDTH < m_rightX) m_resizeComponent.setBounds(m_rightX-MIN_WIDTH, y, MIN_WIDTH, h+(ey-m_dragY)); else m_resizeComponent.setBounds(x, y, w, h+(ey-m_dragY)); } else if (!(h+(ey-m_dragY) >= MIN_HEIGHT) && w-(ex-m_dragX) >= MIN_WIDTH) m_resizeComponent.setBounds(x + (ex-m_dragX), y, w-(ex-m_dragX), MIN_HEIGHT); else m_resizeComponent.setBounds(m_rightX-MIN_WIDTH, y, MIN_WIDTH, MIN_HEIGHT); break; } m_rightX = x + w; m_resizeComponent.validate(); } public void mouseEntered(MouseEvent e) { mouseMoved(e); } public void mouseExited(MouseEvent e) { if (!m_dragging) setCursor(Cursor.getPredefinedCursor( Cursor.DEFAULT_CURSOR)); } public void mousePressed(MouseEvent e) { toFront(); m_dragging = true; m_dragX = e.getX(); m_dragY = e.getY(); if (e.getX() < CORNER) { m_mode = SOUTHWEST; } else if(e.getX() > getWidth()-CORNER) { m_mode = SOUTHEAST; } else { m_mode = SOUTH; } } } }
Understanding The Code:
Class InnerFrame
New class variables:
ImageIcon
DEFAULT_FRAME_ICON: default image used for the frame icon =int BORDER_THICKNESS
: default thickness of resize edges (borders).int FRAME_ICON_PADDING
: default thickness of padding around the title bar icon label.int ICONIZED_WIDTH
: default width in the iconified state.Color BORDER_COLOR
: default resize border background color.New instance variables:
int m_titleBarHeight
: title bar height.int m_width
: used for recalling the frame’s previous width when deiconifying.int m_height
: used for recalling the frame’s previous height when deiconifying.int m_iconizedWidth
: frame width in the iconified state.JLabel m_iconLabel
: label used to display the frame icon in the title bar.boolean m_iconizeable
: determines whether the frame can be iconified.boolean m_resizeable
: determines whether the frame is resizable.boolean m_closeable
: determines whether the frame is closeable.JPanel m_frameContentPanel
: used to wrap the title bar and contentPanel for placement in InnerFrame’s CENTER region.ImageIcon m_frameIcon
: the frame icon displayed by m_iconLabel in the title bar.NorthResizeEdge m_northResizer
: Used for north, northeast, and northwest resizing.SouthResizeEdge m_southResizer
: Used for south, southeast, and southwest resizing,EastResizeEdge m_eastResizer
: Used for east resizing.WestResizeEdge m_westResizer
: Used for west reszing.There are noew four
InnerFrame constructors. The first creates an InnerFrame with no title and default frame icon. The second creates an InnerFrame with a title and default icon, and the third creates one with both a title and a specified frame icon. The first three constructors all end up calling the fourth to do the actual work.The fourth
InnerFrame constructor calls four methods to attach our custom resize components to its edges. Then our populateInnerFrame() method is called which is responsible for encapsulating the title bar and content panel in m_frameContentPanel, which is then added to the CENTER of InnerFrame. The constructor ends by calling methods to set the title, frame icon, and resizeable, iconizeable, and closeable properties (discussed below).The
add() and setLayout() methods of JComponent are overridden to perform these actions on the content panel contained inside m_frameContentPanel. Thus, anything we add to InnerFrame will be placed in m_contentPanel, and any layout we assign to InnerFrame will actually be assigned to m_contentPanel.
Note: This functionality is not on par with fundamental Swing containers such as
JFrame, and JInternalFrame which use a JRootPane to manage their contents. We will fix this in the next section by implementing the RootPaneContainer interface.The
setIconizeable() and setCloseable() methods set their respective properties and hide or show the corresponding title bar buttons using setVisible(). We call revalidate() to perform any layout changes that may be necessary after a button is shown or hidden.The
setIconified() method is modified store the width and height of InnerFrame before it is iconified. The size of an iconified InnerFrame is now determined by the title bar height, which is in turn determined by the frame icon size, and the thickness of the resize edges:setBounds(getX(), getY(), ICONIZED_WIDTH,
m_titleBarHeight + 2*BORDER_THICKNESS);
Whenever an iconification occurs we call
setResizable(false) to remove mouse listeners from each resize edge. If a deiconification occurs we call setResizable(true) to add mouse listeners back to each resize edge. Also, the width and height saved in an iconification is used to set the size of InnerFrame when it is deiconified.The title bar code has several methods added to it.
setFrameIcon() is responsible for replacing the icon in the title bar icon label, m_iconLabel, and calculating the new title bar height based on this change:public void setFrameIcon(ImageIcon fi) { m_frameIcon = fi; if (fi != null) { if (m_frameIcon.getIconHeight() > TITLE_BAR_HEIGHT) setTitleBarHeight(m_frameIcon.getIconHeight() + 2*FRAME_ICON_PADDING); m_iconLabel.setIcon(m_frameIcon); } else setTitleBarHeight(TITLE_BAR_HEIGHT); revalidate(); }
The title bar height will never be smaller than
TITLE_BAR_HEIGHT (25). If the icon’s height is greater than this default value the title bar height will then be based on that icon’s height, plus the default icon padding, FRAME_ICON_PADDING, above and below the icon. This is necessary because when the frame icon is added to the title bar within our createTitleBar() method, it is placed in a JLabel surrounded by an EmptyBorder as follows:m_iconLabel.setBorder(new EmptyBorder(
FRAME_ICON_PADDING, FRAME_ICON_PADDING,
FRAME_ICON_PADDING, FRAME_ICON_PADDING));
This label is then added to the
WEST region of m_titlePanel (the title bar).The custom resize components are the key to making
InnerFrame resizable. They are built as inner classes inside InnerFrame and are discussed below. Before we discuss them in detail, it is helpful to clarify InnerFrame‘s structure. Figure 15.5 illustrates this and shows which cursor will appear when the mouse pointer is placed over different portions of each resize edge (this functionality is something we have come to expect from frames in any modern desktop environment):Figure 15.5. InnerFrame structure and resize edge cursor regions
<<file figure15-5.gif>>
The
Cursor class defines several class fields representing pre-defined cursor icons that we use in the resize classes discussed below:N_RESIZE_CURSOR, NE_RESIZE_CURSOR, NW_RESIZE_CURSOR, S_RESIZE_CURSOR,
SE_RESIZE_CURSOR, SW_RESIZE_CURSOR, E_RESIZE_CURSOR, W_RESIZE_CURSOR,
DEFAULT_CURSOR
Note: Upon first inspection you might think it would be easier to implement your own border and build resizable functionality into that. The problem is that borders are not part of the
Component hierarchy. That is, they do not inherit from the Component class. In fact, every border is a subclass of AbstractBorder which is a direct subclass of Object. There is no way to associate mouse events with a border.
Class InnerFrame.EastResizeEdge
EastResizeEdge
is the component that is placed in the east portion of InnerFrame’s BorderLayout. This component allows resizing InnerFrame horizontally. This is the simplest of the four edges because when it is used to resize, InnerFrame always stays in the same location (defined by its northwest corner) and the height doesn’t change. Two class variables and two instance variables are necessary:int WIDTH
: constant thickness.int MIN_WIDTH
: minimum width of m_resizeComponent.boolean m_dragging
: true when dragging.JComponent m_resizeComponent
: the component to resize.The constructor, as with all of the resize edge component constructors, takes a
JComponent as a parameter. This JComponent is the component that is resized whenever the edge detects a mouse drag event. The setPreferredSize() method ensures that our edges will have a constant thickness.
Note: We have designed these components to be significantly generic, while still being encapsulated as inner classes. They can easily be modified for use within other classes or as stand-alone resizable utility components. In the next chapter we create a package called
resize which contains each of these classes as separate entities that can be wrapped around any JComponent. See section 16.4. (For the resize package source code, refer to \Chapter16\2\resize.)The
mouseEntered() method is used to detect whenever the cursor is over this edge. When invoked it changes the cursor to E_RESIZE_CURSOR. The mouseExited() method changes the cursor back to the normal DEFAULT_CURSOR when the mouse leaves this component. In both methods cursor changes only occur when the m_dragging flag is false (i.e. when the border is not bei! ng dragged). We do not want the cursor to change while we are resizing.The
mousePressed() method sets the m_dragging flag and moves the component associated with this resize edge to the foremost position in its JLayeredPane layer (in other words it calls toFront()). The mouseDragged method actually handles the resizing and defines two cases to ensure that m_resizeComponent‘s width is never made smaller than MIN_WIDTH.Class InnerFrame.WestResizeEdge
WestResizeEdge
works very similar to EastResizeEdge except that it must handle an extra case in resizing because the position, as well as width, of m_resizeComponent changes. An extra variable, m_rightX, is used to keep track of the coordinate of the northeast corner of m_resizeComponent to ensure that the right side of m_resizeComponent never moves during a resize.Class InnerFrame.NorthResizeEdge, InnerFrame.SouthResizeEdge
NorthResizeEdge
and SouthResizeEdge are very complicated because there are three regions in each. The left-most and right-most portions are reserved for resizing in the northwest, southwest, and northeast, southeast directions. The most complicated cases are resizing from the northwest and southwest corners because the height, width, as well as both the x and y coordinates of m_resizeComponent can change. Thus, the mouseDragged method in each of these classes is quite extensive.
Note: You are encouraged to work through this code and understand each case. We will not explain it in detail because of its repetitive and mathematical nature.
After each mouse drag,
validate()is called on m_resizeComponent to lay out its contents again. If we did not include this we would see that as we resized an InnerFrame the size change and rendering of its contents would not occur properly. (Try this by commenting out these lines in each of the resize edge classes.) Validation forces the container to lay out its contents based on the current size. Normally we would call revalidate() rather than validate() for the sake thread safety. However, there are often performance bottlenecks associated with revalidate()Running The Code
Figure 15.6 shows
LayeredPaneDemo in action. First try resizing frames from all directions to see that this works as expected. Now try replacing the frame icon with one of a different size (you can use the tiger.gif image included in this example’s directory). You will see that the title bar and the minimum height of our frames changes accordingly. Figure 15.7 shows our InnerFrames with a much larger frame icon.15.5 Creating a custom MDI: part III - Enhancements
There are a few things about the previous example to take special note of at this stage. You may have noticed that mouse events are still propagating right through our frames. We can also still drag
InnerFrames completely outside the layered pane view. In this section we address these issues, implement maximizable functionality, and take the final step in making InnerFrame a fundamental Swing container by implementing the RootPaneContainer interface.Figure 15.8. Custom MDI: part III
<<file figure15-8.gif>>
The Code: InnerFrame.java
see \Chapter15\4\mdi
package mdi; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.EmptyBorder; public class InnerFrame extends JPanel implements RootPaneContainer { private static String IMAGE_DIR = "mdi" + java.io.File.separator; private static ImageIcon ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"iconize.gif"); private static ImageIcon RESTORE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"restore.gif"); private static ImageIcon CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"close.gif"); private static ImageIcon MAXIMIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"maximize.gif"); private static ImageIcon MINIMIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"minimize.gif"); private static ImageIcon PRESS_CLOSE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressclose.gif"); private static ImageIcon PRESS_RESTORE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressrestore.gif"); private static ImageIcon PRESS_ICONIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressiconize.gif"); private static ImageIcon PRESS_MAXIMIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressmaximize.gif"); private static ImageIcon PRESS_MINIMIZE_BUTTON_ICON = new ImageIcon(IMAGE_DIR+"pressminimize.gif"); private static ImageIcon DEFAULT_FRAME_ICON = new ImageIcon(IMAGE_DIR+"default.gif"); private static int BORDER_THICKNESS = 4; private static int WIDTH = 200; private static int HEIGHT = 200; private static int TITLE_BAR_HEIGHT = 25; private static int FRAME_ICON_PADDING = 2; private static int ICONIZED_WIDTH = 150; private static Color TITLE_BAR_BG_COLOR = new Color(108,190,116); private static Color BORDER_COLOR = new Color(8,90,16); private int m_titleBarHeight = TITLE_BAR_HEIGHT; private int m_width = WIDTH; private int m_height = HEIGHT; private int m_iconizedWidth = ICONIZED_WIDTH; private int m_x; private int m_y; private String m_title; private JLabel m_titleLabel; private JLabel m_iconLabel; private boolean m_iconified; private boolean m_maximized; private boolean m_iconizeable; private boolean m_resizeable; private boolean m_closeable; private boolean m_maximizeable; // only false when maximized private boolean m_draggable = true; private JRootPane m_rootPane; // used to wrap m_titlePanel and m_rootPane private JPanel m_frameContentPanel; private JPanel m_titlePanel; private JPanel m_contentPanel; private JPanel m_buttonPanel; private JPanel m_buttonWrapperPanel; private InnerFrameButton m_iconize; private InnerFrameButton m_close; private InnerFrameButton m_maximize; private ImageIcon m_frameIcon = DEFAULT_FRAME_ICON; private NorthResizeEdge m_northResizer; private SouthResizeEdge m_southResizer; private EastResizeEdge m_eastResizer; private WestResizeEdge m_westResizer; public InnerFrame() { this(""); } public InnerFrame(String title) { this(title, null); } public InnerFrame(String title, ImageIcon frameIcon) { this(title, frameIcon, true, true, true, true); } public InnerFrame(String title, ImageIcon frameIcon, boolean resizeable, boolean iconizeable, boolean maximizeable, boolean closeable) { super.setLayout(new BorderLayout()); attachNorthResizeEdge(); attachSouthResizeEdge(); attachEastResizeEdge(); attachWestResizeEdge(); populateInnerFrame(); setTitle(title); setResizeable(resizeable); setIconizeable(iconizeable); setCloseable(closeable); setMaximizeable(maximizeable); if (frameIcon != null) setFrameIcon(frameIcon); } protected void populateInnerFrame() { m_rootPane = new JRootPane(); m_frameContentPanel = new JPanel(); m_frameContentPanel.setLayout(new BorderLayout()); createTitleBar(); m_contentPanel = new JPanel(new BorderLayout()); m_rootPane.setContentPane(m_contentPanel); m_frameContentPanel.add(m_titlePanel, BorderLayout.NORTH); m_frameContentPanel.add(m_rootPane, BorderLayout.CENTER); setupCapturePanel(); super.add(m_frameContentPanel, BorderLayout.CENTER); } protected void setupCapturePanel() { CapturePanel mouseTrap = new CapturePanel(); m_rootPane.getLayeredPane().add(mouseTrap, new Integer(Integer.MIN_VALUE)); mouseTrap.setBounds(0,0,10000,10000); } // don't allow this in root pane containers public Component add(Component c) { return null; } // don't allow this in root pane containers public void setLayout(LayoutManager mgr) { } public JMenuBar getJMenuBar() { return m_rootPane.getJMenuBar(); } public JRootPane getRootPane() { return m_rootPane; } public Container getContentPane() { return m_rootPane.getContentPane(); } public Component getGlassPane() { return m_rootPane.getGlassPane(); } public JLayeredPane getLayeredPane() { return m_rootPane.getLayeredPane(); } public void setJMenuBar(JMenuBar menu) { m_rootPane.setJMenuBar(menu); } public void setContentPane(Container content) { m_rootPane.setContentPane(content); } public void setGlassPane(Component glass) { m_rootPane.setGlassPane(glass); } public void setLayeredPane(JLayeredPane layered) { m_rootPane.setLayeredPane(layered); } public void toFront() { if (getParent() instanceof JLayeredPane) ((JLayeredPane) getParent()).moveToFront(this); } public void close() { if (getParent() instanceof JLayeredPane) { JLayeredPane jlp = (JLayeredPane) getParent(); jlp.remove(InnerFrame.this); jlp.repaint(); } } public boolean isIconizeable() { return m_iconizeable; } public void setIconizeable(boolean b) { m_iconizeable = b; m_iconize.setVisible(b); m_titlePanel.revalidate(); } public boolean isCloseable() { return m_closeable; } public void setCloseable(boolean b) { m_closeable = b; m_close.setVisible(b); m_titlePanel.revalidate(); } public boolean isMaximizeable() { return m_maximizeable; } public void setMaximizeable(boolean b) { m_maximizeable = b; m_maximize.setVisible(b); m_titlePanel.revalidate(); } public boolean isIconified() { return m_iconified; } public void setIconified(boolean b) { m_iconified = b; if (b) { if (isMaximized()) setMaximized(false); toFront(); m_width = getWidth(); // remember width m_height = getHeight(); // remember height setBounds(getX(), getY(), ICONIZED_WIDTH, m_titleBarHeight + 2*BORDER_THICKNESS); m_iconize.setIcon(RESTORE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_RESTORE_BUTTON_ICON); setResizeable(false); } else { toFront(); setBounds(getX(), getY(), m_width, m_height); m_iconize.setIcon(ICONIZE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON); setResizeable(true); } revalidate(); } public boolean isMaximized() { return m_maximized; } public void setMaximized(boolean b) { m_maximized = b; if (b) { if (isIconified()) setIconified(false); toFront(); m_width = getWidth(); // remember width m_height = getHeight(); // remember height m_x = getX(); // remember x m_y = getY(); // remember y setBounds(0, 0, getParent().getWidth(), getParent().getHeight()); m_maximize.setIcon(MINIMIZE_BUTTON_ICON); m_maximize.setPressedIcon(PRESS_MINIMIZE_BUTTON_ICON); setResizeable(false); setDraggable(false); } else { toFront(); setBounds(m_x, m_y, m_width, m_height); m_maximize.setIcon(MAXIMIZE_BUTTON_ICON); m_maximize.setPressedIcon(PRESS_MAXIMIZE_BUTTON_ICON); setResizeable(true); setDraggable(true); } revalidate(); } //////////////////////////////////////////// //////////////// Title Bar ///////////////// //////////////////////////////////////////// public void setFrameIcon(ImageIcon fi) { m_frameIcon = fi; if (fi != null) { if (m_frameIcon.getIconHeight() > TITLE_BAR_HEIGHT) setTitleBarHeight(m_frameIcon.getIconHeight() + 2*FRAME_ICON_PADDING); m_iconLabel.setIcon(m_frameIcon); } else setTitleBarHeight(TITLE_BAR_HEIGHT); revalidate(); } public ImageIcon getFrameIcon() { return m_frameIcon; } public void setTitle(String s) { m_title = s; m_titleLabel.setText(s); m_titlePanel.repaint(); } public String getTitle() { return m_title; } public void setTitleBarHeight(int h) { m_titleBarHeight = h; } public int getTitleBarHeight() { return m_titleBarHeight; } public boolean isDraggable() { return m_draggable; } private void setDraggable(boolean b) { m_draggable = b; } // create the title bar: m_titlePanel protected void createTitleBar() { m_titlePanel = new JPanel() { public Dimension getPreferredSize() { return new Dimension(InnerFrame.this.getWidth(), m_titleBarHeight); } }; m_titlePanel.setLayout(new BorderLayout()); m_titlePanel.setOpaque(true); m_titlePanel.setBackground(TITLE_BAR_BG_COLOR); m_titleLabel = new JLabel(); m_titleLabel.setForeground(Color.black); m_close = new InnerFrameButton(CLOSE_BUTTON_ICON); m_close.setPressedIcon(PRESS_CLOSE_BUTTON_ICON); m_close.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.close(); } }); m_maximize = new InnerFrameButton(MAXIMIZE_BUTTON_ICON); m_maximize.setPressedIcon(PRESS_MAXIMIZE_BUTTON_ICON); m_maximize.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.setMaximized(!InnerFrame.this.isMaximized()); } }); m_iconize = new InnerFrameButton(ICONIZE_BUTTON_ICON); m_iconize.setPressedIcon(PRESS_ICONIZE_BUTTON_ICON); m_iconize.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { InnerFrame.this.setIconified(!InnerFrame.this.isIconified()); } }); m_buttonWrapperPanel = new JPanel(); m_buttonWrapperPanel.setOpaque(false); m_buttonPanel = new JPanel(new GridLayout(1,3)); m_buttonPanel.setOpaque(false); m_buttonPanel.add(m_iconize); m_buttonPanel.add(m_maximize); m_buttonPanel.add(m_close); m_buttonPanel.setAlignmentX(0.5f); m_buttonPanel.setAlignmentY(0.5f); m_buttonWrapperPanel.add(m_buttonPanel); m_iconLabel = new JLabel(); m_iconLabel.setBorder(new EmptyBorder( FRAME_ICON_PADDING, FRAME_ICON_PADDING, FRAME_ICON_PADDING, FRAME_ICON_PADDING)); if (m_frameIcon != null) m_iconLabel.setIcon(m_frameIcon); m_titlePanel.add(m_titleLabel, BorderLayout.CENTER); m_titlePanel.add(m_buttonWrapperPanel, BorderLayout.EAST); m_titlePanel.add(m_iconLabel, BorderLayout.WEST); InnerFrameTitleBarMouseAdapter iftbma = new InnerFrameTitleBarMouseAdapter(this); m_titlePanel.addMouseListener(iftbma); m_titlePanel.addMouseMotionListener(iftbma); } // title bar mouse adapter for frame dragging class InnerFrameTitleBarMouseAdapter extends MouseInputAdapter { InnerFrame m_if; int m_XDifference, m_YDifference; boolean m_dragging; public InnerFrameTitleBarMouseAdapter(InnerFrame inf) { m_if = inf; } // don't allow dragging outside of parent public void mouseDragged(MouseEvent e) { int ex = e.getX(); int ey = e.getY(); int x = m_if.getX(); int y = m_if.getY(); int w = m_if.getParent().getWidth(); int h = m_if.getParent().getHeight(); if (m_dragging & m_if.isDraggable()) { if ((ey + y > 0 && ey + y < h) && (ex + x > 0 && ex + x < w)) m_if.setLocation(ex-m_XDifference + x, ey-m_YDifference + y); else if (!(ey + y > 0 && ey + y < h) && (ex + x > 0 && ex + x < w)) { if (!(ey + y > 0) && ey + y < h) m_if.setLocation(ex-m_XDifference + x, 0-m_YDifference); else if (ey + y > 0 && !(ey + y < h)) m_if.setLocation(ex-m_XDifference + x, h-m_YDifference); } else if ((ey + y > 0 && ey + y < h) && !(ex + x > 0 && ex + x < w)) { if (!(ex + x > 0) && ex + x < w) m_if.setLocation(0-m_XDifference, ey-m_YDifference + y); else if (ex + x > 0 && !(ex + x < w)) m_if.setLocation(w-m_XDifference, ey-m_YDifference + y); } else if (!(ey + y > 0) && ey + y < h && !(ex + x > 0) && ex + x < w) m_if.setLocation(0-m_XDifference, 0-m_YDifference); else if (!(ey + y > 0) && ey + y < h && ex + x > 0 && !(ex + x < w)) m_if.setLocation(w-m_XDifference, 0-m_YDifference); else if (ey + y > 0 && !(ey + y < h) && !(ex + x > 0) && ex + x < w) m_if.setLocation(0-m_XDifference, h-m_YDifference); else if (ey + y > 0 && !(ey + y < h) && ex + x > 0 && !(ex + x < w)) m_if.setLocation(w-m_XDifference, h-m_YDifference); } } public void mousePressed(MouseEvent e) { m_if.toFront(); m_XDifference = e.getX(); m_YDifference = e.getY(); m_dragging = true; } public void mouseReleased(MouseEvent e) { m_dragging = false; } } // custom button class for title bar class InnerFrameButton extends JButton { Dimension m_dim; public InnerFrameButton(ImageIcon ii) { super(ii); m_dim = new Dimension(ii.getIconWidth(), ii.getIconHeight()); setOpaque(false); setContentAreaFilled(false); setBorder(null); } public Dimension getPreferredSize() { return m_dim; } public Dimension getMinimumSize() { return m_dim; } public Dimension getMaximumSize() { return m_dim; } } /////////////////////////////////////////////// /////////// Mouse Event Capturing ///////////// /////////////////////////////////////////////// class CapturePanel extends JPanel { public CapturePanel() { MouseInputAdapter mia = new MouseInputAdapter() {}; addMouseListener(mia); addMouseMotionListener(mia); } } // Unchanged code }
Understanding The Code:
Class InnerFrame
New class variables:
ImageIcon MAXIMIZE_BUTTON_ICON
: icon used for the maximize button = [black interior]ImageIcon PRESS_MAXIMIZE_BUTTON_ICON
: icon used for the maximize button in the pressed state = [dark green interior]ImageIcon MINIMIZE_BUTTON_ICON
: icon used for the maximize button to represent minimization = [black interior]ImageIcon PRESS_MINIMIZE_BUTTON_ICON
: icon used for the maximize button in the pressed state, representing minimization = [dark green interior]New instance variables:
int m_x
: used to record the location of InnerFrame before a maximize occursint m_y
: used to record the location of InnerFrame before a maximize occursboolean m_maximizable
: true when frame can be maximized.boolean m_resizable
: True when InnerFrame is not iconified or maximized.JRootPane m_rootPane
: central InnerFrame container--all external access is restricted to this container and its panes.InnerFrameButton m_maximize
: the maximize title bar button.The
InnerFrame constructors now support a fourth boolean parameter specifying whether the frame will be maximizable or not.The
populateInnerFrame() method is now responsible for creating a JRootPane to be used as InnerFrame’s central container. Since InnerFrame now implements the RootPaneContainer interface, we are required to implement access to this JRootPane and its contentPane, layeredPane, glassPane and JMenuBar just as a JFrame or JInternalFrame. Thus get() and set() methods have been implemented for each of these constituents.The
setupCapturePanel() method places an instance of our mouse-event-consuming panel, CapturePanel (see below), in the lowest possible layer of of our rootPane’s layeredPane:protected void setupCapturePanel() { CapturePanel mouseTrap = new CapturePanel(); m_rootPane.getLayeredPane().add(mouseTrap, new Integer(Integer.MIN_VALUE)); mouseTrap.setBounds(0,0,10000,10000); }
We set the bounds of this
CapturePanel to be extremely large so we are, for all practical purposes, guaranteed that mouse events will not pass through the ‘back’ of InnerFrame.The
add() and setLayout() methods we had redirected to another panel in the last section, have been modified to return null and do nothing respectively. This enforces InnerFrame container access through its JRootPane constituents, similar to all other primary Swing containers.The
setMaximizable() method has been added to control the state of the m_maximizeable property and the visibility of the m_maximize button in the title bar.Method
setMaximized() has also been added for maximize and minimize functionality. When InnerFrame is told to maximize it first checks to see if it is iconified. If it is it deiconifies itself. Then it records its dimensions and location and resizes itself to be the size of its parent container. It swaps the maximize button icon for that representing minimize, and setResizable(false) is called to remove mouse listeners (we should not be able to resize a maximized frame). Finally a new method called setDraggable() is called and passed a false value. This method controls a flag that the title bar’s mouseDragged() method checks before along InnerFrame to be dragged. If we set this flag to false InnerFrame will not be draggable. In the maximized state this is desirable.public void setMaximized(boolean b) { m_maximized = b; if (b) { if (isIconified()) setIconified(false); toFront(); m_width = getWidth(); // remember width m_height = getHeight(); // remember height m_x = getX(); // remember x m_y = getY(); // remember y setBounds(0, 0, getParent().getWidth(), getParent().getHeight()); m_maximize.setIcon(MINIMIZE_BUTTON_ICON); m_maximize.setPressedIcon(PRESS_MINIMIZE_BUTTON_ICON); setResizeable(false); setDraggable(false); }
When a minimize occurs,
InnerFrame is moved to the recorded location, set to its stored width and height, and the maximize/minimize button icons are swapped again. setResizable(true) and setDraggable(true) restore full resizeable and draggable functionality:else { toFront(); setBounds(m_x, m_y, m_width, m_height); m_maximize.setIcon(MAXIMIZE_BUTTON_ICON); m_maximize.setPressedIcon(PRESS_MAXIMIZE_BUTTON_ICON); setResizeable(true); setDraggable(true); }
The
setIconified() method has been modified to take into account the possibility that InnerFrame may be iconified from within the maximized state. In this case we call setMaximized(false) before proceeding with the iconfication.The
m_maximize button is created for placement in the title bar, and an ActionListener is attached with an actionPerformed() method that invokes setMaximized(). The title bar’s button panel then allocates an additional cell (it uses GridLayout) for m_maximize, and it is added between the iconify and close buttons.Class InnerFrame.InnerFrameTitleBarMouseAdapter
This class’s
mouseDragged() method is now much more involved. It is somewhat overwhelming at first, but all of this code is actually necessary to smoothly stop the selected InnerFrame from being dragged outside of the visible region of its parent. This code handles all mouse positions allowing vertical movement when horizontal is not possible, horizontal movement when vertical is not possible, and all combinations of possible dragging, while making sure that InnerFrame never leaves the visible region of its parent. It is not necessary that you work through the details, but it is encouraged (similar to the code for the XXResizeEdge classes), as it will provide an appreciation for how complicated situations such as this can be dealt with in an organized mannar.
Reference: Similar code will be used in Chapter 16, section 16.5, where we build an X windows style pager that is not allowed to leave the
JDesktopPane view.
Note: We have not implemented code to stop the user from resizing an
InnerFrame so that its title bar lies outside of the layered pane view. This can result in a lost frame. In order to provide a solution to this we would have to add a considerable amount of code to the NorthResizeEdge mouseDragged() method. It can be done but we will avoid it here because other issues deserve more attention. In a commercial implementation we would want to include code to watch for this. It is interesting that this is not handled in the JDesktopPane/JInternalFrame MDI.
Class InnerFrame.CapturePanel
As we noticed in the past two stages of development, mouse events would pass right through our
InnerFrames. By constructing a component to capture mouse events and placing it in our rootPane‘s layeredPane, we can stop this from happening. This is the purpose of CapturePanel. It is a simple JPanel with an empty MouseInputAdapter shell attached as a MouseListener and MouseMotionListener. This adapter will consume any mouse events passed to it. When an InnerFrame is constructed, as we discussed above, a CapturePanel instance is added at the lowest possible layer of its layeredPane. Thus, mouse events that don’t get handled by a component in a higher layer, such as its contentPane, will get trapped here.Running The Code
Note that we’ve added one line to
LayeredPaneDemo in this section that we didn’t mention yet:frames[i].getContentPane().add(new JScrollPane(new JLabel(ii)));
This places a
JScrollPane containing a JLabel with an image in InnerFrame’s contentPane.Figure 15.8 shows
LayeredPaneDemo in action. Experiment with maximizing, iconifying and restoring. Drag frames all around the layered pane and ensure that they cannot be lost from view. Now resize a frame and notice that we can lose the title bar if it is resized above our JFrame title bar. (This is a flaw that should be accounted for in any commercial MDI.)Now try maximizing an
InnerFrame and changing the size of the JFrame. You will notice that the maximized InnerFrame does not change size along with its parent. In the next section we show how to implement this as well as other important features.15.6 Creating a custom MDI: part IV - Selection and management
When we resize our
JFrame it would be nice if iconified frames lined up and stayed at the bottom. This would prevent them from being lost from the layered pane view, and would also increase the organized feel of our MDI. Similarly, when an InnerFrame is maximized it should always fill the entire viewable region of its parent. Thus, it would be nice to have some way of controlling the layout of our InnerFrames when the parent is resized. We can do this by extending JLayeredPane and implementing the java.awt.event.ComponentListener interface to listen for resize ! FONT>ComponentEvents. We can capture and handle events sent to the componentResized() method to lay out iconified frames and resize maximized frames in a manner similar to most modern MDI environments. (Note that we could also extend ComponentAdapter and use an instance of the resulting class as a ComponentListener attached to our JLayeredPane. As is often the case, there is more than one way to implement the functionality we are looking for for.) In this section we’ll build a JLayeredPane subclass that implements ! ComponentListener, and add it to our mdi package.Another limiting characteristic of
InnerFrame is that the only way to get the focus, and to move an InnerFrame to the front of the layered pane view, is to click on its title bar. Ideally, clicking anywhere on an InnerFrame should move it to the front of the layered pane view. This is where JRootPane’s glassPane comes in handy. Since our InnerFrames now contain a JRootPane as their main container we can use the glassPane! to intercept mouse events and move the selected InnerFrame to the front. We will need to make our glassPane completely transparent, and it should only be active (i.e. receiving mouse events) within an InnerFrame when that InnerFrame is not in the foremost position of its layer (see section 15.1). Thus, only one InnerFrame per layer should have an inactive glassPane. This one frame is the selected InnerFrame--the one with t! he current user focus.It is customary to visually convey to the user which frame is selected. We will do this by adding a new boolean property to our
InnerFrame representing whether or not it is selected. We will manipulate this property in such a way that there can be only one selected InnerFrame per layer. A selected InnerFrame will be characterised by unique border and title bar colors.Figure 15.9. Custom MDI: part IV
<<file figure15-9.gif>>
The Code: InnerFrame.java
see \Chapter15\5\mdi
package mdi; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.EmptyBorder; public class InnerFrame extends JPanel implements RootPaneContainer { // Unchanged code private static Color DEFAULT_TITLE_BAR_BG_COLOR = new Color(108,190,116); private static Color DEFAULT_BORDER_COLOR = new Color(8,90,16); private static Color DEFAULT_SELECTED_TITLE_BAR_BG_COLOR = new Color(91,182,249); private static Color DEFAULT_SELECTED_BORDER_COLOR = new Color(0,82,149); private Color m_titleBarBackground = DEFAULT_TITLE_BAR_BG_COLOR; private Color m_titleBarForeground = Color.black; private Color m_BorderColor = DEFAULT_BORDER_COLOR; private Color m_selectedTitleBarBackground = DEFAULT_SELECTED_TITLE_BAR_BG_COLOR; private Color m_selectedBorderColor = DEFAULT_SELECTED_BORDER_COLOR; private boolean m_selected; // Unchanged code protected void setupCapturePanel() { CapturePanel mouseTrap = new CapturePanel(); m_rootPane.getLayeredPane().add(mouseTrap, new Integer(Integer.MIN_VALUE)); mouseTrap.setBounds(0,0,10000,10000); setGlassPane(new GlassCapturePanel()); getGlassPane().setVisible(true); } // Unchanged code public void toFront() { if (getParent() instanceof JLayeredPane) ((JLayeredPane) getParent()).moveToFront(this); if (!isSelected()) setSelected(true); } // Unchanged code public boolean isSelected() { return m_selected; } public void setSelected(boolean b) { if (b) { if (m_selected != true && getParent() instanceof JLayeredPane) { JLayeredPane jlp = (JLayeredPane) getParent(); int layer = jlp.getLayer(this); Component[] components = jlp.getComponentsInLayer(layer); for (int i=0; i<components.length; i++) { if (components[i] instanceof InnerFrame) { InnerFrame tempFrame = (InnerFrame) components[i]; if (!tempFrame.equals(this)) tempFrame.setSelected(false); } } m_selected = true; updateBorderColors(); updateTitleBarColors(); getGlassPane().setVisible(false); repaint(); } } else { m_selected = false; updateBorderColors(); updateTitleBarColors(); getGlassPane().setVisible(true); repaint(); } } //////////////////////////////////////////// //////////////// Title Bar ///////////////// //////////////////////////////////////////// public void setTitleBarBackground(Color c) { m_titleBarBackground = c; updateTitleBarColors(); } public Color getTitleBarBackground() { return m_titleBarBackground; } public void setTitleBarForeground(Color c) { m_titleBarForeground = c; m_titleLabel.setForeground(c); m_titlePanel.repaint(); } public Color getTitleBarForeground() { return m_titleBarForeground; } public void setSelectedTitleBarBackground(Color c) { m_titleBarBackground = c; updateTitleBarColors(); } public Color getSelectedTitleBarBackground() { return m_selectedTitleBarBackground; } protected void updateTitleBarColors() { if (isSelected()) m_titlePanel.setBackground(m_selectedTitleBarBackground); else m_titlePanel.setBackground(m_titleBarBackground); } // Unchanged code protected void createTitleBar() { // Unchanged code m_titleLabel.setForeground(m_titleBarForeground); } // Unchanged code /////////////////////////////////////////////// ///////////// GlassPane Selector ////////////// /////////////////////////////////////////////// class GlassCapturePanel extends JPanel { public GlassCapturePanel() { MouseInputAdapter mia = new MouseInputAdapter() { public void mousePressed(MouseEvent e) { InnerFrame.this.toFront(); } }; addMouseListener(mia); addMouseMotionListener(mia); setOpaque(false); } } /////////////////////////////////////////////// //////////////// Resizability ///////////////// /////////////////////////////////////////////// public void setBorderColor(Color c) { m_BorderColor = c; updateBorderColors(); } public Color getBorderColor() { return m_BorderColor; } public void setSelectedBorderColor(Color c) { m_selectedBorderColor = c; updateBorderColors(); } public Color getSelectedBorderColor() { return m_selectedBorderColor; } protected void updateBorderColors() { if (isSelected()) { m_northResizer.setBackground(m_selectedBorderColor); m_southResizer.setBackground(m_selectedBorderColor); m_eastResizer.setBackground(m_selectedBorderColor); m_westResizer.setBackground(m_selectedBorderColor); } else { m_northResizer.setBackground(m_BorderColor); m_southResizer.setBackground(m_BorderColor); m_eastResizer.setBackground(m_BorderColor); m_westResizer.setBackground(m_BorderColor); } } // Unchanged code }
The Code: MDIPane.java
see \Chapter15\5\mdi
package mdi; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class MDIPane extends JLayeredPane implements ComponentListener { public MDIPane() { addComponentListener(this); setOpaque(true); // default background color setBackground(new Color(244,232,152)); } public void componentHidden(ComponentEvent e) {} public void componentMoved(ComponentEvent e) {} public void componentShown(ComponentEvent e) {} public void componentResized(ComponentEvent e) { lineup(); } public void lineup() { int frameHeight, frameWidth, currentX, currentY, lheight, lwidth; lwidth = getWidth(); lheight = getHeight(); currentX = 0; currentY = lheight; Component[] components = getComponents(); for (int i = components.length-1; i > -1; i--) { if (components[i] instanceof InnerFrame) { InnerFrame tempFrame = (InnerFrame) components[i]; frameHeight = tempFrame.getHeight(); frameWidth = tempFrame.getWidth(); if (tempFrame.isMaximized()) { tempFrame.setBounds(0,0,getWidth(),getHeight()); tempFrame.validate(); tempFrame.repaint(); } else if (tempFrame.isIconified()) { if (currentX+frameWidth > lwidth) { currentX = 0; currentY -= frameHeight; } tempFrame.setLocation(currentX, currentY-frameHeight); currentX += frameWidth; } } } } }
Understanding The Code:
Class LayeredPaneDemo
The only changes that have been made to this class is the replacement of the default
JLayeredPane in our application frame with an instance of our custom MDIPane (see below).Class InnerFrame
The following class variables have been added:
Color DEFAULT_TITLE_BAR_BG_COLOR
: default title bar background.Color DEFAULT_BORDER_COLOR
: default border background.Color DEFAULT_SELECTED_TITLE_BAR_BG_COLOR
: default selected title bar background.Color DEFAULT_SELECTED_BORDER_COLOR
: default selected frame border.New instance variables:
Color m_titleBarBackground
: current title bar background.Color m_titleBarForeground
: current title bar foreground.Color m_BorderColor
: current border.Color m_selectedTitleBarBackground
: current selected title bar background.Color m_selectedBorderColor
: current selected border background.boolean m_selected
: true when frame is selected.The
setupCapturePanel() method now adds a call to set InnerFrame’s glassPane to an instance of our custom class GlassCapturePanel (see below). This allows selection via clicking on any region of an inactive InnerFrame.We’ve inserted an additional check in the
toFront() method to call setSelected(true) if that frame is not already selected:if (!isSelected())
setSelected(true);
The
isSelected() method has been added to simply return the current value of m_selected, and method setSelected() is what actually controlls this property.Method
setSelected() takes a boolean value representing whether the frame should be selected or de-selected. If it is to be selected and it resides in a JLayeredPane, this method searches for all other InnerFrame siblings in the same layer of that JLayeredPane and calls setSelected(false) on each one it finds. Then we set the current InnerFrame’s selected property, m_selected, to true and call updateBorderColors() and updateTitleBarColors() (see below) to visually convey that this is the selected frame:m_selected = true;
updateBorderColors();
updateTitleBarColors();
getGlassPane().setVisible(false);
repaint();
The
glassPane is hidden whenever a frame is selected so that mouse events will no longer be trapped (see GlassCapturePanel below). When a frame is de-selected (i.e. setSelected(false) has been called), this method disables its selected property, calls the updateXXColors() methods, and brings its glassPane out of hiding so that it may intercept mouse events for future selection:m_selected = false;
updateBorderColors();
updateTitleBarColors();
getGlassPane().setVisible(true);
repaint();
This whole scheme provides us with a guarantee that only one
InnerFrame will be selected per JLayeredPane layer.Methods
setTitleBarBackground(), setTitleBarForeground(), and setSelectedTitleBarBackground() have all been added to manage the state of the current title bar color properties. Each of these methods calls updateTitleBarColors() so that the changes made are actually applied to the title bar and border components. In the JavaBeans spirit, we’ve also added get() methods to retreive these properties.Similarly, methods
setBorderColor(), setSelectedBorderColor(), updateBorderColors(), and associated get() methods, to manage the border color properties. The updateBorderColors() method is responsible for applying these colors to each of the resize edge components.Class mdi.InnerFrame.GlassCapturePanel
This class is almost identical to our
CapturePanel inner class. The only difference is that its MouseInputAdapter overrides the mousePressed() method to call toFront() on the associated InnerFrame.public void mousePressed(MouseEvent e) {
InnerFrame.this.toFront();
}
As we saw above,
toFront() calls setSelected() as necessary. Instances of this class are used as the glassPane of each InnerFrame’s JRootPane. GlassCapturePanel is active (visible) when its parent InnerFrame is not selected. It is inactive (hidden) when the associated InnerFrame is selected. This activation is controlled by the setSe! lected() method, as we saw above. The only function of this component is to provide a means of switching InnerFrame selection by clicking on any portion of an unselected InnerFrame.Class mdi.MDIPane
This class extends
JLayeredPane and implements the java.awt.event.ComponentListener interface. Whenever this component is resized the componentResized() method is invoked. This method invokes lineup()which grabs an array of all Components within the MDIPane. We then loop through this array, each time checking whether the Component at the current index is an instance of InnerFrame.Component[] components = getComponents();
for (int i=components.length-1; i>-1; i--) {
if (components[i] instanceof InnerFrame) {
If it is we then check if it is maximized or iconified. If it is maximized we reset its bounds to completely fill the visible region of the
MDIPane. If it is iconified we place it at the bottom of the layered pane. This method locally maintains the position where the next iconified frame should be placed (currentX and currentY) and places these frames in rows, stacked from bottom up, that completely fit within MDIPane’s horizontal visible region (refer back to the code for details).Running The Code
Figure 5.9 shows
LayeredPaneDemo in action. Iconify the InnerFrames and adjust the size of the application frame to see the layout change. Now maximize an InnerFrame and adjust the size of the JFrame to see that the InnerFrame is resized appropriately. You may also want to experiment with the LayeredPaneDemo constructor and add another set or two of InnerFrames to different layers. You will see that there can only be one selectedThis method of organizing iconified frames is certainly not adequate for professional implementations. However, developing it any further would take us a bit too far into the details of MDI construction. Ideally we might implement some sort of manager that
InnerFrames and MDIPane can use to communicate with one another. (In the next chapter we will discuss a class called DesktopManager which functions as such a communications bridge between JDesktopPane and its JInternalFrame children. We will also learn that such a manager, as simple as it is, provides us with a great deal of flexibility.)15.7 Creating a custom MDI: part V - JavaBeans compliance
The functionality of our
InnerFrame is pretty much complete at this point. However, there is still much to be desired if we plan to use InnerFrame in the field. JavaBeans compliance is one feature that not only is popular, but has come to be expected of each and every Java GUI component. In this section we will enhance InnerFrame by implementing the Externalizable interface, providing us with full control over its serialization. Although JComponent provides a default serialization mechanism for all Swing components, this is far from reliable at the time of this w! riting. Implementing our own serialization mechanism is not only reliable and safe for both long and short-term persistency, but it is also efficient. The default serialization mechanism tends to store much more information than we actually need.Figure 15.10. Custom MDI: part V
<<file figure15-10.gif>>
The Code: InnerFrame.java
see \Chapter15\6\mdi
package mdi; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.EmptyBorder; public class InnerFrame extends JPanel implements RootPaneContainer, Externalizable { // Unchanged code /////////////////////////////////////////////// /////////////// Serialization ///////////////// /////////////////////////////////////////////// public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(m_titleBarBackground); out.writeObject(m_titleBarForeground); out.writeObject(m_BorderColor); out.writeObject(m_selectedTitleBarBackground); out.writeObject(m_selectedBorderColor); out.writeObject(m_title); out.writeBoolean(m_iconizeable); out.writeBoolean(m_resizeable); out.writeBoolean(m_closeable); out.writeBoolean(m_maximizeable); out.writeObject(m_frameIcon); out.writeObject(getBounds()); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { setTitleBarBackground((Color)in.readObject()); setTitleBarForeground((Color)in.readObject()); setBorderColor((Color)in.readObject()); setSelectedTitleBarBackground((Color)in.readObject()); setSelectedBorderColor((Color)in.readObject()); setTitle((String)in.readObject()); setIconizeable(in.readBoolean()); setResizeable(in.readBoolean()); setCloseable(in.readBoolean()); setMaximizeable(in.readBoolean()); setSelected(false); setFrameIcon((ImageIcon)in.readObject()); Rectangle r = (Rectangle)in.readObject(); r.x = getX(); r.y = getY(); setBounds(r); } }
Understanding The Code:
The added support for
InnerFrame serialization here is quite simple. The readExternal() method will be invoked when readObject() is called on a given ObjectInput stream pointing to a previouly serialized InnerFrame. The writeExternal() method will be invoked when writeObject() is passed an InnerFrame and called on a given ObjectOutput stream. Refer back to chapter 4, section 4.7 to see h! ow this is implemented in our BeanContainer JavaBeans environment.Running The Code
Figure 5.10 shows an instance of
InnerFrame, instantiated from a serialized InnerFrame saved to disk, and loaded into our JavaBeans property editing environment. We started the construction of this environment in chapter 4 and it will be completed (as shown) in chapter 18. The point to make here is that InnerFrame is now a JavaBean. There are certainly many ways to make InnerFrame a better bean. Specifically, many of the class variables would allow greater flexibility as properties, such as the default title bar height, border thickness, frame icon padding, button icons, etc. (Some of these might actually be better off within UI delegate code. Colors and button icons should change with look-and-feel, not be part of the component itse! lf.) We could also add support for communication (which is completely lacking in InnerFrame now). For instance, we could make m_maximized into a bound or constrained property by sending out PropertyChangeEvents or VetoableChangeEvents respectively (refer back to chapter 2 for a discussion of JavaBeans and properties.). In this way we could notify interested listeners that a miximization is about to occur (in the case that m_maximize is constrained), and give them an opportunity to veto it.Another major feature lacking in
InnerFrame is look-and-feel support. The title bar and borders look like standard army-issued components at best. They should respond to look-and-feel changes just like any other Swing component. In chapter 21 we will implement support for all the major look-and-feels (Metal, Motif, Windows) for InnerFrame, plus our own custom look-and-feel (Malachite).