In my free time, I spend a lot of time creating small and large applications that use Swing components. In a recent attempt to create cleaner and better code by OO standards I decided to create a central Action class that acts as an ActionListener for ALL components in my classes. The Action interface implements the ActionListener interface, therefore implementing the Action interface in my Action class takes care of the need to implement the ActionPerformed method for clicks on buttons and so. It also provides the possibility to map KeyEvents to ActionEvents by binding a KeyStroke to an ActionMapKey through the InputMap of a component and binding that ActionMapKey to an Action through the ActionMap of that component. Any KeyStroke that is mapped in this way, results in an ActionEvent being picked up by the ActionListener(s) that has (have) been registered with this component, i.e. my Action class.
In order to identify the correct component as source of the ActionEvents that happen, I thought I’d set a name to all components that have Actions registered and compare the name of the source of the ActionEvent to a list of names that are set in my application. One of my big frustrations with this approach is the lack of being able to search for a specific component in my frames by querying for the name of the component. In this next article I explain what method I implemented to get this functionality.
Generally, in a Swing application, a strong hierarchy exists between containers and components. Any Window usually contains several Panels which in their turn may contain several other JPanels and Components. As far as I know, all Swing containers and components are subclasses of AWT classes and all of them are subclasses of the java.awt.Component class (actually, they all are subclass of the java.awt.Container class which is a direct subclass of the java.awt.Component class). The java.awt.Container class contains the method getComponents() which returns an array of Components. The reason an array of components is returned, and not an array of Containers, is that in AWT Button, Canvas, Checkbox, Choice, Label, List, Scrollbar and TextComponent are Components and not Containers. In other words, the getComponent() method is an AWT method, not a Swing method. But that doesn’t mean it can’t be used in Swing!
So, the getComponents() method allows to loop through all components in a java.awt.Container instance. This let me to reason like this: while looping through all Components in a Container, if the name of a Component equals the one I am looking for, return that Component. If the Component turns out to be a Container, recursively call this piece of code, now using this Component cast to a Container. In the end, if the Component is not found, return null. This leads to this piece of code:
public Component findComponentByName(Container container, String componentName) { for (Component component: container.getComponents()) { if (componentName.equals(component.getName())) { return component; } if (component instanceof Container) { Container newContainer = (Container)component; return findComponentByName(newContainer, componentName); } } return null; }
There are several problems with this code. First of all, ALL Swing Components are Containers. This code will try to find a Component with the name “componentName” in every Swing component it encouters. And then there is another problem.
All Swing components representing a window-like object (JFrame, JDialog, JWindow, JApplet and JInternalFrame) have a pane called the contentpane to which all Components are added. Unfortunately, the contentpane is not directly accessible via the getComponents() method. The reason is that these contantpanes are attrached to a JRootPane. Fortunately, the JRootPane of a Container (if it has one) is accessible via the getComponents() method. So I modifed my code to look like this:
public Component findComponentByName(Container container, String componentName) { for (Component component: container.getComponents()) { if (componentName.equals(component.getName())) { return component; } if (component instanceof JRootPane) { // According to the JavaDoc for JRootPane, JRootPane is // "A lightweight container used behind the scenes by JFrame, // JDialog, JWindow, JApplet, and JInternalFrame.". The reference // to the RootPane is set up by implementing the RootPaneContainer // interface by the JFrame, JDialog, JWindow, JApplet and // JInternalFrame. See also the JavaDoc for RootPaneContainer. // When a JRootPane is found, recurse into it and continue searching. JRootPane nestedJRootPane = (JRootPane)component; return findComponentByName(nestedJRootPane.getContentPane(), componentName); } } return null; }
This code works for all Containers with a JRootPane. It only fails in two cases: when the Container is an actual AWT class (not a subclass like the Swing classes) or when the Container is a JPanel. I only solved the last case, so when the Container is a JPanel.
JPanels don’t have a JRootPane nor a contentpane. Instead, Components are directly added to the JPanel instance. This situation can be solved by simply adding a few lines of code:
public Component findComponentByName(Container container, String componentName) { for (Component component: container.getComponents()) { if (componentName.equals(component.getName())) { return component; } if (component instanceof JRootPane) { // According to the JavaDoc for JRootPane, JRootPane is // "A lightweight container used behind the scenes by JFrame, // JDialog, JWindow, JApplet, and JInternalFrame.". The reference // to the RootPane is set up by implementing the RootPaneContainer // interface by the JFrame, JDialog, JWindow, JApplet and // JInternalFrame. See also the JavaDoc for RootPaneContainer. // When a JRootPane is found, recurse into it and continue searching. JRootPane nestedJRootPane = (JRootPane)component; return findComponentByName(nestedJRootPane.getContentPane(), componentName); } if (component instanceof JPanel) { // JPanel found. Recursing into this panel. JPanel nestedJPanel = (JPanel)component; return findComponentByName(nestedJPanel, componentName); } } return null; }
This code works like a charm. Please let me know if you have created a different approach to this problem. I’ll be very interested to learn any other possible solutions to finding a Component with a certain name, or even to identifying the source of an ActionEvent in case it is a Swing component.
I disagree that they’re the same. getAncestorNamed is a simplistic getParent() loop that checks each component moving *up* the hierarchy (and not laterally at all). getComponentByName searches *every* component moving *down* the hierarchy which is far more useful and rather idiotic that Java doesn’t already provide a more efficient version (say by mapping each component when setName is used). Why have setName at all if there’s no mechanism for searching by it?!?
We sure do. On the other hand, despite the fact that the method I was trying to implement already exists, I did learn quite a lot about the Swing class hierarchy and the interfaces involved in Swing. I also learned to scrutinize the jdk JavaDoc even better than I already do.
Wouter
I thought that code looked somewhat familiar. 🙂
We live and learn don’t we?
And of course it turns out in the class SwingUtilities there is a method called getAncestorNamed(String name, Component comp) which essentially does the same. Oh well…