There are many ways to create your own window type in Java. The level of complexity often depends on the level of customization that you want. I’m going to walk you through the creation of a simple, custom, “frameless”, window that extends JDialog. I started off using JWindow, but noticed that it was not passing focus to my JTextField components. I probably could have implemented a focus traversal policy, but since the JDialog‘s focus behavior was what I needed anyway, I just extended it instead.
public class BasicDraggableWindow extends JDialog |
If there’s one thing I’ve learned it’s that there’s no sense in reinventing the wheel. If something already exists that will fit your needs, then use it. If it’s not exactly what you need, then try to extend it, if possible, rather than starting from scratch, unless you’re doing it for educational purposes. Think about what you want to use it for and how it should be different than what already exists.
I wanted a window that didn’t have a title bar and frame, but I still wanted it to be draggable by clicking anywhere in the window. I wanted my own custom “close button” in the top right. I also wanted it to have a slightly 3D look to it.
I usually have an initialize() method that I call from my constructor to – you guessed it – initialize my components.
private void initialize() { // don’t show a frame or title bar setUndecorated( true ); // Create JPanel and set it as the content pane contentPane = new JPanel(); setContentPane( contentPane ); // If main has not already been created, create it. // Explained later if ( main == null ) { main = new JPanel(); } // Create panel for close button closePanel = new JPanel( new BorderLayout() ); // Create point to catch initial mouse click coordinates initialClick = new Point(); } |
First, I call setUndecorated( true ) to get rid of the frame and title bar. Next, I create the JPanels that I will be using. The content pane of a window is where all Swing components are placed by default when calling the Containter method add(). I like to create my own content pane and keep a reference to it. In this case, my content pane is going to have a BorderLayout with closeButton at the top and main in the center. The closeButton panel is where I’m going to put the button that will close my window. The main panel is what users of my class will see as the content pane and therefore where all of their components will be placed (I’ll explain why it’s inside an if statement later). If I’m hiding the actual content pane, I need to override the getContentPane() and add() methods.
public JPanel getContentPane() { return main; } public Component add( Component comp ) { return main.add( comp ); } |
You may be wondering why on earth I would want to do this. I want to ensure that my content pane always has a BorderLayout and that my close button is always at the top right. Obscuring the actual content pane keeps a user of my class from messing up the layout of my window by calling getContentPane() or add(), and referencing the actual content pane.
Another issue I faced was how to override the setLayout() method. It wasn’t as trivial as the other two because the Window.init() method calls setLayout() when creating a new Window, so if I override it I need to take that into account.
public void setLayout( LayoutManager manager ) { if ( main == null ) { main = new JPanel(); main.setLayout( new FlowLayout() ); } else { main.setLayout( manager ); } if ( !(getLayout() instanceof BorderLayout) ) { super.setRootPaneCheckingEnabled( false ); super.setLayout( new BorderLayout() ); super.setRootPane( super.getRootPane() ); super.setRootPaneCheckingEnabled( true ); } } |
Here I check to see if main is null, just as I did in my initialize() method. The reason for this is because when I create a JDialog, its superclass constructor (Window) calls setLayout() before anything else in my class is created, so main is always null at this point, and this will avoid NullPointerExceptions when the window is created. Subsequent calls to setLayout() will fall into the else portion of the block. The next if block handles calls to this method from Window. It ensures that the JDialog has a BorderLayout with its root pane in the center, as is the default. Notice that this block of code begins with super.setRootPaneCheckingEnabled( false ) and ends with super.setRootPaneCheckingEnabled( true). This sets whether calls to add() and setLayout are forwarded to the content pane. So, for the duration of this block I want them to act on the JDialog itself and not the content pane.
After my constructor calls initialize() it calls showWindow():
private void showWindow() { // If not set, default to FlowLayout if ( main.getLayout() == null ) { setLayout( new FlowLayout() ); } // close “button” – show this image by default closeNormal = new ImageIcon( getClass().getResource( “close.gif” ) ); // close “button” – show this when the mouse enter is detected closeHover = new ImageIcon( getClass().getResource( “close_hov.gif” ) ); // close “button” – show this image when mouse press is detected closePress = new ImageIcon( getClass().getResource( “close_press.gif” ) ); closeLabel = new JLabel( closeNormal ); // Put the label with the image on the far right closePanel.add( closeLabel, BorderLayout.EAST ); // Add the two panels to the content pane contentPane.setLayout( new BorderLayout() ); contentPane.add( closePanel, BorderLayout.NORTH ); contentPane.add( main, BorderLayout.CENTER ); // set raised beveled border for window contentPane.setBorder( BorderFactory.createRaisedBevelBorder() ); // Set position somewhere near the middle of the screen Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); setLocation ( screenSize.width / 2 – ( getWidth()/ 2 ), screenSize.height / 2 – ( getHeight() / 2 ) ); // keep window on top of others setAlwaysOnTop( true ); } |
Here I create three images that represent the three states of my “button” (I’m calling it a button, but it’s just a JLabel with an image that changes in response to certain MouseEvents.). Everything else is pretty much self-explanatory. I set the layouts so the button panel is at the top and the image as at the far right and the main panel is in the center. I use BorderFactory to create the outset, beveled border around the content pane to give the window a sort of 3D look. Then I set the default location of the window near the center of the screen and make sure that it’s always on top of other windows. The last bit is optional, but for my purposes was useful.
Last, my constructor calls the installListeners() method:
private void installListeners() { // Get point of initial mouse click addMouseListener( new MouseAdapter() { public void mousePressed( MouseEvent e ) { initialClick = e.getPoint(); getComponentAt( initialClick ); } }); // Move window when mouse is dragged addMouseMotionListener( new MouseMotionAdapter() { public void mouseDragged( MouseEvent e ) { // get location of Window int thisX = getLocation().x; int thisY = getLocation().y; // Determine how much the mouse moved since the initial click int xMoved = ( thisX + e.getX() ) – ( thisX + initialClick.x ); int yMoved = ( thisY + e.getY() ) – ( thisY + initialClick.y ); // Move window to this position int X = thisX + xMoved; int Y = thisY + yMoved; setLocation( X, Y ); } }); // Close “button” (image) listeners closeLabel.addMouseListener( new MouseAdapter() { public void mousePressed( MouseEvent e ) { closeLabel.setIcon( closePress ); } public void mouseReleased( MouseEvent e ) { closeLabel.setIcon( closeNormal ); } public void mouseEntered( MouseEvent e ) { closeLabel.setIcon( closeHover ); } public void mouseExited( MouseEvent e ) { closeLabel.setIcon( closeNormal ); } public void mouseClicked( MouseEvent e ) { close(); } }); } // close and dispose public void close() { setVisible( false ); dispose(); } |
The first mouse listener is added to the JDialog and is meant to detect a mouse click and set initialClick to that position. You need this initial position get the window to drag “properly.” The next listener is detects when the mouse is dragged over the window. In order to make the window drag properly, you need three pieces of information each time the mouse is dragged to a new position.
- The window’s position – (X,Y) coordinates of the top left of the window.
- The current position of the mouse cursor
- the initial position of the mouse cursor when the dragging began
Once you have this information, you can calculate the new position of the window after each mouse drag event, as I have above. The next mouse listener changes the image displayed for my close button. Most windows have these types of buttons and they “highlight” when the mouse moves over them, and “depress” when the mouse is pressed over them. I chose images the create this illusion and switch them each time the appropriate event is detected. And when the mouse is clicked over the button the close() method is called, which makes the window invisible and disposes of it.
The full code and images for this component can be found here. And here are a couple of boring, uncolored screen captures: