Islanti Thingvellir

  Pääsivulle  |  TIETOTEKNIIKAN PÄÄSIVULLE

Ohjelmointivinkkejä (C#)

C# ja Visual Studio


Seuraavassa esitetään muutamia ohjelmointivinkkejä asioista, joita aikoinaan jouduin miettimään hieman enemmän.
- Vain yksi sovellusikkuna (WPF)
- Kartan näyttäminen ja koordinaatit
- Koordinaattipisteiden keskinäisen etäisyyden laskeminen
- Ovatko koordinaatit Suomesta
- Onko meillä nettiyhteys
- Videoleikkeet
- Macista haetun tiedostonimen skandit
- Exif-koodit
- Alasvetovalikko (WPF)
- Kuva ikkunassa (WPF)
- Tietolähteen käyttäminen (WPF)
- Kuvamatriisi (WPF)
- Datagrid (taulukkonäkymä) (WPF)

Vain yksi sovellusikkuna (WPF)

Sovellukseni etenee tyypillisesti näkymästä toiseen, esimerkiksi: päävalikon sisältämä näyttö -> Kasvin haku (hakuehdot ja taulukko hakutuloksista) -> Katsotaan kasvin tiedot. Tähän asti jokainen näkymä on minulla avautunut omaan ikkunaansa. Tämä tekee yleisvaikutelman sekavaksi ja joskus se aktiivinen ikkuna on hukassa.

Löysin syksyllä WPF:n elementin "UserControl". Se voi sisältää kaikkia osaelementtejä mitä ikkunassakin on eli buttoneita, tekstikenttiä, taulukoita jne. Ensin tehdään edellämainittu päävalikon sisältämä näyttö. Siinä on Window-elementti ja sen sisällä UserControl, jossa on tarpeelliset buttonit ym. Muissa näkymissä (xaml-tiedostoissa) ei ole ollenkaan Window-elementtiä, vaan se on yksi UserControl buttoneineen ym.

Kun aloitusikkunassa esimerkiksi klikataan buttonia haluten siirtyä seuraavaan näkymään (UserControlliin) nimeltä Kasvihaku, niin aloitusikkunan sisältö korvataan uudella näkymällä:
Window win = Window.GetWindow(this);    // saadaan taustaikkuna
win.Content = new Kasvihaku();   // sisältö vaihdetaan

Ohjelmallisesti tämä merkitsee myös sitä, että näkymää avatessa sen sisältö määritellän kokonaan uudestaan. Jos olemme tehneet kasvihaun näkymässä Kasvihaku(), ja siellä klikataan katsomaan jonkun kasvin tietoja, niin näkymää Kasvihaku() ei enää ole "alla". Ja kun palataan kasvin tietoja katsomasta, niin avautuu tyhjä Kasvihaku. Yleensä tässä tilanteessa haluaa nähdä samat hakuehdot ja tulokset kuin hetki sitten.

Jokaisen avattavan näkymän alkuarvojen on siis oltava olemassa. Tämän olen toteuttanut listarakenteella:
public static List<Tila> seurx = new(); // usercontrollien input

Eli meillä on listana avattavien "päällekkäisten" usercontrollien lähtödata. Kun esimerkiksi Kasvihaku-näytöllä kirjoitetaan hakuehtoja, ne tallennetaan myös alkuarvojen listaan. Edellämainittu Tila on esimerkiksi seuraavaa:
public class Tila
{
   public string Seurx { get; set; } = "";   // seuraava näkymä, esim "KASVIHAKU"
   public KashakuKent Kkent { get; set; } = new();  // Kasvihaku
   // ....
   public string Kpolkuz { get; set; } = "";  // kuvan koko polku mm  -> Kuvaikku
   public string Kinfoz { get; set; } = "";  // kuvaan ym liittyvä info
   // ...
   public int IDkohde { get; set; } = EIOO;
   public int IDkasvi { get; set; } = EIOO;
}

Aina kun avataan seuraava näkymä (usercontrol), se tapahtuu näin:
Window win = Window.GetWindow(this);  // tallennetaan oma ikkunakoko poistumishetkellä
seurx[0].Wheight = win.Height;
seurx[0].Wwidth = win.Width;
// kerätään seuraavan ikkunan data:
Tila tila = new(); 
tila.Seurx = "KASVIKATSO";  // seuraavan ikkunan nimi
tila.IDkasvi = 55;  // esimerkki inputista
tila.Wheight = 400;  // seuraavan ikkunan koon vakioarvot
tila.Wwidth = 700;
seurx.Add(tila);        // lisätään avattavan näkymän tiedot pinoon
seurx.Insert(0, tila);
// ikkunan koko on annettava ennen näkymän avaamista
win.Height = tila.Wheight;
win.Width = tila.Wwidth; 
win.Content = new Kasvikatso();  // avataan seuraava näkymä

Ja kun palataan takaisinpäin, niin tarvitaan tällaisia vaiheita:
// katsotaan edellisen, nyt avattavan ikkunan mitat
Window win = Window.GetWindow(this);
win.Height = seurx[1].Wheight;
win.Width = seurx[1].Wwidth;  // ikkunalle koko ennen näkymän avaamista
seurx[1].IDkuva = ... // voidaan viedä tietoa nyt avattavalle näkymälle
seurx.RemoveAt(0);  // poistetaan pinosta suljettava näkymä
win.Content = ValitseNakyma(seurx[0].Seurx); // edellinen näkymä avataan

Edellä oleva funktio ValitseNakyma nimensä mukaan suorittaa kutsun, jolla haluttu näkymä avataan. Funktion olennainen sisältö on switch-rakenne, esimerkiksi (C# 8.0):
return nimiz switch
{
    "KUVAIKKU" => new Kuvaikku(),
    "PAAIKKU" => new Paaikku(),
    "KAIKIKKU" => new Kaikikku(),
}

Näkymässä muutettuja arvoja (edellä kasvihaun hakuehtoja) voi xaml-rivillä määritellä meneväksi haluttuu paikkaan, esimerkiksi:
<TextBox x:Name="txtSunimi" Text="{Binding Kkent.Sunimi}"  Margin="3" Width="100" />

Tämä ei ole täydellinen ratkaisu, koska binding-paikkaan vietäessä avatun luokan (näkymän) sisällä, tekstikenttä ei päivity. Lisäksi ComboBox toimii hieman eri logiikalla. Yksinkertaisinta voi olla:
- juuri ennen seuraavaan näkymään menemistä kerätään ilman bindingeja kentissä olevat arvot talteen pinon Tila-muuttujaan
- heti näkymän avaamisen jälkeen viedään Tila-muuttujasta arvot tekstikenttiin tyyliin
   txtSunimi.Text = "Valkovuokko";

Näillä periaatteilla olen päivittänyt Herbaariosovellukseni onnistuneesti. Loppuviimeistelyaikaa meni ikkunakokojen ja värien määrittelyyn.

Kartan näyttäminen ja koordinaatit

Monissa sovelluksissani asioiden sijaintia näytetään kartalla. Esimerkiksi albumisovelluksessa kuvan ottopaikka ja herbaariosovelluksessa kasvin löytöpaikka. Näiden sovellusten tietokannoissa on EUREF-FIN -koordinaatit, jotka nykyään saa helpoiten suoraan kameran tai kännykän tekemästä kuva- tai videotiedostosta. Saahan ne myös GoogleMapsin näytöltä valitsemalla hiiren oikealla painikkeella 'Mitä täällä on'. Sovellukseni näyttävät sijainnin käyttäjän valinnan mukaan joko OpenStreetMapin karttapohjalla tai Maanmittauslaitoksen maastokartalla.
 
Käytin aiemmin Googlen APIa (GoogleMap), mutta sitten Google vaati luottokortin numeroa ja suostumusta veloitukseen, vaikka tämäntyyppisestä käytöstä Google ei (toistaiseksi) laskuta. Luottamukseni ei ole kovin suurta siihen, etteikö siinäkin asiassa asiakkaalle ilmoittamatta muutettaisi sopimusehtoja.

Karttapiste on helpointa esittää urlin avulla, jossa on sisään leivottuna koordinaatit. OpenStreetMap haluaa koordinaatit LatiLongi, kun taas Maanmittauslaitoksen maastokartta haluaa koordinaatit muodossa NorthEast:
karttojen urlit

Sovelluksessa ensin haetaan koordinaattiarvot (tietokannasta, tai käyttäjä antaa, tai valokuvasta jne.) ja sitten muodostetaan ylläolevan mallin mukainen merkkijono. Sovelluksessa on otettu käyttöön kirjasto WebView2, jonka kanssa näyttö tehdään näin:

<Window x:Class="Albumi8.Karttaselain"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Albumi8"
          xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        mc:Ignorable="d"
        Title="Karttaselain" Height="600" Width="800">
    <DockPanel>
        <wv2:WebView2 Name="webView"
      Source="https://www.microsoft.com"
        />
    </DockPanel>
</Window>


Ja sitten cs-tiedostossa annetaan näkymälle lähde:

webView.Source = new Uri(maasz);   // tai openz edeltä

Ennen edelläkuvattua koodausta kuitenkin asennuksia:

Microsoftin WebView2 -kirjasto otetaan Visual Studion ja sovelluksen käyttöön seuraavasti:
1) Kehityskoneeseen ja loppukäyttäjän koneeseen on asennettava Webview2-kirjasto täältä
   Lataa sieltä Evergreen WebView2 Runtime Bootstrapper 
   asennettavaksi tulee MicrosoftEdgeWebview2Setup.exe
2) Kehityskoneessa Visual Studioon menusta Project "Manage NuGet Packages" ja asenna sieltä
   Microsoft.Web.WebView2 by Microsoft

Sovelluksessa voi olla tarpeen tehdä koordinaattimuunnoksia LatiLongi -> NorthEast. Täällä on käyttöösi koodia.

Koordinaattipisteiden keskinäisen etäisyyden laskeminen

Sovelluksessa voi olla tarpeen selvittää kuinka kaukana jokin paikka on toisesta paikasta, kun molempien koordinaatit tunnetaan. Minulla tämä tuli eteen, kun halusin löytää tietokannasta kuvat, joiden ottopaikat on tietyn ympyrän sisällä tietystä koordinaattipisteestä. Tietokannassa on kuvien koordinaatit. Seuraava funktio laskee onko kahden pisteen etäisyys alle vai yli annetun etäisyyden ja tarvittaessa myös pisteiden etäisyyden kilometreinä. Funktion sisällä on nopea karkealaskenta ja lisäksi noin 7 kertaa hitaampi tarkka laskenta:

//-----------------------------------------------------------------------
// (Windows C#)
// Lasketaan etäisyys näiden välillä: koord_1 ja koord_2. Outputtina
//   etäisyys metreinä tai
//   -1 jos oli ongelma tai
//   detmax + 1000.0, jos pikalaskenta antoi, että etäisyys > detmax
// Jos detmax < 0.0, tuloksena aina etäisyys metreinä käyttäen tarkempaa laskentaa
// Jos detmax > 0.0, tehdään ensin nopea karkealaskenta ja sen jälkeen tarvittaessa tarkempi
// Jos detmax > 0.0 ja etäisyys sitä suurempi, tuloksena joko -1, detmax + 1000 tai todellinen etäisyys
// Käytetään etäisyyden laskentaan Pythagooran teoreemaa
// Ennen kutsua tulee olla tarkastettu, että koordinaateissa vain numeroita ja piste
public static long KoordEtaisyys(double dlat1, double dlon1, string lat2, string lon2, double detmax)
{
    long R = 6371000;  // maapallon säde metrejä
    if ((detmax > 1000000.0) || (detmax < 0.0 && detmax != -1.0)) // yli 1000 km ei kelpaa  
    {
        MessageBox.Show($"Input etmax virheellinen: {detmax}", "(M25-01) Ohjelmointivirhe");
        return -1;
    }
    if (lat2.IsEmpty() || lon2.IsEmpty())
    {
        MessageBox.Show("Input-koordinaateissa tyhjää", "(M25-02) Ohjelmointivirhe");
        return -1;
    }
    double dlat2 = double.Parse(lat2, CultureInfo.InvariantCulture);
    double dlon2 = double.Parse(lon2, CultureInfo.InvariantCulture);

    // Tarkastetaan ovatko koordinaatit identtiset
    double ero1 = dlat1 - dlat2;
    double ero2 = dlon1 - dlon2;
    if (ero1 < 0.0001 && ero1 > -0.0001 && ero2 < 0.0001 && ero2 > -0.0001)
        return 0;

    // Muunnetaan inputit radiaaneiksi
    double fii1 = (Math.PI / 180) * dlat1;
    double fii2 = (Math.PI / 180) * dlat2;
    if (detmax > 0.0)  // Pikalaskenta leveyspiiriä pitkin:
    {
        double ddlat = Math.Abs(R * (fii2 - fii1));
        if (ddlat > detmax)
            return (long)detmax + 1000;   // liian kaukana toisistaan
    }

    double lan1 = (Math.PI / 180) * dlon1;
    double lan2 = (Math.PI / 180) * dlon2;
           
    if (detmax > 0.0)  // Pikalaskenta pituuspiiriä pitkin:
    {
        double ddlon = Math.Abs(R * (lan2 - lan1) * Math.Cos((fii1 + fii2) / 2));
        if (ddlon > detmax)
            return (long)detmax + 1000;     // liian kaukana toisistaan
    }

    // Lasketaan etäisyys tarkemmalla kaavalla
    double x = (lan2 - lan1) * Math.Cos((fii1 + fii2) / 2);
    double y = fii2 - fii1;
    double dd = Math.Sqrt(x * x + y * y) * R;
    return (long)dd;
}

Ovatko koordinaatit Suomessa

using System.Net.Http;
//------------------------------------------------------------
// 26.1.2022    M27
// Missä maassa koordinaatit ovat
// Output  "Suomi", "Muu"
//       "" ei selvinnyt eli joko ei nettiä tai googlen palvelu ei vastannut
// Maa selvitetään ensisijaisesti oheisten koordinaattien avulla:
// - Suomi suorakaiteen sisällä: ulkopuolinen alue on "Muu"
// - Suomen sisäpuoli (alla taulukko luvut): sisäpuolinen alue on "Suomi"
// Em. koordinaattien välinen alue tutkitaan neOpenStreettistä -palvelun avulla: onko inputtina
// olevat koordinaatit Suomea vai ei, tulos "Suomi", "Muu" tai "" (ei nettiä)
// Input latiz ja longiz oltava oheisessa muodossa:
// string maaz = EtsiMaa("60.582535", "25.682804");
public static string EtsiMaa(string latiz, string longiz)
{
    // Suomen sisäpuoli on "viipaloitu vaakasuoraan"
    // Vaakasuora latitude-viiva, pystysuorat rajat logi vasen ja longi oikea:
    // vaaka[i], lonvas[i], lonoik[i]
    // lati, vaakasuoran viivan vasen reuna (lonvas), oikea reuna (lonoik)
    double[] luvut =
    {
        59.77325, 21.80186,     23.58164,
        59.92775, 19.94517,     24.72422,
        60.15270, 19.45078,     25.95469,
        60.34352, 19.36289,     27.65757,
        60.54943, 19.91221,     27.75645,
        60.68419, 20.97788,     27.96519,
        61.32852, 21.27451,     29.15171,
        62.69959, 20.67257,     31.31554,
        63.35327, 20.96920,     30.75787,
        63.69035, 22.30675,     29.97521,
        64.74155, 23.94419,     29.62364,
        65.52683, 24.96544,     29.52477,
        65.91980, 24.11949,     29.75548,
        66.36346, 23.77057,     29.48346,
        66.80510, 24.04259,     28.81733,
        68.40746, 23.53290,     28.50567,
        68.54852, 25.18085,     28.27900,
        69.02590, 25.79411,     28.36766,
        69.27517, 25.75291,     28.84556,
        69.58398, 25.96440,     28.98907,
        69.88141, 26.46625,     28.14716
    };

    double[] vaaka = new double[luvut.Length / 3];
    double[] lonvas = new double[luvut.Length / 3];
    double[] lonoik = new double[luvut.Length / 3];

    int j = 0;
    for (int i = 0; i < luvut.Length; i += 3)
    {
        vaaka[j] = luvut[i];
        lonvas[j] = luvut[i + 1];
        lonoik[j] = luvut[i + 2];
        j++;
    }

    double lond = 0.0;
    bool ctu = double.TryParse(latiz.Replace('.', ','), out double latd);
    if (ctu)
    {
       ctu = double.TryParse(longiz.Replace('.', ','), out lond);
    }

    if (latd is < 59.65135 or > 70.15173)  // Suomi kokonaan tämän suorakaiteen sisällä
        return "Muu";
    if (lond is < 18.87949 or > 31.71152)
        return "Muu";
    if (latd >= vaaka[0] && latd <= vaaka[vaaka.Length - 1])
    {
        for (int i = 0; i < vaaka.Length - 1; i++)
        {
            if (latd >= vaaka[i] && latd < vaaka[i+1])
            {
                // Kun piste on vaakasuorien vakioviivojen välissä, tehdään
                // Uusi vaakaviiva pisteen kautta, päät lineaarisesti
                // ylä- ja alapuolten päiden suhteen
                double kerr = (latd - vaaka[i]) / (vaaka[i+1] - vaaka[i]);
                double lonva = lonvas[i] + (lonvas[i+1] - lonvas[i]) * kerr;
                double lonoi = lonoik[i] + (lonoik[i+1] - lonoik[i]) * kerr;
                if (lond >= lonva && lond <= lonoi)
                {
                    return "Suomi";
                }
            }
        }
    }

    // jos jouduimme määriteltyjen alueiden väliin, niin kysytään maa netistä:
    StringBuilder sb = new("");
    int imaa = EIOO;  // EIOO = ei Suomi, > 0 Suomi, VIRHE = ei yhteyttä palvelimeen
    // Etsi maa OpenStreet-palvelusta
    _ = sb.Append("https://nominatim.openstreetmap.org/reverse?format=xml&lat=");

    _ = sb.Append(latiz);
    _ = sb.Append("&lon=");
    _ = sb.Append(longiz);
    _ = sb.AppendLine("&zoom=18&addressdetails=1");
    try
    {
      using (HttpClient client = new())
      {
        string kysyz = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)";      
        client.DefaultRequestHeaders.Add("user-agent", kysyz);
        Task<string> t = client.GetStringAsync(sb.ToString());
        string z = t.Result;
        imaa = z.IndexOf("<country_code>fi<");
        if (imaa == EIOO && z.Contains("<country_code>") == false)
        {
           _ = MessageBox.Show("Nettipalvelu toimii, mutta ei saatu mitään maatietoa:\n" + z, "Ilmoitus");
        }
      }
    }
    catch (Exception ex)
    {
       _ = MessageBox.Show($"Virhe: {ex.Message}", "Ilmoitus");
       imaa = VIRHE;
    }
    finally
    {
    }
    return (imaa == VIRHE) ? "" : ((imaa == EIOO) ? "Muu" : "Suomi");
}

Onko meillä nettiyhteyttä

Edelläesitettyjen funktioiden kanssa voi olla hyödyllistä ensin tarkistaa onko tietokoneemme nettiyhteydessä vai ei. Tarkistuksen tulee olla nopea eikä se saa kaataa sovellusta. Seuraava funktio pingaa google.com:ia.

using System.Net.NetworkInformation;

//-----------------------------------------------------------
// 1.8.2017  M45
// Tarkistetaan onko pääsy nettiin, tai tarkemmin Googlen palvelimelle
// Tämä tarkistus on nopea eikä kaada softaa
public static bool OnkoNetti()
{
    try
    {
        Ping myPing = new Ping();
        String host = "google.com";
        byte[] buffer = new byte[32];
        int timeout = 1000;
        PingOptions pingOptions = new PingOptions();
        PingReply reply = myPing.Send(host, timeout, buffer, pingOptions);
        return (reply.Status == IPStatus.Success);
    }
    catch (Exception)
    {
        return false;
    }
}

Videoleikkeet

Videoleikkeiden katsomiseen on yksinkertaisinta, varsinkin Windows Forms -ohjelmoinnissa, käyttää Windows-työaseman oletussovellusta:
// filez = videoleikkeen polku
System.Diagnostics.Process.Start(filez);


WPF-ohjelmoinnissa on kätevintä käyttää kirjaston omaa työkalua:
<MediaElement x:Name="itseVideo" LoadedBehavior="Manual"/>
Tämä ei tue flv-tiedostoja, mutta tukee laajasti muita. Lisäksi määritellään painikkeita esimerkiksi videon pysäyttämistä ja soittamista varten. Koodiin seuraavia funktioita sopiviin kohtiin:
itseVideo.Source = new Uri(failiz);   // määritellään soitettava video, failiz = täysi polku
itseVideo.Play();  // videon käynnistys
itseVideo.Pause();  // videon keskeyttäminen, Play() jatkaa siitä
itseVideo.Stop();   // videon pysäytys, Play() jatkaa alusta


 Täällä  on esitelty tekemäni käytännöllinen videoleikkeiden katseluun tarkoitettu sovellus. Sen rakenteita ja tekniikoita olen käyttänyt omissa sovelluksissani.

Videoleikkeistä tarvitaan hakutuloksiin ja metatiedon katselunäyttöihin yksittäinen kuva. Se otetaan videoleikkeen alusta kolmen sekunnin päästä. Siihen käytetään sovellusta ffmpeg. Aina voi käyttää sovelluksen 32-bittistä versiota.

Sovelluksella ffmpeg on valtava määrä vaihtoehtoisia lippuja sen toiminnan määrittämiseksi. Alla oleva komento ottaa videopätkästä 120x90 -kokoisen kuvan kolmen sekunnin päästä videon alusta. Huomaa, että otettavan kuvan koon määreiden on oltava parillisia. Sovelluksessa tämä rivi tarkistuksineen on tietysti C#-koodin sisällä.
ffmpeg.exe -y -i video.mpg -an -ss 00:00:03 -s 120x90 -vframes 1 -f mjpeg  tuloskuva.jpg

Tässä tarpeelliset koodirivit, jotka tekevän kuvan, jonka mitat on sama kuin videon:
// vidz = videotiedoston koko polku
// polz = lopputuloksena olevan kuvan koko polku
// progz == ffmpeg.exe:n koko polku
using (System.Diagnostics.Process proc = new System.Diagnostics.Process())
{
    proc.EnableRaisingEvents = false;
    StringBuilder sb = new StringBuilder("-y -i \"");
    sb.Append(vidz);
    sb.Append("\" -an -ss 00:00:03 ");
    sb.Append("-vframes 1 -f mjpeg \"");
    sb.Append(polz);
    sb.Append("\"");
    proc.StartInfo.Arguments = @sb.ToString();
    proc.StartInfo.FileName = progz;
    proc.StartInfo.UseShellExecute = false;
    proc.StartInfo.CreateNoWindow = false;    // ei tee hommaansa jos tässä on true
    proc.StartInfo.RedirectStandardOutput = true;
    proc.Start();
    proc.WaitForExit();
    proc.Close();
}

Macista haetun tiedostonimen skandit

Macissä sovellukset voivat tallentaa tiedostonimiin skandeja omintakeisella tavalla, eli ne voivat olla koottu kahdesta perättäisestä ascii-koodista. Tämä voi tulla vastaan tilanteessa, kun tiedosto on tallennettu Macin sovelluksesta uudella nimellä, joka sisältää skandeja. Taustalla on se, että sovellusten ohjelmakirjaston NSOpenPanel (failidialogi) tallentaa failin nimen skandit kahdessa osassa "a cluster of two scalars".  Jos tiedostonimiä (tiedostoja) on tarkoitus käyttää myös Windowsissa, on skandit korjattava yksiosaisiksi. Tällaista skandia osaa sekä Mac että Windows käsitellä oikein ja se tulee myös näytölle oikein.

Muussa yhteydessä Mac käsittelee skandeja Windows-yhteensopivasti.

Tarkastellaan tiedostonimiä Windowsissa:
Tämä kohde (Käenkukka326) on nyt tietokannassa väärässä muodossa eli tallennettu Macissa kantaan ilman korjausta:
dos listaus

Tämä kohde (Heinäratamo89279) on tallennettu Macissä korjattuna, tai Windowsissa:
dos listaus

Jos samaa tietokantaa käytetään sekä macissä, että Windowsissa, niin tämä skandiasia tulee ottaa huomioon joko Mac-sovelluksessa tai Windows-sovelluksessa, tai molemmissa. Itse olen huolehtinut tästä Macin puolella, ks. Ohjelmointivinkkejä (Swift)

Exif-koodit

Exif-koodit kertovat yksityiskohtaisia tietoja valokuvasta eli kameralla tehdystä kuvatiedostosta. Netistä löytyy helposti tietoja mitä exif-koodeja ylipäätään on olemassa ja miten niitä saa ulos esimerkiksi C#:lla.

Visual C#:ssa exif-koodeihin pääsee helposti käsiksi. Alla on esimerkki siitä, miten voidaan käsitellä kuvan ominaisuus "orientation" eli onko kohde kuvassa oikeassa suunnassaan. Turvallisinta on sijoittaa exif-koodien tutkiminen lausekkeiden try - catch sisään, koska GetPropertyItem() saattaa pahastua vääristä parametriarvoista ja kokeillessa se kaatoi minulta jopa Visual C#:n.

Orientation löytyy numerolla 274 eli 0x112 kuten koodista näkyy. Esimerkkejä muista numeroista:
2  PropertyTagGpsLatitude
256 ImageWidth
257 ImageLength
258 BitsPerSample
272 Model
306 DateTime
34853 GPSInfo Pointer

Jotkut suureista on lukuja, toiset merkkijonoja tai bittijonoja joissa esimerkiksi tietyt kaksi merkkiä tarkoittavat jotain.
Exif-avaimet ja tarkoitukset on selitetty mm. täällä

Alla on Windows Forms -esimerkki (C#) toimivasta Orientation-suureen käsittelystä, se on peräisin täältä.
WPF-ympäristössä vastaava löytyy esimerkkikoodista, joka on täällä.

//------------------------------------------------------------
// Windows Forms, dotnet Framework 4.6
// Tarkista kuvan tiedoista pitääkä kuvaa kääntää
public static void tarkistaAsento(Image img)
{
    if (Array.IndexOf(img.PropertyIdList, 274) > -1)
    {
        var orientation = (int)img.GetPropertyItem(274).Value[0];
        switch (orientation)
        {
            case 1:
                // No rotation required.
                // MessageBox.Show("Property Orientation: ei tarvitse kiertää", "(M13-01) exif");
                break;
            case 2:
                // MessageBox.Show("Property Orientation: flip X-suunnassa", "(M13-02) exif");
                img.RotateFlip(RotateFlipType.RotateNoneFlipX);
                break;
            case 3:
                // MessageBox.Show("Property Orientation: Rotate 180", "(M13-03) exif");
                img.RotateFlip(RotateFlipType.Rotate180FlipNone);
                break;
            case 4:
                // MessageBox.Show("Property Orientation: Rotate 180 + flip X-suunnassa", "(M13-04) exif");
                img.RotateFlip(RotateFlipType.Rotate180FlipX);
                break;
            case 5:
                // MessageBox.Show("Property Orientation: Rotate 90 + flip X-suunnassa", "(M13-05) exif");
                img.RotateFlip(RotateFlipType.Rotate90FlipX);
                break;
            case 6:
                // MessageBox.Show("Property Orientation: Rotate 90", "(M13-06) exif");
                img.RotateFlip(RotateFlipType.Rotate90FlipNone);
                break;
            case 7:
                // MessageBox.Show("Property Orientation: Rotate 270 + flip X-suunnassa", "(M13-07) exif");
                img.RotateFlip(RotateFlipType.Rotate270FlipX);
                break;
            case 8:
                // MessageBox.Show("Property Orientation: Rotate 270", "(M13-08) exif");
                img.RotateFlip(RotateFlipType.Rotate270FlipNone);
                break;
            default:
                // This EXIF data is now invalid and should be removed.
                img.RemovePropertyItem(274);
                break;
        }
    }
    else
    {
        // MessageBox.Show("Property Orientation ei löydy", "(M13-09) exif");
    }
}

Muutamia koodiesimerkkejä  C# WPF

Tietolähteen käyttäminen

Sovelluksessa voi esittää tietolähteen kahdella päätavalla: DataContext ja ItemsSource. Lisäksi datan voi kerätä koodissa luokkaan DataView, minkä käyttämisellä saadaan xaml tiiviimmäksi. Lisäksi tietolähde voidaan määritellä joko C#-koodissa tai xaml-tiedostossa. Itse olen etupäässä käyttänyt ItemsSource:a ja määritellyt tietolähteen C#-koodissa (katso esimerkkejä alla viitatuissa koodiesimerkeissä).

dgTiedot.ItemsSource = kuvataulu;  // taulukko rivit esittävistä luokista, ks. koodiesimerkit

// Tietolähde perustettu ekan kerran tai uudestaan
dgTiedot.ItemsSource = null;   // Tietolähteen tyhjennys
dgTiedot.Items.Refresh();    // Näkymän päivitys, erityisesti tarpeen jos yksittäistä itemiä muutettu
// Refresh() tarpeen myös
jos tietolähteen linkki vaihtui new...

Parempi laittaa koodiin lbHlot.ItemsSource = Hlolista;
kuin xaml:ään ItemsSource="{Binding Hlolista}"


Kuvan esittäminen ikkunassa
ja sen näyttäminen pikselitarkkana tai ikkunan kokoisena. Esimerkkikoodi on täällä.

Alasvetovalikon (comboboxin) käyttöönotto ja käyttäminen. Esimerkkikoodi on täällä. Tietolähteen käyttämisessä useimmissa tapauksissa on kätevin ItemsSource-ominaisuuden käyttäminen, niin myös tässä esimerkissä.


Kuvajoukon esittäminen matriisina ja valitun kuvan tunnistaminen jatkotehtäviä varten on myös usein perustehtäviä. Tässä on esitetty esimerkkikoodi.

Tietojen esittäminen taulukkomuodossa on monessa tapauksessa käytännöllistä. Tämän voi tehdä WPF-ympäristössä monella tavalla. Itse käytän yleisimmin Datagridiä sen monipuolisuuden ja koodauksen yksinkertaisuuden takia. Periaatteessa ListView-GridView olisi kevyempi rakenne kuin Datagrid, kun arvoja vain katsotaan ja valitaan, mutta olen todennut, että käytännössä Datagrid on selkeämpi ohjelmoida niiden asioiden osalta mitä olen tarvinnut. Datagridin sarakkeet voi lajitella näytöllä, ListView-GridView:n vain ohjelmallisesti. Täältä löytyy esimerkkikoodi.

--------------------------------
(sivua muokattu 3.3.2022)