Kapitola 1. Třídy Action a Observer

Obsah

1.1. Koordinovaná chování tlačítek v nástrojové liště (toolbar) a položek v menu (navazuje na minulou přednášku)
1.2. Komunikace pomocí Observable-Observer

1.1. Koordinovaná chování tlačítek v nástrojové liště (toolbar) a položek v menu (navazuje na minulou přednášku)

  • ve většině případů jsou ikony použity pro několik málo nejčastěji používaných akcí, přičemž seznam všech možných akcí je dostupný v menu

    • pak je velmi vhodné zařídit, aby nebylo nutné psát dvě víceméně stejné obsluhy událostí – jednu pro ikonu a druhou pro položku v menu

    • dalším problémem by mohla být synchronizace kontextového znepřístupňování ikon a položek menu

  • oba problémy lze snadno vyřešit tím, že nepoužíváme běžný způsob obsluh událostí, ale použijeme potomka třídy AbstractAction, který splňuje rozhraní Action

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

public class ToolBarAMenuAktivita {
  JTextField vystupTF;
  SpolecnaAkce spolecnaAkce;    

  public ToolBarAMenuAktivita() {
    JFrame oknoF = new JFrame("ToolBarAMenuAktivita");
    oknoF.setLayout(new BorderLayout());

    spolecnaAkce = new SpolecnaAkce();
    
    oknoF.setJMenuBar(vytvorMenuBar());
      
    oknoF.add(vytvorToolBar(), BorderLayout.PAGE_START);
    
    vystupTF = new JTextField();
    vystupTF.setHorizontalAlignment(SwingConstants.CENTER);
    oknoF.add(vystupTF, BorderLayout.CENTER);
    
    oknoF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    oknoF.pack();
    oknoF.setVisible(true);
  }
  
  public static void main(String[] args) throws Exception {
    new ToolBarAMenuAktivita();
  }

  
  class SpolecnaAkce extends AbstractAction {
    int pocet = 0;
    public SpolecnaAkce() {
      ImageIcon ikona = nactiObrazekIkony("krizek");  
      putValue(NAME, "Křížek - jméno");
      putValue(SMALL_ICON, ikona);
      putValue(SHORT_DESCRIPTION, "Křížek - text tooltipu");
      putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_K));
      putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_W, 
                                              ActionEvent.CTRL_MASK));
    }

    public void actionPerformed(ActionEvent e) {
      pocet++;
      vystupTF.setText("" + pocet + ". akce");
    }
  }
  
  ImageIcon nactiObrazekIkony(String jmeno) {
    URLClassLoader urlLoader = (
               URLClassLoader) this.getClass().getClassLoader();

    URL ikonaURL = urlLoader.findResource("ikony/" + jmeno + ".gif");
    ImageIcon ikonaIK = new ImageIcon(ikonaURL);
    return ikonaIK; 
  }
  
  JButton vytvorTlacitko(Action spolecnaAkceSMenu) {
    JButton tlac = new JButton(spolecnaAkceSMenu);
    if (tlac.getIcon() != null) {
      tlac.setText("");     // je-li obrazek, neni treba popis
    }        
    tlac.setMargin(new Insets(0,0,0,0));
    return tlac; 
  }
  
  JToolBar vytvorToolBar() {
    JToolBar hlavniTB = new JToolBar("Jméno ToolBaru");
    hlavniTB.add(vytvorTlacitko(spolecnaAkce));    
    hlavniTB.addSeparator();
      
    final JCheckBox povoleniCHB = new JCheckBox("Akce křížku povolena", true);
    povoleniCHB.setMnemonic(KeyEvent.VK_A);
    povoleniCHB.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        boolean b = povoleniCHB.isSelected();
        spolecnaAkce.setEnabled(b);
      }
    });
    hlavniTB.add(povoleniCHB);

    return hlavniTB;
  }
  
  JMenuBar vytvorMenuBar() {
    JMenuBar menuBar = new JMenuBar();
    
    JMenu svisleMenu = new JMenu("Menu");
    svisleMenu.setMnemonic(KeyEvent.VK_M);
      
    JMenuItem polozkaSvislehoMenu = new JMenuItem(spolecnaAkce);
    // je-li popis, nepouzivat ikonu
    polozkaSvislehoMenu.setIcon(null);  
    svisleMenu.add(polozkaSvislehoMenu);

    svisleMenu.add(new JMenuItem("nefunkční položka"));

    menuBar.add(svisleMenu);
    return menuBar;
  }
  
}

Základem společné obsluhy je javax.swing.AbstractAction, která se zdědí

class SpolecnaAkce extends AbstractAction {
  • zde se na jednom místě (typicky v konstruktoru) přehledně nastaví následující vlastnosti:

    • NAME – popis použitý na řádce v menu a případně i jako popis ikony, v tomto příkladu v toolbar vypnuto pomocí

      if (tlac.getIcon() != null) {
        tlac.setText("");
      }
    • SMALL_ICON – obrázek ikony na tlačítku, případně může být i před popisem v menu, v tomto příkladu v menu vypnuto pomocí

      polozkaSvislehoMenu.setIcon(null);
    • SHORT_DESCRIPTION – text tool tipu, který se zobrazí nad ikonou i nad položkou menu

    • MNEMONIC_KEY – zkratková klávesa, typicky písmena A až Z, na velikosti nezáleží

      • má jiný význam v ikoně – volá se odkudkoliv přes Alt+písmeno a zobrazuje se v tool tipu

      • v menu podtrhává stejné písmeno v popisu, samotná klávesa slouží jako rychlý výběr z rozvinutého menu

        • mnemonic key slouží stejně i pro ostatní komponenty (JCheckBox)

    • ACCELERATOR_KEY – použití jen v položkách menu, kde:

      • vypisuje se za jménem položky a v tool tipu

      • možnost volit libovolné klávesy (i číslice nebo funkční klávesy)

      • možnost volit modifikátory a jejich kombinace (Ctrl, Alt, Shift)

      • pomocí modifikátoru (Ctrl+W) lze zavolat odkudkoliv

  • dále je nutné napsat obsluhu události třídy SpolecnaAkce

    public void actionPerformed(ActionEvent e)

  • v našem příkladu je nejdůležitější zděděná metoda této třídy:

    void setEnabled(boolean newValue)

    která zpřístupní / znepřístupní současně ikonu i položku menu

Poznámka

do toolbaru lze umístit i jinou komponentu, než ikonu, v našem příkladu to je:

final JCheckBox povoleniCHB = new JCheckBox("Akce křížku povolena", true);

není to většinou rozumný nápad, rozumné použití je vidět ve Wordu, kde jsou v toolbaru např. nastavení fontů v JTextField

1.2. Komunikace pomocí Observable-Observer

  • princip zasílání a příjmu událostí z GUI lze zobecnit a využít kdekoliv, kde mají objekty komunikovat bez přímé viditelnosti

    • třídy o sobě nemusejí vědět – zamezí se nelogickým asociacím

    • bez přímé viditelnosti je to samé, co jsem viděli na první přednášce jako rozhraní Archivable

  • vychází z návrhových vzorů (kniha Design Paterns, Elements of Reusable Object-Oriented Software; Gamma a kol.)

  • What is a Design Pattern? .... credits http://www.tomlauren.com/notes/designPatterns.html

    • A design pattern is a reusable solution to a reoccurring problem in software.

    • Advantages of Design Patterns:

      • Benefiting from the experience of others

      • Establishing common terminology

      • Giving a higher-level perspective on problems

      • Finding appropriate domain objects

      • Helping individual learning and team development

      • Making software easier to comprehend, maintain, extend, and reuse

      • Increasing the understanding of basic object-oriented design principles

    • Once you understand the design patterns and have had an "Aha!" (and not just a "Huh?") experience with them, you won't ever think about object-oriented design in the same way.

  • Observer pattern je zobecněný MVC pattern (to je jen informace jen pro ty, co již znají MVC tj. Model-View-Controler)

  • založený na subscribe/notify protokolu

  • další jména pro totéž: publish/subscribe, dependents, česky: vydavatel-odběratel

  • tři objekty "pozorují" jeden subjekt; chceme aby všechny tři objekty reagovali na změnu subjektu

  • V Javě z historických důvodů – Observer je rozhraní a Observable je třída.

  • třída vydavatele (=subjektu) je zděděna od java.util.Observable

    • to jí umožňuje používat (základní) metody:

      • void addObserver(Observer o) – zaregistrování posluchače (na obrázku výše je metoda zapsána jen jako attach())

      • void setChanged() – nastavení příznaku změny stavu vydavatele (=subjektu)

      • void notifyObservers() – signál všem zaregistrovaným posluchačům, že u vydavatele (=subjektu) došlo ke změně (na obrázku výše je metoda zapsána jen jako notify())

      • void notifyObservers(Object parametr) – jako předchozí, navíc předá objekt popisující změnu (na obrázku výše je metoda zapsána jen jako notify())

        • v Javě jsou jména metod trochu jiná než v Observer design pattern např.:

          • attach() v patternu se v Javě jmenuje addObserver()

          • notify() v patternu se v Javě jmenuje notifyObservers() ... pozn.: metoda notify() v Javě je o vláknech tj. je to něco úplně jiného

  • třída odběratele (posluchače) musí implementovat rozhraní java.util.Observer, aby mohla být zaregistrována jako posluchač

    • představuje to implementaci metody

      public void update(Observable zdroj, Object param)

      která je automaticky volána po každém volání notifyObservers() od vydavatele

      • všimněte si, že v Javě má metoda update() parametry, zatímco v návrhovém vzoru je nemá

V příkladu je vydavatelem třída Citac, která se bude zvětšovat či zmenšovat o jedničku, což uživatel provede stiskem tlačítek +1 nebo –1. Odběrateli budou dvě na sobě nezávislé třídy – první bude odděděna od JTextField a druhá od JSlider. Obě musí implementovat rozhraní Observer. Metoda update() z tohoto rozhraní má dva parametry. První popisuje objekt zdroje a tento parametr v příkladu není dále k ničemu kloudnému využíván, protože vydavatel je pouze jeden (takže v tomto našem konkrétním případě nemá smysl zjišťovat od kterého z vydavatelů zpráva vzešla). Druhý parametr bude využit ve formě objektu třídy Integer, ve kterém se předá aktuální hodnota čítače (tj. nová hodnota po změně).

Je zcela zásadní, abyste se podívali na implementaci třídu Observable a rozhraní Observer ve zdrojových kódech Javy. Uvidíte, že to není žádna magie. Určitě se podívejte na implementaci obou metod notifyObservers().

Jen poznameném, že rozhraní *Listener a třídy *Adapter ve Swing, jak jsme je viděli na druhé přednášce, fungují analogicky.

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

// vydavatel
class Citac extends Observable {
  private int hodnota;

  public Citac(int hodnota) {
    setHodnota(hodnota);
  }

  public void setHodnota(int hodnota) {
    this.hodnota = hodnota;
    setChanged();   // došlo ke změně
    // zmena je reprezentovana novym Integer objektem vyrobeným z int hodnota:
    notifyObservers(new Integer(hodnota)); 
    // zde rovnou posílám změnu -- tj. "push" varianta, která nevyužívá 
    // getState() metodu, jak nás učí pattern (což by byla "pull" varianta)
  }

  public void plusJedna() {
    setHodnota(++hodnota);
  }

  public void minusJedna() {
    setHodnota(--hodnota);
  }
}

// odběratel č.1
class CitacTextField extends JTextField implements Observer {
  CitacTextField(int hodnota) {
    this.setColumns(6);
    this.setEditable(false);
    this.setHorizontalAlignment(JTextField.CENTER);
    this.setText("" + hodnota);
  }

  // DŮLEŽITÁ METODA, KTERA JE VOLÁNA PŘI ZMĚNĚ SUBJEKTU :
  public void update(Observable o, Object arg) {
    this.setText(arg.toString());
  }
}

// odběratel č.2
class CitacSlider extends JSlider implements Observer {
  CitacSlider(int hodnota, int min, int max) {
    super(JSlider.HORIZONTAL, min, max, hodnota);
    this.setMajorTickSpacing(2);
    this.setMinorTickSpacing(1);
    this.setPaintTicks(true);
    this.setPaintLabels(true);
  }

  // DŮLEŽITÁ METODA, KTERA JE VOLÁNA PŘI ZMĚNĚ SUBJEKTU :
  public void update(Observable o, Object arg) {
    int pozice = ((Integer) arg).intValue();
    this.setValue(pozice);
  }
}

// hlavní okno aplikace
public class MujObserver extends JFrame {
  Citac           citac;
  CitacTextField  ctf;
  CitacSlider     csl;
  JButton         plusBT, minusBT;

  MujObserver() {
    this.setTitle(getClass().getName());
    this.getContentPane().add(vytvorVnitrek());
    obsluhyUdalosti();
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.pack();
    this.setVisible(true);
  }
  
  Container vytvorVnitrek() {
      . . . // umisteni ctf, csl, plusBT, minusBT na panel & return panel

  }

  private void obsluhyUdalosti() {
    minusBT.addActionListener(new MinusBT());
    plusBT.addActionListener(new PlusBT());
    citac = new Citac(0);
    citac.addObserver(ctf);
    citac.addObserver(csl);
  }
  
  private class PlusBT implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      citac.plusJedna();
    }
  }

  private class MinusBT implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      citac.minusJedna();
    }
  }

  public static void main(String[] args) {
    new MujObserver();
  }
}