import java.awt.*; // ScrollPane, PopupMenu, MenuShortcut, jne. import java.awt.datatransfer.*; // Clipboard, Transferable, DataFlavor, jne. import java.awt.event.*; // Uusi tapahtumakäsittelymalli. import java.io.*; // Olioiden serialisointivirrat. import java.util.zip.*; // Tiedon pakkaus- ja purkuvirrat. import java.util.Vector; // Piirustus laitetaan vektoriin. import java.util.Properties; // Tulostusasetukset laitetaan Properties-olioon /** * Tässä luokassa sijoitetaan Scribble-komponentti ScrollPane-säiliöön, * ja Scrollpane-säiliö ikkunaan, johon lisätään yksinkertainen * alasvetovalikko. Valikon valinnat voi valita pikanäppäimillä. Tapahtumat * käsitellään anonyymeillä luokilla. */ public class ScribbleFrame extends Frame { /** Hyvin yksinkertainen main()-metodi ohjelmaa varten. */ public static void main(String[] args) { new ScribbleFrame(); } /** Auki olevien ikkunoiden määrä täytyy muistaa, jotta osataan lopettaa * kun viimeinen ikkunoista suljetaan. */ protected static int num_windows = 0; /** Luodaan kehysikkuna, valikko ja ScrollPane-komponentti */ public ScribbleFrame() { super("ScribbleFrame"); // Luodaan ikkuna. num_windows++; // Kasvatetaan ikkunalaskuria. ScrollPane pane = new ScrollPane(); // Luodaan ScrollPane. pane.setSize(300, 300); // Määritellään sen koko. this.add(pane, "Center"); // Lisätään se kehykseen. Scribble scribble; scribble = new Scribble(this, 500, 500); // Suurempi piirustusalue. pane.add(scribble); // Lisätään ScrollPane-säiliöön. MenuBar menubar = new MenuBar(); // Luodaan valikkorivi. this.setMenuBar(menubar); // Lisätään se kehykseen. Menu file = new Menu("File"); // Luodaan File-valikko. menubar.add(file); // Lisätään se valikkoriville. // Luodaan kolme valikkovalintaa, joihin liittyy pikanäppäimet, ja // lisätään ne valikkoon. MenuItem n, c, q; file.add(n=new MenuItem("New Window", new MenuShortcut(KeyEvent.VK_N))); file.add(c=new MenuItem("Close Window",new MenuShortcut(KeyEvent.VK_W))); file.addSeparator(); // Laitetaan valikkoon erotin file.add(q = new MenuItem("Quit", new MenuShortcut(KeyEvent.VK_Q))); // Luodaan ja rekisteröidään tapahtumakuunteluoliot // valikkovalintoja varten. n.addActionListener(new ActionListener() { // Avaa uuden ikkunan. public void actionPerformed(ActionEvent e) { new ScribbleFrame(); } }); c.addActionListener(new ActionListener() { // Sulkee tämän ikkunan. public void actionPerformed(ActionEvent e) { close(); } }); q.addActionListener(new ActionListener() { // Sulkee ohjelman. public void actionPerformed(ActionEvent e) { System.exit(0); } }); // Toinen tapahtumakuuntelija. Tämä kuuntelee ikkunan sulkupyyntöjä. this.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { close(); } }); // Asetetaan ikkunan koko ja tuodaan se näkyville. this.pack(); this.show(); } /** Suljetaan ikkuna. Jos tämä on viimeinen auki oleva ikkuna, lopetetaan */ void close() { if (--num_windows == 0) System.exit(0); else this.dispose(); } } /** * Tämä luokka on itse tehty komponentti, johon voi piirtää. Komponentissa on * myös pikavalikko, jonka kautta voi valita piirustusvärin ja jota kautta * pääsee käsiksi tulostus, leikkaa-liitä ja tiedoston avaamis- sekä * talletustoimintoihin. Huomaa, että luokka periytyy Component-luokastas * eikä Canvas-luokasta, joten se on resurssien käytön kannalta kevyt. */ class Scribble extends Component implements ActionListener { protected short last_x, last_y; // Viimeisen napautuksen // koordinaatit. protected Vector lines = new Vector(256,256); // Tähän laitetaan talteen // piirustus. protected Color current_color = Color.black; // Parhaillaan käytettävä // väri. protected int width, height; // Toivottu koko. protected PopupMenu popup; // Pikavalikko. protected Frame frame; // Kehysikkuna, jossa // komponentti on. /** Tälle konstruktorille pitää antaa Frame-ikkuna ja koko */ public Scribble(Frame frame, int width, int height) { this.frame = frame; this.width = width; this.height = height; // Piirteleminen hoidetaan matalan tason tapahtumilla, joten täytyy // määritellä, mistä tapahtumista ollaan kiinnostuneita. this.enableEvents(AWTEvent.MOUSE_EVENT_MASK); this.enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK); // Luodaan pikavalikko silmukassa. Huomaa, miten valikon // komentomerkkijono erotetaan valikon otsikosta. Tämä on hyvä asia // kansainvälistämistä ajatellen. String[] labels = new String[] { "Clear", "Print", "Save", "Load", "Cut", "Copy", "Paste" }; String[] commands = new String[] { "clear", "print", "save", "load", "cut", "copy", "paste" }; popup = new PopupMenu(); // Luodaan valikko for(int i = 0; i < labels.length; i++) { MenuItem mi = new MenuItem(labels[i]); // Luodaan valikon valinta. mi.setActionCommand(commands[i]); // Asetetaan sen // komentomerkkijono. mi.addActionListener(this); // Ja tapahtumakuuntelija. popup.add(mi); // Lisätään valinta // pikavalikkoon. } Menu colors = new Menu("Color"); // Luodaan alivalikko. popup.add(colors); // Ja lisätään se // pikavalikkoon. String[] colornames = new String[] { "Black", "Red", "Green", "Blue"}; for(int i = 0; i < colornames.length; i++) { MenuItem mi = new MenuItem(colornames[i]); // Luodaan alivalikon mi.setActionCommand(colornames[i]); // valinnat samalla mi.addActionListener(this); // tavalla. colors.add(mi); } // Lopuksi, rekisteröidään pikavalikko sille komponentille, jonka // kohdalla sen pitää näkyä. this.add(popup); } /** Kertoo kuinka suuri komponentista pitäisi tehdä. Metodi palauttaa aina * Scribble()-konstruktorille välitetyn koon */ public Dimension getPreferredSize() { return new Dimension(width, height); } /** Tämä on ActionListener-metodi, jota pikavalikon valinnat kutsuvat */ public void actionPerformed(ActionEvent event) { // Kysytään tapahtumaa vastaava komentomerkkijono ja kutsutaan sen // perusteella metodia. Tässä metodissa kutsutaan useita tässä luokassa // olevia mielenkiintoisia metodeita. String command = event.getActionCommand(); if (command.equals("clear")) clear(); else if (command.equals("print")) print(); else if (command.equals("save")) save(); else if (command.equals("load")) load(); else if (command.equals("cut")) cut(); else if (command.equals("copy")) copy(); else if (command.equals("paste")) paste(); else if (command.equals("Black")) current_color = Color.black; else if (command.equals("Red")) current_color = Color.red; else if (command.equals("Green")) current_color = Color.green; else if (command.equals("Blue")) current_color = Color.blue; } /** Piirretään kaikki piirustukseen kuuluvat viivat oikeilla väreillä */ public void paint(Graphics g) { for(int i = 0; i < lines.size(); i++) { Line l = (Line)lines.elementAt(i); g.setColor(l.color); g.drawLine(l.x1, l.y1, l.x2, l.y2); } } /** * Tämä on matalan tason tapahtumakäsittelymetodi, jota kutsutaan aina kun * tapahtuu hiiritapahtuma, johon ei liity hiiren liikettä. Huomaa, kuinka * isPopupTrigger()-metodilla tarkistetaan, onko kyse ajoympäristössä * käytettävästä pikavalikon käynnistystoiminnosta. Pikavalikko tuodaan * näkyviin show()-metodilla. Jos valikkoa ei näytetä, metodi laittaa * talteen hiiren painalluksen koordinaatit tai kutsuu yliluokan metodia. */ public void processMouseEvent(MouseEvent e) { if (e.isPopupTrigger()) // Jos pikavalikon käynnistys, popup.show(this, e.getX(), e.getY()); // näytetään valikko. else if (e.getID() == MouseEvent.MOUSE_PRESSED) { last_x = (short)e.getX(); last_y = (short)e.getY(); // Laitetaan // positio talteen. } else super.processMouseEvent(e); // Reititetään muunlaiset // tapahtumat eteenpäin. } /** * Tätä metodia kutsutaan, kun hiiri liikkuu. Metodissa lisätään * piirustukseen viiva joka tuodaan myös näytölle ja laitetaan talteen. */ public void processMouseMotionEvent(MouseEvent e) { if (e.getID() == MouseEvent.MOUSE_DRAGGED) { Graphics g = getGraphics(); // Olio, johon piirretään. g.setColor(current_color); // Asetetaan väri. g.drawLine(last_x, last_y, e.getX(), e.getY()); // Piirretään viiva lines.addElement(new Line(last_x, last_y, // ja laitetaan se myös (short) e.getX(), // talteen. (short)e.getY(), current_color)); last_x = (short) e.getX(); // Pannaan hiiren koordinaatit talteen. last_y = (short) e.getY(); } else super.processMouseMotionEvent(e); // Tärkeä! } /** Tyhjennetään piirustus. Tätä kutsutaan pikavalikon kautta */ void clear() { lines.removeAllElements(); // Poistetaan piirustus ja piirretään repaint(); // kaikki uudestaan. } /** Tulostetaan piirustus. Tätä kutsutaan pikavalikon kautta */ void print() { // Pyydetään PrintJob-olio. Olio tuo näkyviin tulostusdialogin. // Tulostusasetukset laitetaan talteen printprefs-olioon (joka // luodaan alla). Toolkit toolkit = this.getToolkit(); PrintJob job = toolkit.getPrintJob(frame, "Scribble", printprefs); // Jos käyttäjä peruutti tulostuksen tulostusdialogissa, ei tehdä mitään. if (job == null) return; // Pyydetään Graphics-olio ensimmäistä sivua varten. Graphics page = job.getGraphics(); // Kysytään Scribble-komponentin ja sivun koko. Dimension size = this.getSize(); Dimension pagesize = job.getPageDimension(); // Keskitetään tulostus keskelle sivua. Muuten se tulisi sivun vasempaan // yläreunaan. page.translate((pagesize.width - size.width)/2, (pagesize.height - size.height)/2); // Piirretään tulostusalueen ympärille kehys, jotta kuva näyttäisi // paremmalta. page.drawRect(-1, -1, size.width+1, size.height+1); // Asetetaan tulostuksen rajausalue, jotta piirustus ei menisi rajojen // yli. Näytölle piirrettäessä rajaaminen tapahtuu automaattisesti mutta // ei paperille tulostettaessa. page.setClip(0, 0, size.width, size.height); // Tulostetaan tämä Scribble-komponentti. Oletusarvoisesti kutsutaan vain // paint()-metodia. this.print(page); // Lopetetaan tulostaminen. page.dispose(); // Lopetetaan sivu — lähetetään se kirjoittimelle. job.end(); // Lopetetaan tulostustyö. } /** Tähän Properties-olioon laitetaan talteen tulostusdialogissa tehdyt * tulostusasetukset. */ private static Properties printprefs = new Properties(); /** * Tämä on DataFlavor-olio, joka kuvastaa tässä ohjelmassa leikkaa-liitä- * tyyppisesti siirrettävää tietoa. Tieto siirretään serialisoituna Vector- * oliona. Huomaa, että Java 1.1.1-versiossa tämä toimii ohjelman sisällä, * mutta ei ohjelmien välillä. Java 1.1.1-versiolla ohjelmien välillä * siirrettävätieto on rajoitettu etukäteen määriteltyihin merkkijono- ja * tekstityyppeihin. */ public static final DataFlavor dataFlavor = new DataFlavor(Vector.class, "ScribbleVectorOfLines"); /** * Kopioidaan piirustus ja laitetaan se talteen SimpleSelection-olioon * (joka on määritelty alla). Viedään tämä olio sitten leikepöydälle * liittämistä varten. */ public void copy() { // Pyydetään järjestelmän leikepöytä. Clipboard c = this.getToolkit().getSystemClipboard(); // Kopioidaan ja talletetaan piirustus Transferable-olioon. SimpleSelection s = new SimpleSelection(lines.clone(), dataFlavor); // Laitetaan olio leikepöydälle. c.setContents(s, s); } /** Leikkaa toimii samalla tavalla kuin kopoiointi, paitsi että piirustus * tyhjennetään sen jälkeen. */ public void cut() { copy(); clear(); } /** * Pyydetään järjestelmän leikepöydältä Transferable-olion sisältö ja * pyydetään saadulta oliolta vastaamansa piirustuksen sisältö. Jos * jompikumpi toimenpiteistä epäonnistuu, annetaan äänimerkki. */ public void paste() { Clipboard c = this.getToolkit().getSystemClipboard(); // Pyydetään // leikepöytä. Transferable t = c.getContents(this); // Pyydetään sen // sisältö. if (t == null) { // Ellei ole liitettäviä tietoja, äänimerkki. this.getToolkit().beep(); return; } try { // Pyydetään, että leikepöydän sisältö muunnetaan tässä tuettuun // muotoon. Muunnoksesta aiheutuu poikkeus, mikäli se ei onnistu. Vector newlines = (Vector) t.getTransferData(dataFlavor); // Lisätään kaikki liitetyt viivat kuvaan. for(int i = 0; i < newlines.size(); i++) lines.addElement(newlines.elementAt(i)); // Ja piirretään koko kuva uudestaan. repaint(); } catch (UnsupportedFlavorException e) { this.getToolkit().beep(); // Jos leikepöydällä on muun tyyppistä tietoa } catch (Exception e) { this.getToolkit().beep(); // Jos jokin muu menee pieleen... } } /** * Tässä sisemmässä luokassa on toteutettu Transferable- ja ClipboardOwner- * rajapinnat, joita tarvitaan, kun siirretään tietoja. Luokka on * yksinkertainen ja se lähinnä vain muistaa valitun olion ja antaa sen * tarjolle jossakin määritellyistä muodoista. */ static class SimpleSelection implements Transferable, ClipboardOwner { protected Object selection; // Siirrettävät tiedot. protected DataFlavor flavor; // Ainoa tuettu esitysmuoto public SimpleSelection(Object selection, DataFlavor flavor) { this.selection = selection; // Määritellään tiedot. this.flavor = flavor; // Määritellään muoto. } /** Palautetaan luettelo tuetuista muodoista. Tässä tapauksessa vain yksi * muoto */ public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] { flavor }; } /** Tarkistetaan, tuetaanko kysyttyä esitysmuotoa */ public boolean isDataFlavorSupported(DataFlavor f) { return f.equals(flavor); } /** Jos esitysmuoto sopii, siirretään tiedot (eli palautetaan ne) */ public Object getTransferData(DataFlavor f) throws UnsupportedFlavorException { if (f.equals(flavor)) return selection; else throw new UnsupportedFlavorException(f); } /** Tämä on ClipboardOwner-metodi. Sitä kutsutaan, kun tiedot eivät enää * ole leikepöydällä. Tilanteessa ei tarvitse tehdä juuri mitään. */ public void lostOwnership(Clipboard c, Transferable t) { selection = null; } } /** * Kysytään käyttäjältä tiedostonimi, talletetaan piirustus kyseiseen * tiedostoon. Serialisoidaan viivat sisältävä vektori ObjectOutputStream- * oliolla. Serialisoidut oliot pakataan GZIPOutputStream-oliolla. Pakattu * ja serialisoitu data kirjoitetaan tiedostoon FileOutputStream-oliolla. * Ei pidä unohtaa tyhjentää ja sulkea virtaa. */ public void save() { // Luodaan tiedostodialogi, jolla kysytään käyttäjältä tiedostonimi. FileDialog f = new FileDialog(frame, "Save Scribble", FileDialog.SAVE); f.show(); // Näytetään dialogi. Kutsusta palataan // pois vasta kun dialogi kuitataan. String filename = f.getFile(); // Pyydetään käyttäjän vastaus. if (filename != null) { // Jos ei painettu Peruuta-nappia. try { // Luodaan piirustuksen talletuksessa tarvittavat virrat. // Talletetaan tiedostoon FileOutputStream fos = new FileOutputStream(filename); GZIPOutputStream gzos = new GZIPOutputStream(fos); // Pakattuna ObjectOutputStream out = new ObjectOutputStream(gzos); // Oliot out.writeObject(lines); // Kirjoitetaan koko vektori. out.flush(); // Tulos pitää aina tyhjentää. out.close(); // Ja sulkea virta. } // Tulostetaan poikkeukset. Nämä pitäisi kyllä näyttää dialogissa... catch (IOException e) { System.out.println(e); } } } /** * Kysytään tiedostonimi ja ladataan piirustus kyseisestä tiedostosta. * Luetaan pakattu, serialisoitu data FileInputStream-virralla. * Puretaan data GZIPInputStream-virralla. Deserialisoidaan viivavektori * ObjectInputStream-oliolla. Korvataan parhaillaan muistissa olevat * viivat ladatuilla viivoilla ja piirretään kuva uudestaan. */ public void load() { // Luodaan tiedostodialogi, jolla kysytään käyttäjältä tiedostonimi. FileDialog f = new FileDialog(frame, "Load Scribble", FileDialog.LOAD); f.show(); // Näytetään dialogi. String filename = f.getFile(); // Kysytään käyttäjän vastaus. if (filename != null) { // Jos ei painettu peruutusnäppäintä. try { // Luodaan tarvittavat virrat // Luetaan tiedostosta FileInputStream fis = new FileInputStream(filename); GZIPInputStream gzis = new GZIPInputStream(fis); // Puretaan ObjectInputStream in = new ObjectInputStream(gzis); // Luetaan oliot // Luetaan olio sisään. Olion pitäisi olla viivavektori. Vector newlines = (Vector)in.readObject(); in.close(); // Suljetaan virta. lines = newlines; // Asetetaan viivat. repaint(); // Ja näytetään piirustus uudestaan. } // Tulostetaan poikkeukset. Nämä pitäisi kyllä näyttää dialogissa... catch (Exception e) { System.out.println(e); } } } /** Luokka, joka vastaa piirustuksen yhden viivan koordinaatteja ja väriä. * Koko piirustus on talletettu vektoriin, joka sisältää tällaisia olioita */ static class Line implements Serializable { public short x1, y1, x2, y2; public Color color; public Line(short x1, short y1, short x2, short y2, Color c) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; this.color = c; } } }