JAVA - André CLARINVAL
Transcription
JAVA - André CLARINVAL
JAVA – Les Interfaces Homme–Machine A. Clarinval 02/2003 Notes complémentaires de synthèse (cf. JAVA Tutorial) LE PAQUETAGE JAVA.APPLET...............................................................................................................................1 1. Overview.....................................................................................................................................................1 1.1. 1.2. 1.3. 1.4. Example.................................................................................................................................................................1 Methods for Milestones .........................................................................................................................................2 Methods inherited from java.awt (Abstract Window Toolkit) ........................................................................3 What Applets Can and Can't Do ............................................................................................................................3 2. La classe java.applet.Applet .........................................................................................................3 LE PAQUETAGE JAVAX.SWING...............................................................................................................................5 1. About the JFC and Swing ...........................................................................................................................5 1.1. What Are the JFC and Swing?...............................................................................................................................5 1.2. How Are Swing Components Different from AWT Components? .......................................................................5 2. A Quick Tour of a Swing Application's Code .............................................................................................6 3. Swing Features and Concepts ....................................................................................................................8 3.1. 3.2. 3.3. 3.4. 3.5. 3.6. 4. 5. 6. 7. Swing Components and the Containment Hierarchy .............................................................................................8 Layout Management ..............................................................................................................................................9 Event Handling....................................................................................................................................................11 Painting................................................................................................................................................................12 Threads and Swing ..............................................................................................................................................13 More Swing Features and Concepts ....................................................................................................................14 La classe javax.swing.JApplet.....................................................................................................14 La classe de base javax.swing.JComponent.................................................................................15 Liste des matières .....................................................................................................................................17 Exemple : Eurolette — Conversion en euros...........................................................................................20 7.1. 7.2. 7.3. 7.4. 7.5. Les constantes locales (euro.Constantes)............................................................................................................21 Le modèle abstrait (euro.Calculateur).................................................................................................................21 L'interface graphique (euro.Boitier) ...................................................................................................................25 Le programme (euro.EuroPgm) ..........................................................................................................................28 L'applette (euro.Eurolette) ..................................................................................................................................29 8. Exemple : Jeu des 8 reines.......................................................................................................................31 8.1. 8.2. 8.3. 8.4. 8.5. 8.6. 8.7. L'algorithme programmé (chess.Game)..............................................................................................................32 Le programme de test (chess.Test) .....................................................................................................................35 L'échiquier (chess.Board) ...................................................................................................................................35 Le moteur dynamique (chess.Animator).............................................................................................................37 Le texte de présentation (chess.Introduction) .....................................................................................................39 Le panneau de commande (chess.Control) .........................................................................................................40 Le programme (chess.Queens) ...........................................................................................................................40 9. Exemple : Répertoire téléphonique..........................................................................................................42 9.1. 9.2. 9.3. 9.4. 9.5. 9.6. 9.7. Interface générique des éléments de la collection (repert.Entry) ........................................................................43 L'enregistrement personnel (repert.PhoneEntry) ................................................................................................43 Le fichier (repert.Book) ......................................................................................................................................44 L'éditeur (repert.Editor) ......................................................................................................................................46 Les menus (repert.Commands, repert.Menus) ....................................................................................................50 Assemblage des composants (repert.Operator)...................................................................................................52 Le programme (repert.PhoneBook) ....................................................................................................................53 Les textes anglais sont extraits du JAVA tutorial (cf. http://java.sun.com/). Le paquetage java.applet 1. Overview Every applet is implemented by creating a subclass of the Applet class. The following figure shows the inheritance hierarchy of the Applet class. This hierarchy determines much of what an applet can do and how, as you'll see on the next few pages. 1.1. Example /** * L'applette 'Simple' affiche les étapes de son fonctionnement. * EX.: initializing... starting... stopping... starting... */ import java.applet.Applet; import java.awt.Graphics; public class Simple extends Applet { /** * Affichage des messages. */ StringBuffer buffer; void addMessage (String newWord) { buffer.append(newWord); // ajouter au texte déjà constitué repaint(); // rafraîchir l'écran } public void paint (Graphics g) // méthode d'affichage { // Draw a Rectangle around the applet's display area. g.drawRect(0, 0, getSize().width - 1, getSize().height - 1); // Draw the current string inside the rectangle. g.drawString(buffer.toString(), 5, 15); } JAVA — IHM 1 /** * Jalons d'exécution : init(), start(), stop(), destroy(). * Ces méthodes peuvent être redéfinies dans les classes dérivées. */ // méthode exécutée lors du chargement de l'applette en mémoire // (méthode fonctionnellement équivalente à un constructeur) public void init () { buffer = new StringBuffer(); addMessage("initializing... "); } // méthode exécutée chaque fois que l'utilisateur // entre dans la page HTML contenant l'applette public void start () { addMessage("starting... "); } // méthode exécutée chaque fois que l'utilisateur // sort de la page HTML contenant l'applette public void stop () { addMessage("stopping... "); } // méthode exécutée avant le "déchargement" de l'applette // (fonctionnellement équivalente à finalize) public void destroy () { addMessage("preparing for unloading..."); } } 1.2. Methods for Milestones The Applet class provides a framework for applet execution, defining methods that the system calls when milestones -- major events in an applet's life cycle -- occur. Most applets override some or all of these methods to respond appropriately to milestones. • The init method is useful for one-time initialization that doesn't take very long. In general, the init method should contain the code that you would normally put into a constructor. The reason applets shouldn't usually have constructors is that an applet isn't guaranteed to have a full environment until its init method is called. For example, the Applet image loading methods simply don't work inside of a applet constructor. The init method, on the other hand, is a great place to call the image loading methods, since the methods return quickly. • Every applet that does something after initialization (except in direct response to user actions) must override the start method. The start method either performs the applet's work or (more likely) starts up one or more threads to perform the work. • Most applets that override start should also override the stop method. The stop method should suspend the applet's execution, so that it doesn't take up system resources when the user isn't viewing the applet's page. For example, an applet that displays animation should stop trying to draw the animation when the user isn't looking at it. • Many applets don't need to override the destroy method, since their stop method (which is called before destroy) does everything necessary to shut down the applet's execution. However, destroy is available for applets that need to release additional resources. JAVA — IHM 2 1.3. Methods inherited from java.awt (Abstract Window Toolkit) Applets inherit the drawing and event handling methods of the AWT Component class. Drawing refers to anything related to representing an applet on-screen -- drawing images, presenting user interface components such as buttons, or using graphics primitives. Event handling refers to detecting and processing user input such as mouse clicks and key presses, as well as more abstract events such as saving files and iconifying windows. Applets inherit from the AWT Container class. This means that they are designed to hold Components - user interface objects such as buttons, labels, pop-up lists, and scrollbars. Like other Containers, applets use layout managers to control the positioning of Components. 1.4. What Applets Can and Can't Do Current browsers impose the following restrictions on any applet that is loaded over the network: • • • • • • An applet cannot load libraries or define native methods. It cannot ordinarily read or write files on the host that's executing it. It cannot make network connections except to the host that it came from. It cannot start any program on the host that's executing it. It cannot read certain system properties. Windows that an applet brings up look different than windows that an application brings up. The java.applet package provides an API that gives applets some capabilities that applications don't have. For example, applets can play sounds, which other programs can't do yet. Here are some other things that current browers and other applet viewers let applets do: • • • • Applets can usually make network connections to the host they came from. Applets running within a Web browser can easily cause HTML documents to be displayed. Applets can invoke public methods of other applets on the same page. Applets that are loaded from the local file system have none of the restrictions above. 2. La classe java.applet.Applet public class Applet extends java.awt.Panel { // étapes de l'exécution d'une applette void init() Called by the browser to inform this applet that it has been loaded into the system. void start() Called by the browser to inform this applet that it should start its execution. void stop() Called by the browser to inform this applet that it should stop its execution. void destroy() Called by the browser to inform this applet that it is being reclaimed and that it should destroy any resources that it has allocated. boolean isActive() Determines if this applet is active. JAVA — IHM 3 // obtention des informations de la page HTML String getParameter(String name) Returns the value of the named parameter in the HTML tag. String[][] getParameterInfo() Information about the parameters understood by this applet – null by default. Locale getLocale() Gets the Locale for the applet, if it has been set. String getAppletInfo() Returns information about this applet (author, version ...) – null by default. AppletContext getAppletContext() Determines this applet's context (i.e. document), which allows the applet to query and affect the environment in which it runs. URL getCodeBase() Gets the applet's base URL. URL getDocumentBase() Gets the document URL. // capacités particulières des applettes // (use base URL if name is also given) // (java.net.URL constructors : see JAVA API documentation) Image getImage(URL url, String name) Returns an Image object that can then be painted on the screen. AudioClip getAudioClip(URL url, String name) Returns the AudioClip object specified by the URL and name arguments. static AudioClip newAudioClip(URL url) Get an audio clip from the given URL void play(URL url, String name) Plays the audio clip given the URL and a specifier that is relative to it. void resize(Dimension d) Requests that this applet be resized. void resize(int width, int height) Requests that this applet be resized. void showStatus(String msg) Requests that the argument string be displayed in the "status window". } JAVA — IHM 4 Le paquetage javax.swing 1. About the JFC and Swing 1.1. What Are the JFC and Swing? JFC is short for JavaTM Foundation Classes, which encompass a group of features to help people build graphical user interfaces (GUIs). The JFC is defined as containing the following features: • The Swing1 Components : Include everything from buttons to split panes to tables. • Pluggable Look & Feel Support : Gives any program that uses Swing components a choice of looks and feels. For example, the same program can use either the JavaTM Look & Feel or the Windows Look & Feel. • Accessibility API : Enables assistive technologies such as screen readers and Braille displays to get information from the user interface. • Java 2DTM API : Enables developers to easily incorporate high-quality 2D graphics, text, and images in applications and in applets. • Drag and Drop Support : Provides the ability to drag and drop between a Java application and a native application. 1.2. How Are Swing Components Different from AWT Components? The AWT ("Abstract Window Toolkit") components are those provided by the JDK 1.0 and 1.1 platforms. Although JDK 1.2 still supports the AWT components, we strongly encourage you to use Swing components instead. You can identify Swing components because their names start with J. The AWT button class, for example, is named Button, while the Swing button class is named JButton. Additionally, the AWT components are in the java.awt package, while the Swing components are in the javax.swing package. The biggest difference between the AWT components and Swing components is that the Swing components are implemented with absolutely no native code and aren't restricted to the least common denominator -- the features that are present on every platform. Swing lets you specify which look and feel your program's GUI uses. By contrast, AWT components always have the look and feel of the native platform. Another interesting feature is that Swing components with state use models to keep the state. A JSlider, for instance, uses a BoundedRangeModel object to hold its current value and range of legal values. Models are set up automatically, so you don't have to deal with them unless you want to take advantage of the power they can give you. 1 "Swing" was the codename of the project that developed the new components. It's immortalized in the package names for the Swing API, which begin with javax.swing. JAVA — IHM 5 2. A Quick Tour of a Swing Application's Code The application below brings up a window that looks like this (each time the user clicks the button, the label is updated). // import the main Swing package : import javax.swing.*; // most programs need to import the two main AWT packages : import java.awt.*; import java.awt.event.*; public class MySwingApplication { private static String labelPrefix = "Number of button clicks: "; private int numClicks = 0; // step (1) DEFINE the graphical interface : public Component createGUIComponents() { // (1.1) label (atomic component) /// create the label, with initial text : final JLabel label = new JLabel(labelPrefix + "0 "); // (1.2) button (atomic component) /// create button with identifying label : JButton button = new JButton("I'm a Swing button!"); /// define alt+I as a keyboard alternative : button.setMnemonic(KeyEvent.VK_I); /// associate an event handler (ActionListener) button.addActionListener ( new ActionListener() // (anonymous class definition) { // provide a reacting method : public void actionPerformed (ActionEvent e) { numClicks++; label.setText(labelPrefix + numClicks); } } ); JAVA — IHM 6 // (1.3) panel (container component) : /// create the pane : JPanel pane = new JPanel(); /// choose tabular GridLayout manager : pane.setLayout(new GridLayout(2, 1)); // 2 rows, 1 column /// add contained components (= references linking) : pane.add(button); pane.add(label); /// create an "empty" border to put space around the components: pane.setBorder(BorderFactory.createEmptyBorder(30,30,10,30)); // pixels at top,left,bottom,right /// return GUI contents as a panel component : return pane; } // step (2) PREPARE executing : public static void main (String[] args) { // load pluggable Look & Feel for the above GUI : try { UIManager.setLookAndFeel( // CrossPlatform = Java L&F UIManager.getCrossPlatformLookAndFeelClassName()); } catch (Exception e) { } // instantiate the GUI definition and create the components : MySwingApplication appl = new MySwingApplication(); Component contents = appl.createGUIComponents(); // define the main window : /// choose and create a top-level container : JFrame frame = new JFrame("SwingApplication"); /// add the Panel of application specific components /// in the ContentPane (work area) of the top-level frame : frame.getContentPane().add(contents, BorderLayout.CENTER); /// associate an event handler (WindowListener, using Adapter) frame.addWindowListener ( new WindowAdapter() // (anonymous class definition) { // redefine the windowClosing method public void windowClosing(WindowEvent e) { System.exit(0); } } ); // construct the window, adjusting (packing) the dimensions : frame.pack(); // show the window : frame.setVisible(true); } // step (3) EXECUTE only by means of the event reacting methods } JAVA — IHM 7 3. Swing Features and Concepts 3.1. Swing Components and the Containment Hierarchy Here, again, is a picture of the SwingApplication program This application creates four commonly used Swing components: – – – – a frame, or main window (JFrame) a panel, sometimes called a pane (JPanel) a button (JButton) a label (JLabel) The frame is a top-level container. It exists mainly to provide a place for other Swing components to paint themselves. The other commonly used top-level containers are dialogs (JDialog) and applets (JApplet). The panel is an intermediate container. Its only purpose is to simplify the positioning of the button and label. Other intermediate Swing containers, such as scroll panes (JScrollPane) and tabbed panes (JTabbed Pane), typically play a more visible, interactive role in a program's GUI. The button and label are atomic components -- components that exist not to hold random Swing components, but as self-sufficient entities that present bits of information to the user. Often, atomic components also get input from the user. The Swing API provides many atomic components, including combo boxes (JCombo Box), text fields (JTextField), and tables (JTable). Here is a diagram of the containment hierarchy for the window shown by SwingApplication. JAVA — IHM 8 Every top-level container indirectly contains an intermediate container known as a content pane. For most programs, you don't need to know what's between a top-level container and its content pane. (If you really want to know, see Tutorial > How to Use Root Panes.) As a rule, the content pane contains, directly or indirectly, all of the visible components in the window's GUI. The big exception to the rule is that if the top-level container has a menu bar, then by convention the menu bar goes in a special place outside of the content pane. To add a component to a container, you use one of the various forms of the add method. The add method has at least one argument -- the component to be added. Sometimes an additional argument is required to provide layout information. For example, the last line of the following code sample specifies that the panel should be in the center of the its container (the content pane). frame = new JFrame(...); button = new JButton(...); label = new JLabel(...); pane = new JPanel(); pane.add(button); pane.add(label); frame.getContentPane().add(pane, BorderLayout.CENTER); To see all the Swing components, go to Tutorial > A Visual Index to the Swing Components. 3.2. Layout Management Layout management is the process of determining the size and position of components. By default, each container has a layout manager -- an object that performs layout management for the components within the container. Components can provide size and alignment hints to layout managers, but layout managers have the final say on the size and position of those components. The Java platform supplies five commonly used layout managers: BorderLayout, BoxLayout, Flow Layout, GridBagLayout, and GridLayout. These layout managers are designed for displaying multiple components at once, and are shown in the following figure. A sixth provided class, CardLayout, is a special-purpose layout manager used in combination with other layout managers. JAVA — IHM 9 Whenever you use the add method to put a component in a container, you must take the container's layout manager into account. Some layout managers, such as BorderLayout, require you to specify the component's relative position in the container, using an additional argument with the add method. Occasionally, a layout manager such as GridBagLayout requires elaborate setup procedures. Many layout managers, however, simply place components based on the order they were added to their container. Generally, you only set the layout manager of two types of containers: content panes (which use Border Layout by default) and JPanels (which use FlowLayout by default). ☛ Exemple : voir ci-après, § 7.3. You can easily change the layout manager that a container uses. Just invoke the container's setLayout method.2 For example, here's the code that makes a panel use BorderLayout: JPanel pane = new JPanel(); pane.setLayout(new BorderLayout()); Sometimes you need to customize the size hints that a component provides to its container's layout manager, so that the component will be laid out well. You do this by specifying the minimum, preferred, and maximum sizes of the component. You can invoke the component's methods for setting size hints -- setMinimumSize, setPreferredSize, and setMaximumSize -- or you can create a subclass of the component that overrides the appropriate getter methods -- getMinimum Size, getPreferredSize, and getMaximumSize. Currently, the only layout manager in the Java platform that pays attention to a component's requested maximum size is BoxLayout. Besides providing size hints, you can also provide alignment hints. For example, you can specify that the top edges of two components should be aligned. You set alignment hints either by invoking the component's setAlignmentX and setAlignmentY methods, or by overriding the component's getAlignmentX and getAlignmentY methods. Currently, BoxLayout is the only layout manager that pays attention to alignment hints. Three factors influence the amount of space between visible components in a container: – The layout manager. Some layout managers automatically put space between components; others don't. Some let you specify the amount of space between components. – Invisible components. You can create lightweight components that perform no painting, but that can take up space in the GUI. Often, you use invisible components in containers controlled by Box Layout. – Empty borders. No matter what the layout manager, you can affect the apparent amount of space between components by adding empty borders to components. The best candidates for empty borders are components that typically have no default border, such as panels and labels. Some other components might not work well with borders in some look-and-feel implementations, because of the way their painting code is implemented. 2 Although we recommend that you use layout managers, you can perform layout without them. By setting a container's layout property to null, you make the container use no layout manager. With this strategy, called absolute positioning, you must specify the size and position of every component within that container. JAVA — IHM 10 After the GUI is constructed, the pack method is invoked on the JFrame. This specifies that the frame should be at its preferred size. To determine the best size for the frame, the system determines the sizes of the containers at the bottom of the containment hierarchy. These sizes then percolate up the containment hierarchy, eventually determining the frame's total size. Similar calculations occur when the frame is resized. 3.3. Event Handling Le contrôleur central de l'interface graphique impute automatiquement les clics de la souris à l'objet graphique visible à la position indiquée par le curseur; cet objet devient la "source" logique des événements créés par la souris. Le programmeur doit désigner les objets "cibles" qui, par des méthodes appropriées, devront réagir à ces événements. Pour qu'il puisse invoquer ces méthodes, tout objet source contient la liste des objets cibles ("listeners") "à l'écoute" des événements dont il est la source. Il est fréquent que l'objet source d'un événement en soit également la cible (c'est-à-dire qu'il exécute lui-même la méthode de réaction). Every time the user types a character or pushes a mouse button, an event occurs. Any object can be notified of the event. All it has to do is implement the appropriate interface and be registered as an event listener on the appropriate event source. Swing components can generate many kinds of events. Here are a few examples: Act that results in the event Listener type User clicks a button, presses Ret while typing in a text field, or chooses a menu item ActionListener WindowListener User closes a frame (main window) MouseListener User presses a mouse button while the cursor is over a component MouseMotionListener User moves the mouse over a component ComponentListener Component becomes visible FocusListener Component gets the keyboard focus ListSelectionListener Table or list selection changes Each event is represented by an object that gives information about the event and identifies the event source. Event sources are typically components, but other kinds of objects can also be event sources. As the following figure shows, each event source can have multiple listeners registered on it. Conversely, a single listener can register with multiple event sources. Every event handler requires three bits of code: 1. In the declaration for the event handler class, code that specifies that the class either implements a listener interface or extends a class that implements a listener interface. For example: public class MyClass implements ActionListener. 2. Code that registers an instance of the event handler class as a listener upon one or more components. For example: someComponent.addActionListener(instanceOfMyClass); 3. Code that implements the methods in the listener interface. Ex.: public void actionPerformed (ActionEvent e) { ... } JAVA — IHM 11 To detect when the user clicks an on-screen button (or does the keyboard equivalent), a program must have an object that implements the ActionListener interface. The program must register this object as an action listener on the button (the event source), using the addActionListener method. When the user clicks the on-screen button, the button fires an action event. This results in the invocation of the listener's actionPerformed method (the only method in the ActionListener interface). The single argument to the method is an ActionEvent object that gives information about the event and its source. Event handlers can be instances of any class. Often, an event handler that has only a few lines of code is implemented using an anonymous inner class. Event-handling code executes in a single thread, the event-dispatching thread. This ensures that each event handler will finish executing before the next one executes. Painting code also executes in the eventdispatching thread. This means that while the actionPerformed method is executing, the program's GUI is frozen -- it won't repaint or respond to mouse clicks, for example. Important: The code in event handlers should execute very quickly! Otherwise, your program's perceived performance will be poor. If you need to perform some lengthy operation as the result of an event, do it by starting up another thread (or somehow sending a request to another thread) to perform the operation. 3.4. Painting You might not need the information in this section at all. However, if your components don't seem to be painting themselves correctly, understanding the concepts in this section might help you figure out what's wrong. If you plan to create custom painting code for a component, this section is required reading. Each component paints itself before any of the components it contains. When a Swing GUI needs to paint itself -- whether for the first time, in response to becoming unhidden, or because it needs to reflect a change in the program's state -- it starts with the highest component that needs to be repainted and works its way down the containment hierarchy. Swing components generally repaint themselves whenever necessary. When you invoke the setText method on a component, for example, the component should automatically repaint itself and, if appropriate, resize itself. If it doesn't, it's a bug. The workaround is to invoke the repaint method on the component to request that the component be scheduled for painting. If the component's size or position needs to change but doesn't do so automatically, you should invoke revalidate upon the component before invoking repaint. ☛ Exemple : voir ci-après, § 8.3. Like event-handling code, painting code executes on the event-dispatching thread. While an event is being handled, no painting will occur. Similarly, if a painting operation takes a long time, no events will be handled during that time. It might slightly help performance if you make a Swing component opaque, so that the Swing painting system can know not to paint anything behind the component. To make a Swing component opaque, invoke setOpaque(true) on the component. JAVA — IHM 12 Note: It's important that the content pane be opaque. Otherwise, messy repaints will result. Because the JPanel is opaque, we could make it the content pane (by subsituting setContentPane for the existing code getContentPane().add). This would slightly simplify the containment hierarchy and painting by removing an unnecessary container. Although their available painting area is always rectangular, non-opaque Swing components can appear to be any shape. A button, for instance, might display itself by painting a filled octagon. The component behind the button (its container, most likely) would then be visible, showing through at the corners of the button's bounds. The button would have to include special hit detection code to avoid acting pressed if the user happens to click on its corners. 3.5. Threads and Swing L'unique flot des événements extérieurs (souris, clavier ...) peut servir plusieurs programmes; la gestion de ces événements est donc nécessairement effectuée dans un "event-dispatching thread" distinct. There are a few exceptions to the rule that all code that might affect a realized Swing component must run in the event-dispatching thread: – – – – An application's GUI can often be constructed and shown in the main thread (main method). An applet's GUI can be constructed and shown in the init method. Two JComponent methods are safe to call from any thread: repaint and revalidate. Listener lists can be modified from any thread. Once the GUI is visible, most programs are driven by events such as button actions or mouse clicks, which are always handled in the event-dispatching thread. However, some programs need to perform non-eventdriven GUI work after the GUI is visible. Here are two examples: • Programs that must perform a lengthy initialization operation before they can be used. The initialization should not occur in the event-dispatching thread; otherwise, repainting and event dispatch would stop. However, after initialization the GUI update/change should occur in the eventdispatching thread, for thread-safety reasons. • Programs whose GUI must be updated as the result of nonstandard events. For example, suppose a server program can get requests from other programs that might be running on different machines. These requests can come at any time, and they result in one of the server's methods being invoked in some possibly unknown thread. How can that method update the GUI? By executing the GUI-update code in the event-dispatching thread. The SwingUtilities class provides two methods to help you run code in the event-dispatching thread: invokeLater Requests that some code be executed in the event-dispatching thread. This method returns immediately, without waiting for the code to execute. invokeAndWait Acts like invokeLater, except that this method waits for the code to execute. As a rule, you should use invokeLater rather than this method. ☛ Autre exemple : voir ci-après, § 8, où l'on utilise un timer pour générer des événements internes. JAVA — IHM 13 3.6. More Swing Features and Concepts • Features that JComponent Provides. Except for the top-level containers, all components that begin with J inherit from the JComponent class. They get many features from JComponent, such as the ability to have borders, tool tips, and a configurable look and feel. They also inherit many convenient methods. • Icons. Many Swing components -- notably buttons and labels -- can display images. You specify these images as Icon objects. • Actions. With Action objects, the Swing API provides special support for sharing data and state between two or more components that can generate action events. For example, if you have a button and a menu item that perform the same function, consider using an Action object to coordinate the text, icon, and enabled state for the two components. • Pluggable Look & Feel. A single program can have any one of several looks and feels. You can let the user determine the look and feel, or you can specify the look and feel programatically. • Support for Assistive Technologies. Assistive technologies such as screen readers can use the Accessibility API to get information from Swing components. Because support for the Accessibility API is built into the Swing components, your Swing program will probably work just fine with assistive technologies, even if you do nothing special. • Separate Data and State Models. Most noncontainer Swing components have models. A JButton, for example, has a model (ButtonModel) that stores the button's state -- what its keyboard mnemonic is, whether it's enabled, selected, or pressed, and so on. Some components have multiple models. A list (JList), for example, uses a ListModel to hold the list's contents, and a ListSelectionModel to track the list's current selection. You don't usually need to know about the models that a component uses. For example, almost all programs that use buttons deal directly with the JButton object, and don't deal with the Button Model object.3 Why then do separate models exist? Because they give you the ability to work with components more efficiently and to easily share data and state between components. One common case is when a component, such as a list or table, contains lots of data. It can be much faster to manage the data by directly working with a data model than by having the component forward each data request to the model. You can use the component's default model, or you can implement your own. 4. La classe javax.swing.JApplet public class JApplet extends Applet The JApplet class is slightly incompatible with its super-class java.applet.Applet. The content Pane should be the parent of any children of the JApplet. This is different than java.applet.Applet, e.g. to add a child to a java.applet.Applet you'd write: applet.add(child); However using JApplet you need to add the child to the JApplet's contentPane instead: applet.getContentPane().add(child); 3 En disant que, dans une architecture Modèle–Vue–Contrôleur, par exemple, un bouton est un contrôleur, on s'exprime par raccourci; en toute rigueur, le bouton dessiné est une vue du mécanisme contrôleur ... JAVA — IHM 14 The same is true for setting LayoutManagers, removing components, listing children, etc. All these methods should normally be sent to the contentPane() instead of the JApplet itself. Both Netscape Communicator and Internet Explorer 4.0 unconditionally print an error message to the Java console when an applet attempts to access the AWT system event queue. Swing applets do this once, to check if access is permitted. To prevent the warning message in a production applet one can set a client property called "defeatSystemEventQueueCheck" on the JApplets RootPane to any non null value, e.g: JRootPane rp = myJApplet.getRootPane(); rp.putClientProperty("defeatSystemEventQueueCheck", Boolean.TRUE); We hope that future versions of the browsers will not have this limitation and we'll be able to retire this hack. 5. La classe de base javax.swing.JComponent With the exception of top-level containers (JFrame, JApplet, JDialog), all Swing components whose names begin with "J" descend from the JComponent class, which provides many common methods. Customizing Component Appearance void setBorder(Border) Border getBorder() void setForeground(Color) Color getForeground() void setBackground(Color) Color getBackground() void setOpaque(boolean) boolean isOpaque() void setFont(Font) Font getFont() FontMetrics getFontMetrics(Font) Set or get the border of the component. Set or get the foreground or background color for the component. Set or get whether the component is opaque. Set or get the component's font. If a font has not been set for the component, the font of its parent is returned. Get the font metrics for the specified font. Setting Component State void setToolTipText(String) void setEnabled(boolean) boolean isEnabled() void setLocale(Locale) Locale getLocale() void setCursor(Cursor) Cursor getCursor() void setVisible(boolean) boolean isVisible() JAVA — IHM Set the text to display in a tool tip. An enabled component can respond to user input and generate events. Set or get the component's locale. If the component does not have a locale, the locale of its parent is returned. Set or get the cursor image to display when the mouse is over the component. Components are initially visible, with the exception of top-level components. 15 Handling Events void addComponentListener(ComponentListener) void removeComponentListener(ComponentListener) void addKeyListener(KeyListener) void removeKeyListener(KeyListener l) addMouseListener(MouseListener) void removeMouseListener(MouseListener) Component listeners are notified when component is hidden, shown, moved, or resized. Key listeners are notified when the user types at the keyboard and the listened-to component has the focus. Mouse listeners are notified when the user uses the mouse to interact with the listened-to component. Mouse motion listeners are notified when the user movoid addMouseMotionListener(MouseMotionListener) ves the mouse within the listened-to component's void removeMouseMotionListener(MouseMotionListener) bounds. void addContainerListener(ContainerListener) Container listeners are notified when a component is void removeContainerListener(ContainerListener) added to or removed from the listened-to container. void addFocusListener(FocusListener) Focus listeners are notified when the listened-to comvoid removeFocusListener(FocusListener) ponent gains or loses keyboard focus. Set or get the next focusable component. null indicaComponent getNextFocusableComponent() tes that the focus manager should choose the next focuvoid setNextFocusableComponent(Component) sable component automatically. void requestFocus() Request that the component get the keyboard focus, or boolean hasFocus() detect whether it has the focus. boolean contains(int, int) Determine whether the specified point is within the boolean contains(Point) component. Return the top-most component that contains the speciComponent getComponentAt(int, int) fied x, y position. Painting Components void repaint() void repaint(int, int, int, int) void repaint(Rectangle) void revalidate() void paintComponent(Graphics) Request that all or part of the component be repainted. The four int arguments specify the bounds (x, y, width, height) of the rectangle to be painted. Request that the specified area within the component be repainted. Request that the component and its affected containers be laid out again. You shouldn't generally need to invoke this method unless you explicitly change a component's size/alignment hints after it's visible, change a containment hierarchy after it's visible, or perhaps change the data in a component's model directly. You might need to invoke repaint after revalidate. Paint the component. Override this method for custom components. Dealing with the Containment Hierarchy Add the specified component to the container. The one-argument version of this Component add(Component) method adds the component to the end of the container. The int argument indiComponent add(Component, int) cates the new component's position within the container. The Object argument void add(Component, Object) provides layout constraints to the current layout manager. void remove(int) Remove one of or all of the components from the container. The int argument void remove(Component) indicates the position within the container of the component to remove. void removeAll() JRootPane getRootPane() Get the root pane ancestor for the component. Container getParent() Get the component's parent. int getComponentCount() Get the number of components in the container. Component getComponent(int) Get the one of or all of the components in the container. The int argument indiComponent[] getComponents() cates the position of the component to get. JAVA — IHM 16 Laying Out Components void setPreferredSize(Dimension) void setMaximumSize(Dimension) void setMinimumSize(Dimension) Dimension getPreferredSize() Dimension getMaximumSize() Dimension getMinimumSize() void setAlignmentX(float) void setAlignmentY(float) float getAlignmentX() float getAlignmentY() void setLayout(LayoutManager) LayoutManager getLayout() Set the component's specified size, measured in pixels. Be aware that these are hints only and might be ignored by certain layout managers. Get the specified size of the component, measured in pixels. Set the alignment along the x or y axis. These values indicate how the component would like to be aligned relative to other components. The value should be a number between 0 and 1 where 0 represents alignment along the origin. These are hints only and might be ignored by certain layout managers. Get the size of the component. Set or get the component's layout manager. The layout manager is responsible for sizing and positioning the components within a container. Getting Size and Position Information int getWidth() int getHeight() Dimension getSize() Dimension getSize(Dimension) int getX() int getY() Rectangle getBounds() Rectangle getBounds(Rectangle) Point getLocation() Point getLocation(Point) Point getLocationOnScreen() Insets getInsets() Get the current width or height of the component measured in pixels. Get the component's current size measured in pixels. Get the current x or y coordinate of the component's origin relative to the parent's upper left corner measured in pixels. Get the bounds of the component measured in pixels. The bounds specify the component's width, height, and origin relative to its parent. Gets the current location of the component relative to the parent's upper left corner measured in pixels. The getLocationOnScreen method returns the position relative to the upper left corner of the screen. Get the insets of the component. Specifying Absolute Size and Position void setLocation(int, int) void setLocation(Point) void setSize(int, int) void setSize(Dimension) void setBounds(int, int, int, int) void setBounds(Rectangle) Set the location of the component, in pixels, relative to the parent's upper left corner. Use these methods when you aren't using layout manager. Set the size of the component measured in pixels. Use these methods when you aren't using layout manager. Set the size and location relative to the parent's upper left corner, in pixels, of the component. The four int arguments specify x, y, width, and height, in that order. Use these methods when you aren't using layout manager. 6. Liste des matières JAVA — IHM 17 Using Swing Components Using Top-Level Containers How to Make Frames (Main Windows) How to Make Dialogs How to Make Applets Using Intermediate Swing Containers How to Use Panels How to Use Scroll Panes How to Use Split Panes How to Use Tabbed Panes How to Use Tool Bars How to Use Internal Frames How to Use Layered Panes How to Use Root Panes Using Atomic Components How to Use Buttons, Check Boxes, and Radio Buttons How to Use Color Choosers How to Use Combo Boxes How to Use File Choosers How to Use Labels How to Use Lists How to Use Menus How to Monitor Progress How to Use Sliders How to Use Tables Using Swing's Text Components An Example of Using Each Text Component General Rules for Using Text Components How to Use Text Fields Concepts: About Editor Panes and Text Panes Summary of Text How to Use Tool Tips How to Use Trees Using Other Swing Features How to Use Actions How to Support Assistive Technologies How to Use Borders How to Use Icons How to Set the Look and Feel How to Use Threads How to Use Timers Laying Out Components Within a Container Using Layout Managers General Rules for Using Layout Managers How to Use BorderLayout How to Use BoxLayout How to Use CardLayout How to Use FlowLayout How to Use GridLayout How to Use GridBagLayout How to Use GridBagLayout: Specifying Constraints How to Use GridBagLayout: The Example Explained Creating a Custom Layout Manager Doing Without a Layout Manager (Absolute Positioning) JAVA — IHM 18 Writing Event Listeners Some Simple Event-Handling Examples General Information about Writing Event Listeners Listeners Supported by Swing Components Implementing Listeners for Commonly Handled Events How to Write an Action Listener How to Write a Caret Listener How to Write a Change Listener How to Write a Component Listener How to Write a Container Listener How to Write a Document Listener How to Write a Focus Listener How to Write an Internal Frame Listener How to Write an Item Listener How to Write a Key Listener How to Write a List Data Listener How to Write a List Selection Listener How to Write a Mouse Listener How to Write a Mouse-Motion Listener How to Write a Table Model Listener How to Write a Tree Expansion Listener How to Write a Tree Model Listener How to Write a Tree Selection Listener How to Write a Tree-Will-Expand Listener How to Write an Undoable Edit Listener How to Write a Window Listener Summary of Listener API Working with Graphics Overview of Custom Painting Using Graphics Primitives Painting Shapes Working with Text Using Images Loading Images Displaying Images Performing Animation Creating an Animation Loop with Timer Moving an Image Across the Screen Displaying a Sequence of Images Improving the Appearance and Performance of Image Animation JAVA — IHM 19 7. Exemple : Eurolette — Conversion en euros L'Eurolette dessinée ci-dessous est une calculatrice permettant de convertir un montant de francs belges en euros et vice-versa. L'organisation des classes poursuit un double objectif : 1) respecter l'architecture Modèle–Vue–Contrôleur; 2) faciliter au maximum la transformation du programme en applette et l'inverse. Le paquetage euro comprend les fichiers suivants : – euro\Constantes.java interface fournissant les constantes d'une version nationale – euro\Calculateur.java (extends java.util.Observable) modèle abstrait du calculateur : – état actuel du nombre enregistré ou converti – méthodes de mise à jour et d'interrogation – euro\Boitier.java interface graphique comprenant deux parties : – le panneau de commande (implements ActionListener) l'unique méthode actionPerformed, exécutée par tous les boutons, invoque la méthode modifier du calculateur et lui donne en paramètre le texte identifiant le bouton – le panneau d'affichage (implements java.util.Observer) la méthode update, invoquée par le calculateur à chaque modification de son état, compose le texte à afficher – euro\EuroPgm.java (utilise la classe JFrame) incorpore le boîtier à un programme autonome – euro\Eurolette.java (extends JApplet) incorpore le boîtier à une applette – Eurolette.html page hébergeant l'applette N.B. Les procédures de compilation (javac euro\...) et d'exécution (java euro.EuroPgm, appletviewer Eurolette.html) doivent être commandées à partir du répertoire contenant le sousrépertoire euro\. Autre possibilité : employer l'option classpath. JAVA — IHM 20 7.1. Les constantes locales (euro.Constantes) package euro; interface Constantes // valeurs pour la Belgique { // valeur d'un EURO en monnaie locale : float UN_EURO = 40.3399F; // nbre. max. de chiffres après la virgule en monnaie locale : int MAX_DECIMALES = 0; // libellés des devises : String EURO = "EUR", LOCAL = "BEF"; // libellés des boutons d'effacement : String DELETE = "DEL", CLEAR = "CLR"; // N.B. Les commandes émises par les boutons sont les noms en gras // virgule décimale : String VIRGULE = ","; } 7.2. Le modèle abstrait (euro.Calculateur) Le Calculateur conserve en mémoire divers indicateurs de l'état du nombre enregistré ou converti; en même temps, il adapte un modèle d'affichage (défini dans la classe interne Format, ce modèle utilise un générateur de formats java.text.DecimalFormat et ses conventions). Le calculateur possède des méthodes de mise à jour et de consultation, ainsi qu'une méthode centrale d'aiguillage vers les différentes opérations de mise à jour. Pour des raisons de lisibilité, on a donné le même nom aux méthodes de mise à jour de l'état du nombre et aux méthodes de mise à jour du modèle d'affichage. package euro; import java.text.DecimalFormat; class Calculateur extends java.util.Observable implements Constantes { //// ETAT DE LA MACHINE -----------------------------------------// le système monétaire : private String unité; // la devise (EURO ou LOCAL) private int maxDécimales; // nbre. max. chiffres après virgule // (tout montant en EURO comporte 2 chiffres après la virgule) // le nombre : private int chiffres; // les chiffres private boolean estEntier; private int nbreDécimales; // nbre. effectif de ch. après virg. // contrôle de l'enchaînement des opérations : private boolean entréeChiffresEnCours; JAVA — IHM 21 //// CONSULTATION -----------------------------------------------// calcul de la valeur d'après l'état de la machine : double valeur () { return chiffres * Math.pow(10,-nbreDécimales); } // renvoi du code de devise : String devise () { return unité; } //// MISE A JOUR ------------------------------------------------// modèle d'affichage : private Format format = new Format(); // type d'exception : erreur de syntaxe dans le nombre private class ErreurSyntaxe extends Exception { } // aiguillage vers les fonctions de modification : void modifier (String commande) { try { char code = commande.charAt(0); if (code >= '0' && code <= '9') nouveauChiffre(code); else switch (code) { case '.' : virgule(); break; case 'E' : convertir(EURO); break; // EURO case 'L' : convertir(LOCAL); break; // LOCAL case 'D' : effacer(); break; // DELETE case 'C' : réinitialiser(); break; // CLEAR } setChanged(); notifyObservers (format.nombre()); // = affichage formaté } catch (ErreurSyntaxe erreur) { setChanged(); notifyObservers(erreur); } // = signal err } // 1ère initialisation (= constructeur) : { réinitialiser(); // initialisation du nombre et unité = LOCAL; maxDécimales = MAX_DECIMALES; // de la devise } // CLR : réinitialisation (du nombre, pas de la devise) : private void réinitialiser () { chiffres = 0; nbreDécimales = 0; estEntier = true; format.réinitialiser(); entréeChiffresEnCours = true; } JAVA — IHM 22 // introduction d'un chiffre : private void nouveauChiffre (char chiffre) throws ErreurSyntaxe { if (!entréeChiffresEnCours) réinitialiser(); if (!estEntier && nbreDécimales == maxDécimales) throw new ErreurSyntaxe(); chiffres = chiffres * 10 + (chiffre - '0'); if (!estEntier) ++nbreDécimales; format.nouveauChiffre(); } // introduction de la virgule : private void virgule () throws ErreurSyntaxe { if (!entréeChiffresEnCours) réinitialiser(); if (!estEntier || maxDécimales == 0) throw new ErreurSyntaxe(); estEntier = false; format.virgule(); } // DEL : effacement du dernier caractère (chiffre ou virgule) : private void effacer () throws ErreurSyntaxe { if (!entréeChiffresEnCours) throw new ErreurSyntaxe(); if (nbreDécimales == 0 && !estEntier) // effacer la virgule { estEntier = true; } else { // effacer un chiffre chiffres /= 10; if (nbreDécimales > 0) --nbreDécimales; } format.effacer(); } // EURO/LOCAL : conversion vers la devise indiquée : private void convertir (String devise) { entréeChiffresEnCours = false; if (unité.equals(devise)) return; unité = devise; float facteur; // mémoriser la nouvelle devise // facteur de conversion if (unité.equals(EURO)) // LOCAL ===> EURO { maxDécimales = 2; facteur = 1 / UN_EURO; } else // EURO ===> LOCAL { maxDécimales = MAX_DECIMALES; facteur = UN_EURO; } // modifier les données : chiffres = (int)Math.round (valeur()*Math.pow(10,maxDécimales)*facteur); nbreDécimales = maxDécimales; estEntier = (nbreDécimales == 0); format.convertir(); } JAVA — IHM 23 //// FORMAT D'AFFICHAGE -----------------------------------------private class Format { private DecimalFormat générateur = new DecimalFormat(); private StringBuffer modèle = new StringBuffer("0"); // "0.00" // présentation du nombre à afficher : String nombre () { générateur.applyPattern (modèle.toString()); return générateur.format (valeur()); } // CLR : réinitialisation : void réinitialiser () // ==> format d'un entier : "0" { modèle.replace (/*de*/0, /*à*/modèle.length(), /*par*/"0"); } // introduction de la virgule : void virgule () // ==> format "0." { modèle.ensureCapacity (modèle.length()+1); modèle.append ('.'); } // introduction d'un chiffre : void nouveauChiffre () { if (!estEntier) // ajouter des "0" à la fraction { modèle.ensureCapacity (modèle.length()+1); modèle.append ('0'); } } // DEL : effacement du dernier caractère (chiffre ou virgule) : void effacer () { if (modèle.length() > 1) // garder au moins "0" modèle.setLength(modèle.length()-1); } // EURO/LOCAL : conversion vers la devise indiquée : void convertir () { réinitialiser(); // "0" if (maxDécimales > 0) // ajouter ".00" { virgule(); for (int i = 0; i < maxDécimales; ++i) nouveauChiffre(); } } } // END class Format } // END class Calculateur JAVA — IHM 24 7.3. L'interface graphique (euro.Boitier) L'interface est formée d'un emboîtement de panneaux (JPanel) : JPanel console, BorderLayout(NORTH,SOUTH) avec écartement de 4 > JPanel écran, FlowLayout(RIGHT) // ==> texte cadré à droite > JLabel montant + code devise > JPanel clavier, BorderLayout(WEST,EAST) avec écartement de 2 > JPanel pavéNumérique, BorderLayout(NORTH,SOUTH) avec écartement de 1 > JPanel effacements, GridLayout(1,2) // = bande horizontale > Boutons CLR,DEL > JPanel chiffres, GridLayout(4,3) // = rectangle 4x3 > Boutons chiffres,virgule > JPanel conversions, GridLayout(4,1) // = bande verticale > Boutons BEF,EUR + 2 boutons Invisibles pour centrage Chaque déclaration de panneau — JPanel nom = new JPanel() — est suivie d'un bloc d'initialisation où l'on procède au choix de la mise en page (setLayout) et à l'enregistrement des objets contenus (add). En vue de simplifier le traitement des boutons, on a défini une class Bouton extends JButton dans laquelle est programmée la méthode commune actionPerformed de traitement des ActionEvents; Dans cette classe sont également définies les propriétés communes à tous les boutons : identification de la commande à exécuter (setActionCommand), options de rendu : relief et couleurs. Pour faciliter le positionnement de certains boutons, on a également défini un type de bouton Invisible. La classe interne Montant définit une "vue" (Observer) du "modèle" Calculateur (Observable). La méthode update, activée par ce modèle chaque fois que son état se modifie, reçoit en paramètre soit la valeur sous la forme d'une String créée par le générateur de formats du Calculateur, soit le signal d'exception ErreurSyntaxe. Dans le premier cas, elle affiche le texte; dans le second, elle émet un signal sonore (bip). package euro; import import import import import JAVA — IHM javax.swing.*; javax.swing.border.*; java.awt.*; java.awt.event.*; java.util.*; 25 class Boitier implements Constantes { //// MODELE EN ARRIERE-PLAN -------------------------------------private Calculateur calculateur = new Calculateur(); //// SYSTEME D'AFFICHAGE ----------------------------------------private class Montant extends JLabel implements Observer { private Montant (Calculateur calculateur) // constructeur { super(" "); calculateur.addObserver(this); } public void update (Observable modèle, Object nombre) { if (nombre instanceof Exception) // erreur de syntaxe ... System.out.print('\u0007'); // ===> bip else { Calculateur calculateur = (Calculateur) modèle; setText(nombre + " " + calculateur.devise()); // afficher } } } // END class Montant // écran : private JPanel écran = new JPanel(); { écran.setLayout(new FlowLayout(FlowLayout.RIGHT,3,3)); écran.add(new Montant(calculateur)); // texte cadré à droite écran.setOpaque(true); écran.setBackground(Color.cyan); // createLoweredBevelBorder() rend insuffisamment les couleurs ... Border enCreux = BorderFactory.createBevelBorder (BevelBorder.LOWERED,Color.white,Color.darkGray); écran.setBorder(enCreux); } //// SYSTEME DE COMMANDE ----------------------------------------private class Bouton extends JButton implements ActionListener { private Bouton (String étiq, String commande) // constructeur { super(étiq); setActionCommand(commande); addActionListener(this); setBackground(Color.green); setForeground(Color.blue); setBorder(BorderFactory.createRaisedBevelBorder()); setFocusPainted(false); } public void actionPerformed (ActionEvent clic) { calculateur.modifier(clic.getActionCommand()); } } // END class Bouton JAVA — IHM 26 private class Invisible extends JButton { { setVisible(false); } } // chiffres : private JPanel chiffres = new JPanel(); { chiffres.setLayout(new GridLayout(4,3)); for (int i = 9; i >= 0; --i) chiffres.add( new Bouton( String.valueOf(i), String.valueOf(i) ) ); chiffres.add(new Invisible()); chiffres.add(new Bouton(VIRGULE, ".")); } // boutons d'effacement : private JPanel effacements = new JPanel(); { effacements.setLayout(new GridLayout(1,2)); Bouton clr = new Bouton(CLEAR, "CLEAR"); clr.setForeground(Color.darkGray); Bouton del = new Bouton(DELETE, "DELETE"); del.setForeground(Color.darkGray); effacements.add(clr); effacements.add(del); } // pavé numérique : private JPanel pavéNumérique = new JPanel(); { pavéNumérique.setLayout(new BorderLayout(1,1)); pavéNumérique.add(effacements, BorderLayout.NORTH); pavéNumérique.add(chiffres, BorderLayout.SOUTH); } // demandes de conversion : // (boutons accessibles au pgm pour commander l'affichage initial) Bouton euro = new Bouton(EURO, "EURO"); { euro.setBackground(Color.orange); euro.setForeground(Color.blue); } Bouton local = new Bouton(LOCAL, "LOCAL"); { local.setBackground(Color.orange); local.setForeground(Color.blue); } private JPanel conversions = new JPanel(); { conversions.setLayout(new GridLayout(4,1)); conversions.add(new Invisible()); conversions.add(euro); conversions.add(local); conversions.add(new Invisible()); } JAVA — IHM 27 // clavier : private JPanel clavier = new JPanel(); { clavier.setLayout(new BorderLayout(2,2)); clavier.add(pavéNumérique, BorderLayout.WEST); clavier.add(conversions, BorderLayout.EAST); } //// CONSOLE (accessible au programme) --------------------------JPanel console = new JPanel(); { console.setLayout(new BorderLayout(4,4)); console.add(écran,BorderLayout.NORTH); console.add(clavier,BorderLayout.SOUTH); console.setBorder(BorderFactory.createEmptyBorder(3,3,3,3)); } } // END class Boitier ☛ Exercice. Parce qu'on prévoit qu'un seul boîtier de manipulation interagira avec le calculateur d'arrièreplan, celui-ci est construit (par new) à l'intérieur même du boîtier. Mais, en toute généralité, l'architecture Modèle–Vue–Contrôleur permet d'attacher plusieurs vues et plusieurs contrôleurs à un seul modèle; dans ce dispositif général, vues et contrôleurs ne contiennent plus que la référence du modèle d'arrière-plan. Modifiez dans ce sens la classe Boitier (et celles qui, ci-après, l'utilisent) : la référence du calculateur doit être donnée à un constructeur de l'objet boîtier et, compte tenu de l'enchaînement des étapes lors de la création d'un objet, le texte programmé dans la classe Boitier doit être restructuré. ☛ Exercice. Modifiez l'apparence du pavé numérique pour que le bouton 0 ait une largeur double et touche le bouton . . Le mode de mise en page GridBagLayout doit se substituer au mode GridLayout. 7.4. Le programme (euro.EuroPgm) package euro; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class EuroPgm { public static void main (String[] args) { // choix du style graphique : try { UIManager.setLookAndFeel( // CrossPlatform = Java Look & Feel UIManager.getCrossPlatformLookAndFeelClassName() ); } catch (Exception e) { } JAVA — IHM 28 // création du boîtier : Boitier boîtier = new Boitier(); // création de la fenêtre : JFrame cadre = new JFrame("Conversion EURO"); cadre.getContentPane().add(boîtier.console); cadre.setResizable(false); cadre.addWindowListener ( new WindowAdapter() { public void windowClosing (WindowEvent e) { System.exit(0); } } ); cadre.pack(); // calculer les dimensions // affichage : cadre.setVisible(true); boîtier.local.doClick(); // montrer // affichage initial en LOCAL } // END main() } // END class EuroPgm 7.5. L'applette (euro.Eurolette) package euro; import javax.swing.*; import java.awt.*; public class Eurolette extends JApplet { public void init () { // choix du style graphique : try { UIManager.setLookAndFeel( // CrossPlatform = Java Look & Feel UIManager.getCrossPlatformLookAndFeelClassName() ); } catch (Exception e) { } // création du boîtier : Boitier boîtier = new Boitier(); JAVA — IHM 29 // insertion dans la fenêtre : getContentPane().add(boîtier.console); // (N.B. Les dimensions de la fenêtre sont dans la page HTML) // affichage : setVisible(true); boîtier.local.doClick(); // affichage initial en LOCAL } // END init() } // END class Eurolette • La page HTML (Eurolette) <HTML> <BODY> <APPLET CODE="euro/Eurolette.class" WIDTH=150 HEIGHT=150> </APPLET> </BODY> </HTML> • Version awt 1.1 Le paquetage javax.swing est disponible depuis la version 1.2 du JDK or, à ce jour, les navigateurs web utilisent encore la version 1.1. Pour qu'ils puissent exécuter l'applette, les transformations suivantes doivent être effectuées. 1) modifier les textes sources : [Boitier] – supprimer l'importation du paquetage javax.swing et de ses sous-paquetages; – créer les composants graphiques à partir du paquetage java.awt (ex.: Panel au lieu de JPanel, Button au lieu de JButton, etc.); – puisque les composants sont peints sur l'écran par le système hôte, supprimer les options de dessin (ex.: setOpaque, setBorder, setFocusPainted); [Eurolette] – supprimer l'importation du paquetage javax.swing; – faire dériver l'applette de la classe java.applet.Applet; – supprimer le choix du style graphique (setLookAndFeel); – incorporer (add) les composants graphiques directement à l'applette plutôt qu'au ContentPane (pour cela, supprimer l'appel de la méthode getContentPane); – la méthode doClick n'étant pas définie pour la classe java.awt.Button, appeler directement la méthode modifier du calculateur pour provoquer l'affichage initial. 2) compiler et corriger les erreurs; 3) tester et remédier aux anomalies de présentation. JAVA — IHM 30 8. Exemple : Jeu des 8 reines Le jeu des 8 reines présenté ci-dessus est programmé selon les mêmes principes que l'exemple précédent (Eurolette) : il respecte l'architecture Modèle–Vue–Contrôleur. On notera cependant les deux différences suivantes : – les modifications d'état du programme ne résultent pas d'une action de l'utilisateur, comme l'enfoncement d'un bouton, mais d'une logique programmée; – le composant Crown, représentant une reine, est un composant standard (JPanel) qui est toutefois dessiné par le programme plutôt que par les procédures ordinaires. Le paquetage chess comprend les fichiers suivants : – chess\Game.java (extends java.util.Observable) modèle logique du jeu : contient l'algorithme programmé – la méthode nextSolution livre les solutions une à une – chess\Test.java (implements java.util.Observer) programme pour le test de l'algorithme, sans gestion d'interface graphique – la classe Log liste les solutions sur la console JAVA — IHM 31 – chess\Board.java (implements java.util.Observer) vue de l'échiquier sur lequel sont présentées les solutions – la méthode update affiche chaque solution fournie par l'objet Game – chess\Animator.java (implements ActionListener) moteur (contrôleur) de la dynamique – une minuterie (Timer) appelle en boucle temporisée la méthode actionPerformed pour faire calculer la nextSolution à afficher : Timer() dispositif d'entrée ➥ Animator().actionPerformed() contrôleur ➥ Game().nextSolution() modèle ➥ Board().update() vue – un curseur (slider) permet de régler la vitesse de défilement en modifiant le délai de temporisation – chess\Introduction.java contient le texte explicatif affiché dans la partie droite de la fenêtre – chess\Control.java panneau regroupant les deux objets précédents (partie droite de la fenêtre) – chess\Queens.java (utilise la classe JFrame) incorpore les composants à un programme autonome 8.1. L'algorithme programmé (chess.Game) Un échiquier 8 x 8 contient 8 lignes ou rangées, 8 colonnes et deux fois 15 diagonales. Une reine prend toute pièce située sur la même rangée, la même colonne, la même diagonale montante ou la même diagonale descendante qu'elle-même. Comment placer 8 reines sur l'échiquier de manière telle qu'aucune ne soit directement menacée par aucune autre ? 8 reines, 8 lignes, 8 colonnes : il y a nécessairement dans toute éventuelle solution une et une seule reine dans chaque rangée et dans chaque colonne. Fixons dès le départ la colonne assignée à chaque reine; il reste à trouver pour chacune une rangée "sûre". L'algorithme déclare une case "sûre" (provisoirement) si ni la rangée ni les deux diagonales passant par cette case ne sont occupées par aucune des reines déjà placées. L'échiquier est décrit par 4 vecteurs (!) indiquant l'état libre ou occupé respectivement des 8 colonnes, des 8 rangées, des 15 diagonales montantes et des 15 diagonales descendantes. Le vecteur des colonnes contient 8 reines (queen) portant les numéros des rangées occupées; les trois autres vecteurs contiennent des indicateurs booléens. (Dans les 4 vecteurs, les positions sont comptées à partir de 0.) La recherche des positions sûres procède colonne par colonne, de gauche à droite; une solution est trouvée dès que l'on a réussi à placer la reine de la dernière colonne. Dans la colonne examinée, on déplace la reine du haut vers le bas; si, ce faisant, on ne trouve pas de rangée sûre, on retourne à la colonne précédente où l'on cherche de la même manière une nouvelle rangée sûre pour la reine qui s'y trouve. De proche en proche, on arrivera ainsi à ne plus trouver de nouvelle case libre dans la première colonne; le jeu est alors terminé : on a listé toutes les solutions. Le déplacement vertical d’une reine dans sa colonne et la recherche des différentes solutions constituent deux boucles interrompues pour le traitement de chaque solution trouvée. L’instruction while() conduisant à la solution suivante n’effectue donc qu’une partie de la boucle complète et, pour cette raison, les variables de contrôle ne sont pas réinitialisées avant chaque exécution de while(). JAVA — IHM 32 La définition d'une reine est donnée par la classe interne Queen. Les méthodes propres à une reine sont les suivantes : boolean safe() void setBusy(boolean) int newRow() indique si la position essayée est (provisoirement) sûre modifie l'état (libre ou occupé) dans les vecteurs booléens fournit un nouveau numéro de rangée sûre (-1 en cas d'échec) La classe Game fournit les méthodes suivantes : boolean nextSolution() void run() calcule la solution suivante – la transmet aux vues (Observer) pour affichage – signale par false la fin du jeu boucle sur le calcul de toutes les solutions REMARQUE. La méthode run (contrôle central) ne peut pas être utilisée avec un système de pilotage par événements, sinon se poseraient des problèmes de synchronisation avec le processus parallèle assurant la gestion des événements et la peinture à l'écran (cf. supra, § 3.5. Threads and Swing). On l'a prévue ici à l'usage de programmes de test. package chess; import java.util.*; class Game extends Observable { final int DIM = 8; // dimension de l'échiquier private final int DIAGS = (2*DIM)-1; // nombre de diagonales // au départ, // toutes les lignes, colonnes et diagonales sont libres // (les tableaux sont --automatiquement-- initialisés à FALSE) : private boolean[] busyRow = new boolean [DIM], // [ - ] // busyCol = new boolean [DIM], // [ | ] busySW_NE = new boolean [DIAGS], // [ / ] busyNW_SE = new boolean [DIAGS]; // [ \ ] //---------------------------------------------------------------class Queen { int row, col; // accessibles aux autres classes du paquet private Queen (int row, int col) { this.row = row; this.col = col; } // constructeur // la position essayée est-elle sûre (càd. non menacée) ? private boolean safe () { return ! ( busyRow[row] /* || busyCol[col] */ || busySW_NE[row+col] || busyNW_SE[DIM-1+row-col] ); } JAVA — IHM 33 // occuper ou libérer la position actuelle de la reine : private void setBusy (boolean status) { busyRow[row] = /* busyCol[col] = */ busySW_NE[row+col] = busyNW_SE[DIM-1+row-col] = status; } // déplacer la reine dans sa colonne, // càd. chercher la prochaine case (rangée) non menacée : private int newRow () { if (row >= 0) // (la 1ère fois, row = -1) setBusy (false); // libérer la case actuellement occupée while (++row < DIM) // chercher la prochaine ... if (safe()) // rangée sûre (non menacée) { setBusy (true); // occuper la position et return (row); // rendre le n° de la rangée } return (row = -1); // pas de case disponible dans la colonne } } // END class Queen //---------------------------------------------------------------// initialiser : 1 reine "devant" chaque colonne : Queen[] queen = new Queen [DIM]; { for (int i = 0; i < DIM; ++i) queen[i] = new Queen ( /*row*/ -1, /*col*/ i ); } // calculer la solution suivante : private int col = 0; // 0 au 1er appel, DIM-1 aux suivants boolean nextSolution () { while (col >= 0) { if (queen[col].newRow() >= 0) if (col == DIM-1) // nous avons une solution ! { setChanged(); notifyObservers(); // afficher return (true); } else ++col; // placer la reine suivante else --col; // déplacer la reine précédente } return (false); // il n'y a plus de solution : fin du jeu } // calculer l'ensemble des solutions : void run () { while (nextSolution()) { } } } // END class Game JAVA — IHM 34 8.2. Le programme de test (chess.Test) package chess; import java.util.* ; class Log implements Observer // listage des solutions { private int num = 0; // pour numéroter les solutions public void update (Observable model, Object nullParam) { Game game = (Game) model; System.out.print (++num + " : "); // numéroter la solution for (int i = 0; i < game.DIM; ++i) // montrer la solution System.out.print ((game.queen[i].row + 1) + " "); System.out.println (); } } // END class Log public class Test { public static void main (String[] args) { Game game = new Game(); game.addObserver (new Log()); game.run(); } } // END class Test 8.3. L'échiquier (chess.Board) Chaque case (classe interne Cell) de l'échiquier est un petit panneau JPanel susceptible de contenir la couronne (classe interne Crown) d'une reine. La couronne est dessinée par la méthode paintComponent sur un panneau transparent se superposant exactement à une case. (Pour garantir une superposition exacte, on a simplement programmé : class Crown extends Cell.) L'échiquier (classe Board) est une matrice de 8 x 8 cases. La méthode update, conforme à l'interface Observer, est activée par l'algorithme du jeu en arrière-plan chaque fois qu'une solution est trouvée; elle efface sur l'échiquier les couronnes de la solution précédente puis place celles de la nouvelle solution. NOTE. Comme la méthode générale paint, la méthode paintComponent dit comment dessiner une couronne, mais ce composant n'est la cible d'aucun événement donnant l'ordre d'effectivement le dessiner (en effet, new Crown() ne fait que créer les données de l'objet en mémoire centrale !). Parce qu’on modifie la composition graphique (on supprime et on ajoute des composants), les méthodes revalidate et repaint doivent être explicitement invoquées. (Cf. supra, § 3.4. Painting.) Les solutions montrées sont numérotées; le numéro apparaît sur le bord supérieur de l'échiquier. JAVA — IHM 35 package chess; import import import import java.util.*; javax.swing.*; javax.swing.border.*; java.awt.*; class Board extends JPanel implements Observer { //---------------------------------------------------------------private class Cell extends JPanel { private Cell () // constructeur { setPreferredSize (new Dimension(50,50)); setOpaque (true); } } // END class Cell private class Crown extends Cell { public void paintComponent (Graphics g) // dessiner la couronne { setOpaque (false); // panneau transparent, à superposer g.setColor (Color.green); // pointes de la coiffe int[] x = { 9, 5,16,25,34,45,41}, y = {30, 0,20, 0,20, 0,30}; g.fillPolygon (x, y, x.length); g.setColor (Color.blue.brighter()); // bijou central g.fillOval (20,20,10,7); g.setColor (Color.magenta); // bandeau g.fillRoundRect (9,30,32,10,4,4); g.setColor (Color.darkGray); // contours g.drawOval (20,20,10,7); g.drawPolygon (x, y, x.length); g.drawRoundRect (9,30,32,10,4,4); } } // END class Crown //---------------------------------------------------------------// l'échiquier : private Cell[][] cell; // 8 x 8 cases // l'encadrement : private int solution = 0; // numéro à placer dans le titre private TitledBorder border // bordure avec titre = new TitledBorder( new LineBorder(Color.blue.darker(),3), "?", TitledBorder.CENTER, TitledBorder.TOP ); { border.setTitleFont (border.getTitleFont().deriveFont((float)16)); border.setTitleColor(Color.red); } JAVA — IHM 36 // le modèle algorithmique en arrière-plan : private Game game; Board (Game game) // constructeur { this.game = game; // attacher cette vue game.addObserver (this); // au modèle d'arrière-plan cell = new Cell[game.DIM][game.DIM]; // 8 x 8 cases setLayout (new GridLayout(game.DIM,game.DIM)); // dessiner for (int row = 0; row < game.DIM; ++row) for (int col = 0; col < game.DIM; ++col) { cell[row][col] = new Cell(); if ((row + col) % 2 == 0) // alterner deux couleurs cell[row][col].setBackground(Color.orange.brighter()); else cell[row][col].setBackground(Color.orange); add (cell[row][col]); } setBorder (border); // dessiner le cadre } // afficher une nouvelle solution : public void update (Observable model, Object param) { border.setTitle(String.valueOf(++solution)); // numéroter for (int row = 0; row < game.DIM; ++row) // dessiner for (int col = 0; col < game.DIM; ++col) { cell[row][col].removeAll(); // effacer l'ancien contenu if (game.queen[col].row == row) // placer une couronne cell[row][col].add (new Crown()); } revalidate(); // redessiner les couronnes repaint(); // et les afficher } } // END class Board 8.4. Le moteur dynamique (chess.Animator) L'objet Animator est le moteur dynamique du programme. Pour l'essentiel, il se compose d'une minuterie (Timer) qui, toutes les x millisecondes, crée un "événement" déclencheur d'une action. Cette actionPerformed consiste à demander au modèle algorithmique (objet de la classe Game) de calculer une nouvelle solution. Accessoirement, l'Animator comporte un curseur (JSlider) permettant à l'utilisateur de régler la vitesse de défilement des images. La méthode stateChanged de ce curseur modifie la cadence du timer. (Pour arrêter l'animation, choisir la cadence minimum, autrement dit le délai de temporisation maximum.) JAVA — IHM 37 package chess; import import import import import javax.swing.*; java.awt.*; java.awt.event.*; javax.swing.event.*; javax.swing.border.*; // Timer -> ActionEvent // JSlider -> ChangeEvent class Animator extends JPanel // partie visuelle : le curseur implements ActionListener, // timer -> actionPerf. ChangeListener // slider -> stateChgd. { private Game game; // le jeu à animer private private private private final int maxDelay = 2400; // millis. (maxD. = arrêt !!) int delay = maxDelay; // -> au départ : timer arrêté Timer timer = new Timer (delay, this); JSlider slider = new JSlider (100, maxDelay, delay); { timer.setInitialDelay (maxDelay / 3); slider.setInverted (true); // valeurs hautes <<>> basses slider.addChangeListener (this); } Animator (Game game) // constructeur { this.game = game; // lier au jeu à animer add (slider); // visualiser le curseur TitledBorder border // encadrer le curseur = new TitledBorder ( new LineBorder(Color.darkGray), "Réglez la vitesse de défilement", TitledBorder.CENTER, TitledBorder.TOP ); setBorder (border); } // si l'état actuel le permet, démarrer l'animation : void start () { if (! timer.isRunning() && delay < maxDelay) timer.start(); } // si l'état actuel le permet, arrêter l'animation : void stop () { if (timer.isRunning()) timer.stop(); } JAVA — IHM 38 // action déclenchée par le timer : demander une nouvelle solution public void actionPerformed (ActionEvent event) { if (game.nextSolution()) { } // faire calculer une solution else { stop(); // en fin de partie : arrêter System.out.print('\u0007'); // avec un bip } } // action déclenchée par le curseur : modifier le délai du timer public void stateChanged (ChangeEvent event) { // JSlider slider = (JSlider)event.getSource(); ... y en a qu'un ! delay = slider.getValue(); if (delay == maxDelay) stop(); // curseur à gauche = arrêter else { timer.setDelay (delay); start(); } } } // END class Animator 8.5. Le texte de présentation (chess.Introduction) package chess; import javax.swing.*; import java.awt.*; class Introduction extends JPanel { private JTextArea intro = new JTextArea ( "Au jeu d'échecs, " + "\n" + "la reine prend toute pièce située sur " + "la même rangée, la même colonne, " + "la même diagonale montante ou " + "la même diagonale descendante. " + "\n\n" + "Comment placer 8 reines sur l'échiquier " + "sans qu'aucune ne soit menacée par une autre ? " + "\n\n" + "Combien pensez-vous qu'il y ait de solutions ? " + "\n\n" + "Découvrez-les sur cet échiquier. " ); JAVA — IHM 39 { intro.setPreferredSize (new Dimension(200,280)); // dimensions intro.setLineWrap (true); // couper les lignes ... intro.setWrapStyleWord (true); // sur les frontières de mots intro.setEditable (false); // empêcher de modifier le texte // intro.setFont (new Font(... police par défaut ...) ); intro.setBorder (BorderFactory.createEmptyBorder(5,5,5,5)); add (intro); } } // END class Introduction 8.6. Le panneau de commande (chess.Control) package chess; import javax.swing.*; import java.awt.*; class Control extends JPanel { Animator anim; Control (Game game) // constructeur { setLayout (new BorderLayout()); add (new Introduction(), BorderLayout.NORTH); add (anim = new Animator(game), BorderLayout.SOUTH); } } // END class Control 8.7. Le programme (chess.Queens) package chess; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Queens { private static Game game; private static Board board; private static Control control; JAVA — IHM // modèle // vue // contrôleur 40 public static void main (String[] args) { // choix du style graphique : try { UIManager.setLookAndFeel( // CrossPlatform = Java Look & Feel UIManager.getCrossPlatformLookAndFeelClassName()); } catch (Exception e) { } // composition fonctionnelle du jeu : game = new Game(); // modèle board = new Board (game); // vue principale control = new Control (game); // contrôleur game.addObserver (new Log()); // vue supplémentaire (console) // composition visuelle : JFrame frame = new JFrame ("Les 8 Reines"); JPanel window = (JPanel) frame.getContentPane(); window.add (board, BorderLayout.WEST); // échiquier (vue) window.add (control, BorderLayout.EAST); // commandes (ctrl) // ajustement empirique des positions à l'écran : control.setBorder (BorderFactory.createEmptyBorder(20,1,60,1)); window.setBorder (BorderFactory.createEmptyBorder(0,4,4,4)); frame.setResizable (false); // suspendre l'affichage lorsque la fenêtre est inactive : frame.addWindowListener ( new WindowAdapter() { public void windowActivated (WindowEvent event) { control.anim.start(); } public void windowDeactivated (WindowEvent event) { control.anim.stop(); } public void windowClosing (WindowEvent event) { System.exit(0); } } ); //! L'objet <control> étant utilisé par ce gestionnaire //! d'événement, il ne peut pas être une variable locale //! construite sur la pile pour la méthode main(), mais //! doit être construit en mémoire statique. Pour cette //! raison, les composants du jeu (game, board, control) //! sont déclarés comme membres 'static' de l'objet//! classe Queens. On aurait également pu les déclarer //! comme variables 'final' à l'intérieur de main(). // exécuter : frame.pack(); frame.setVisible(true); // calculer les dimensions // montrer } // END main() } // END class Queens JAVA — IHM 41 9. Exemple : Répertoire téléphonique Le programme décrit ici assure la gestion complète d'un document : consultation à l'écran, mise à jour interactive et sauvegarde dans un fichier. Le document est une collection d'entrées dans un répertoire. Cette collection doit pouvoir être triée (en ordre alphabétique) et doit supporter l'adressage en tableau; c'est pourquoi on lui a donné le type standard d'une ArrayList. Les entrées de cette liste doivent appartenir à une même classe, conforme à l'interface générique Entry définie ci-après. (Le répertoire téléphonique traité en exemple n'est qu'une application particulière du programme.) La collection constitue l'unique objet — composite — à copier dans le fichier. La présentation en tableau est assurée par un objet JTable standard, affiché dans une fenêtre JScrollPane décorée de barres de défilement. Objet standard lié : le ListSelectionModel décrivant les éventuelles sélections de lignes opérées dans le tableau par l’utilisateur. Le paquetage repert comprend les fichiers suivants : – repert\Book.java fichier de sauvegarde du document le fichier est sélectionné via une boîte de dialogue (JFileChooser) – exécute les méthodes load() et save() pour le document – repert\Editor.java (extends AbstractTableModel) contrôleur des opérations – possède les méthodes standards d'interfaçage avec le tableau JTable prévues dans l'interface TableModel – contient les méthodes commandées à partir des menus : . actions sur le tableau : insert() et delete() de lignes choisies . actions sur le fichier : load() et save() – repert\Entry.java interface générique des objets (entrées de la collection) pouvant être traités par l'Editor ci-dessus – repert\PhoneEntry.java (implements Entry) application : entrée dans le répertoire téléphonique JAVA — IHM 42 – repert\Menus.java (extends JMenuBar) menus du programme – repert\Commands.java interface fournissant les textes des menus – repert\Operator.java assemblage des différents composants du programme générique – repert\PhoneBook.java application : programme pour la gestion du répertoire téléphonique 9.1. Interface générique des éléments de la collection (repert.Entry) package repert; public interface Entry extends java.io.Serializable, // pour la sauvegarde java.lang.Comparable // pour le tri { // méthode de comparaison définie dans l'interface 'Comparable' : // // int compareTo (Object other); // création d'une entrée pré-garnie (= ligne "blanche") : void setDefault (); // lecture/écriture de la donnée affichée ds la colonne indiquée : Object get (int col); Object set (int col, Object value); /**************************************************************** get/set sont délibérément conformes à l'interface 'util.List' de sorte que la classe effective des entrées puisse être définie comme une sous-classe de 'util.Vector' ou 'util.ArrayList' et utiliser les méthodes fournies par ces super-classes ***************************************************************/ // fourniture des titres de colonnes pour le tableau : String[] getHeader (); } // END interface Entry 9.2. L'enregistrement personnel (repert.PhoneEntry) Conforme à l'interface Entry et définie comme une sous-classe de la classe java.util.Vector, la classe PhoneEntry décrit les données personnelles enregistrées dans le répertoire téléphonique et fournit les méthodes nécessaires à leur traitement. Les méthodes get() et set() utilisées sont celles de la classe Vector. JAVA — IHM 43 package repert; class PhoneEntry extends java.util.Vector implements Entry { public String[] getHeader () // titres des colonnes { final String[] header = {"Nom1","Nom2","Professionnel","Privé","Mobile"}; return header; } public void setDefault () { // pour que le vecteur possède une dimension et // pour que soit déterminé le type des colonnes : for (int i = getHeader().length; i > 0; --i) add(""); } // public Object get (int col); // public Object set (int col, Object value); // cf. Vector // cf. Vector public int compareTo (Object other) // clé = (Nom1 + Nom2) { PhoneEntry p2 = (PhoneEntry) other; return (this.get(0)+" "+this.get(1)) .compareToIgnoreCase // comparaison "normalisée" (p2.get(0)+" "+p2.get(1)); } } // END class PhoneEntry 9.3. Le fichier (repert.Book) JAVA — IHM 44 Le choix du fichier s'effectue au moyen de la boîte de dialogue standard reproduite ci-dessus (classe JFile Chooser). On exécute pour cela les méthodes showOpenDialog() et showSaveDialog() de l'objet JFileChooser; en réutilisant le même JFileChooser, on conserve et retrouve toujours la dernière sélection effectuée. REMARQUES A l'écran, la boîte de dialogue sera superposée sur le composant qu'on lui associe, appelé chooserFrame dans le texte ci-après. Dans le texte ci-après, la référence chooser est déclarée constante (final) pour forcer la réutilisation du même JFileChooser. package repert; import java.io.*; import java.util.*; import javax.swing.*; public class Book { // le fichier : private File file; // boîte de dialogue pour la sélection du fichier : private final JFileChooser chooser = new JFileChooser(); private JComponent chooserFrame; // pour le placement à l'écran //// construction de l'objet ------------------------------------public Book () public Book (JComponent frame) { } { setChooserFrame (frame); } public void setChooserFrame (JComponent frame) { chooserFrame = frame; } //// opération auxiliaire (signaler une exception) --------------private void beep () { System.out.print ('\u0007'); } //// gestion du contenu -----------------------------------------// sauvegarde d'un document : public void save (Object document) { if ( chooser.showSaveDialog (chooserFrame) == JFileChooser.CANCEL_OPTION ) return; file = chooser.getSelectedFile(); JAVA — IHM 45 try { FileOutputStream stream = new FileOutputStream (file); ObjectOutputStream out = new ObjectOutputStream (stream); out.writeObject (document); out.flush(); out.close(); } catch (IOException e) { beep(); System.err.println ("data cannot be saved because: " + e); } } // chargement d'un document sauvegardé : public Object load () { Object document = null; if ( chooser.showOpenDialog (chooserFrame) == JFileChooser.CANCEL_OPTION ) return null; file = chooser.getSelectedFile(); try { FileInputStream stream = new FileInputStream (file); ObjectInputStream in = new ObjectInputStream (stream); document = in.readObject(); in.close(); } catch (IOException e) { System.err.println ("data cannot be loaded because: " + e); } finally { return document; } } } // END class Book 9.4. L'éditeur (repert.Editor) L'éditeur contrôle et coordonne les opérations du programme : gestion du fichier de sauvegarde : load() et save(); contrôle du format du tableau d'affichage, conformément à l'interface TableModel; affichage et modification des cellules du tableau : getValueAt() et setValueAt(); insertion et suppression de lignes dans le tableau : insert() et delete(). Des méthodes utilitaires privées existent pour : gérer des lignes blanches de manœuvre dans le tableau; répercuter les modifications tant à l'écran que dans le fichier. On notera plusieurs techniques de programmation particulières, dont l'usage est généralisable : 9 La construction de l'objet Editor est paramétrée par la classe des éléments de la collection, classe qui doit être conforme à l'interface générique Entry définie plus haut. Voir comment la méthode newLine() utilise cet objet Class. JAVA — IHM 46 9 Le chargement d'un document en mémoire utilise une boîte de dialogue JFileChooser. Cette opération d'initialisation doit être différée après la mise en place de l'environnement graphique et ne peut donc pas être incorporée au constructeur du composant Editor. En lieu et place, le programme activera la commande ad hoc des menus par une simulation d'événement doClick(). > Voir ci-après la classe PhoneBook. 9 La destruction de l'objet Editor comporte la sauvegarde du document modifié. Cette opération utilise une boîte de dialogue JFileChooser et doit précéder la destruction de l'environnement graphique. La fermeture de la fenêtre (événement windowClosing) invoquera donc la méthode finalize() chargée de clôturer préalablement et "proprement" les opérations en cours. > Voir ci-après les classes Operator et PhoneBook. 9 Pour identifier les lignes sélectionnées dans le tableau, les méthodes insert() et delete() utilisent le ListSelectionModel de l'objet JTable. Or l'objet Editor est le TableModel utilisé pour la création de l'objet JTable. On met donc ici en jeu deux références réciproques JTable → Table Model et TableModel → JTable [→ ListSelectionModel]; la première est obtenue par le constructeur JTable() et la seconde, par la méthode setSelectionModel() de l'objet Editor. > Voir ci-après la classe Operator. package repert; import java.util.*; import javax.swing.*; import javax.swing.table.*; class Editor extends AbstractTableModel // interface avec JTable { //// les données ------------------------------------------------private private private private Book file; List data; Class dataClass; String[] header; // // // // fichier de collection classe des titres des sauvegarde du document des données entrées colonnes du tableau //// utilitaires : (1) contrôle des mises à jour ----------------private ListSelectionModel selection; private boolean modified = false; // lignes sélectionnées // état du document // prendre note d'une modification de données : private void setModified (boolean state) { modified = state; } // rafraîchir l'écran : private void refreshScreen () { fireTableDataChanged(); } JAVA — IHM // événement reçu par JTable 47 //// utilitaires : (2) gestion de lignes blanches ---------------// créer une entrée vide : private Entry newLine () { Entry entry = null; try { entry = (Entry) dataClass.newInstance(); entry.setDefault(); // ligne blanche ou pré-garnie } catch (Exception e) { System.err.println("cannot create new line because: " + e); } finally { return entry; } } // ajouter qq. lignes blanches au tableau pour des mises à jour : private void append (int n) { while (n-- > 0) data.add( newLine() ); } // supprimer les lignes blanches dans la liste à sauvegarder : private void trim () { final Entry empty = newLine(); // pour comparaison for (int i = data.size(); i > 0;) if ( ((Entry)data.get(--i)).compareTo(empty) == 0 ) data.remove(i); } //// gestion du fichier de sauvegarde ---------------------------// charger en mémoire le contenu d'un fichier : void load () { List newData = (List) file.load(); if (newData != null) // si le document existe ... { data = newData; // remplacer le précédent Collections.sort (data); // trier les données setModified (false); // répercuter ... append(3); refreshScreen(); // ... la modification } } // enregistrer les données dans un fichier : void save () { trim(); // supprimer les éléments nuls if (data.size() > 0) // si la collection n'est pas vide ... { Collections.sort (data); // trier les données file.save (data); // enregistrer le document setModified (false); // répercuter ... } append(3); refreshScreen(); // ... la modification } JAVA — IHM 48 //// initialisation et terminaison ------------------------------Editor (Book file, Class dataClass) // constructeur { // mémoriser la référence du fichier : this.file = file; // noter la classe des éléments de la liste : this.dataClass = dataClass; // obtenir les titres -- et le nombre -- de colonnes : header = newLine().getHeader(); // construire une liste vide : data = new ArrayList(); append (3); } // référencer les sélections faites dans le tableau JTable : void setSelectionModel (ListSelectionModel model) { selection = model; } // en fin d'exécution, sauvegarder les données si nécessaire : public void finalize () { if (modified) save(); try {super.finalize();} catch(Throwable t) {} // dont forget ! } //// format du tableau d'affichage (cf. interface TableModel) ---public String getColumnName (int col) public int getColumnCount () public int getRowCount () { return header[col]; } { return header.length; } { return data.size(); } //// gestion des cellules du tableau (cf. interface TableModel) -public boolean isCellEditable (int row, int col) { return true; } public Object getValueAt (int row, int col) { return ((Entry) data.get(row)) .get(col); } public void setValueAt (Object value, int row, int col) { ((Entry) data.get(row)) .set(col,value); setModified (true); // prendre note de la mise à jour } //// gestion des lignes sélectionnées dans le tableau -----------// insérer une ligne blanche devant la sélection : void insert () { int line = selection.getMinSelectionIndex(); if (line >= 0) // si une ligne est sélectionnée ... { data.add (line, newLine()); // insérer un élt. dans liste refreshScreen(); // répercuter } } JAVA — IHM 49 // supprimer les lignes sélectionnées : void delete () { int min = selection.getMinSelectionIndex(); if (min >= 0) // si des lignes sont sélectionnées ... { for ( int i = selection.getMaxSelectionIndex(); i >= min; --i ) // pour chaque ligne if ( selection.isSelectedIndex(i) ) // ... sélectionnée data.remove(i); // supprimer l'entrée dans la liste setModified (true); // répercuter ... selection.clearSelection(); // ... la ... refreshScreen(); // ... modification } } } // END class Editor 9.5. Les menus (repert.Commands, repert.Menus) File Open Save Edit Insert Delete L'interface Commands contient le texte des menus — ici, la version anglaise. package repert; interface Commands { // le 1er caractère doit être un raccourci discriminant String EDIT = "Edit", INSERT = "Insert", DELETE = "Delete"; String FILE = "File", OPEN = "Open", SAVE = "Save"; } // END interface Commands La classe Menus est définie comme une sous-classe de la classe standard JMenuBar. Outre la construction graphique des menus, on y assure la sélection des services à activer en réponse aux diverses commandes. JAVA — IHM 50 package repert; import javax.swing.*; import java.awt.event.*; class Menus extends JMenuBar implements Commands, // textes affichés ActionListener // activation des méthodes { // procédures simplifiées pour la création graphique des menus : private JMenu createMenu (JMenuBar bar, String command) { JMenu menu = new JMenu (command); menu.setMnemonic (command.charAt(0)); bar.add (menu); return menu; } private JMenuItem createItem (JMenu menu, String command, ActionListener actor) { char key = command.charAt(0); JMenuItem item = new JMenuItem (command, key); item.setAccelerator (KeyStroke.getKeyStroke (key, ActionEvent.CTRL_MASK) ); item.addActionListener (actor); menu.add (item); return item; } // commandes accessibles au programme par doClick() : public JMenuItem open, save; // référence de l'exécutant : private Editor editor; // construction des menus : Menus (Editor editor) { this.editor = editor; // constructeur JMenu file = createMenu (this, FILE); open = createItem (file, OPEN, this); save = createItem (file, SAVE, this); JMenu edit = createMenu (this, EDIT); JMenuItem insert = createItem (edit, INSERT, this); JMenuItem delete = createItem (edit, DELETE, this); } JAVA — IHM 51 // exécution des commandes (cf. interface ActionListener) : public void actionPerformed (ActionEvent event) { String command = event.getActionCommand(); if (command.equals(INSERT)) editor.insert(); else if (command.equals(DELETE)) editor.delete(); else if (command.equals(OPEN)) editor.load(); else if (command.equals(SAVE)) editor.save(); } } // END class Menus 9.6. Assemblage des composants (repert.Operator) Menus Editor getMenu() selection TableModel SelectModel Frame getWindow() Table Book chooserFrame ScrollPane Operator On assemble ici en un objet complexe les différents composants de l'application : trois objets de types standards (JScrollPane → JTable → ListSelectionModel) et trois de types spécifiques (Menus → Editor → Book). Deux méthodes getWindow() et getMenu() sont utilisables par la classe JFrame pour obtenir les composants du programme final. Une troisième méthode, getHeader(), sera éventuellement utilisée pour adapter la présentation graphique (couleur et police) des titres de colonnes. NOTE. La méthode finalize() est définie de manière à permettre de clôturer "proprement" les opérations en cours lors de la fermeture impromptue de la fenêtre. Ceci semble être une bonne pratique, à généraliser pour toute application pilotée par des événements. JAVA — IHM 52 package repert; import java.util.*; import javax.swing.*; import java.awt.*; public class Operator { // composants : private Book file; private Editor editor; private JTable sheet; private JScrollPane window; private Menus menus; // constructeur : public Operator (Class dataClass) // classe des élts de la liste { // constructions et liaisons : file = new Book(); editor = new Editor (file, dataClass); sheet = new JTable (editor); window = new JScrollPane (sheet); menus = new Menus (editor); // liens inverses supplémentaires : editor.setSelectionModel ( sheet.getSelectionModel() ); file.setChooserFrame (window); } // accès aux composants : public JScrollPane getWindow () { return window; } public Menus getMenu () { return menus; } public JComponent getHeader () { return sheet.getTableHeader(); } // terminaison "propre" des opérations : public void finalize () { editor.finalize(); } } // END class Operator 9.7. Le programme (repert.PhoneBook) package repert; import javax.swing.*; import java.awt.*; import java.awt.event.*; JAVA — IHM 53 public class PhoneBook { public static void main (String[] args) { //// (1) PREPARATION ------------------------------------------// choix du style graphique : try { UIManager.setLookAndFeel( // CrossPlatform = Java Look & Feel UIManager.getCrossPlatformLookAndFeelClassName() ); } catch (Exception error) { } // création de l'application : // ('final' = en mémoire statique - cf. Remarque à l'exercice 8) final Operator application = new Operator (PhoneEntry.class); // ajustement graphique : couleurs des titres de colonnes : JComponent header = application.getHeader(); header.setBackground (Color.magenta.darker()); header.setForeground (Color.white); header.setFont (header.getFont().deriveFont(Font.BOLD)); // création de la fenêtre : JScrollPane window = application.getWindow(); window.setPreferredSize (new Dimension(540, 360)); JFrame frame = new JFrame ("Répertoire téléphonique"); frame.getContentPane().add (window); frame.setJMenuBar (application.getMenu()); frame.addWindowListener ( new WindowAdapter() { public void windowClosing (WindowEvent e) { application.finalize(); System.exit(0); } /** RECOMMANDATION : * programmer une méthode 'finalize()' * pour terminer "proprement" toute application * dont la clôture, comme ici, * peut être brusquée par un événement extérieur. */ } ); //// (2) DEMARRAGE --------------------------------------------frame.pack(); // calculer les dimensions frame.setVisible (true); // montrer application.getMenu().open.doClick(); // ouvrir doc. initial } // END main() } // END class PhoneBook ☛ Exercice. Adaptez ce programme à la gestion d'un catalogue de vos livres, cassettes ou CD. JAVA — IHM 54 JAVA — IHM 55