SE450: Horstmann Chapter 5

Contents [0/72]

Object-Oriented Design & Patterns [1/72]
Chapter Topics [2/72]
List Iterators [3/72]
Classical List Data Structure [4/72]
High-Level View of Data Structures [5/72]
List with Cursor [6/72]
The Pattern Concept [7/72]
Short Passages Pattern [8/72]
Short Passages Pattern [9/72]
Short Passages Pattern [10/72]
Iterator Pattern [11/72]
Iterator Pattern [12/72]
Iterator Pattern [13/72]
Iterator Pattern [14/72]
Model/View/Controller [15/72]
Model/View/Controller [16/72]
Model/View/Controller [17/72]
Model/View/Controller [18/72]
Model/View/Controller [19/72]
Observer Pattern [20/72]
Observer Pattern [21/72]
Observer Pattern [22/72]
Names in Observer Pattern [23/72]
Layout Managers [24/72]
Layout Managers [25/72]
Layout Managers [26/72]
Layout Managers [27/72]
Layout Managers [28/72]
Voice Mail System GUI [29/72]
Voice Mail System GUI [30/72]
Voice Mail System GUI [31/72]
Voice Mail System GUI [32/72]
Voice Mail System GUI [33/72]
Custom Layout Manager [34/72]
The LayoutManager Interface Type [35/72]
Form Layout [36/72]
Strategy Pattern [37/72]
Strategy Pattern [38/72]
Strategy Pattern [39/72]
Strategy Pattern: Layout Management      [40/72]
Strategy Pattern: Sorting [41/72]
Containers and Components [42/72]
Composite Pattern [43/72]
Composite Pattern [44/72]
Composite Pattern [45/72]
Scroll Bars [46/72]
Scroll Bars [47/72]
Decorator Pattern [48/72]
Decorator Pattern [49/72]
Decorator Pattern [50/72]
Decorator Pattern: Scroll Bars [51/72]
Streams [52/72]
Decorator Pattern: Input Streams [53/72]
How to Recognize Patterns [54/72]
Litmus Test [55/72]
Litmus Test [56/72]
Putting Patterns to Work [57/72]
Bundles [58/72]
Bundles [59/72]
Discounted Items [60/72]
Discounted Items [61/72]
Model/View Separation [62/72]
Change Listeners [63/72]
Change Listeners [64/72]
Observing the Invoice [65/72]
Iterating Through Invoice Items [66/72]
Iterators [67/72]
Iterators [68/72]
Formatting Invoices [69/72]
Formatting Invoices [70/72]
Formatting Invoices [71/72]
Formatting Invoices [72/72]

Object-Oriented Design & Patterns [1/72]

Cay S. Horstmann

Chapter 5

Patterns and GUI Programming

horstmann-oodp2

Chapter Topics [2/72]

List Iterators [3/72]

LinkedList<String> list = . . .;
ListIterator<String> iterator = list.listIterator();
while (iterator.hasNext())
{
String current = iterator.next();
. . .
}

Classical List Data Structure [4/72]

High-Level View of Data Structures [5/72]

List with Cursor [6/72]

.

for (list.reset(); list.hasNext(); list.next())
{
Object x = list.get();
. . .
}

The Pattern Concept [7/72]

Short Passages Pattern [8/72]

.

Short Passages Pattern [9/72]

Context

"...Long, sterile corridors set the scene for everything bad about modern architecture..."

Problem

a lengthy description of the problem, including

Short Passages Pattern [10/72]

Solution

Keep passages short. Make them as much like rooms as possible, with carpets or wood on the floor, furniture, bookshelves, beautiful windows. Make them generous in shape and always give them plenty of light; the best corridors and passages of all are those which have windows along an entire wall.

.

Iterator Pattern [11/72]

Context

  1. An aggregate object contains element objects
  2. Clients need access to the element objects
  3. The aggregate object should not expose its internal structure
  4. Multiple clients may want independent access

Iterator Pattern [12/72]

Solution

  1. Define an iterator that fetches one element at a time
  2. Each iterator object keeps track of the position of the next element
  3. If there are several aggregate/iterator variations, it is best if the aggregate and iterator classes realize common interface types.

Iterator Pattern [13/72]

.

Iterator Pattern [14/72]

Name in Design Pattern
Actual Name (linked lists)
Aggregate
List
ConcreteAggregate
LinkedList
Iterator
ListIterator
ConcreteIterator
anonymous class implementing ListIterator
createIterator()
listIterator()
next()
next()
isDone()
opposite of hasNext()
currentItem()
return value of hasNext()


Model/View/Controller [15/72]

Model/View/Controller [16/72]



Model/View/Controller [17/72]

Model/View/Controller [18/72]

Model/View/Controller [19/72]


.

Observer Pattern [20/72]

Observer Pattern [21/72]

Context

  1. An object, called the subject, is source of events
  2. One or more observer objects want to be notified when such an event occurs.

Solution

  1. Define an observer interface type. All concrete observers implement it.
  2. The subject maintains a collection of observers.
  3. The subject supplies methods for attaching and detaching observers.
  4. Whenever an event occurs, the subject notifies all observers.

Observer Pattern [22/72]

.

Names in Observer Pattern [23/72]

Name in Design Pattern
Actual Name (Swing buttons)
Subject
JButton
Observer
ActionListener
ConcreteObserver
the class that implements the ActionListener interface type
attach()
addActionListener()
notify() actionPerformed()

Layout Managers [24/72]

Layout Managers [25/72]

Layout Managers [26/72]

.

Layout Managers [27/72]

Layout Managers [28/72]

.

Voice Mail System GUI [29/72]

Voice Mail System GUI [30/72]

. 

Voice Mail System GUI [31/72]

Voice Mail System GUI [32/72]

.

Voice Mail System GUI [33/72]

.

file:horstmann/ch05_mailgui/Telephone.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package horstmann.ch05_mailgui;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;

/**
   Presents a phone GUI for the voice mail system.
 */
public class Telephone
{
  /**
      Constructs a telephone with a speaker, keypad,
      and microphone.
   */
  public Telephone()
  {
    JPanel speakerPanel = new JPanel();
    speakerPanel.setLayout(new BorderLayout());
    speakerPanel.add(new JLabel("Speaker:"),
        BorderLayout.NORTH);
    speakerField = new JTextArea(10, 25);
    speakerPanel.add(speakerField,
        BorderLayout.CENTER);

    String keyLabels = "123456789*0#";
    JPanel keyPanel = new JPanel();
    keyPanel.setLayout(new GridLayout(4, 3));
    for (int i = 0; i < keyLabels.length(); i++)
    {
      final String label = keyLabels.substring(i, i + 1);
      JButton keyButton = new JButton(label);
      keyPanel.add(keyButton);
      keyButton.addActionListener(event -> connect.dial(label));
    }

    final JTextArea microphoneField = new JTextArea(10,25);

    JButton speechButton = new JButton("Send speech");
    speechButton.addActionListener(event -> {
      connect.record(microphoneField.getText());
      microphoneField.setText("");
    });

    JButton hangupButton = new JButton("Hangup");
    hangupButton.addActionListener(event -> connect.hangup());

    JPanel buttonPanel = new JPanel();
    buttonPanel.add(speechButton);
    buttonPanel.add(hangupButton);

    JPanel microphonePanel = new JPanel();
    microphonePanel.setLayout(new BorderLayout());
    microphonePanel.add(new JLabel("Microphone:"),
        BorderLayout.NORTH);
    microphonePanel.add(microphoneField, BorderLayout.CENTER);
    microphonePanel.add(buttonPanel, BorderLayout.SOUTH);

    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.add(speakerPanel, BorderLayout.NORTH);
    frame.add(keyPanel, BorderLayout.CENTER);
    frame.add(microphonePanel, BorderLayout.SOUTH);

    frame.pack();
    frame.setVisible(true);
  }

  /**
      Give instructions to the mail system user.
   */
  public void speak(String output)
  {
    speakerField.setText(output);
  }

  public void run(Connection c)
  {
    connect = c;
  }

  private JTextArea speakerField;
  private Connection connect;
}

Custom Layout Manager [34/72]


The LayoutManager Interface Type [35/72]

public interface LayoutManager 
{
void layoutContainer(Container parent);
Dimension minimumLayoutSize(Container parent);
Dimension preferredLayoutSize(Container parent);
void addLayoutComponent(String name, Component comp);
void removeLayoutComponent(Component comp);
}

Form Layout [36/72]

file:horstmann/ch05_layout/FormLayout.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package horstmann.ch05_layout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;

/**
   A layout manager that lays out components along a central axis
 */
public class FormLayout implements LayoutManager
{
  public Dimension preferredLayoutSize(Container parent)
  {
    Component[] components = parent.getComponents();
    left = 0;
    right = 0;
    height = 0;
    for (int i = 0; i < components.length; i += 2)
    {
      Component cleft = components[i];
      Component cright = components[i + 1];

      Dimension dleft = cleft.getPreferredSize();
      Dimension dright = cright.getPreferredSize();
      left = Math.max(left, dleft.width);
      right = Math.max(right, dright.width);
      height = height + Math.max(dleft.height,
          dright.height);
    }
    return new Dimension(left + GAP + right, height);
  }

  public Dimension minimumLayoutSize(Container parent)
  {
    return preferredLayoutSize(parent);
  }

  public void layoutContainer(Container parent)
  {
    preferredLayoutSize(parent); // Sets left, right

    Component[] components = parent.getComponents();

    Insets insets = parent.getInsets();
    int xcenter = insets.left + left;
    int y = insets.top;

    for (int i = 0; i < components.length; i += 2)
    {
      Component cleft = components[i];
      Component cright = components[i + 1];

      Dimension dleft = cleft.getPreferredSize();
      Dimension dright = cright.getPreferredSize();

      int height = Math.max(dleft.height, dright.height);

      cleft.setBounds(xcenter - dleft.width, y + (height
          - dleft.height) / 2, dleft.width, dleft.height);

      cright.setBounds(xcenter + GAP, y + (height
          - dright.height) / 2, dright.width, dright.height);
      y += height;
    }
  }

  public void addLayoutComponent(String name, Component comp)
  {}

  public void removeLayoutComponent(Component comp)
  {}

  private int left;
  private int right;
  private int height;
  private static final int GAP = 6;
}

file:horstmann/ch05_layout/FormLayoutTester.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package horstmann.ch05_layout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;

public class FormLayoutTester
{
  public static void main(String[] args)
  {
    JFrame frame = new JFrame();
    frame.setLayout(new FormLayout());
    frame.add(new JLabel("Name"));
    frame.add(new JTextField(15));
    frame.add(new JLabel("Address"));
    frame.add(new JTextField(20));
    frame.add(new JLabel("City"));
    frame.add(new JTextField(10));
    frame.add(new JLabel("State"));
    frame.add(new JTextField(2));
    frame.add(new JLabel("ZIP"));
    frame.add(new JTextField(5));
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }
}



Strategy Pattern [37/72]

Strategy Pattern [38/72]

Context

  1. A class can benefit from different variants for an algorithm
  2. Clients sometimes want to replace standard algorithms with custom versions

Solution

  1. Define an interface type that is an abstraction for the algorithm
  2. Actual strategy classes realize this interface type.
  3. Clients can supply strategy objects
  4. Whenever the algorithm needs to be executed, the context class calls the appropriate methods of the strategy object

Strategy Pattern [39/72]

.

Strategy Pattern: Layout Management      [40/72]

Name in Design Pattern
Actual Name (layout management)
Context
Container
Strategy
LayoutManager
ConcreteStrategy
a layout manager such as BorderLayout
doWork()
a method such as layoutContainer

Strategy Pattern: Sorting [41/72]

Name in Design Pattern
Actual Name (sorting)
Context
Collections
Strategy
Comparator
ConcreteStrategy
a class that implements Comparator
doWork()
compare

Containers and Components [42/72]

Composite Pattern [43/72]

Context

  1. Primitive objects can be combined to composite objects
  2. Clients treat a composite object as a primitive object

Solution

  1. Define an interface type that is an abstraction for the primitive objects
  2. Composite object collects primitive objects
  3. Composite and primitive classes implement same interface type.
  4. When implementing a method from the interface type, the composite class applies the method to its primitive objects and combines the results

Composite Pattern [44/72]

.

Composite Pattern [45/72]

Name in Design Pattern
Actual Name (AWT components)
Primitive
Component
Composite
Container
Leaf
a component without children (e.g. JButton)
method()
a method of Component (e.g. getPreferredSize)

Scroll Bars [46/72]


Scroll Bars [47/72]

.

Decorator Pattern [48/72]

Context

  1. Component objects can be decorated (visually or behaviorally enhanced)
  2. The decorated object can be used in the same way as the undecorated object
  3. The component class does not want to take on the responsibility of the decoration
  4. There may be an open-ended set of possible decorations

Decorator Pattern [49/72]

Solution

  1. Define an interface type that is an abstraction for the component
  2. Concrete component classes realize this interface type.
  3. Decorator classes also realize this interface type.
  4. A decorator object manages the component object that it decorates
  5. When implementing a method from the component interface type, the decorator class applies the method to the decorated component and combines the result with the effect of the decoration.

Decorator Pattern [50/72]

.

Decorator Pattern: Scroll Bars [51/72]

Name in Design Pattern
Actual Name (scroll bars)
Component Component
ConcreteComponent JTextArea
Decorator JScrollPane
method()
a method of Component (e.g. paint)

Streams [52/72]

InputStreamReader reader = new InputStreamReader(System.in); 
BufferedReader console = new BufferedReader(reader);

Decorator Pattern: Input Streams [53/72]

Name in Design Pattern
Actual Name (input streams)
Component Reader
ConcreteComponent InputStreamReader
Decorator BufferedReader
method()
read

How to Recognize Patterns [54/72]

Litmus Test [55/72]


Litmus Test [56/72]

  1. Component objects can be decorated (visually or behaviorally enhanced)
    PASS
  2. The decorated object can be used in the same way as the undecorated object
    PASS
  3. The component class does not want to take on the responsibility of the decoration
    FAIL--the component class has setBorder method
  4. There may be an open-ended set of possible decorations

Putting Patterns to Work [57/72]

file:horstmann/ch05_invoice/LineItem.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
package horstmann.ch05_invoice;
/**
   A line item in an invoice.
 */
public interface LineItem
{
  /**
      Gets the price of this line item.
      @return the price
   */
  double getPrice();
  /**
      Gets the description of this line item.
      @return the description
   */
  String toString();
}

file:horstmann/ch05_invoice/Product.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package horstmann.ch05_invoice;
/**
   A product with a price and description.
 */
public class Product implements LineItem
{
  /**
      Constructs a product.
      @param description the description
      @param price the price
   */
  public Product(String description, double price)
  {
    this.description = description;
    this.price = price;
  }
  public double getPrice() { return price; }
  public String toString() { return description; }
  private String description;
  private double price;
}

Bundles [58/72]

file:horstmann/ch05_invoice/Bundle.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package horstmann.ch05_invoice;
import java.util.ArrayList;

/**
   A bundle of line items that is again a line item.
 */
public class Bundle implements LineItem
{
  /**
      Constructs a bundle with no items.
   */
  public Bundle() { items = new ArrayList<LineItem>(); }

  /**
      Adds an item to the bundle.
      @param item the item to add
   */
  public void add(LineItem item) { items.add(item); }

  public double getPrice()
  {
    double price = 0;

    for (LineItem item : items)
      price += item.getPrice();
    return price;
  }

  public String toString()
  {
    String description = "Bundle: ";
    for (int i = 0; i < items.size(); i++)
    {
      if (i > 0) description += ", ";
      description += items.get(i).toString();
    }
    return description;
  }

  private ArrayList<LineItem> items;
}

Bundles [59/72]

.

Discounted Items [60/72]

file:horstmann/ch05_invoice/DiscountedItem.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package horstmann.ch05_invoice;
/**
   A decorator for an item that applies a discount.
 */
public class DiscountedItem implements LineItem
{
  /**
      Constructs a discounted item.
      @param item the item to be discounted
      @param discount the discount percentage
   */
  public DiscountedItem(LineItem item, double discount)
  {
    this.item = item;
    this.discount = discount;
  }

  public double getPrice()
  {
    return item.getPrice() * (1 - discount / 100);
  }

  public String toString()
  {
    return item.toString() + " (Discount " + discount
        + "%)";
  }

  private LineItem item;
  private double discount;
}

Discounted Items [61/72]

.

Model/View Separation [62/72]

Change Listeners [63/72]

Change Listeners [64/72]

Observing the Invoice [65/72]

.

Iterating Through Invoice Items [66/72]

Iterators [67/72]

file:horstmann/ch05_invoice/Invoice.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package horstmann.ch05_invoice;
import java.util.ArrayList;
import java.util.Iterator;

import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/**
   An invoice for a sale, consisting of line items.
 */
public class Invoice
{
  /**
      Constructs a blank invoice.
   */
  public Invoice()
  {
    items = new ArrayList<LineItem>();
    listeners = new ArrayList<ChangeListener>();
  }

  /**
      Adds an item to the invoice.
      @param item the item to add
   */
  public void addItem(LineItem item)
  {
    items.add(item);
    // Notify all observers of the change to the invoice
    ChangeEvent event = new ChangeEvent(this);
    for (ChangeListener listener : listeners)
      listener.stateChanged(event);
  }

  /**
      Adds a change listener to the invoice.
      @param listener the change listener to add
   */
  public void addChangeListener(ChangeListener listener)
  {
    listeners.add(listener);
  }

  /**
      Gets an iterator that iterates through the items.
      @return an iterator for the items
   */
  public Iterator<LineItem> getItems()
  {
    return new
        Iterator<LineItem>()
    {
      public boolean hasNext()
      {
        return current < items.size();
      }

      public LineItem next()
      {
        return items.get(current++);
      }

      public void remove()
      {
        throw new UnsupportedOperationException();
      }

      private int current = 0;
    };
  }

  public String format(InvoiceFormatter formatter)
  {
    String r = formatter.formatHeader();
    Iterator<LineItem>iter = getItems();
    while (iter.hasNext())
      r += formatter.formatLineItem(iter.next());
    return r + formatter.formatFooter();
  }

  private ArrayList<LineItem> items;
  private ArrayList<ChangeListener> listeners;
}

Iterators [68/72]


.

Formatting Invoices [69/72]

Formatting Invoices [70/72]

file:horstmann/ch05_invoice/InvoiceFormatter.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package horstmann.ch05_invoice;
/**
   This interface describes the tasks that an invoice
   formatter needs to carry out.
 */
public interface InvoiceFormatter
{
  /**
      Formats the header of the invoice.
      @return the invoice header
   */
  String formatHeader();

  /**
      Formats a line item of the invoice.
      @return the formatted line item
   */
  String formatLineItem(LineItem item);

  /**
      Formats the footer of the invoice.
      @return the invoice footer
   */
  String formatFooter();
}

file:horstmann/ch05_invoice/SimpleFormatter.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package horstmann.ch05_invoice;
/**
   A simple invoice formatter.
 */
public class SimpleFormatter implements InvoiceFormatter
{
  public String formatHeader()
  {
    total = 0;
    return "     I N V O I C E\n\n\n";
  }

  public String formatLineItem(LineItem item)
  {
    total += item.getPrice();
    return (String.format(
        "%s: $%.2f\n",item.toString(),item.getPrice()));
  }

  public String formatFooter()
  {
    return (String.format("\n\nTOTAL DUE: $%.2f\n", total));
  }

  private double total;
}

file:horstmann/ch05_invoice/InvoiceTester.java [source] [doc-public] [doc-private]
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package horstmann.ch05_invoice;
import java.awt.BorderLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.event.ChangeListener;

/**
   A program that tests the invoice classes.
 */
public class InvoiceTester
{
  public static void main(String[] args)
  {
    final Invoice invoice = new Invoice();
    final InvoiceFormatter formatter = new SimpleFormatter();

    // This text area will contain the formatted invoice
    final JTextArea textArea = new JTextArea(20, 40);

    // When the invoice changes, update the text area
    ChangeListener listener = event -> textArea.setText(invoice.format(formatter));
    invoice.addChangeListener(listener);

    // Add line items to a combo box
    final JComboBox<LineItem> combo = new JComboBox<>();
    Product hammer = new Product("Hammer", 19.95);
    Product nails = new Product("Assorted nails", 9.95);
    combo.addItem(hammer);
    Bundle bundle = new Bundle();
    bundle.add(hammer);
    bundle.add(nails);
    combo.addItem(new DiscountedItem(bundle, 10));

    // Make a button for adding the currently selected
    // item to the invoice
    JButton addButton = new JButton("Add");
    addButton.addActionListener(event -> {
      LineItem item = (LineItem) combo.getSelectedItem();
      invoice.addItem(item);
    });

    // Put the combo box and the add button into a panel
    JPanel panel = new JPanel();
    panel.add(combo);
    panel.add(addButton);

    // Add the text area and panel to the content pane
    JFrame frame = new JFrame();
    frame.add(new JScrollPane(textArea),
        BorderLayout.CENTER);
    frame.add(panel, BorderLayout.SOUTH);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setVisible(true);
  }
}

Formatting Invoices [71/72]

.

Formatting Invoices [72/72]

.

Revised: 2007/09/11 16:27