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