Java : Modifier et customiser un JTabbedPane

Partager cet article

Temps estimé pour la lecture de cet article : 30 min

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 :

JTabbedPane : comportement par défaut

JTabbedPane : comportement par défaut

Et voila le résultat que l’on va obtenir :

ClosableTabbedPane : notre nouveau JTabbedPane

ClosableTabbedPane : notre nouveau JTabbedPane

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 !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.