Innholdsoversikt for programutvikling

Grafiske brukergrensesnitt, grunnleggende komponenter

Generelt om vinduer

Grunnleggende vindusprogrammering er behandlet i notatene Vindusbaserte programmer og Programmering av vinduslytter. I dette notatet skal vi se nærmere på vinduer, samt på bruken av de vanligste grafiske skjermkomponentene og hvordan disse kan plasseres i et vindu.

Frem til JavaFX ble lansert med Java 1.8, har meste av det som har med vinduer og vinduskomponenter å gjøre i javas klassebibliotek befunnet seg i pakken javax.swing. Vi har her valgt å fokusere på swing-komponentene, da det er disse læreboka bruker. Når vi skal programmere vinduer må vi derfor importere denne pakken. I noen tilfeller må vi importere andre pakker i tillegg. Dette vil gå fram av eksemplene i det følgende.

Et vindu på toppnivå (det vil si som ikke befinner seg inni et annet vindu) blir i Java benevnt med det engelske ordet frame. Den vanlige norske oversettelsen for 'frame' er 'ramme', men det gir litt gale assosiasjoner. Det engelske ordet som brukes i Java for rammen rundt en komponent på skjermen, for eksempel ramme rundt et bilde, er border, som vi vel på norsk vanligvis oversetter med 'grense'. (Slike rammer er omtalt i notatet Litt om bruk av rammer (borders).) I Javas swing-bibliotek har vi klassen JFrame som grunnklasse for vinduer. (Den er subklasse til Frame som var Javas opprinnelige vindusklasse, før swing-biblioteket ble opprettet.) Et JFrame-vindu er en av de få swing-komponentene som ikke blir tegnet på en tegneflate av swing-funksjonaliteten. Isteden blir selve vinduet med dets ramme og dekorasjoner (det vil si knapper (i vinduets øverste høyre hjørne), tittellinje, ikoner, etc.) tegnet av operativsystemets funksjonalitet for vinduer, ikke av Javas swing-funksjonalitet.

Hittil har vi brukt følgende framgangsmåte for å opprette og vise et vindu som vi på forhånd har definert i en klasse Vindu og satt størrelse på:

    Vindu vindu = new Vindu();
    vindu.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    vindu.setVisible(true);

Dette har pleid å gå bra, så sant det ikke har vært noe feil i programkoden. Etter hvert som teknologien ble utviklet og swing-komponentene ble mer komplekse, viste det seg at java-utviklerne ikke lenger kunne garantere sikkerheten av denne framgangsmåten. Sannsynligheten for en feil er uhyre liten, men for å unngå å bli en av de få uheldige, er det likevel grunn til å følge den framgangsmåte som nå blir anbefalt. Den går ut på å vise vinduet ved hjelp av den såkalte tråden for hendelseshåndtering, istedenfor ved hjelp av programmets hovedtråd, som er den som blir brukt når det gjøres som ovenfor. For å vise vinduet ved hjelp av tråden for hendelseshåndtering skriver vi følgende kode:

    EventQueue.invokeLater(new Runnable()
      {
        public void run()
        {
          Vindu vindu = new Vindu();
          vindu.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          vindu.setVisible(true);
        }
      });

Tråder er det gjort nærmere rede for i notatet Multiprosessering.

Plassering av vinduer på skjermen

Dersom vi selv ikke bestemmer noe annet, er det operativsystemet som bestemmer vinduers plassering på skjermen når de blir vist. På Windows-plattform blir de plassert i øverste venstre hjørne. For å overstyre dette, samt gjøre andre tilpasninger for vinduet, har vi for vårt vindusobjekt tilgang til følgende metoder:

Vi skal se litt nærmere på bruken av noen av metodene. Vi antar da at vi har et vindusobjekt vindu.

Metodekallet

  vindu.setLocation(x, y);

plasserer vinduet på skjermen slik at vinduets øverste venstre hjørne har koordinater (x, y) i forhold til øverste venstre hjørne av skjermen. (Måleenhet for koordinatene er piksel.)

Metodekallet

  vindu.setLocationRelativeTo(null);

sentrerer vinduet på skjermen.

Metoden setBounds bestemmer både størrelse og plassering for vinduet. Metodekallet

  vindu.setBounds(x, y, bredde, høyde);

gir plassering som ved metoden setLocation, mens parametrene bredde og høyde bestemmer vinduets bredde og høyde.

En annen mulighet er å gi plattformens vindussystem kontrollen over plassering ved følgende metodekall før vinduet blir gjort synlig:

  vindu.setLocationByPlatform(true);

Vinduet vil da vanligvis bli plassert litt forskjøvet i forhold til det vinduet som sist ble vist på skjermen.

Bestemme en god størrelse for vinduet

I utgangspunktet har et vindu størrelse lik 0 piksler både i bredde og høyde. Vi er derfor nødt til å sette størrelse på det på en eller annen måte. Det finnes flere muligheter. Hittil kjenner vi til metodene setSize og setBounds. Begge metodene krever bestemte verdier for parametrene som angir bredde og høyde. Et vindu kan imidlertid komme til å bli vist på skjermer av svært forskjellig størrelse. Et profesjonelt program bør sjekke skjermstørrelsen og bestemme størrelse på vinduet slik at den passer til skjermstørrelse og oppløsning. Skjermstørrelsen i den oppløsningen som blir brukt får vi tak i på følgende måte:

  Toolkit verktøykasse = Toolkit.getDefaultToolkit();
  Dimension skjermdimensjon = verktøykasse.getScreenSize();
  int bredde = skjermdimensjon.width;
  int høyde = skjermdimensjon.height;

Klassen Toolkit må vi importere fra pakken java.awt. Når vi kjenner skjermstørrelsen, har vi godt grunnlag for å bestemme en god størrelse på vinduet i antall piksler for bredde og høyde. Dersom vinduet bare inneholder standardkomponenter slik som knapper, tekstfelter og noen andre standardkomponenter, kan vi sette størrelse ved å gjøre kall på vindusmetoden pack. Størrelsen blir da satt akkurat stor nok til å vise komponentene med sin såkalte prefererte størrelse. Skal dette være vellykket, forutsetter det imidlertid at vi bruker en bedre layout enn typen FlowLayout som vi har lært om hittil. De vanligste typene layout er beskrevet i notatet Layout-managere.

Bestemme ikon

Behandling og representasjon av bilder er også systemavhengig, slik at vi må bruke verktøykassa når vi skal opprette bilder:

  Toolkit verktøykasse = Toolkit.getDefaultToolkit();
  String bildefil = ...;
  Image ikon = verktøykasse.getImage(bildefil);
  vindu.setIconImage(ikon);

Dersom vi for bildefila bare angir filnavnet, forutsetter det at den ligger i samme katalog som vinduets class-fil. Ellers kan vi angi filsti. Husk da at vi i java må bruke vanlig skråstrek som katalogskille.

Image-klassen må vi, slik som Toolkit, importere fra pakken java.awt. Når ikonet blir vist på vinduets tittellinje, vil størrelsen automatisk bli tilpasset det som er standard for vindustypen. På Windows-plattformen vil ikonet også bli vist i vinduet for aktive oppgaver når vi trykker ned tastene Alt+Tab. Da blir størrelsen også automatisk tilpasset.

Merknad

I praksis viser det seg at den framgangsmåten som er beskrevet ovenfor for å opprette bilde ikke alltid er til å stole på. En sikrere framgangsmåte, som dessuten gir oss bedre kontroll over hva som skal skje i tilfelle det ikke er mulig å opprette bilde, er å gå veien om et URL-objekt på følgende måte:

    String bildefil = "bilder/hioalogo.gif"; //referer til riktig bildefil
    URL kilde = Vindu.class.getResource(bildefil);
    if (kilde != null)
    {
      ImageIcon bilde = new ImageIcon(kilde);
      Image ikon = bilde.getImage();
      setIconImage(ikon);
    }

URL-klassen må importeres fra pakken java.net. En nærmere omtale om bruk av bilder finnes i notatet Litt om bilder.

Eksempel

Vi skal opprette et tomt vindu på den måten som er beskrevet ovenfor, og der vi bruker det meste av funksjonaliteten som er beskrevet ovenfor. Som ikon for vinduet blir det brukt ikonet i høgskolens logo, med bildefil hiologo_90x100.gif". Slik programmet er skrevet, må den ligge i underkatalogen bilder i forhold til programmets class-fil. Kjøring av programmet gir følgende vindu:

Fullstendig kode for programmet finnes i fila Vindustest.java som er gjengitt nedenfor.

 1 import java.awt.*;
 2 import javax.swing.*;
 3 import java.net.URL;
 4
 5 public class Vindustest
 6 {
 7   public static void main(String[] args)
 8   {
 9     EventQueue.invokeLater(new Runnable()
10           {
11             public void run()
12             {
13               Vindu vindu = new Vindu();
14               vindu.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
15               vindu.setVisible(true);
16             }
17       });
18    }
19 }
20
21 class Vindu extends JFrame
22 {
23   public Vindu()
24   {
25     Toolkit verktøykasse = Toolkit.getDefaultToolkit();
26     Dimension skjermdimensjon = verktøykasse.getScreenSize();
27     int bredde = skjermdimensjon.width;
28     int høyde = skjermdimensjon.height;
29
30     //Setter bredde og høyde. Lar plattformen velge plassering.
31     setSize( bredde / 4, høyde / 4 );
32     setLocationByPlatform(true);
33     //Bildefil for ikon er plassert i underkatalogen bilder:
34     String bildefil = "bilder/hiologo_90x100.gif";
35     URL kilde = Vindu.class.getResource(bildefil);
36     if (kilde != null)
37     {
38       ImageIcon bilde = new ImageIcon(kilde);
39       Image ikon = bilde.getImage();
40       setIconImage(ikon);
41     }
42     setTitle("Testvindu");
43   }
44 }

Generelt om grafiske skjermkomponenter

For å kunne bruke grafiske skjermkomponenter på en effektiv måte, er det nyttig å ha noe kjennskap til den delen av klassehierarkiet som definerer komponentene i javas klassebibliotek. De fleste av de komponentene vi skal bruke er definert i form av klasser i pakken javax.swing. De er - direkte eller indirekte - subklasser til klassen JComponent. Den inngår i klassehierarkiet på følgende måte:

java.lang.Object
 ¦
java.awt.Component
 ¦
java.awt.Container
 ¦
javax.swing.JComponent

Klassen JComponent inneholder den funksjonaliteten som er felles for alle grafiske komponenter. En del av metodene er modifikasjoner av metoder som blir arvet fra klassen Component. For alle komponenter definert av subklasser til JComponent er det blant annet mulig å

Et objekt av type Container er en komponent som kan inneholde en samling av komponenter. Når vi hittil har plassert komponenter, har vi plassert dem i vinduets såkalte contentPane. Dette er av type Container. Av klassehierarkiet ser vi at alle komponenter definert i form av en subklasse til JComponent i seg selv er en container. Dette gjør det mulig å legge komponenter inni hverandre og bruke dem som byggeklosser, slik at vi får stor fleksibilitet til å bygge opp komplekse komponenter av enklere, mer grunnleggende komponenter, samt bruke disse om igjen i mange forskjellige sammenhenger.

MVC-arkitektur

Javas swing-komponenter er bygget opp etter såkalt MVC-arkitektur. En slik arkitektur blir også kalt et mønster, engelsk: pattern. Forkortelsen MVC står for Model-View-Controller. Når man skal programmere bruken av komponentene i et program, er det nyttig å kjenne litt til hva denne arkitekturen går ut på. Følgende framstilling er basert på beskrivelsen i boka Core Java av Cay S. Horstmann og Gary Cornell. Boka inneholder en mer detaljert beskrivelse.

Som innledning til å beskrive MVC-arkitekturen, skal vi først se litt nærmere på hva som karakteriserer en vanlig komponent som en knapp, en avkryssingsboks, eller et tekstfelt. Hver slik komponent er karakterisert ved tre forskjellige aspekter:

Videre er det slik at utseendet til for eksempel en knapp varierer fra plattform til plattform, det er også avhengig av knappens tilstand: når den er trykket ned, må den tegnes ut på nytt for at den skal se annerledes ut enn når den ikke er trykket ned. Knappens tilstand er avhengig av hvilke hendelser den har registrert. Når brukeren klikker på knappen med musa, registrerer knappen denne hendelsen og knappen blir trykket ned.

Når vi bruker en knapp i et program, trenger vi ikke tenke på disse tingene. Programmererne som implementerte knapper og andre komponenter måtte derimot tenke svært mye på hvordan det var gunstig å programmere dem. Og for oss som skal bruke komponentene i våre programmer er det nyttig å kjenne til hvordan det ble gjort.

Et gunstig prinsipp å følge i all objektorientert programmering er dette: La ikke et objekt få altfor stort ansvar. Som eksempel: La ikke én enkelt knappeklasse stå ansvarlig for alle de tre aspektene som er beskrevet ovenfor. La isteden ansvaret for utseende være delegert til ett objekt, og lagre komponentens innhold i et annet objekt. Model-view-controller-arkitekturen forteller nettopp hvordan vi skal realisere denne måten å gjøre det på. Implementer tre separate klasser:

MVC-mønsteret beskriver hvordan de tre klassene skal spille sammen. Modellen lagrer komponentens innhold og har ikke noe brukergrensesnitt. For en knapp er innholdet kort og godt noen flagg som indikerer om knappen for øyeblikket er trykket ned eller ikke, om den er aktiv eller inaktiv, etc. For et tekstfelt omfatter innholdet blant annet en streng som lagrer tekstfeltets aktuelle tekst. Merk at dette ikke er det samme som visningen av innholdet. Teksten kan være større enn det er plass til å vise. Visningen for brukeren vil da bare inneholde en del av tekstfeltets tekstinnhold. Modellen må implementere metoder for endring av innholdet og for å fortelle hva det er for øyeblikket. Men modellen er altså helt usynlig for brukeren. Det er visningens oppgave å gi en visuell representasjon (helt eller delvis) av de data som er lagret i modellen.

For de fleste komponentene implementerer modellklassen et interface med navn som ender på Model, slik som ButtonModel for knapper. Swing-biblioteket inneholder en klasse DefaultButtonModel som implementerer dette interface. Hver JButton lagrer et objekt av denne typen og vi kan om vi ønsker det, få tak i objektet:

  JButton knapp  = new JButton("Testknapp");
  ButtonModel modell = knapp.getModel();

Ved å følge linken DefaultButtonModel kan du finne en beskrivelse av hva knappemodellen inneholder. Det er også interessant å legge merke til hva som ikke er der: Den inneholder ikke informasjon om knappens tekst og eventuelle ikon. (Disse tingene har med visningen å gjøre.)

For knapper har vi normalt ikke behov for å få tak i modellobjektet og gjøre noe med det. Knapper er for øvrig nærmere omtalt i notatet Knapper. Det er grunn til å merke seg at den samme modellen (DefaultButtonModel) blir brukt for alle typer knapper (vanlige knapper, radioknapper, avkryssingsbokser, menyalternativer). Men disse forskjellige typene har vidt forskjellig visning og har forskjellige kontrollere. Når det gjelder visning, bruker JButton klassen BasicButtonUI for visning i vanlig java-utseende, og den bruker en kontroller av type BasicButtonListener.

En kontroller behandler hendelser som følge av bruker-input, slik som museklikk og bruk av taster på tastaturet. Den må også avgjøre om hendelsene skal medføre endringer i modellen eller visningen. Dersom for eksempel brukeren trykker ned en tegntast mens markøren er i et tekstområde, vil kontrolleren gjøre kall på "sett inn tegn"-metoden til modellen. Modellen vil på sin side fortelle visningen at den må oppdatere seg. Visningen vet ikke hvorfor teksten endret seg. Dersom brukeren derimot trykket på en piltast, kan det være at kontrolleren ber visningen om å skrolle. Skrolling av visningen har ingen innvirkning på den underliggende teksten, så modellen vet ikke at denne hendelsen skjedde.

I egenskap av programmerere som bruker swing-komponenter, trenger vi som regel ikke å tenke på model-view-controller-arkitekturen når vi bruker de vanligste grafiske skjermkomponentene. Hver komponent har det vi kan kalle en innpakningsklasse (slik som JButten og JTextField) som lagrer modellen og visningen. Når vi ønsker å sjekke innholdet (slik som teksten i et tekstfelt, ved bruk av getText-metoden), så vil innpakningsklassen spørre modellen om dette og returnere svaret til oss. Ønsker vi å endre visningen, for eksempel flytte markøren til en annen posisjon i et tekstfelt, vil innpakningsklassen oversende ønsket til visningsobjektet som sørger for den nødvendige oppdateringen på skjermen. Men vi skal se at når vi kommer til mer komplekse komponenter, så er vi nødt til å hente ut modellen og arbeide direkte på den for å oppnå det vi ønsker. Vi får imidlertid aldri behov for å arbeide direkte med visningen.

Kontrollerne er lytteobjekter knyttet til komponenten. Hver komponent kan ha mange forskjellige lytteobjekter knyttet til seg. Noen av dem er ferdigprogrammert av Javas utviklere, mens vi selv må programmere andre. En viktig del av hendelsesbasert programmering er nettopp å programmere lytteobjekter på en korrekt måte, slik at vi får programmene til å virke slik vi ønsker.

Denne lille introduksjonen til hva som egentlig foregår under overflata til en vanlig skjermkomponent som en knapp, har kanskje gjort deg mer forvirret enn du var før, og du lurer kanskje på: Hva er egentlig en grafisk skjermkomponent som en knapp av type JButton? Vi kan kort og godt si at det er en innpakningsklasse som arver fra klassen JComponent, og som inneholder et DefaultButtonModel-objekt, noen visningsdata (slik som knappetekst og eventuelt ikon), og et BasicButtonUI-objekt som er ansvarlig for visning av knappen.

JLabel-objekter

JLabel-objekter har vi notatet Vindusbaserte programmer brukt til å skrive ut tekster på skjermen, fortrinnsvis overskrifter og ledetekster. JLabel-objekter kan i tillegg inneholde bilder, eller eventuelt bare bilder. Dessuten kan de utstyres med tekst som vil "poppe opp" når musepekeren befinner seg over komponenten, såkalt "tooltip-tekst". En label representerer egentlig et lite vindu på skjermen (der kantene ikke synes). Ved bruk av passende konstanter kan vi bestemme hvordan tekst og bilder skal plasseres i dette vinduet. Eksempler på dette kan du finne i programeksemplet nedenfor.

I en label som inneholder bare tekst, blir teksten, dersom vi ikke bestemmer noe annet, plassert mot venstre kant og sentrert i vertikal retning. For en label som inneholder tekst og ikon (bilde), er default plassering at ikonet er ved venstre kant og teksten til høyre for dette, begge deler sentrert i vertikal retning. For å sette tooltip-tekst for JLabel-objektet labelobjekt, bruker vi kode på formatet

  labelobjekt.setToolTipText( "ønsket tekst" );

Bilder kan vi opprette ved instruksjoner på formen

  new ImageIcon( < bildefil > );

der bildefil blant annet kan ha de vanlige filformatene gif, jpeg (jpg) og png. Dersom bildefila ligger i en annen filkatalog enn vedkommende class-fil, må vi skrive filsti istedenfor filnavn.

Som konstruktørparameter kan vi også bruke et URL-objekt, som i sin tur er opprettet på grunnlag av en bildefil. Det er denne framgangsmåten som blir brukt i eksemplet nedenfor.

For hver klasse som brukes av et java-program, blir det opprettet et Class-objekt som inneholder all informasjon om klassen, blant annet koden for klassens metoder. Hvert objekt av vedkommende klasse har en referanse til dette Class-objektet, og metoden getClass returnerer referansen. Class-objektets metode getResource prøver å opprette og returnere et URL-objekt på grunnlag av den adressen den får oppgitt. Dersom dette ikke lykkes, vil den returnere null. Se for øvrig notatet Litt om bilder.

Swing-klassen ImageIcon implementerer interface Icon, som definerer et bilde av fast størrelse. (Det kan altså ikke skaleres.) En variabel som skal referere til et slikt bilde kan derfor også deklareres til å være av datatypen Icon. For å få plassert et slikt bilde i en label, kan vi enten bruke bildeobjektet som konstruktørparameter, eller som parameter i kall på metoden setIcon.

Eksempel

Vindusklassen LabelFrame med tilhørende testprogram LabelTest er gjengitt nedenfor. Eksemplet er hentet fra læreboka til Deitel & Deitel, 9. utgave. Programmet gir eksempel på hvordan vi kan opprette og tilpasse JLabel-objekter. Vær imidlertid oppmerksom på at koden for plassering av komponentene i vinduet forutsetter at man ikke bruker eldre java-versjon enn versjon 5. Dersom tidligere versjoner brukes, må man gå veien om contentPane, slik det er forklart i notatet Vindusbaserte programmer. Når programmet kjøres, vil det gi et vindu som vist på følgende bilde. På bildet er musepekeren blitt plassert over label nummer 2, slik at tooltip-teksten for denne har poppet opp.

 1 import java.awt.FlowLayout; // specifies how components are arranged
 2 import javax.swing.JFrame; // provides basic window features
 3 import javax.swing.JLabel; // displays text and images
 4 import javax.swing.SwingConstants; // common constants used with Swing
 5 import javax.swing.Icon; // interface used to manipulate images
 6 import javax.swing.ImageIcon; // loads images
 7
 8 //Demonstrating the JLabel class.
 9 public class LabelFrame extends JFrame
10 {
11    private JLabel label1; // JLabel with just text
12    private JLabel label2; // JLabel constructed with text and icon
13    private JLabel label3; // JLabel with added text and icon
14
15    // LabelFrame constructor adds JLabels to JFrame
16    public LabelFrame()
17    {
18       super( "Testing JLabel" );
19       setLayout( new FlowLayout() ); // set frame layout
20
21       // JLabel constructor with a string argument
22       label1 = new JLabel( "Label with text" );
23       label1.setToolTipText( "This is label1" );
24       add( label1 ); // add label1 to JFrame
25
26       // JLabel constructor with string, Icon and alignment arguments
27       Icon bug = new ImageIcon( getClass().getResource(
28               "bilder/bug1.gif" ) );
29       label2 = new JLabel( "Label with text and icon", bug,
30          SwingConstants.LEFT );
31       label2.setToolTipText( "This is label2" );
32       add( label2 ); // add label2 to JFrame
33
34       label3 = new JLabel(); // JLabel constructor no arguments
35       label3.setText( "Label with icon and text at bottom" );
36       label3.setIcon( bug ); // add icon to JLabel
37       label3.setHorizontalTextPosition( SwingConstants.CENTER );
38       label3.setVerticalTextPosition( SwingConstants.BOTTOM );
39       label3.setToolTipText( "This is label3" );
40       add( label3 ); // add label3 to JFrame
41    } // end LabelFrame constructor
42 } // end class LabelFrame

 1 import java.awt.EventQueue;
 2
 3 import javax.swing.JFrame;
 4
 5 public class LabelTest
 6 {
 7   public static void main( String args[] )
 8   {
 9     EventQueue.invokeLater(new Runnable()
10     {
11       public void run()
12       {
13         LabelFrame labelFrame = new LabelFrame(); // create LabelFrame
14         labelFrame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
15         labelFrame.setSize( 275, 180 ); // set frame size
16         labelFrame.setVisible( true ); // display frame
17       }
18     });
19   } // end main
20 } // end class LabelTest

Merknad

I teksten til en JLabel er det (fra og med Java-versjon 1.3) tilatt å bruke html-tekst i tillegg til vanlig tekst. Den må i så fall avgrenses med taggene <html>...</html>, som i følgende eksempel:

  JLabel ledetekst = new JLabel("<html><b>NB!</b> Fyll ut navnefelt: </html>");

Vær imidlertid oppmerksom på at når programutførelsen kommer til den første komponenten som inneholder html-tekst, så vil det ta litt tid å få vist den på skjermen fordi det må lastes inn en del forholdsvis kompleks hjelpekode for å få utført det.

Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2014

Start på kapittel om grafiske brukergrensesnitt Neste avsnitt