Islanti

  Pääsivulle  |  TIETOTEKNIIKAN PÄÄSIVULLE

Java-ohjelmointikieli

Java SWT

Vuosina 1998-2002 rakensin Herbaariosovellustani Javalla Linuxissa ja Windows 98:ssa. Projektikansioni ovat edelleen tallella ja helmikuussa 2020 kokeilin vieläkö sovellus toimii. Hieman yllätyin, että kyllähän se toimi sekä Macissä että Windows 10:ssä. Erikoinen kasvien tietokantaratkaisu, kasvikuvien katselu ja tietojen tallennus, kaikki toimi. Ainoa kauneusvirhe oli, että yhdessä näytössä elementit eivät olleet täysin oikeassa kohdassa.

Niinpä päätin perehtyä uudestaan Java-ohjelmointiin. Googlaamalla löytyi lukuisia versioita ja koodaustyökaluja. Lopuksi päädyin siihen, että Eclipse on sopivin ohjelmointityökalu ja alan tekemään koodia käyttäen versiota Java SWT ja tarvittaessa muitakin kirjastoja. Eclipsen kanssa otan käyttöön Windowbuilderin.

Asensin Eclipsen kolmeen käyttöjärjestelmään: macOS Catalina, Windows 8.1 ja Windows 10. Nopeasti tekemäni testiohjelmat osoittivat, että lähdekoodi on siirrettävissä käyttöjärjestelmästä toiseen. Pian opin, että olennaisen tärkeää oli käyttää kaikissa tekstitiedostossa samaa koodausta (encoding), ja valitsin tekstille UTF-8 ja rivinvaihdoille UNIX. Nämä saattoi laittaa Eclipsen asetuksissa oletusarvoiksi.

Ohjelmointikielenä Java on suuresti samankaltainen kuin C# myös syntaksiltaan, mutta ei yhtä monipuolinen. Samalla tavalla rakennetaan luokkia ja niiden funktioita ja järjestetään niiden yhteydet. Yksityiskohdissa on toki eroja.

Seuraavassa on esitetty joitain oppimiani perusasioita.

Eclipsen peruskonffaus

Kun lataat Eclipse-, java- ym. pakkauksia, niin käytä Windowsissa niiden purkamiseen 7zip-softaa.

Päävaiheet:

1. Lataa java-kirjasto "OpenJDK" ja asenna paikoilleen - http://jdk.java.net/13/
2. Lataa Eclipse ja asenna paikoilleen, mutta ennen käynnistystä konffaa eclipse.ini
3. Lataa sqlite-jdbc ja asenna paikoilleen
4. Käynnistä Eclipse
5. Tutki asetukset, mm. ruksaa että src ja bin eri kansioissa, valitse encoding ja valitse koodinkirjoitustyylisi. Kannatta tallentaa oma profiilinsa, jos joutuu asentamaan Eclipsen uudestaan
6. WindowBuilderin asentaminen  (Help / Eclipse Marketplace)
7. Sovellusprojektien käynnistäminen

Windows-koneissa kopioin java-kansion levyn juureen C:\jdk-13.0.2\ jotta sen täyspolkuun ei tule yhtään välilyöntejä. Macciä varten tuli kansio jdk-13.0.2.jdk ja sen vein paikkaan:
Library / Java / JavaVirtualMachines

Kohdassa 2 on tärkeää ilmoittaa Eclipselle mistä java-kirjasto löytyy. Se tehdään tekemällä tiedostoon eclipse.ini kaksi riviä juuri rivin "-vmargs" eteen:
Windowsissa:
-vm
C:\jdk-13.0.2\bin\javaw.exe


Macissä:
-vm
/Library/Java/JavaVirtualMachines/jdk-13.0.2.jdk/Contents/Home/bin

Ennen Eclipsen käynnistymistä päätä minne sijoitat Java-projektisi ja sitä varten kansio. Kyseisen kansion nimessä eikä absoluuttisessa polussa ei saa olla välilyöntejä eikä skandeja. Tämä tulee olemaan Eclipsen "workspace", jota heti kysytään.

Kohdassa 3 on mainittu SQLite:n asentaminen. Paketti löytyy GitHubin kautta ja sieltä latautuu tiedosto
sqlite-jdbc-3.30.1.jar
Tätä samaa käytetään sekä Macille että Windowsille. Macissä asensin sen kansioon:
Library / Java / Extensions
mutta Windowsissa vein sen suoraan projektieni projektikansioihin. Tämän jälkeen jar-paketti on Eclipsessä merkittävä ryhmään Modulepath.

Eclipse IDE:ä ja WindowBuilderiä päivitetään jatkuvasti. Macissa Eclipse ilmoitti, että päivitys on tarjolla, asennetaanko? Klikkasin, että kyllä, ja kaikki meni hyvin. Projektit toimivat edelleen jne. Windows 8.1:ssä ei mennyt yhtä hyvin ja lopuksi piti perustaa uusi workspace, asentaa Eclipse IDE lataamalla uusi versio, ja aloittaa kaikki projektit alusta. Mutta Windows 10:ssä ei kysytty edes lupaa uuden version asennukselle, se asentui itse sovellusta käynnistettäessä. Mutta onneksi päivitys onnistui hyvin ilman yhtäkään pulmaa.

Uuden projektin käynnistys

Tässä perustetaan SWT-projekti, jossa hyödynnetään WindowBuilderiä:
- menusta valitse File -> New -> Other
- valitaan SWT/JFace Java Project
- seuraavassa ikkunassa annetaan projektille nimi, ja klikataan Next (eikä Finish)
- poistetaan ruksi paikasta "Creat module-info.java file" eli ei rakenneta paketteja mikä mutkistaa monia asioita perusprojekteissa

Seuraavaksi perustetaan sovelluksen aloitusikkuna:
- projektin nimen päällä context menusta valitaan New -> Other
- ja sieltä WindowBuilder -> SWT Designer -> SWT -> Application Window
- klikataan Next ja päästään antamaan aloitusluokan nimi (usein sama kuin projektin nimi, mutta saa olla muutakin)

Vastaavasti perustetaan muita luokkia, tyyppiä esimerkiksi SWT Dialog tai Java Class.

WindowBuilder auttaa elementtien sijoittamisessa ikkunaan, näyttää millainen ikkunasta on tulossa ja erityisen havainnollinen se on tapahtumien määrittelyssä: kontrolli valitaan ja context menussa Add event handler ja sieltä esimerkiksi select -> selection -> widgetselected. Mutta composite- ja layout-määrittelyissä se välillä epäonnistuu ja lopputulosta pitää käsin viimeistellä koodissa. Lisäksi ikkunan osien sijoittelussa lopputulos ei ole aina samanlainen Windowsissa ja Macissä, eikä WindowBuilder aina näytä muutosta mitä merkitsen numeroilla elementin sijainnille. Koodiin pitää välillä kirjoittaa tyyliin että jos ollaan Macissä, arvo on 75 ja Windowsissa 62.

Projektin testaaminen

Eclipse toimii erinomaisesti sekä Windowsissa että Macissä. Jos olet koodannut oikein, määritellyt aputiedostojen sijainnin oikein jne., niin sovelluksen ajaminen onnistuu hyvin. Testi-ilmoitukset saat luontevasti esiin. Ainoa poikkeus on Macissä, jossa menut eivät toimi suoraan. Jotta ne saisi toimimaan, pitää klikata jotain muuta ikkunaa näytöllä ja sitten takaisin oman sovelluksen ikkunaa. Nyt menutkin toimivat.

Kokonaan toinen asia on oman softan testaaminen ja toimimaan saaminen Eclipsen ulkopuolella. Jos olet kirjoittanut 5000 riviä ohjelmakoodia, niin itse koodit kyllä toimii eri ympäristöissä (minulla nyt kokemusta Windowsista ja Macistä). Mutta konffaaminen on tehtävä erikseen ja eri tavoin eri ympäristöihin, seuraavassa lyhyesti mistä on kyse.

Sovelluksessa tarvitaan usein vakiotiedostoja, kuten kuvia tai tekstitiedostoja. Nämä voidaan sijoittaa omaan kansioonsa ja kansio sijoittaa Eclipsessä projektin juureen, ja määritellä projektin resurssiksi (buildpathiin). Kaikki toimii hienosti Eclipsessä, mutta ei Eclipsen ulkopuolella, ei vaikka käyttäisi suositeltuja funktioita getResource() ja getResourceAsStream(). Nämä ovat toimineet joskus, mutta eivät enää macOS Catalinassa eikä Windows 10:ssä. Joten päädyin ratkaisuun, että en määrittele vakiotiedostoja Eclipsessä resurssiksi, vaan etsin niiden sijainnin ohjelmakoodissa (ks. jäljempänä).

Eclipsessä voi sovelluksensa tallentaa jar-paketiksi jota kutsutaan jopa nimellä "Runnable JAR File". Tämä toimii mainiosti Windowsissa, ks alempana, mutta ei toimi ollenkaan Macissä. Macissä voi Eclipsessä muodostaa myös app-paketin "Mac OS X application bundle", mutta jo termikin viittaa vanhoihin Macceihin, eikä se toimi macOS Catalinassa. Kun sen käynnistää Macissä (Catalina), tulee ilmoitus "Vanha ajonaikainen Java SE 6 pitää asentaa, jotta Herba.app voidaan avata". Ilmoituksessa oleva linkki vie sivulle, jossa kerrotaan että Java SE 6 toimii korkeintaan macOS High Sierrassa.

Java-sovelluksen avaaminen Windowsissa

Ensimmäiseksi tulee konffata käyttämäsi java-kirjaston polku ympäristömuuttujiin. Ja sitten voi jatkaa edellä saadun jar-tiedoston kanssa, nimi on esimerkiksi Herbawin.jar. Sijoita se koneellasi sopivaan paikkaan, ja rinnalle aputiedostokansiosi. Tee sitten tekstitiedosto nimeltä Herba.bat ja sinne rivi
java -jar Herbawin.jar

Nyt kaksoiklikkaamalla tiedostoa Herba.bat sovelluksesi käynnistyy.

Java-sovelluksen avaaminen Macissä

Tämä toimii uudessa käyttöjärjestelmässä parhaiten käyttäen vanhaa periaateohjetta:
https://www.eclipse.org/swt/macosx/

Sieltä löytyi demosovellus HelloSWT, joka oli pakattu Macin app bundleksi, ja toimi jopa macOS Catalinassa. Sitä voi käyttää omaan sovellukseensa. Oheisessa kuvassa on kyseisen app bundlen sisältö, jonne olen sijoittanut oman yksinkertaisen testisovellukseni:
mac app bundle java
Tässä tietoja:
- kansioon Resources sijoitin omia aputiedostoja
- info.plist en koskenut paitsi myöhemmin muutin sinne skriptitiedoston nimen
- kansiossa MacOS on Eclipsen projektikansiosta kerätyt java- ja class-tiedostot
- swthello on skriptitiedosto (ks alempana), jolla sovellus käynnistetään; myöhemmin muutin nimeksi omatesti
- kansiossa swt on netistä haettu uusin versio swt.jar, löytyi nimellä swt-4.15-cocoa-macosx-x86_64.zip
- kansion swt muihin tiedostoihin en koskenut, mutta lisäsin sinne myöhemmin muita jar-kirjastoja

Skriptin swthello sisältö on:
#!/bin/sh
BASEDIR=`dirname $0`
exec java \
    -XstartOnFirstThread \
    -classpath $BASEDIR/swt/swt.jar:$BASEDIR \
    Aloitus

Kun kansioon swt lisätään sovelluksen tarvitsemia muita kirjastoja, kuten minun tapauksessani sqlite-jdbc-3.30.1.jar, on tieto siitä kirjoitettava skriptitiedostoon.

Minulla ongelmaksi muodostui menujen avautuminen ja sqlite-tietokannan avaaminen. Tässä testattuja versioita:
1) Kaksoisklikkaamalla app, tai käynnistys context-menusta:  tietokantaa ei saa auki mutta menut toimivat suoraan
2) Kaksoisklikkaamalla skriptitiedostoa, tai sen käynnistys context-menusta: tietokannan saa auki, mutta menut saa toimimaan vasta kun ensin klikkaa muualle näytöllä
3) Vein skriptitiedoston ulos app-paketista, samoin aputiedostojeni kansion ja MacOS-kansion, jotka yhdistän: toimii samoin kuin edellä kohta 2

Joten itse käytän toistaiseksi tapaa 3 itse ja antaessani sovelluksia lähipiirini käyttöön. Välilyönnit ja skandit ovat kiellettyjä skriptitiedoston sisältämän kansion nimessä ja sen koko polussa. Ja jos sovellus asennetaan muuhun kuin kehityskoneeseen, on java-kehityspaketti asennettava edellä mainittuun Eclipsen käyttämään kansioon, minulla nyt
/Library/Java/JavaVirtualMachines/

Aputiedostojen kansion paikan tunnistaminen

Koska minulla aputiedostojen kansio sijaitsee eri paikassa riippuen ympäristöstä, niin kansion polku on tunnistettava koodissa. Minulla on neljä eri tilannetta Herba-sovellukseni osalta:
- Eclipse IDE:ssä kansio on nimeltään Herbatied ja sijaitsee projektikansion juuressa
- Windowsissa kansio on nimeltään Herbatied ja sijaitsee jar-tiedoston rinnalla samassa kansiossa
- Macissä käytössä app-bundle, ja olen sijoittanut aputiedostot bundlen kansioon Resources
- Macissä app-bundle ei ole käytössä, ja aputiedostojen kansio on nimeltään Herbatied, jossa on lisäksi java-tiedostot alikansiossa koodit

String userdirz = System.getProperty("user.dir");

// Selvitetään aluksi aputiedostojen kansio. Se on riippuvainen ympäristöstä
// minne sovellus on asennettu.
tyodirz = userdirz + "/Herbatied/";  // jos olemme Eclipse IDE:ssä
if (!((new File(tyodirz)).exists()))
{
    // ajammeko Windowsin jar-tiedostoa
    File currDir = new File("");
    tyodirz = currDir.getAbsolutePath() + "Herbatied/";
    if (!((new File(tyodirz)).exists()))
    {
        // ajammeko Macin app-pakettia (app-doubleclick tai context avaa):
        String pathz = this.getClass().getClassLoader().getResource("").getPath();
        if (pathz.length() < 10)
            pathz = "eiloydy";
        tyodirz = pathz.substring(0, pathz.length() - 6) + "Resources/";
        if (!((new File(tyodirz)).exists()))
        {
            // ajammeko Macissä skripti-doubleclick tai context avaa, eikä
            // skripti ole app-bundlen sisällä (apukansiossa java on kansiossa 'koodit'):                   
            tyodirz = pathz.substring(0, pathz.length() - 7);
            if (!((new File(tyodirz)).exists()))
            {
                 mz = "Ei löydy työtiedostojen kansiota\n\n(Aloitus A01-01)";
                System.out.println("Ei löydy työtiedostojen kansiota\n\n(Aloitus A01-01)");
                return false;
            }
        }
    }
}
System.out.println("tyodirz = " + tyodirz);


Ohjelmoinnin yksityiskohtia

SQLite-tietokannan käsittely

Tässä on toimiva funktio tietokannan avaamiseksi. Funktion inputtina on tietokantatiedoston koko polku:
import java.sql.*

// -------------------------------------------
// 21.3.2020    A09
// avataan tietokanta
// Output on virheteksti, ja jos "" niin kaikki ok
protected String avaaTietokanta(String dbpolz)
{
   String z = "";
       
   try
   {
      Class.forName("org.sqlite.JDBC");
      String dbURL = "jdbc:sqlite:" + dbpolz;
      conlite = DriverManager.getConnection(dbURL);  // jos tiedostoa ei ole, se luodaan
      if (conlite != null)
         System.out.println("Connected to the database");
   }
   catch (ClassNotFoundException ex)
   {
      ex.printStackTrace();  // TODO: mitä tämä tekee?
      z = "ei löytynyt sqlite-kirjastoa";
   }
   catch (SQLException ex)
   {
      z = ex.getMessage();
      if (z.toLowerCase().indexOf("out of memory") != -1)
         z = "Ei löytynyt tai saatu aikaan tietokantaa " + dbpolz;
   }
   if (conlite == null && z.isEmpty())
      z = "Tietokantaa ei saatu avattua";
   if (!z.isEmpty())
      System.out.println(z);
   return z;
}

Tässä on normaali luuppi tietojen lukemiseksi tietokannasta:

Statement kome = null;
ResultSet rs = null;
String sqz = "SELECT * from Laadut";
try
{
   kome = conlite.createStatement();
   rs = kome.executeQuery(sqz);
   while (rs.next())
   {
      String z = rs.getString("LaatuNimi");   // SQL NULL antaa null
      int itu = rs.getInt("idLaatu");  // SQL NULL antaa 0
      int ju = rs.getInt(2);  // first column is 1
      // tee arvoilla z, itu, ju jotain
   }
}
catch (SQLException e)
{
   String vz = e.getLocalizedMessage();
}  
finally  // tällä tavalla mikään ei jää auki
{
   if (rs != null) { try { rs.close(); } catch (SQLException e) { /*ignored*/ }}
   if (kome != null) { try { kome.close(); } catch (SQLException e) { /*ignored*/ }}
}


Näin käsitellään kun kentässä on SQL NULL eikä sinulle  0 / null kelpaa
int iVal = 0;
Statement kome;
ResultSet rs = kome.executeQuery(sqz);
if (rs.next())
{
   iVal = rs.getInt("IDjoku");
   if (rs.wasNull())
   {
       // käsittele SQL NULL
   }
}

Funktio 'preparedStatement' on kätevä ja tehokas toistuvissa INSERT- ja UPDATE-komennoissa:
PreparedStatement pskome = null;
String sqz = "INSERT INTO apuc24 VALUES (?)";
try
{
   pskome = conlite.prepareStatement(sqz);
   for (Integer ipai : pait)
   {
      pskome.setInt(1, ipai);
      pskome.execute();
   }
}
catch (SQLException e)
{
   vz = e.getLocalizedMessage();
   System.out.println(vz);
}  
finally  // tällä tavalla mikään ei jää auki
{
   if (pskome != null) { try { pskome.close(); } catch (SQLException e) { /*ignored*/ }}
}


Ilmoitus- ja kyselyikkunat

Javasta löytyy luokkia ja funktioita yksinkertaisten ilmoitusikkunoiden ja kyselyikkunoiden esittämiseksi. Ilmoitusikkuna on sellainen, jossa on esimerkiksi ilmoitus "Antamasi päivämäärä on muodoltaan väärin" ja OK-painike sen kuittaamiseksi. Kyselyikkunassa sovellus kysyy jotain asiaa ja voit painikkesta valita esimerkiksi OK tai Peruuta. Mutta valitettavasti ne eivät toimi kunnolla, joten niitä ei kannata käyttää (macOS Catalina, Windows 10):
-  JOptionPane-luokan eri ikkunat eivät toimi lainkaan Macissä (jos composite=null) tai eivät sovellu enää SWT-ympäristöön, koska tarvitaan JFrame. Null-arvolla toimivat hyvin Windowsissa
- MessageBox (SWT-widget) ei osaa näyttää oikeanlaista ilmoituksen ikonia Macissä, mutta Windows on ok
- MessageDialog (JFace-widget) antaa osan ilmoitusikoneista väärin Macissä, mutta Windows on ok

Joten ensimmäinen sopiva Java-harjoitustyö itsekullekin on tehdä itse oma ilmoitus- ja kyselyikkuna. Minä tein oman luokan "Mesboksi" käyttäen pohjana SWT Dialog:ia WindowBuilderissä.

Merkkijonojen vertailu

Javassa ei tule vertailla merkkijonoja käyttäen operaattoreita == ja != kuten on soveliasta Swiftissä ja C#:ssa. Javassa ne kertovat ovatko molemmat merkkijonot samassa muistikohdassa. Normaalivertailut ovat:
if (nimiz.isEmpty())    // sama kuin nimiz.length() == 0
if (nimiz == null || nimiz.isEmpty())   // näin jos String voi olla null
if (nimiz.contentEquals("Erkki"))    // merkkijonon vertailu toiseen


Merkkijonon vertailu aakkostusta varten:
"a".compareTo("b"); // tuloksena negatiivinen luku -1
"a".compareTo("a"); // tuloksena  0
"b".compareTo("a"); // tuloksena positiivinen luku 1


Taulukot kuten Array

Tavallinen taulukko (array of int) määritellään (declaration) allakuvatulla tavalla ja samalla voi myös luoda objektin (toinen rivi):
int[] ai;
int[] ai = new int[7];


ArrayList on hyödyllinen taulukkorakenne, jonka kokoa voidaan kasvattaa alkio kerrallaan:
ArrayList<String> nimet = new ArrayList<String>();
nimet.add("Timo");
nimet.remove("Matti");
int kohta = nimet.indexOf("Timo");     // -1 ei löydy


Listaa pitkin voidaan mennä luupissa näin:
for(String str : nimet) 
     System.out.println(str);


Listaan voi laittaa myös omatekoisia luokkia. Jos tällaisen listan haluaa laittaa järjestykseen jollain itse määritellyllä tavalla, se tapahtuu seuraavasti, kun meillä on ArrayList<Kuvainfo> kuvasto. Luokka Kuvainfo on oma luokka. Sinne on laitettava nimirivin loppuun aakkostusta varten:
implements Comparable<Object>
ja luokan sisään on laitettava funktio:
@Override
public int compareTo(Object kix)   // järjestetään kentän Sunimiz perusteella
{
   String suz = ((Kuvainfo)kix).getSunimi();
   return this.Sunimiz.compareTo(suz);
}

Itse aakkostus tapahtuu näin:
Collections.sort(kuvasto);


HashMap on toimiva rakenne, joka vastaa monista muista kielistä tuttua Dictionaryä. On Javassa omakin Dictionary, mutta se on outo ja buginen, ei kannata yrittää käyttää. Seuraavassa HashMapin peruskäyttötapoja:

HashMap<String, String> nimet = new HashMap<String, String>();

nimet.put("nimi1", "john");   // lisätään avain ja tietoja
nimet.put("nimi2", "marry");

nimet.get("nimi2");  // saadaan avaimella tieto ulos
String tuz = nimet.getOrDefault("nimi9", "ei oo");  // hyvä käyttää, jos ei ole varma löytyykö avainta

nimet.remove("nimi1");   // poistetaan avain tietoineen

nimet.isEmpty()   // onko sisältöä lainkaan
int montako = nimet.size();
nimet.clear();   // hashmapin tyhjennys

for (String z : nimet.keySet())  // avainten käyttäminen
    System.out.println(z);

for (String z : nimet.values())  // tietojen käyttäminen
    System.out.println(z);
   
for (String z : nimet.keySet())  // avainten ja tietojen käyttäminen
    System.out.println("key: " + z + " value: " + nimet.get(z));

// Tässä Hashmap purettuna nurinpäin:
for (String key: nimet.keySet())
{
    String valz = nimet.get(key);
    if (valz.contentEquals("john"))
    {
        System.out.println(key);
    }
}

Myös kelpaa esimerkiksi HashMap<String, Integer> mutta ei saa käyttää int, vaan Integer, Boolean, Character, Double jne.

Polut ja tiedostot

Windowsissa ei tarvita kenoja vaan esimerkiksi tämä kelpaa:  "E:/KUVAT/KoeKuvat/IMG_1154_orient_1.JPG"

Tekstitiedoston lukeminen sisään muuttujaan ja luetun tiedon käsittelyä rivi kerrallaan:

String failiz;     // tekstitiedoston nimi polkuineen
String tuloz = ""; // luetut tiedot tänne
StringBuilder sb = new StringBuilder();  // tänne luetaan tekstiä
try

    BufferedReader bufr = new BufferedReader(new FileReader(failiz));
    try
    {
        String riviz;
        while ((riviz = reader.readLine()) != null)
        {
            // käsitellään riviä riviz
            sb.append(riviz).append("\n");
        }
    }
    finally
    {
        bufr.close();
    }
}
catch(IOException e)

    System.out.print("Lukeminen epäonnistui\n"+ e.getLocalizedMessage());
    sb.setLength(0);
}
tuloz = sb.toString();

Tehdään tilapäistiedosto:
File tilap = File.createTempFile("myTempFile", ".txt", null);

Onko tiedosto olemassa
File tef = new File("c:/temp/temp.txt");  // tämä ei tee failia
boolean exists = tef.exists();


Missä käyttöjärjestelmässä ollaan

Tiettyjen kontrollien koot voivat olla erikokoisia Mac- ja Windows-näytöillä samalla lähdekoodilla. Tämän takia koodissa voidaan tarvita käyttöjärjestelmästä riippuvia mittoja. Käyttöjärjestelmän selvittäminen ohjelmakoodissa tapahtuu näin:

String osz = System.getProperty("os.name").toLowerCase();
if (osz.indexOf("mac") != -1)
    return "MAC";
if (osz.indexOf("windows 10") != -1)
    return "WIN10";
return "WIN8";

Joissain tapauksissa voi olla tarpeen selvittää käyttäjän kotikansio:

System.getProperty("user.home");  // antaa esimerkiksi "C:\Users\erkki"

Label, jonka voi kopioida leikepöydälle

Joskus voi olla hyödyllistä saada kopioida näytölle kirjoitettava teksti leikepöydälle ja niin, että teksti näyttää tavalliselta Labeliltä. Se käy määrittelemällä kontrolli Text samannäköisesti kuin Label:

Text textInfo = new Text(sc, SWT.WRAP);
textInfo.setSize(500,240);
textInfo.setEditable(false);
textInfo.setBackground(null);
textInfo.setText("Oma tilanneilmoitus:\n" + tilannez);

Labelin fontti

Fontteja pääsee javassa määrittelemään, mutta joskus voi lopputulos olla ongelmallinen: valittu sopiva fontti ei ehkä olekaan käytettävissä toisessa järjestelmässä (Mac / Win / Linux). Lisäksi Eclipsessä voi huomata, että mukaan tulee pitkiä kansiopolkuja, mistä ei sinänsä haittaa ole.

Itselleni vakiofontin lisäksi tarvetta on ollut kyseisen fontin lihavoidulle versiolle. Näin sen saa yksinkertaisesti käyttöön SWT - WindowBuilder - Dialog -luokassa:

// määritellään luokkamuuttuja:
private Font boldFont;

// open()-funktion kohdassa missä poistutaan sovelluksesta:
boldFont.dispose();

createContents()-funktion alussa missä ekan kerran otetaan label (tässä lblX) käyttöön:
// lihavoidun fontin määrittely vakiofontille:
FontDescriptor boldDes = FontDescriptor.createFrom(lblX.getFont()).setStyle(SWT.BOLD);
boldFont = boldDes.createFont(lblX.getDisplay());
       
// ja sitten fontin käyttäminen
lblLati.setFont(boldFont);


Selainnäyttö

Selaimen katselu sovelluksen ikkunassa olikin hyvin yksinkertaista, verrattuna Swift- ja C#-sovelluksiin: tehdään uusi luokka (ikkuna) projektin, luokan tyyppi SWT Dialog. WindowBuilderin avulla sijoitetaan ikkunaan kontrolli Browser (sen nimeksi laitetaan alla browser), ja viedään sille sisältö:

String htmz = "https://www.tukes.fi";  // htm-teksti muuttujan sisällä
browser.setText(htmz);
 // tiedosto, jonka sisällä on htm-teksti:
String htmfilez = "c:/tilap/testifile.htm";
browser.setUrl(htmfilez);
// aina lopuksi:
browser.dispose();

Minä käytän tätä kontrollia kartan näyttämiseen (OpenStreetMap tai Maanmittauslaitoksen maastokartta). Apukansion (ks. edellä) sisällä minulla on kansioita, joissa on javascriptiä. Katso koodausesimerkit sivuillani, jossa Swift- ja C# -koodausohjeita. Java-version olen toteuttanut samalla tavalla.

Joissain sovelluksissa voi olla tarpeen ennen selaimen käyttämistä tarkistaa, onko netti käytettävissä. Allaoleva funktio on nopea myös jos nettiä ei löydy. Omassa sovelluksessa kannattaa yrittää tavoitella palvelinta mitä aikoo käyttää, tässä on esimerkkinä google.

import java.net.HttpURLConnection;
// -------------------------------------------------
// Tarkistetaan onko pääsy nettiin, tai tarkemmin Googlen palvelimelle
// Tämä tarkistus on nopea
// https://stackoverflow.com/questions/1402005/how-to-check-if-internet-connection-is-present-in-java
public static boolean onkoNetti()
{
    HttpURLConnection connection = null;
    try
    {
        URL url = new URL("https://www.google.com");
        connection = (HttpURLConnection) url.openConnection();
        connection.getContent();   // jos ei nettiä, niin tämä vie -> exception
    }
    catch (IOException e)
    {
        return false;   // Failed to get a connection
    }
    finally
    {
        if (connection != null)     // cleanup
            connection.disconnect();
    }
    return true;
}

Seuraava funktio kertoo ovatko inputin koordinaatit Suomessa vai ei. Funktio käyttää ns. käänteiskyselyä. Funktiota käyttävän on joko annettava ostettu tunnus tai oma käytössä oleva sähköpostiosoitteensa.

// ---------------------------------------------------------
// ovatko koordinaatit Suomessa, esim. latiz = "61.234", longiz = "22.345"
// output = "Suomi", "Muu" , ""
public static String annaMaa(String latiz, String longiz)
{
    String maaz = "";
       
    // Etsi maa OpenStreet-palvelusta
    // Tässä annettava kysyjän identifikaatio
    String zzz = "https://nominatim.openstreetmap.org/reverse?email=ETUNIMI.SUKUNIMI@zoner.fi&format=xml&lat="
                    + latiz + "&lon=" + longiz + "&zoom=18&addressdetails=1";
    try
    {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder().uri(URI.create(zzz)).build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        String respaz = response.body();
        if (omaTrim(respaz).isEmpty())    // ei ollut nettiä tai palvelua
        {
            System.out.println("M27: ei ollut nettiä tai OpenStreetMap-palvelua");
            return "";
        }
        if (respaz.indexOf("<country_code>") == -1)  // oli palvelu mutta ei saatu tietoa maasta
        {
            System.out.println("M27 ei maatietoa, respaz =" + respaz);               
            return "";               
        }
        maaz = (respaz.indexOf("<country_code>fi</country_code>") > -1) ? "Suomi" : "Muu";
    }
    catch (Exception e)
    {
        System.out.println("M27: " + e.getLocalizedMessage());
    }

    return maaz;
}

Värialueet

Ikkunoihin on usein mukava saada jotain väriä. Värejä en ole noukkinut WindowBuilderin tarjoamista vaihtoehdoista, koska ne mutkistavat koodia ja lähdekansioita. Tässä ovat kaikinpuolin yksinkertaiset koodirivit:
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Device;

private Color vaalsin;

Device device = Display.getCurrent();            // määritellään värejä
vaalsin = new Color(device, 221, 238, 255);
Composite composite = new Composite(shellPaikatso, SWT.NONE);
composite.setBackground(vaalsin);

vaalsin.dispose();  // tämä erillisenä tarpeen, kun poistutaan ikkunasta


Ikkunallisen luokan käsittely

Kutsuttaessa dialogi-ikkunaa (SWT Dialog Window) käytetään yleensä tällaisia lausekkeita:
1)  Dialogi-ikkuna, modaalinen, ja otsassa normaalit painikkeet:
Mesboksi msb = new Mesboksi(shell, SWT.APPLICATION_MODAL | SWT.DIALOG_TRIM);

2)  Dialogi-ikkuna, modaalinen, avautuu Macissä sheet-ikkunana, Windowsissa tavallisena:
Paihaku pha = new Paihaku(shell, SWT.APPLICATION_MODAL | SWT.SHEET);

Luokassa on vakiofunktiot open(), jossa on aloitus ja lopetus, ja createContents(), jossa luodaan näytön kontrollit ja osat.

Kontrollien rakentaminen ikkunaan:
-    näytön koko merkitään koodissa halutuksi
-    WindowBuilderissa koko näytölle formlayout
-    näytölle lisätään compositeja, eli alueita joissa on eri tehtäviä tai vaikkapa vain muista poikkeava väri
-    tämä sujui Windowsissa paljon paremmin kuin vastaava homma Macissä, eli vähemmän korjailtavaa lähdekoodissa
-    usein joutuu koodissa paikkaamaan composite-määrittelyjä
-   WindowBuilder voi kaatuakin, joten mitä tahansa lisäätkään WindowBuilderissä, tallenna heti sen jälkeen

Composite:n asettelussa tulee käyttää luokkaa FormData, mikä ei ole kovin havainnollinen, Tässä esimerkkejä:
FormData fd = new FormData();

// Fix the left edge of the control to 25% of the parent's width + 10px offset:
fd.left = new FormAttachment(25, 10);

// Fix the top edge of the control to the parent's top edge:
fd.top = new FormAttachment(0);

// Fix the top edge of the control 5 px below the bottom edge of another:
fd.top = new FormAttachment(comp0, 5, SWT.BOTTOM);

// Fix the top edge of the control on same level as top of another:
fd.top = new FormAttachment(comp0, 0, SWT.TOP);

// Fix the lower edge of the control to 75% of the parent's height + 0px offset:
fd.bottom = new FormAttachment(75);

// Fix bottom edge at exactly the same height as the one of otherControl
fd.bottom = new FormAttachment(otherControl, 0, SWT.BOTTOM);

// Fix bottom edge  40 px below the bottom edge of another
fd.bottom = new FormAttachment(otherControl, 40, SWT.BOTTOM);

// Fix the right edge of the control to the parent's left edege + 620px offset:
fd.right = new FormAttachment(0, 620);

// Fix the right edge of the control to the parent's right edge:
fd.right = new FormAttachment(100, 0);

// Fix left edge 10px to the right of the right edge of otherControl
fd.left = new FormAttachment(otherControl, 10, SWT.RIGHT);

// finally:
composite.setLayoutData(fd);


Välilehti, jossa on taulukko: kun ensimmäinen välilehti on paikoillaan, niin vie seuraavaksi taulukko, se tekee itse uuden välilehden taulukkoineen. Lopuksi koodissa poista ensin tehty välilehti (ei aina onnistu WindowBuilderissä). Jos haluat välilehdelle erilaisia kontrolleja, esimerkiksi Label, Button ja Table, niin ne on ensin koottava Compositelle joka määritellään koko TabFolderille. Compositeen kootaan eri kontrollit. Ja lopuksi composite asetetaan halutulle välilehdelle:
tabitem.setControl(munComposite);

Ikkunasta poistumisen aloite mistä tahansa luokan funktiosta:
if (!shell.isDisposed())
   shell.dispose();

tai
shell.close();

Näistä logiikka siirtyy open()-funktion loppuriveille, jossa valmistellaan outputtina lähtevä tieto.
Dialog-ikkunasta saa ulos melkein minkämuotoista dataa tahansa.
Output on muotoa Object, joten käypiä ovat esimerkiksi String tai omatekoinen luokka.

Kutsuva luokka käsittelee open()-funktion antaman tuloksen.

Combobox

Tässä tärkeimpiä comboboxin käsittelyfunktioita:

private CCombo combo;

combo.removeAll();    // tyhjennetään combobox
combo.add("Alavus");  // lisätään alkio comboboxiin

// mikä on valittuna
int sele = combo.getSelectionIndex();    // -1 jos ei valintaa
aluez = combo.getItem(aluesele);

// alue valituksi comboboxissa
int ai = combo.indexOf(paikkaz);
combo.select(ai);
combo.select(0);  // valitsee ekan

int kpl =  combo.getItemCount();  // montako vaihtoehtoa  valikossa on


Menut

Menun tekeminen WindowBuilderissä:
-    MenuBar ikkunan yläpuolelle
-    CascadeMenu on 'alasvetomenu' joita laitetaan vierekkäin menubaariin
-    Menuitem on niitä joita laitetaan päällekkäin CascadeMenuun
-    jos haluaa "sivumenun", niin CascadeMenuun laitetaan yhdeksi alkioksi CascadeMenu
-    tarvittaessa voi laittaa ruksilliset menut, termi CheckMenuitem

Tarvittaessa voidaan menun käyttö estää:
menu.setEnabled(false);

Menut eivät toimi Macissä (Catalina) odotetusti. Menut ovat käytettävissä vasta kun sovelluksen käynnistyksen jälkeen on käynyt klikkaamassa näytöllä jotain muuta. Lisäksi Macissä näkyy menubaarissa ylimääräinen "SWT", jota ei saa pois:
viallinen Mac-menu

Taulut (tables)

Riveille voidaan laittaa tietoja koko rivi kerrallaan, tai sitten yhden sarakkeen kohdalle kerrallaan:
for (int i=0; i < 24; i++)
{
    TableItem item = new TableItem(taulu, SWT.NONE);
    // kaikki sarakkeet:
    item.setText( new String[] { String.valueOf(1000+i), "paikka "+i,
    String.valueOf(1000-i), "alue "+i, "Seli seli "+i } );

    item.setText(1, "paikkax " + i);  // vain toiseen sarakkeeseen
}

Valitulta riviltä tiedon hakeminen:
TableItem[] sss = taulu.getSelection();  // valitut rivit
if (sss != null && sss.length > 0) .....
TableItem ss2 = sss[0];
String stz = ss2.getText(0);  // valitun rivin ekan sarakkeen sisältö
String st2z = ss2.getText(1);  // toka sarake

Taulukoiden aakkostuksen joutuu itse koodaamaan, mikä on usein joustavin ratkaisu (tableViewer sisältää mahdollisuuden, että aakkostus otsaketta klikkaamalla)

Tein sen seuraavasti:
Lisäsin taulukon sarakkeelle tapahtumankäsittelijän, esim:
columnPaikKpl.addSelectionListener(new SelectionAdapter()
{
    @Override
    public void widgetSelected(SelectionEvent e)
    {
        // oma apufunktio joka kirjoittaa koko taulukon sisällön uudestaan:
        lajitteluPaikkasarake("kpl");
    }
});

taulu.setSelection(index);   // merkitään rivi valituksi

taulu.update();  // jos tietoja muutettu tai kaikki poistettu, tällä päivitetään näkymä

taulu.removeAll() // tyhjentää koko taulukon


Radiobuttonit

Radiobuttonit sijoitetaan kehykseen (Group), jos niiden halutaan toimivan automaattisesti oikein (vain yksi valittuna). Tämä kehys tulee Windowsissa väärään kohtaan verrattuna radiobuttoniin, kun taas Macissä kaikki on ok. Jos Windowsissa tämän tekee niin, että radiobuttonit ovat kehyksen päällä (eli kehys ei näy), niin näky on erilainen Macissä. Millään väriasetuksilla kehystä ei saa piiloon. Yksi tapa kiertää ongelma on sijoittaa radiobuttonit paikoilleen ilman kehystä ja tehdä valinnan hallinnointiin omaa koodia (addSelectionListener) joka huolehtii, että vain yksi valittu. WindowBuilder suostuu sijoittamaan radiobuttonit vain kehykseen joten asiaa pitää koodata käsin.

Kuvan skaalaus ja Orientation

Kuvan metatietojen Orientation kertoo onko kuva oikeassa asennossa vai ylösalaisin, kyljellään tai peilikuva tai näiden yhdistelmä.Vääränlaisia kuvia syntyy tyypillisesti nykyaikaisissa älypuhelimissa riippuen siitä missä asennossa puhelinta on pidetty ja onko otettu kuva etukameralla vai takakameralla.

SWT-kirjastoissa ei ole funktioita, joilla voisi tutkia kuvien orientaatiota, tai jotka oikaisisivat kuvan automaattisesti. Nyt käytän siihen metadata-extratctor-kirjastoa, jonka on tehnyt drewnoakes-porukka ja julkaisuut sen GitHubissa. Täältä löytyy tekemäni sovellus, joka näyttää kuvan tarvittaessa oikeinpäin käännettynä Sen orientation-, flip- ja rotate-osat löytyvät luokasta Meka.

Kuvamatriisi

Joihin tarkoituksiin on kätevä esittää joukko pikkukuvia ja valita mille kuvalle tehdään toimenpiteitä. Täällä on sellaiselle periaatekoodaus. Matriisi on toteutettu RowLayout-rakenteella, kuva ja kuvateksti ovat Group-elementissä. Tällä tavalla kaikki kuvat teksteineen saatiin siistiin järjestykseen.  Kuvaa voi kaksoisklikata tehtävää toimenpidettä varten tai valita toimenpide kuvakohtaisesta context menusta. Toimenpiteitä varsinaisessa sovelluksessani ovat mm. kuvan tallennus työpöydälle, kuvan näyttäminen oikean kokoisena, kasvin yksityiskohtaisten tietojen esittäminen. Oheinen periaatekoodaus näyttää kuvat kuva0.jpg, kuva1.jpg jne ja testaamista varten korjaa koodista kuvien polut ja sijoita tämännimiset kuvat koneellasi vastaavaan paikkaan.

Käytännössä lopputulos on melko hidas. Vastaavan näytön esilletuonti Mac Swift-sovelluksessa (samassa Macissä) vei aikaa vajaan kolmasosan verrattuna Java-sovellukseen. Lisäksi Swift-sovelluksessa on mukana kunkin kuvan Orientation-tarkistus mitä ei ole (vielä) oheisessa Java-versioss.



--------------------------------
(sivua muokattu  15.6.2020)