Dans cet article je vais vous présenter comment personnaliser le JTabbedPane de swing, en effet par défaut le composant de swing est plutôt simple. Ici on va chercher à pouvoir fermer les onglets et même les customiser. Pour ce faire on va redéfinir les méthodes du composant BasicTabbedPaneUI.
Comparaison entre le composant par défaut et le résultat que l’on veut obtenir
Afin de pouvoir fermer un onglet, on définit une classe qui extends un JTabbedPane. On redéfinit certaines méthodes afin de dessiner une croix sur l’onglet permettant la fermeture. On crée une classe qui va implémenter MouseListener et MouseMotionListener afin d’ajouter la fermeture de l’onglet désiré. Enfin avant de commencer à expliquer le code, je vais vous montrer ce qu’on a par défaut et ce que l’on va obtenir :
Et voila le résultat que l’on va obtenir :
Créer une fenêtre Java
On va commencer par créer une classe main et fenetre, dans le main on créera seulement une instance de la classe Fenetre.
public class Main { public static void main(String[] args) { Fenetre fen = new Fenetre(); } }
public class Fenetre extends JFrame{ private static final long serialVersionUID = 1L; private JPanel central; public Fenetre() { central = new JPanel(new MigLayout("fill,insets 0")); central.setBackground(new Color(119, 135, 136)); central.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1,Color.GRAY)); ClosableTabbedPane p = new ClosableTabbedPane(); p.setUI(new Tabbed()); JPanel h = new JPanel(); central.add(p,"grow"); p.addTab("Onglet 1", h); p.addTab("Onglet 2",new JPanel()); p.setupTabTraversalKeys(p); this.setTitle("JTabbedPane"); this.setSize(800, 600); this.setLocationRelativeTo(null); // centre l'appli this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.add(central); this.setVisible(true); } }
Rédéfinir l’apparence d’un onglet en héritant de la classe BasicTabbedPaneUI
On va ensuite créé la classe qui extends le BasicTabbedPaneUI qui va nous permettre de redéfinir le style des onglets et des fenêtres.
public class Tabbed extends BasicTabbedPaneUI { private FontMetrics boldFontMetrics; private Font boldFont; protected void paintTabBackground( Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected ) { g.setColor(new Color(170, 180, 179)); g.fillRect(x, y, w, h); if (isSelected) { g.setColor(new Color(62, 193, 189)); g.fillRect(x, y, w, h); } } protected void paintTabBorder( Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected ) { g.setColor(new Color(19, 19, 19)); g.drawRect(x, y, w, h); if (isSelected) { g.setColor(new Color(121, 134, 133)); } } protected int calculateTabHeight(int tabPlacement, int tabIndex, int fontHeight) { int vHeight = fontHeight; if (vHeight % 2 > 0) { vHeight += 2; } return vHeight; } protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) { return super.calculateTabWidth(tabPlacement, tabIndex, metrics) + metrics.getHeight() + 15; } protected int getTabLabelShiftY(int tabPlacement, int tabIndex, boolean isSelected) { return 0; } protected Insets getContentBorderInsets(int tabPlacement) { return new Insets(0, 0, 0, 0); } protected void installDefaults() { super.installDefaults(); tabAreaInsets.left = 0; selectedTabPadInsets = new Insets(0, 0, 0, 0); tabInsets = selectedTabPadInsets; boldFont = tabPane.getFont().deriveFont(Font.BOLD); boldFontMetrics = tabPane.getFontMetrics(boldFont); } protected void paintText( Graphics g, int tabPlacement, Font font, FontMetrics metrics, int tabIndex, String title, Rectangle textRect, boolean isSelected ) { if (isSelected) { int vDifference = (int) (boldFontMetrics.getStringBounds(title, g).getWidth()) - textRect.width; textRect.x -= (vDifference / 2); super.paintText(g, tabPlacement, boldFont, boldFontMetrics, tabIndex, title, textRect, isSelected); } else super.paintText(g, tabPlacement, font, metrics, tabIndex, title, textRect, isSelected); } }
Créer un système d’onglet qui peut se fermer en héritant de la classe JTabbedPane
On crée maintenant la classe qui va extends le JTabbedPane. Cette classe se chagera de dessiner l’onglet et de gérer l’action de fermeture de celui-ci. C’est aussi à ce niveau qu’on va gérer les raccourcis claviers !
public class ClosableTabbedPane extends JTabbedPane { private static final long serialVersionUID = 1L; private TabCloseUI closeUI = new TabCloseUI(this); public void paint(Graphics g) { super.paint(g); closeUI.paint(g); } @Override public void addTab(String title, Component component) { super.addTab(title + " ", component); } public String getTabTitleAt(int index) { return super.getTitleAt(index).trim(); } public boolean tabAboutToClose(int tabIndex) { return true; } /** * Permet d'utiliser les raccourcis : ctrl+ tab et ctrl + shift + tab, afin de naviguer entre les onglets. * Mais aussi, d'utiliser le raccourci ctrl+w afin de fermer l'onglet selectionné. */ public void setupTabTraversalKeys(final JTabbedPane tabbedPane) { KeyStroke ctrlTab = KeyStroke.getKeyStroke("ctrl TAB"); KeyStroke ctrlShiftTab = KeyStroke.getKeyStroke("ctrl shift TAB"); KeyStroke controlW = KeyStroke.getKeyStroke("control W"); Set forwardKeys = new HashSet(tabbedPane.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)); forwardKeys.remove(ctrlTab); tabbedPane.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys); Set backwardKeys = new HashSet(tabbedPane.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)); backwardKeys.remove(ctrlShiftTab); tabbedPane.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, backwardKeys); AbstractAction closeTabAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { if(tabbedPane.getTabCount() > 0) tabbedPane.remove(tabbedPane.getSelectedIndex()); } }; InputMap inputMap = tabbedPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); inputMap.put(ctrlTab, "navigateNext"); inputMap.put(ctrlShiftTab, "navigatePrevious"); inputMap.put(controlW, "closeTab"); tabbedPane.getActionMap().put("closeTab", closeTabAction); } }
Attardons nous sur la méthode setupTabTraversalKeys. On définit un Set qui contiendra l’ensemble des raccourcis du JTabbedPane (getFocusTraversalKeys). A laquelle, on va supprimer les événements qui nous intéresse (ctrl tab, ctrl shift tab) afin de pouvoir les redéfinir.
La classe InputMap fournit une liaison entre un événement d’entrée et un objet. On utilise la classe ActionMap, afin de déterminer une action à effectuer lorsqu’une touche est enfoncée.
On récupère l’entréee du composant utilisé, et on utilise ces constantes. On ajoute avec le put la liaison entre le keystroke et l’action du même nom. Pour le ctrl+w on ajoute la liaison à l’action anonyme.
Implémenter l’action de fermeture de l’onglet
On va donc créer une classe TabCloseUI, qui va prendre en paramètre notre ClosableTabbedPane et qui va lui associer les listeners voulus. C’est grâce à cette dernière que nous allons pouvoir gérer l’affichage d’une croix sur un onglet.
public class TabCloseUI { private final int width = 8, height = 8; private int closeX = 0, closeY = 0, meX = 0, meY = 0; private int selectedTab; private final Rectangle rectangle = new Rectangle(0, 0, width, height); private final ClosableTabbedPane tabbedPane; public TabCloseUI(ClosableTabbedPane pane) { tabbedPane = pane; } private void controlCursor() { if (tabbedPane.getTabCount() > 0) if (closeUnderMouse(meX, meY)) { tabbedPane.setCursor(new Cursor(Cursor.HAND_CURSOR)); if (selectedTab > -1) tabbedPane.setToolTipTextAt(selectedTab, "Close " + tabbedPane.getTitleAt(selectedTab)); } else { tabbedPane.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); if (selectedTab > -1) tabbedPane.setToolTipTextAt(selectedTab, ""); } } private boolean closeUnderMouse(int x, int y) { rectangle.x = closeX; rectangle.y = closeY; return rectangle.contains(x, y); } public void paint(Graphics g) { int tabCount = tabbedPane.getTabCount(); for (int j = 0; j < tabCount; j++) if (tabbedPane.getComponent(j).isShowing()) { int x = tabbedPane.getBoundsAt(j).x + tabbedPane.getBoundsAt(j).width - width - 5; int y = tabbedPane.getBoundsAt(j).y + 5; drawClose(g, x, y); break; } if (mouseOverTab(meX, meY)) { drawClose(g, closeX, closeY); } } private void drawClose(Graphics g, int x, int y) { if (tabbedPane != null && tabbedPane.getTabCount() > 0) { Graphics2D g2 = (Graphics2D) g; drawColored(g2, isUnderMouse(x, y) ? Color.GRAY : new Color(163, 163, 163), x, y); } } private void drawColored(Graphics2D g2, Color color, int x, int y) { // change la taille de l'ombre de la croix g2.setStroke(new BasicStroke(3, BasicStroke.JOIN_ROUND, BasicStroke.CAP_ROUND)); g2.setColor(Color.BLACK); g2.drawLine(x, y, x + width, y + height); g2.drawLine(x + width, y, x, y + height); g2.setColor(color); g2.setStroke(new BasicStroke(3, BasicStroke.JOIN_ROUND, BasicStroke.CAP_ROUND)); g2.drawLine(x, y, x + width, y + height); g2.drawLine(x + width, y, x, y + height); } /** * La methode isUnderMouse renvoit un boolean permettant de savoir si les coordonnes de la souris * et de la croix de l'onglet concordent. */ private boolean isUnderMouse(int x, int y) { return Math.abs(x - meX) < width && Math.abs(y - meY) < height; } private boolean mouseOverTab(int x, int y) { int tabCount = tabbedPane.getTabCount(); for (int j = 0; j < tabCount; j++) if (tabbedPane.getBoundsAt(j).contains(meX, meY)) { selectedTab = j; closeX = tabbedPane.getBoundsAt(j).x + tabbedPane.getBoundsAt(j).width - width - 5; closeY = tabbedPane.getBoundsAt(j).y + 5; return true; } return false; } }
Implémenter l’action de fermeture de l’onglet
Enfin maintenant qu’on a dessiné notre croix on va lui associer une action en implémentant les événements MouseListener et MouseMotionListener.
public class TabCloseUI implements MouseListener, MouseMotionListener { ... public TabCloseUI(ClosableTabbedPane pane) { tabbedPane = pane; tabbedPane.addMouseMotionListener(this); tabbedPane.addMouseListener(this); } ... @Override public void mouseEntered(MouseEvent me) { } @Override public void mouseExited(MouseEvent me) { } @Override public void mousePressed(MouseEvent me) { } @Override public void mouseClicked(MouseEvent me) { } @Override public void mouseDragged(MouseEvent me) { } @Override public void mouseReleased(MouseEvent me) { if (closeUnderMouse(me.getX(), me.getY())) { boolean isToCloseTab = tabAboutToClose(selectedTab); if (isToCloseTab && selectedTab > -1) { tabbedPane.removeTabAt(selectedTab); } selectedTab = tabbedPane.getSelectedIndex(); } } @Override public void mouseMoved(MouseEvent me) { meX = me.getX(); meY = me.getY(); if (mouseOverTab(meX, meY)) { controlCursor(); tabbedPane.repaint(); } } public boolean tabAboutToClose(int tabIndex) { return true; } ... }
Et voilà, c’est bon vous pouvez utiliser votre tout niveau JTabbedPane !