Course of Programming in Java: Model-View-Controller by Łukasz Stafiniak Email: [email protected], [email protected] Web: www.ii.uni.wroc.pl/~lukstafi Head First Design Patterns Chapter 12. Compound Patterns A Swing Architecture Overview The Inside Story on JFC Component Design By Amy Fowler 1 MVC from Head First Design Patterns 2 3 4 5 6 7 8 9 10 11 DJ example: source code • BeatModelInterface.java • BeatModel.java • DJView.java • ControllerInterface.java • BeatController.java • DJTestDrive.java • HeartAdapter.java • HeartController.java • HeartTestDrive.java 12 Architecture of Swing Design Goals 1. Be implemented entirely in Java to promote cross-platform consistency and easier maintenance. 2. Provide a single API capable of supporting multiple look-and-feels so that developers and end-users would not be locked into a single look-andfeel. 3. Enable the power of model-driven programming without requiring it in the highest-level API. 4. Adhere to JavaBeans design principles to ensure that components behave well in IDEs and builder tools. 5. Provide compatibility with AWT APIs where there is overlapping, to leverage the AWT knowledge base and ease porting . 13 Roots in MVC Swing architecture is rooted in the model-view-controller ( MVC ) design that dates back to SmallTalk. MVC architecture calls for a visual application to be broken up into three separate parts: • A model that represents the data for the application. • The view that is the visual representation of that data. • A controller that takes user input on the view and translates that to changes in the model. 14 The delegate We quickly discovered that MVC split didn’t work well in practical terms because the view and controller parts of a component required a tight coupling (for example, it was very difficult to write a generic controller that didn’t know specifics about the view). So we collapsed these two entities into a single UI (user-interface) object, as shown in this diagram: The UI object shown in this picture is sometimes called UI delegate. This new quasi-MVC design is sometimes referred to a separable model architecture. 15 To MVC or not to MVC? • Component’s view/controller responsibilities as being handled by the generic component class (such as. JButton, JTree, and so on). ◦ E.g. the code that implements double-buffered painting is in Swing’s JComponent class (the "mother" of most Swing component classes). • Look-and-feel-specific aspects of those responsibilities to the UI object that is provided by the currently installed look-and-feel. ◦ E.g. the code that renders a JButton’s label is in the button’s UI delegate class. 16 Separable model architecture Swing defines a separate model interface for each component that has a logical data or value abstraction. This separation provides programs with the option of plugging in their own model implementations for Swing components. 17 Component JButton JToggleButton JCheckBox JRadioButton JMenu JMenuItem JCheckBoxMenuItem JRadioButtonMenuItem JComboBox JProgressBar JScrollBar JSlider JTabbedPane JList JList JTable JTable JTree JTree JEditorPane JTextPane JTextArea JTextField JPasswordField Model Interface ButtonModel ButtonModel ButtonModel ButtonModel ButtonModel ButtonModel ButtonModel ButtonModel ComboBoxModel BoundedRangeModel BoundedRangeModel BoundedRangeModel SingleSelectionModel ListModel ListSelectionModel TableModel TableColumnModel TreeModel TreeSelectionModel Document Document Document Document Document 18 Model Type GUI GUI/data GUI/data GUI/data GUI GUI GUI/data GUI/data data GUI/data GUI/data GUI/data GUI data GUI data GUI data GUI data data data data data GUI-state vs. application-data models • GUI-state models are interfaces that define the visual status of a GUI control, such as whether a button is pressed or armed, or which items are selected in a list. • It is possible to manipulate the state of a GUI control through top-level methods on the component, without any direct interaction with the model at all. • Application-data models are interfaces that represent some quantifiable data that has meaning primarily in the context of the application, such as the value of a cell in a table or the items displayed in a list. • Provide clean separation between their application data/logic and their GUI. For truly data-centric Swing components, such as JTree and JTable, interaction with the data model is strongly recommended. • For some components the model categorization falls in between GUI state models and application-data models, depending on the context in which the model is used. E.g. BoundedRangeModel on JSlider or JProgressBar. 19 Shared model definitions Common models enable automatic connectability between component types. For example, because both JSlider and JScrollbar use the BoundedRangeModel interface, a single BoundedRangeModel instance could be plugged into both a JScrollbar and a JSlider and their visual state would always remain in sync. 20 The separable-model API If you don’t set your own model, a default is created and installed internally in the component. The naming convention for these default model classes is to prepend the interface name with "Default." For JSlider, a DefaultBoundedRangeModel object is instantiated in its constructor: public JSlider(int orientation, int min, int max, int value) { checkOrientation(orientation); this.orientation = orientation; this.model = new DefaultBoundedRangeModel(value, 0, min, max); this.model.addChangeListener(changeListener); updateUI(); } 21 If a program subsequently calls setModel(), this default model is replaced, as in the following example: JSlider slider = new JSlider(); BoundedRangeModel myModel = new DefaultBoundedRangeModel() { public void setValue(int n) { System.out.println("SetValue: "+ n); super.setValue(n); } }); slider.setModel(myModel); For more complex models (such as those for JTable and JList), an abstract model implementation is also provided to enable developers to create their own models without starting from scratch. These classes are prepended with "Abstract". For example, JList’s model interface is ListModel, which provides both DefaultListModel and AbstractListModel classes. 22 Model change notification Models must be able to notify any interested parties (such as views) when their data or value changes. Swing models use the JavaBeans Event model. There are two approaches for this notification used in Swing: • Send a lightweight notification that the state has "changed" and require the listener to respond by sending a query back to the model to find out what has changed. The advantage of this approach is that a single event instance can be used for all notifications from a particular model – which is highly desirable when the notifications tend to be high in frequency (such as when a JScrollBar is dragged). • Send a stateful notification that describes more precisely how the model has changed. This alternative requires a new event instance for each notification. It is desirable when a generic notification doesn’t provide the listener with enough information to determine efficiently what has changed by querying the model (such as when a column of cells change value in a JTable). 23 Lightweight notification The following models in Swing use the lightweight notification, which is based on the ChangeListener/ChangeEvent API: Model BoundedRangeModel ButtonModel SingleSelectionModel Listener ChangeListener ChangeListener ChangeListener Event ChangeEvent ChangeEvent ChangeEvent The ChangeListener interface has a single generic method: public void stateChanged(ChangeEvent e) The only state in a ChangeEvent is the event "source." 24 Models that use this mechanism support the following methods to add and remove ChangeListeners: public void addChangeListener(ChangeListener l) public void removeChangeListener(ChangeListener l) To be notified when the value of a JSlider has changed, e.g.: JSlider slider = new JSlider(); BoundedRangeModel model = slider.getModel(); model.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { // need to query the model to get updated value... BoundedRangeModel m = (BoundedRangeModel)e.getSource(); System.out.println("model changed: " + m.getValue()); } }); 25 To provide convenience for programs that don’t wish to deal with separate model objects, some Swing component classes also provide the ability to register ChangeListeners directly on the component (so the component can listen for changes on the model internally and then propagates those events to any listeners registered directly on the component). So we could simplify the preceding example to: JSlider slider = new JSlider(); slider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { // the source will be the slider this time... JSlider s = (JSlider)e.getSource(); System.out.println("value changed: " + s.getValue()); } }); 26 Stateful notification Models that support stateful notification provide event Listener interfaces and event objects specific to their purpose. The following table shows the breakdown for those models: Model ListModel ListSelectionModel ComboBoxModel TreeModel TreeSelectionModel TableModel TableColumnModel Document Document Listener ListDataListener ListSelectionListener ListDataListener TreeModelListener TreeSelectionListener TableModelListener TableColumnModelListener DocumentListener UndoableEditListener 27 Event ListDataEvent ListSelectionEvent ListDataEvent TreeModelEvent TreeSelectionEvent TableModelEvent TableColumnModelEvent DocumentEvent UndoableEditEvent The following code dynamically tracks the selected item in a JList: String items[] = {"One", "Two", "Three"); JList list = new JList(items); ListSelectionModel sModel = list.getSelectionModel(); sModel.addListSelectionListener (new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { // get change information directly // from the event instance... if (!e.getValueIsAdjusting()) { System.out.println("selection changed: " + e.getFirstIndex()); } } }); 28 Ignoring models completely As mentioned previously, most components provide the model-defined API directly in the component class so that the component can be manipulated without interacting with the model at all. This is considered perfectly acceptable programming practice (especially for the GUI-state models). For example, following is JSlider’s implementation of getValue(), which internally delegates the method call to its model: public int getValue() { return getModel().getValue(); } And so programs can simply do the following: JSlider slider = new JSlider(); int value = slider.getValue(); //what’s a "model," anyway? 29 Pluggable look-and-feel architecture A new L&F is a very powerful feature for a subset of applications that want to create a unique identity. PL&F is also ideally suited for use in building GUIs that are accessible to users with disabilities, such as visually impaired users or users who cannot operate a mouse. In a nutshell, pluggable look-and-feel design simply means that the portion of a component’s implementation that deals with the presentation (the look) and event-handling (the feel) is delegated to a separate UI object supplied by the currently installed look-and-feel, which can be changed at runtime. 30 Look-and-feel management The UIManager is the API through which components and programs access look-and-feel information (they should rarely, if ever, talk directly to a LookAndFeel instance). UIManager is responsible for keeping track of which LookAndFeel classes are available, which are installed, and which is currently the default. The UIManager also manages access to the Defaults Table for the current look-and-feel. public static LookAndFeel getLookAndFeel() public static void setLookAndFeel(LookAndFeel newLookAndFeel) public static void setLookAndFeel(String className) As a default look-and-feel, Swing initializes the cross-platform Java look and feel (formerly known as "Metal"). The following code sample will set the default Look-and-Feel to be CDE/Motif: UIManager.setLookAndFeel( "com.sun.java.swing.plaf.motif.MotifLookAndFeel"); 31 The UIManager static methods to programmatically obtain the appropriate LookAndFeel class names: public static String getSystemLookAndFeelClassName() public static String getCrossPlatformLookAndFeelClassName() So, to ensure that a program always runs in the platform’s system look-andfeel, the code might look like this: UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName()); 32 Dynamically Changing the Default Look-and-Feel When a Swing application programmatically sets the look-and-feel (as described above), the ideal place to do so is before any Swing components are instantiated . This is because the UIManager.setLookAndFeel() method makes a particular LookAndFeel the current default by loading and initializing that LookAndFeel instance, but it does not automatically cause any existing components to change their look-and-feel. 33 And so if a program needs to change the look-and-feel of a GUI hierarchy after it was instantiated, the code might look like the following: // GUI already instantiated, where myframe // is top-level frame try { UIManager.setLookAndFeel( "com.sun.java.swing.plaf.motif.MotifLookAndFeel"); myframe.setCursor( Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); SwingUtilities.updateComponentTreeUI(myframe); myframe.validate(); } catch (UnsupportedLookAndFeelException e) { } finally { myframe.setCursor (Cursor.getPredefinedCursor (Cursor.DEFAULT_CURSOR)); } 34 public static LookAndFeelInfo[] getInstalledLookAndFeels() method can be used to programmatically determine which look-and-feel implementations are available, which is useful when building user interfaces which allow the end-user to dynamically select a look-and-feel. 35
© Copyright 2024