Java: Föreläsning 9

(Det här materialet hörde tidigare till föreläsning 10, men vi har ändrat lite i årets kurs.)

Innehåll i föreläsning 9

En detalj om Swing och trådar: invokeLater

Kom ihåg synkronisering mellan trådar: två trådar får inte pilla på samma data samtidigt. Det gäller även objekten i det grafiska gränssnittet.

Ändringar i det grafiska gränssnittet och dess komponenter ska göras av den särskilda Swing-tråden (även kallad event-dispatcher thread eller händelsetråden). Det är den tråden som alla actionPerformed normalt körs i, när man till exempel trycker på knapapr.

Andra trådar får ändra i en grafisk komponent, men bara innan den ritats på skärmen första gången.

Programmet Client7.java utgår från det gamla programmet Client6.java från föreläsning 8 B, men vi har lagt till ett textfält av typen JTextField, som visar den senaste raden som klienten fick från servern:

Fönster med extra chat-knappar och senaste raden

I Client7.java skapar vi ett JTextField och placerar det i fönstret:

            outputField = new JTextField(30);
            outputField.setEditable(false);
            cp.add(outputField);
I klassen ServerListener, som är en tråd som läser rader från servern, sätter vi texten i fältet:
    private class ServerListener extends Thread {
        public void run() {
            String lineFromServer;
            try {
                while ((lineFromServer = in.readLine()) != null &&
                       !lineFromServer.equals("quit")) {
                    System.out.println("Från servern: " +
                                       lineFromServer);
                    outputField.setText(lineFromServer);
                }
            }
            catch (IOException e) {
                System.out.println("Undantag fångat: " + e);
            }
        }
    } // class ServerListener
setText-anropet till utmatningsfältet kommer att ske i ServerListener-tråden, inte i Swing-tråden.

Gör så här i stället (ur programmet Client8.java):

    private class ServerListener extends Thread {

        private class OutputTextSetter implements Runnable {
            private String textToSet;
            public OutputTextSetter(String textToSet) {
                this.textToSet = textToSet;
            }
            public void run() {
                outputField.setText(textToSet);
            }
        }

        public void run() {
            String lineFromServer;
            try {
                while ((lineFromServer = in.readLine()) != null &&
                       !lineFromServer.equals("quit")) {
                    System.out.println("Från servern: " +
                                       lineFromServer);
                    SwingUtilities.invokeLater(new OutputTextSetter(lineFromServer));
                }
            }
            catch (IOException e) {
                System.out.println("Undantag fångat: " + e);
            }
        }
    } // class ServerListener

Look and feel-ändring

Hur man gör för att ändra look and feel medan man kör: anropa bara SwingUtilities.updateComponentTreeUI().

Coolt default-utseende

Fult CDE/Motif-utseende

Halvsnyggt Windows-utseende

Programmet LAFTest.java):

import javax.swing.*;
import java.awt.event.*;
import java.awt.*;

public class LAFTest extends JFrame {
    JComboBox c = new JComboBox();
    JTextField t = new JTextField(30);
    LAFTest outermostWindow = this;

    public LAFTest() {
        final UIManager.LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels();
        for (int i = 0; i < lafs.length; ++i) {
            c.addItem(lafs[i].getName());
        }
        t.setEditable(false);
        c.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e){
                    JComboBox box = (JComboBox)e.getSource();
                    for (int i = 0; i < lafs.length; ++i) {
                        if (lafs[i].getName() == box.getSelectedItem().toString())
                            try {
                                UIManager.setLookAndFeel(lafs[i].getClassName());
                                t.setText(lafs[i].getClassName());
                            }
                            catch (Exception exc) {
                                System.err.println("Error loading " + lafs[i].getClassName() + ": " + exc);
                            }
                    }
                    SwingUtilities.updateComponentTreeUI(outermostWindow);
                }
            });

        Container cp = getContentPane();
        cp.setLayout(new FlowLayout());
        cp.add(t);
        cp.add(c);
    }

    public static void main(String[] args) {
        LAFTest frame = new LAFTest();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(500, 80);
        frame.setVisible(true);
    }
} // class LAFTest

Använd objekt!

I exmplet med LAFTest ovan innehöll callback-metoden actionPerformed en loop. Vi var tvungna att leta igenom listan lafs med LookAndFeelInfo-objekt för att hitta det som har rätt getName, dvs ett getName som stämmer med namnet som användaren valde i comboboxen.

Men vi kan lägga in objekt i stället för strängar i comboboxen. Då är det objektets toString som visas i comboboxen, men det är objektet som returneras av getSelectedItem.

Vi skapar en ny klass, LAFChoice, och lägga in sådana objekt i comboboxen.

Klassen LAFChoice, ur programmet LAFTest2.java):

    private class LAFChoice {
        UIManager.LookAndFeelInfo lafi;
        public LAFChoice (UIManager.LookAndFeelInfo lafi) {
            this.lafi = lafi;
        }
        public String toString() {
            return lafi.getName();
        }
        public String getClassName() {
            return lafi.getClassName();
        }
    } // class LAFChoice
Konstruktorn som gör ett LAFChoice-objekt, fortfarande ur programmet LAFTest2.java):
    public LAFTest2() {
        UIManager.LookAndFeelInfo[] lafs =
            UIManager.getInstalledLookAndFeels();
        for (int i = 0; i < lafs.length; ++i) {
            c.addItem(new LAFChoice(lafs[i]));
        }
        t.setEditable(false);

        c.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e){
                    JComboBox box = (JComboBox)e.getSource();
                    LAFChoice choice = (LAFChoice)box.getSelectedItem();
                    try {
                        UIManager.setLookAndFeel(choice.getClassName());
                        t.setText(choice.getClassName());
                    }
                    catch (Exception exc) {
                        System.err.println("Error loading " +
                                           choice.getClassName() +
                                           ": " + exc);
                    }
                    SwingUtilities.updateComponentTreeUI(outermostWindow);
                }
            });

        Container cp = getContentPane();
        cp.setLayout(new FlowLayout());
        cp.add(t);
        cp.add(c);
    }

Olika look and feel i olika fönster i samma program

Ja, det går: LAFTest3.java

En JPanel är bra till flera saker

Ett exempel på att samla ihop knappar visas i klassen TextEdit, som är hämtad ur Jan Skansholms bok Java direkt med Swing:

En enkel texteditor

Programmet TextEdit.java):

import java.awt.*; 
import java.awt.event.*; 
import java.io.*; 
import javax.swing.*;

public class TextEdit extends JFrame implements ActionListener { 
  JPanel p = new JPanel(); 
  JTextField namn  = new JTextField(); 
  JButton    öppna = new JButton("Öppna"); 
  JButton    spara = new JButton("Spara"); 
  JButton    sluta = new JButton("Avsluta"); 
  JTextArea  area  = new JTextArea(10,60); 
  JScrollPane sp   = new JScrollPane(area,
                      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                      JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

  public TextEdit() { 
    Container c = getContentPane();  
    p.setFont(new Font("SansSerif", Font.PLAIN, 12)); 
    area.setFont(new Font("Monospaced", Font.PLAIN, 12)); 
    // placera ut komponenterna på panelen p 
    p.setLayout(new GridLayout(1,5)); 
    p.add(new JLabel("Filnamn: ", JLabel.RIGHT));  
    p.add(namn); p.add(öppna); p.add(spara); p.add(sluta); 
    namn.addActionListener(this);   
    öppna.addActionListener(this);  
    spara.addActionListener(this); 
    sluta.addActionListener(this); 
    // placera ut panelen och textarean 
    c.add(p,  BorderLayout.NORTH);
    c.add(sp, BorderLayout.CENTER);
    pack();
    setVisible(true); 
    setDefaultCloseOperation(EXIT_ON_CLOSE);  
  } 

  public void actionPerformed(ActionEvent e) { 
    // undersök vilken knapp användaren har tryckt på 
    if (e.getSource() == namn || e.getSource() == öppna)        
      läsInFil(namn.getText());    
    else if (e.getSource() == spara)  
      sparaFil(namn.getText()); 
    else if (e.getSource() == sluta) 
      System.exit(0);      
  } 

  private void läsInFil(String filnamn) { 
    try { 
       FileReader r = new FileReader(filnamn); 
       area.read(r, null); 
    } 
    catch (IOException e) {} 
  }

  private void sparaFil(String filnamn) { 
    try { 
      FileWriter w = new FileWriter(filnamn); 
      area.write(w); 
    }
    catch (IOException e) {}
  } 

  public static void main (String[] arg) { 
    TextEdit t = new TextEdit(); 
  }   
}

Att rita grafik

Klicka på bilden för att se den i stort format:

Ett fönster med tre fula figurer

Programmet Grafikdemo1.java):

import java.awt.*;
import javax.swing.*;
 
class DumFigur extends JPanel {
    public DumFigur() {
        setBackground(Color.blue);
    }

    public void paintComponent(Graphics g) {
        System.out.println("DumFigur.paintComponent...");
        super.paintComponent(g);
        g.setColor(Color.black);
        g.drawRect(50, 100, 150, 200);
        g.drawRect(10, 20, 30, 40);
        g.fillRect(10, 20, 30, 40);
        g.setColor(Color.red);
        g.fillOval(20, 40, 60, 80);
        g.setColor(Color.pink);
        g.drawRoundRect(50, 100, 50, 100, 20, 20);
        g.draw3DRect(100, 150, 50, 100, true);
        g.draw3DRect(150, 200, 50, 100, false);
    }
} // class DumFigur

class Grafikfönster extends JFrame {
    public Grafikfönster(String titel) {
        super(titel);
        Container cp = getContentPane();
        cp.setLayout(new GridLayout(1, 3));
        cp.add(new DumFigur());
        cp.add(new DumFigur());
        cp.add(new DumFigur());
    }
} // class Grafikfönster

public class Grafikdemo1 {
    public static void main(String[] args) {
        Grafikfönster g = new Grafikfönster("Grafikdemo1");
        g.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        g.setSize(800, 400);
        g.setVisible(true);
    } // main
} // class Grafikdemo1

Egna komponenter: cirkeldiagram

Klicka på bilderna för att se dem i stort format:

Ett fönster med tre cirkeldiagram

Samma fönster med tre cirkeldiagram, men nu med annan storlek på fönstret

Programmet Grafikdemo2.java):

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
 
class Cirkeldiagram extends JPanel {
    private int värde;
    private int max;

    public Cirkeldiagram(int värde, int max) {
        if (max <= 0)
            throw new IllegalArgumentException("max = " + max +
                                               ", ska vara > 0");
        if (värde < 0 || värde > max)
            throw new IllegalArgumentException("värde = " + värde +
                                               ", ska vara 0.." + max);
        this.värde = värde;
        this.max = max;
        setBackground(Color.white);
    }

    public Cirkeldiagram(int värde) { this(värde, 100); }
    public Cirkeldiagram() { this(0, 100); }

    public void setVärde(int värde) {
        if (värde < 0 || värde > max)
            throw new IllegalArgumentException("värde = " + värde +
                                               ", ska vara 0.." + max);
        this.värde = värde;
        repaint();
    }

    public void paintComponent(Graphics g) {
        System.out.println("Cirkeldiagram.paintComponent...");
        super.paintComponent(g);
        g.setColor(Color.blue);
        Insets i = getInsets();
        System.out.println("    i = " + i);
        int bredd = getWidth() - i.left - i.right;
        int höjd = getHeight() - i.top - i.bottom;
        int diameter = Math.min(bredd, höjd);
        int x = i.left + (bredd - diameter) / 2;
        int y = i.top + (höjd - diameter) / 2;
        g.drawOval(x, y, diameter, diameter);
        double andel = (double)värde / max;
        int vinkelandel = (int)(andel * 360 + 0.5);
        g.fillArc(x, y, diameter, diameter, 90, -vinkelandel);
    }
} // class Cirkeldiagram

class Grafikfönster extends JFrame {
    public Grafikfönster(String titel) {
        super(titel);
        Container cp = getContentPane();
        cp.setLayout(new GridLayout(1, 3));
        Cirkeldiagram cd1 = new Cirkeldiagram(10, 100);
        Cirkeldiagram cd2 = new Cirkeldiagram(75, 100);
        Cirkeldiagram cd3 = new Cirkeldiagram(55, 100);
        cd2.setBorder(new EtchedBorder());
        cd3.setBorder(new LineBorder(Color.green, 15));
        cp.add(cd1);
        cp.add(cd2);
        cp.add(cd3);
    }
} // class Grafikfönster

public class Grafikdemo2 {
    public static void main(String[] args) {
        Grafikfönster g = new Grafikfönster("Grafikdemo2");
        g.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        g.setSize(800, 400);
        g.setVisible(true);
    } // main
} // class Grafikdemo2

JSlider

Ett fönster med ett cirkeldiagram och en JSlider

Ur programmet Grafikdemo3.java):

class Grafikfönster extends JFrame {
    private Cirkeldiagram cd = new Cirkeldiagram(0, 100);

    public Grafikfönster(String titel) {
        super(titel);
        Container cp = getContentPane();
        cp.setLayout(new BorderLayout());
        cd.setBorder(new EtchedBorder());
        cp.add(cd, BorderLayout.CENTER);
        JSlider s = new JSlider(0, 100, 0);
        cp.add(s, BorderLayout.SOUTH);
        s.addChangeListener(new ChangeListener() {
                public void stateChanged(ChangeEvent e) {
                    cd.sättVärde(((JSlider)e.getSource()).getValue());
                }
            });
    }
} // class Grafikfönster

Ett reglage och en knapp på en panel

Ett fönster med ett cirkeldiagram, plus en JSlider och en JButton

Placera reglaget och knappen på en JPanel.

Ur programmet Grafikdemo4.java):

class Grafikfönster extends JFrame {
    private Cirkeldiagram cd = new Cirkeldiagram(0, 100);
    JSlider s = new JSlider(0, 100, 0);

    public Grafikfönster(String titel) {
        super(titel);
        Container cp = getContentPane();
        cp.setLayout(new BorderLayout());
        cd.setBorder(new EtchedBorder());
        cp.add(cd, BorderLayout.CENTER);
        JPanel p = new JPanel();
        p.setLayout(new FlowLayout());
        s.addChangeListener(new ChangeListener() {
                public void stateChanged(ChangeEvent e) {
                    cd.setVärde(((JSlider)e.getSource()).getValue());
                }
            });
        p.add(s);
        JButton b = new JButton("Nollställ");
        b.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    s.setValue(0);
                    cd.setVärde(0);
                }
            });
        p.add(b);
        cp.add(p, BorderLayout.SOUTH);
    }
} // class Grafikfönster


Thomas Padron-McCarthy (thomas.padron-mccarthy@tech.oru.se), 11 december 2007