Työaikaseuranta (WPF)

Näissä ohjeissa tuotetaan Työaikaraportille WPF-käyttöliittymä (Windows Presentation Foundation). Muista vaihtoehdoista katso Työaikaseurannan esittelysivu.

Oletuksena on, että Työaikaraportti on tehtynä, sillä sen aliohjelmia käytetään tässä.

Työaikaseuranta lukee kaksi tiedostoa (projektit.txt ja nykproj.txt) ja tuottaa kaksi tiedostoa (ajat.txt ja nykproj.txt).

Linkkejä

Tavoite ja lopputulos

Ohjelmassa voi valita pudotusvalikosta työskentelyn kohteena olevan pää- ja aliprojektin, jotka ohjelma etsii projektit.txt-tiedostosta.

Ajanoton voi käynnistää ja lopettaa työskentelyn Aloita-painikkeista. Lopeta-painike tuo esiin tallennusvalikon, jossa voi halutessaan vielä muokata aikoja ja jättää kommentin. Tallenna-painike lisää tiedot riviksi ajat.txt-tiedostoon, mikäli ajat ovat hyväksyttävässä muodossa. Ajan voi myös "unohtaa" Poista-painikkeella, tai ajanottoa jatkaa Peruuta-painikkeella.

Ohjelma osaa myös näyttää Työaikaraportin tekemän siistin raportin, joka aukeaa Tiedosto-valikosta. Auki oleva pää- ja aliprojekti tallennetaan nykproj.txt-tiedostoon, niin että ohjelmaa käynnistettäessä pudotusvalikoissa on valmiiksi valittuna viimeksi valittu projektikokonaisuus.

video ja kuva valmiista ohjelmasta.

Suunitelmakin pitää muuten tehdä, sinne ne kuvat voi piirtää kynällä ja paperilla.

10 Aug 17

Voiko se suunnitelma olla sama kaikille alustoille?

VL: ilman muuta. Suunnitelmasa ei oikeastan edes mietitä alustaa. Eikä suunitelma sido lopullists toteutusta jos jotakin huomaa että on pakko tehdä eri lailla tai joskus jopa paremmin :-)

10 Aug 17 (edited 11 Aug 17)

Valmiin ohjelman esittely.

Valmis koodi

Visual Studio ja käyttöliittymän muokkaaminen

GUI-projektin tekeminen Visual Studiolla luo kaksi tiedostoa: .xaml on käyttöliittymän ulkoasun koodi ja .xaml.cs on sen tominta. Visual Studiolla käyttöliittymää voi muokata graafisesti, jolloin muokkaukset ruudulla muuttuvat realiajassa muutoksiksi .xaml-tiedostossa (tai päinvastoin). Lisäksi Visual Studio näyttää omassa Properties-ikkunassaan kontrollien ominaisuudet (jakoavaimen kuva) ja metodit (salaman kuva), joiden muokkaaminen muuttaa myös .xaml-koodia.

Visual Studion Properties-ikkuna.
Visual Studion Properties-ikkuna.

GUIn .xaml-kielessä kontrollit kirjoitetaan kulmasulkeiden (engl. Angle brackets tai chevrons) väliin. Loppuun tulee aina kauttaviiva.

<StackPanel>
</StackPanel>

Kontrollin ominaisuudet kirjoitetaan kontrollin tyypin nimen jälkeen ensimmäisten kulmasulkeiden sisään siten, että ominaisuuden nimen jälkeen asetetaan sen arvo yhtäsuuruusmerkin oikealle puolelle lainausmerkkeihin.

<StackPanel Background="Gray">
</StackPanel>

Saman voi kirjoittaa myös yhdellä rivillä yksien sulkeiden sisään, jos jälkimmäisen kulmasulkeen eteen laittaa kauttaviivan. (Yllä- ja allaolevat koodit tuottavat identtisen tuloksen.)

<StackPanel Background="Gray"/>

Eri omaisuudet erotellaan välilyönnein toisistaan.

<StackPanel Background="Gray" Margin="10">
</StackPanel>

Kontrollin sisälle tulevat kontrollit kirjoitetaan kontrollin esittelyn ja lopetuksen väliin.

<StackPanel Background="Gray" Margin="10">
    <Button Margin="10" x:Name="buttonAloita" Content="Aloita" Click="Aloita_Click"/>
    <Button Margin="20" x:Name="buttonLopeta" Content="Lopeta" IsEnabled="False"/>
</StackPanel>

1. Aloitus

1.1 Projektin luominen

Kun suunnitelma on tehty, aloitetaan luomalla uusi projekti File/New Project Visual C#/Windows Classic Desktop/WPF App.

Kopioidaan Tyoaikaraportti.cs-tiedostostamme pääohjelmaa lukuunottamatta kaikki aliohjelmat tiedoston MainWindow.xaml.cs luokan sisään, lisätään alkuun
using System.IO; ja luokan sisälle vanhan pääohjelman polut vakioiksi uuteen pääohjelmaamme.

    public partial class MainWindow : Window
    {
        private const string projektipolku = "projektit.txt";
        private const string aikapolku = "ajat.txt";

        public MainWindow()
        {
            InitializeComponent();
        }        

        /// <summary> Koostaa aliprojekteihin, ...
        public static string Raportoi(string[] projektit, string[] ajat)
        { ...

Ajetaan tässä vaiheessa koodi, jotta näemme sen kääntyvän. Ruudulle pitäisi aueta tyhjä ikkuna.

Oikeasti otettaisiin tiedosto projektiin mukaan, ei copy/paste

10 Aug 17 (edited 10 Aug 17)

1.2 Käyttöliittymän ensimmäinen painike

Siirrytään seuraavaksi MainWindow.xaml-välilehteen. Huomaa, että Visual Studio osaa näyttää yhtäaikaisesti graafisen lopputuloksen ja .xaml-koodin.

Seuraavat muokkaukset voit tehdä joko graafisesti tai .xaml-koodia muokkaamalla.

Lisätään ikkunarivien (Window) väliin StackPanel. Jos ikkunarivien välissä on muuta ylimääräistä joka ei ole ikkunan ominaisuuksia, kaiken muun voi ottaa pois.

<Window x:Class="Tyoaikaseuranta.MainWindow"
            ...   // ikkunan ominaisuuksia
        Title="MainWindow" Height="498.63" Width="268.493">
    <StackPanel>
    </StackPanel>  
</Window>

Lisätään luomamme StackPanelin sisään taulukko Grid.

    <StackPanel>
        <Grid>
        </Grid>
    </StackPanel>

Lisätään taulukon sisään yhden rivin syntaksilla painike Button.

        <Grid>
            <Button/>
        </Grid>

Muokataan vielä painikkeen Content-ominaisuutta, eli mitä siinä lukee.

            <Button Content="Aloita"/>

Nyt ohjelman ajaessa meillä pitäisi näkyä yksi iso Aloita-painike ikkunassa.

1.3 Painikkeet gridiin

Määritellään seuraavaksi taulukkomme kolumnit lisäämällä sen sisään ColumnDefinitions-rivit.

        <Grid>
            <Grid.ColumnDefinitions>
            </Grid.ColumnDefinitions>
            <Button Content="Aloita"/>
        </Grid>

Lisätään näiden rivien sisälle kaksi yhtäsuurta kolumnia. Tähtimerkintä Width="1*" tarkoittaa tässä että kolumnien keskinäiset leveydet ovat 1:1 eli yhden suhde yhteen. Täten toinen kolumni on aina yhtä leveä kuin ensimmäinen.

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>

(Vastaavasti jos haluaisimme toisen kolumnin olevan aina tuplasti yhtä leveä, voisimme laittaa sen leveydeksi Width="2*".)

Käyttöliittymän graafiseen esitykseen (vasemmalla) päivittyy realiajassa koodin muutokset.
Käyttöliittymän graafiseen esitykseen (vasemmalla) päivittyy realiajassa koodin muutokset.

Tässä vaiheessa saatetaan huomata, että taulukon sisällä oleva Aloita-painike on jo valmiiksi oikeassa kolumnissa. Taulukon sisällä erikseen määrittelemättömät kontrollit menevät oletusarvoisesti ensimmäisen rivin ensimmäiseen kolumniin. Voimme pakottaa painikkeen haluamaamme kolumniin muokkaammalla painikkeen ominaisuutta Grid.Column.

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <Button Grid.Column="0" Content="Aloita"/>
        </Grid>

Lisätään taulukkoon myös Lopetuspainike.

            <Button Grid.Column="0" Content="Aloita"/>
            <Button Grid.Column="1" Content="Lopeta"/>

1.4 Toiminnallisuutta painikkeisiin

Nimetään ensin luomamme painikkeet, jotta koodissa voidaan viitata niihin.

            <Button x:Name="buttonAloita" Grid.Column="0" Content="Aloita"/>
            <Button x:Name="buttonLopeta" Grid.Column="1" Content="Lopeta"/>

Toiminnallisuutta voi lisätä Properties-ikkunasta tai tietenkin muokkaamalla koodia. Klikkauksen toiminnon saa Visual Studiossa lisättyä myös tuplaklikkaamalla painiketta graafisessa näkymässä. Joka tapauksessa .xaml-koodiin lisätään ominaisuus painikkeelle,

            <Button x:Name="buttonAloita" Grid.Column="0" Content="Aloita" Click="Aloita_Click"/>

ja .xaml.cs-koodiin uusi aliohjelma.

        private void buttonAloita_Click(object sender, RoutedEventArgs e)
        {

        }

Tapahtumakäsittelijän lisääminen joko tuplaklikkaamalla painiketta tai muokkaamalla sitä Properties-ikkunasta generoi automaattisesti tällaisen aliohjelman. (Vaihdetaan aliohjelman nimi kurssin nimeämiskäytänteiden mukaiseksi, muista Ctrl+R+R.)

Nyt kun olemme nimenneet painikkeet, voimme viitata niihin ikään kuin attribuutteina. Asetetaan aloituspainike pois käytöstä ja lopetuspainike käyttöön.

        private void Aloita_Click(object sender, RoutedEventArgs e)
        {
            buttonAloita.IsEnabled = false;
            buttonLopeta.IsEnabled = true;
        }

Tietenkin lopetuspainike on jo oletuksena käytössä, joten asetetaan vielä .xaml-tiedostossa se pois käytöstä.

            <Button x:Name="buttonLopeta" Grid.Column="1" Content="Lopeta" IsEnabled="False"/>

1.5 Toinen alue taulukkoon

Tehdään seuraavaksi suunnitellun ikkunan alin alue, jossa on kolmessa rivissä selitteet ja tekstikentät aloitusajalle, lopetusajalle ja kommentille, sekä neljännellä rivillä Tallennus-, Poistamis- ja Peruutuspainikkeet.

Luodaan tätä varten uusi 3x4-taulukko vanhemman taulukon alapuolelle. Kuten kolumnit luotiin ColumnDefinitions-komennolla, luodaan rivit vastaavanlaisesti RowDefinitions-komennolla.

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
                <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
        </Grid>

Lisätään em. kontrollit tämän uuden taulukon sisään.

        <Grid>
                ...
            <TextBlock Grid.Column="0" Grid.Row="0" Text="Alku"/>
            <TextBlock Grid.Column="0" Grid.Row="1" Text="Loppu"/>
            <TextBlock Grid.Column="0" Grid.Row="2" Text="Kom."/>
            <TextBox Margin="3" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="0"/>
            <TextBox Margin="3" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1"/>
            <TextBox Margin="3" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="2"/>
            <Button Margin="3" Grid.Column="0" Grid.Row="3" Content="Tallenna" />
            <Button Margin="3" Grid.Column="1" Grid.Row="3" Content="Poista" />
            <Button Margin="3" Grid.Column="2" Grid.Row="3" Content="Peruuta" />
        </Grid>

TextBoxeissa käytetty Grid.ColumnSpan="2" "venyttää" kontrollin kahden kolumnin yli.

Vaihdetaan vielä lopuksi taustan väri.

    <StackPanel Background="Gray">
        ...
Lopputulos kutakuinkin
Lopputulos kutakuinkin

Koko koodi tässä vaiheessa

Uuden projektin luominen, Työaikaraportin aliohjelmien käyttöönotto, kenttien asettelu gridiin.

(Videon aikamerkillä 4:44 video jäätyy, jonka aikana raahattiin Toolboxista Button graafiseen ikkunaan, ja 14:02 nauhoitus katkeaa Grid.ColumnSpanin teon ajaksi.)

2. Alueiden näkyvyydet

2.1 Näkyvyyksien asettaminen

Nimetään taulukot, jotta voimme viitata niihin koodista.

        <Grid x:Name="gridAloitus"  Margin="10">
            ...
        </Grid>
        <Grid x:Name="gridLopetus" Margin="10">
            ...

Ohjelmassame on tällä hetkellä kaksi aluetta. Tulevaisuutta silmällä pitäen kannattaa jo tässä vaiheessa kuitenkin tehdä alueiden näkyvyyksien vaihtamisesta sellainen, että se toimii useammallakin alueella. Tämän voisi toteuttaa aliohjelmalla, joka piilottaisi kaikki muut paitsi parametrina saamansa alueen.

Luodaan attribuutiksi uusi lista

        private List<Grid> gridit = new List<Grid>();

Tätä varten täytyy koodin alkuun lisätä using System.Windows.Controls;.

Lisätään kaikki kaksi aluetta tähän listaan pääohjelman sisällä.

        public MainWindow()
        {
            InitializeComponent();
            gridit.Add(gridAloitus); gridit.Add(gridLopetus);
        }

Tehdään Lopeta-painikkeelle myös tapahtumakäsittelijä, joka tekee päinvastaista kuin Aloita. (Ei unohdeta dokumentointia.)

        /// <summary>
        /// Lopeta-painikkeen painallus.
        /// Vaihtaa näkyvyyden tallennusnäkymäksi.
        /// </summary>
        /// <param name="sender">ei käytössä</param>
        /// <param name="e">ei käytössä</param>
        private void Lopeta_Click(object sender, RoutedEventArgs e)
        {
            buttonAloita.IsEnabled = true;
            buttonLopeta.IsEnabled = false;
        }

Kirjoitetaan tämän aliohjelman sisään uusi aliohjelmakutsu, jolle viedään parametrina näkyväksi jäävä alue.

            VaihdaNakyvyysalue(gridLopetus);

Luodaan tällainen aliohjelma (luonnollisesti dokumentointeineen). Käydään foreach-silmukalla gridit-listaa läpi.

        private void VaihdaNakyvyysalue(Grid grid)
        {
            foreach (Grid alue in gridit)
            {
            
            }
        }

Kun alue on sama kuin grid, vaihdetaan sen näkyvyys näkyväksi, ja muulloin piilotetuksi.

            foreach (Grid alue in gridit)
            {
                if (alue.Equals(grid)) alue.Visibility = Visibility.Visible;
                else alue.Visibility = Visibility.Hidden;
            }

Asetetaan vielä pääohjelmassa näkyvyysalueeksi gridAloitus, jotta gridLopetus ei näkyisi ohjelmaa käynnistettäessä.

        public MainWindow()
        {
            InitializeComponent();
            gridit.Add(gridAloitus); gridit.Add(gridLopetus);
            VaihdaNakyvyysalue(gridAloitus);
        }

Nimetään alarivin painikkeet ja lisätään niille vastaavanlaiset tapahtumakäsittelijät.

        /// <summary> Tallenna-painikkeen painallus. ...
        private void Tallenna_Click(object sender, RoutedEventArgs e)
        {
            VaihdaNakyvyysalue(gridAloitus);
        }

        /// <summary> Poista-painikkeen painallus. ...
        private void Poista_Click(object sender, RoutedEventArgs e)
        {
            VaihdaNakyvyysalue(gridAloitus);
        }

        /// <summary>  Peruuta-painikkeen painallus. ...
        private void Peruuta_Click(object sender, RoutedEventArgs e)
        {
            VaihdaNakyvyysalue(gridAloitus);
        }

2.2 Kolmas alue

Tarvitsemme vielä alueen pudotusvalikoille ja ilmoitustekstille. Järkevin vaihtoehto päällekkäisiin kontrolleihin olisi tietysti StackPanel, mutta käytetään yhteneväisyyden vuoksi Grid-kontrollia.

Lisätään kaikkia muita taulukoita ennen vielä yksi taulukko. Lisätään sen sisään kaksi pudotusvalikkoa (ComboBox) ja yksi tekstipalkki (TextBlock).

        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <ComboBox Grid.Row="0" />
            <ComboBox Grid.Row="1" />
            <TextBlock Grid.Row="2" Text="Aloita painamalla Aloita"/>
        </Grid>

Koko koodi tässä vaiheessa

Näkyvyysalueiden muuttaminen, pudotusvalikkojen luominen.

3. Pudotusvalikot

3.1 Pääprojektit pudotusvalikkoon

Lisätään aluksi projektit.txt työskentelykansioon, jotta pääsemme lukemaan tiedostoa.

Luodaan pääohjelmaan uusi aliohjelmakutsu LuoValikko() ja luodaan tynkä. Lisätään uusi attribuutti

        private string[] projektitKaikki;

ja luetaan siihen projektit.txt

        /// <summary>
        /// Lukee tiedostosta projektien nimet ja tuottaa niistä pudotusvalikon vaihtoehdot
        /// sekä asettaa viimeksi valitun projektin oletusprojektiksi
        /// </summary>
        private void LuoValikko()
        {
            projektitKaikki = File.ReadAllLines(PROJEKTIPOLKU);
        }

Käytetään nyt jo olemassaolevaa aliohjelmaamme HaePaaprojektit ja tuotetaan lista pääprojekteista. Laitetaan tämän listan sisältä comboBoxPaaprojektit-pudotusvalikon sisällöksi.

            List<string> paaprojektit = HaePaaprojektit(projektitKaikki);
            foreach (string pp in paaprojektit) comboBoxPaaprojektit.Items.Add(pp);

Nyt pudotusvalikon sisältö toimii, mutta ennen sen avaamista näkyy vain tyhjää. Voimme asettaa ensimmäisen alkion "valituksi" alkioksi.

            comboBoxPaaprojektit.SelectedItem = comboBoxPaaprojektit.Items[0];

3.2 Aliprojektit pudotusvalikkoon

Aliprojektien lisääminen on siinä mielessä vaikeampaa, että aliprojektipudotusvalikon sisältöä täytyy muuttaa joka kerta kun pääprojektia vaihdetaan.

Lisätään pääprojektipudotusvalikolle uusi tapahtumakäsittelijä, kun valintaa vaihdetaan.

            <ComboBox x:Name="comboBoxPaaprojektit" Grid.Row="0" 
                SelectionChanged="Paaprojektivalikko_SelectionChanged"/>

Tehdään tämän tapahtumakäsittelijän sisälle uusi aliohjelmakutsu.

        private void Paaprojektivalikko_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            VaihdaPaaprojektia();
        }

Luodaan tällainen VaihdaPaaprojektia-aliohjelma ja tallennetaan uuteen muuttujaan valittu pääprojekti.

        private void VaihdaPaaprojektia()
        {
            string valittuPaaprojekti = comboBoxPaaprojektit.SelectedItem.ToString();
        }

Käytetään taas valmista aliohjelmaa HaeAliprojektit ja tallennetaan valitun pääprojektin aliprojektit paikallisesti listaan ja lisätään ne vastaavasti aliprojektipudotusvalikkoon.

            List<string> valitunAliprojektit = HaeAliprojektit(projektitKaikki, valittuPaaprojekti);
            foreach (string ap in valitunAliprojektit) comboBoxAliprojektit.Items.Add(ap);
            comboBoxAliprojektit.SelectedItem = comboBoxAliprojektit.Items[0];

Mutta tässä käy huonosti! Nyt pääprojektia vaihtaessa edellisen pääprojektin aliprojektit jäävät kummittelemaan aliprojektipudotusvalikkoon.

Tyhjennetään tämän estämiseksi aliprojektipudotusvalikon alkiot ennen uusien alkioiden lisäämistä.

        private void VaihdaPaaprojektia()
        {
            string valittuPaaprojekti = comboBoxPaaprojektit.SelectedItem.ToString();
            List<string> valitunAliprojektit = HaeAliprojektit(projektitKaikki, valittuPaaprojekti);
            comboBoxAliprojektit.Items.Clear();
            foreach (string ap in valitunAliprojektit) comboBoxAliprojektit.Items.Add(ap);
            comboBoxAliprojektit.SelectedItem = comboBoxAliprojektit.Items[0];
        }

Koko koodi tässä vaiheessa

Pää- ja aliprojektipudotusvalikoiden sisällön asettaminen.

4. Toiminnallisuutta ohjelmaan

4.1 Ajanoton lisääminen

Mitä ajan ottaminen tarkoittaa? Fundamentaalisesti kulunut aika on lopetus- ja aloitusajan erotus. Meidän ei siis tarvitsi käynnistää mitään kelloa systeemin sisällä, vaan riittää että otamme ylös kaksi "nykyhetkeä" ja laskemme niiden välisen erotuksen.

Halutaan siis tallentaa "nykyhetki" DateTime-muuttujaan Aloita-napin painalluksesta.

Voisimme lisätä DateTime-attribuutin ohjelmaan, mutta koska haluamme ajan näkyviin käyttöliittymään (ainakin jossain vaiheessa), voimme tallentaa sen jo nyt sinne ja kaivaa sen sieltä myöhemmin.

Nimetään tämän vuoksi tekstikentät, jotta voimme viitata niihin koodista.

            <TextBox x:Name="textBoxAloitusaika" Margin="3" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="0"/>
            <TextBox x:Name="textBoxLopetusaika" Margin="3" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1"/>
            <TextBox x:Name="textBoxKommentti" Margin="3" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="2"/>

Nyt voimme lisätä Aloita_Click-aliohjelman sisälle nykyhetken tallennuksen textBoxAloitusaika-kentän tekstiksi.

            textBoxAloitusaika.Text = DateTime.Now.ToString();

Sama tehdään Lopeta_Click-aliohjelmalle.

            textBoxLopetusaika.Text = DateTime.Now.ToString();

4.2 Tietojen tallentaminen tiedostoon

Haluamme siis tallentaa ajat.txt-tiedostoon seuraavat tiedot:

pääprojekti|aliprojekti|aloitusaika|lopetusaika|kesto|kommentti

Pää- ja aliprojektien nimet ovat Tallenna-painiketta painaessa pudotusvalikoiden valitut objektit. Aloitus- ja lopetusajat löytyvät tekstikentistä, kuten myös mahdollinen kommentti. Meillä on siis tässä vaiheessa kestoa vaille kaikki tiedot.

Muutetaan Tallenna_Click-aliohjelmassa ajat string-muodosta takaisin DateTime-muotoon.

            DateTime aloitusaika = DateTime.Parse(textBoxAloitusaika.Text);
            DateTime lopetusaika = DateTime.Parse(textBoxLopetusaika.Text);

Tehdään näiden rivien jälkeen uusi aliohjelmakutsu tiedot tallentavalle aliohjelmalle, jolle viedään parametrina kaikki em. tiedot kestoa lukuunottamatta.

            TallennaTiedot(comboBoxPaaprojektit.SelectedItem.ToString(),
                comboBoxAliprojektit.SelectedItem.ToString(),
                aloitusaika, lopetusaika, kommentti);

Luodaan tällainen aliohjelma.

        public static void TallennaTiedot(string paaprojekti, string aliprojekti,
            DateTime aloitusaika, DateTime lopetusaika, string komentti)
        {
            
        }

Kuluneen ajan saamme laskettua vähentämällä lopetusajasta aloitusajan. Tämä onnistuu kätevästi DateTime-tyypin sisäänrakennetuilla funktioilla.

            TimeSpan kulunutAika = lopetusaika.Subtract(aloitusaika);

Ylläoleva on yleismaallinen tapa laskea kesto. Koska C#-kielessä niin funktioita kuin operaattoreitakin voi kuormittaa (engl. overload), myös
TimeSpan kulunutAika = lopetusaika - aloitusaika;
tuottaisi saman lopputuloksen.

Luodaan tallenneteksti, jossa data on halutussa muodossa tolpilla eroteltuna, lopussa rivinvaihto

            string tallenneteksti = paaprojekti + "|" + aliprojekti + "|" +
                aloitusaika.ToString() + "|" + lopetusaika.ToString() + "|" +
                kulunutAika.ToString() + "|" + komentti + Environment.NewLine;

Kirjoitetaan tämä tiedostoon vanhan tekstin jatkeeksi.

            File.AppendAllText(AIKAPOLKU, tallenneteksti);

Ilmeisesti em. komento toimii, vaikkei tiedostoa olisi vielä olemassa.

Tässä vaiheessa saatetaan huomata, että edellisen tallennuksen kommentit jäävät kummitelemaan tekstikenttään. Muokataan Tallenna_Click-aliohjelmaa vielä niin, että kommenttikenttä tyhjenee tallentaessa.

            string kommentti = textBoxKommentti.Text;
            textBoxKommentti.Clear();
            TallennaTiedot(comboBoxPaaprojektit.SelectedItem.ToString(), ... , kommentti);

Huomataan myös, että ohjelma kaatuu mikäli käyttäjä muokkaa aloitus- tai lopetusaikaa virheelliseen muotoon. Ympäröidään tämä kohta try-catch-silmukalla. (Huomaa, että muuttujien esittely täytyy tehdä silmukan ulkopuolella.)

            DateTime aloitusaika;
            DateTime lopetusaika;
            try
            {
                aloitusaika = DateTime.Parse(textBoxAloitusaika.Text);
                lopetusaika = DateTime.Parse(textBoxLopetusaika.Text);
            }
            catch (System.FormatException)
            {
                textBlockIlmoitusteksti.Text = "Anna ajat oikeassa muodossa";
                return;
            }
            VaihdaNakyvyysalue(gridAloitus);

Ilmoitustekstiin viittaamiseksi sen täytyy tietenkin olla nimetty .xaml-tiedostossa.

            <TextBlock x:Name="textBlockIlmoitusteksti" Margin="5" Grid.Row="2" 
                Text="Aloita painamalla Aloita" TextWrapping="Wrap"/>

Onkos tämä käyttäjän kannalta paha asia? Seuraavan kommentin saisi edellisestä pikkuisen korjaamalla. Paras ehkä käyttäjän kannalta olisi että kenttä ons ellainen, että kun siihen tullaan, se tekee Select all, eli voi alkaa heti kirjoittamaan ja se tuhoaa kaiken tai siirtää kursoria jolloin valinta poistuu.

14 Aug 17

4.3 Muiden painikkeiden toiminta

Peruuta-painikkeesta ajanottoa pitäisi voida jatkaa. Nyt Lopeta-painike on kuitenkin harmaana. Korjataan asia Peruuta_Click-aliohjelmaan.

            buttonAloita.IsEnabled = false;
            buttonLopeta.IsEnabled = true;

Lisätään Poista-painikkeen painalluksen yhteyteen ilmoitus tapahtuneesta muokkaamalla Poista_Click-aliohjelmaa.

            textBlockIlmoitusteksti.Text = "Aikaa ei tallennettu";

Aloita-painike voisi vaihtaa ilmoitustekstiin tiedon mm. aloitusajasta. Peruuta-painike voisi tehdä saman, joten luodaan tätä varten uusi aliohjelma, jota kutsutaan Aloita_Click- ja Peruuta_Click-aliohjelmista.

        public void NaytaAloitusaika()
        {
            textBlockIlmoitusteksti.Text = "Projektin " + comboBoxPaaprojektit.SelectedItem.ToString() +
                " parissa työskentely aloitettu " + textBoxAloitusaika.Text.ToString();
        }

Lisätään infoteksti myös Lopeta_Click-aliohjelmaan.

            textBlockIlmoitusteksti.Text = "Muokkaa halutessasi aikoja";

4.4 Tallennetun tiedon näyttäminen

Tietojen tallentuessa onnistuneesti voisimme myös näyttää ilmoitustekstinä tämän tiedon.

Miten saamme kuluneen ajan näytettyä? Kulunut aikahan laskettiin TallennaTiedot-aliohjelmassa. Tämä aliohjelma on kuitenkin staattinen, joten se ei voi sorkkia tekstikenttiä. Muokataan aliohjelmasta seuraavaksi sellainen, että se palauttaa ilmoitusviestin.

Muokataan Tallenna_Click-aliohjelmaan ilmoitustekstin näyttäminen textBlockIlmoitusteksti-kentässä TallennaTiedot-aliohjelman paluuarvona.

            textBlockIlmoitusteksti.Text =
                TallennaTiedot(comboBoxPaaprojektit.SelectedItem.ToString(), ...);

Muokataan TallennaTiedot-aliohjelma palauttamaan merkkijonon.

        public static string TallennaTiedot(string paaprojekti, ...)
        {
            TimeSpan kulunutAika = lopetusaika.Subtract(aloitusaika);
                ...
            return "Projektiin " + paaprojekti + " käytetty aika: " + kulunutAika.ToString();
        }

Koko koodi tässä vaiheessa

Ajanotto, tietojen tallentaminen, ilmoitustekstit.

5. Raportin näyttäminen

Lisätään .xaml-tiedostoon Menu ja sen sisälle MenuItem. Tästä tulee ylätaso menulle (Tiedosto)

        <Menu>
            <MenuItem Header="Tiedosto">
            </MenuItem>
        </Menu>

Luodaan alkuksi poistumiskohta, sillä jokaisen ohjelman valikossa on sellainen. Lisätään Tiedosto-valikonkohdan sisälle uusi MenuItem.

            <MenuItem Header="Tiedosto">
                <MenuItem x:Name="menuItemPoistu" Header="Poistu" Click="Poistu_Click"/>
            </MenuItem>

Asetetaan ohjelma sulkeutumaan tämän tapahtumakäsittelijästä.

        private void Poistu_Click(object sender, RoutedEventArgs e)
        {
            System.Windows.Application.Current.Shutdown();
        }

5.2 Raportti

Lisätään Tiedosto-kohdan sisään toinenkin kohta.

                <MenuItem x:Name="menuItemRaportti" Header="Näytä raportti" Click="Raportti_Click"/>

Luetaan tiedostot ja kutsutaan Raportoi-aliohjelmaa tämän tapahtumakäsittelijässä.

        private void Raportti_Click(object sender, RoutedEventArgs e)
        {
            string[] projektit = File.ReadAllLines(PROJEKTIPOLKU);
            string[] ajat = File.ReadAllLines(AIKAPOLKU);
            string raportti = Raportoi(projektit, ajat);
        }

Luodaan tässä vaiheessa uusi ikkunaluokka Project/Add Window...//Visual C#/Window (WPF). Annetaan luokan nimeksi vaikkapa RaporttiWindow.

Uuden luokan pääohjelman koodi näyttää nyt seuraavanlaiselta:

        public RaporttiWindow()
        {
            InitializeComponent();
        }

Muokataan RaporttiWindow.xaml.cs-tiedostoa niin, että se ottaa parametrina merkkijonon.

        public RaporttiWindow(string raportti)
        {
            InitializeComponent();
        }

Lisätään nyt RaporttiWindow.xaml-tiedostoon uusi tekstikenttä.

        <TextBox x:Name="textBoxRaportti"/>

Lisätään RaporttiWindow.xaml.cs-tiedoston pääohjelmaan rivi, jossa sijoitetaan parametrina saatu raportti tämän tekstikentän tekstisisällöksi.

        public RaporttiWindow(string raportti)
        {
            InitializeComponent();
            textBoxRaportti.Text = raportti;
        }

Luodaan tällainen ikkuna MainWindow.xaml.cs-tiedoston Raportti_Click-aliohjelmassa.

            RaporttiWindow raporttiIkkuna = new RaporttiWindow(raportti);

Avataan luotu ikkuna. Tämän koodin lukeminen seisahtuu tälle riville, kunnes kyseinen ikkuna suljetaan. Jottei ikkuna kuitenkaan jäisi kummittelemaan muistiin, suljetaan se seuraavalla rivillä.

            raporttiIkkuna.ShowDialog();
            raporttiIkkuna.Close();

Uusi ikkuna avautuu nyt valikosta hienosti, mutta nimet ja luvut rivittyvät rumasti. Tämä johtuu siitä ettei oletusfontti ole tasalevyistä (engl. monospaced). Tasalevyisessä fontissa kaikki merkit ovat yhtä leveitä, jolloin merkit ovat nätisti "ruudukossa". Suosittu tasalevyinen fontto on esimerkiksi Courier New. Asetetaan tämä textBoxRaportti-tekstikentän fontiksi.

        <TextBox x:Name="textBoxRaportti" FontFamily="Courier New"/>

Koko koodi tässä vaiheessa (ml. RaporttiWindow)

Menupalkki, uusi raportti-ikkuna.

6. Valitun projektin muistaminen

6.1 Nykyisten projektien tallentaminen tiedostoon

Tahdomme ohjelmasta sellaisen, että ohjelma "muistaa" mikä projektikokonaisuus on ollut viimeksi valittuna. Jos projekteja on paljon, tämä on kätevä ominaisuus, sillä ohjelman voi tällöin huoletta sulkea, ja ensi kerran avatessa voi ajastaa taas edellistä projektia nopeasti.

Tehdään tätä varten uusi tiedosto, johon tallennetaan pää- ja aliprojekti. Luodaan pääluokan attribuutiksi tämän tiedoston polku.

        private const string NYKPROJPOLKU = "nykproj.txt";

Mihin kohtaan koodia tämän tiedoston muokkaaminen pitäisi lisätä? Meidän ei välttämättä ole tarpeen muokata nykyisen pää- ja aliprojektin tietoja erikseen, sillä ne kulkevat aina yhtenä kokonaisuutena. Koska pääprojektin vaihtaminen vaihtaa nykyisellään aina myös aliprojektin, voimme suorittaa tiedostoon tallennuksen aliprojektia vaihtaessa.

Lisätään aliprojektivalikolle uusi tapahtumakäsittelijä.

            <ComboBox x:Name="comboBoxAliprojektit" Grid.Row="1" 
                SelectionChanged="Aliprojektivalikko_SelectionChanged" />

Pidetään pääprojektivalikon tapahtumakäsittelijän tavoin tämä tapahtumakäsittelijä "puhtaana" ja kutsutaan sieltä omaa aliohjelmaa.

        private void Aliprojektivalikko_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            VaihdaAliprojektia();
        }

Luodaan tällainen aliohjelma, jossa tallennetaan merkkijonoon valitutProjektit pudotusvalikoiden valitut projektit rivinvaihdolla erotettuna.

        private void VaihdaAliprojektia()
        {
            string valitutProjektit = comboBoxPaaprojektit.SelectedItem.ToString() +
                Environment.NewLine + comboBoxAliprojektit.SelectedItem.ToString();
        }

Tämä kaataa ohjelman, koska jossain vaiheessa koodia saavumme tänne kun comboBoxAliprojektit.SelectedItem on null. Tällöin ToString-metodia ei voi suorittaa. Lisätään aliohjelman alkuun rivi, joka poistuu aliohjelmasta tällaisessa tapauksessa.

            if (comboBoxAliprojektit.SelectedItem == null) return;

Kirjoitetaan lopuksi valitutProjektit tiedostoon. File.WriteAllText korvaa surutta kaiken aiemman tiedostossa olevan.

            File.WriteAllText(NYKPROJPOLKU, valitutProjektit);

6.2 Tallennettujen projektien lukeminen

Seuraavaksi vaihdetaan ohjelman käynnistyessä pudotusvalikoiden valitut projektit tallennettuihin.

Luetaan luotu tiedosto ohjelman käynnistyksen yhteydessä suoritettavassa LuoValikko-aliohjelmassa.

            string[] valitutProjektit = File.ReadAllLines(NYKPROJPOLKU);

Tässäkin tapauksessa kannattaa ennen tiedoston lukemisen yrittämistä varautua tiedoston puuttumiseen.

            if (!File.Exists(NYKPROJPOLKU)) return;

Pääprojektimme on siis tallennettu valitutProjektit-taulukon ensimmäiseen paikkaan ja aliprojekti toiseen. Voimme nyt sijoittaa nämä pudotusvalikoihin.

            comboBoxPaaprojektit.SelectedItem = valitutProjektit[0];
            comboBoxAliprojektit.SelectedItem = valitutProjektit[1];

Mutta tämä ei toimi. Nykyisellään pää- ja aliprojekteiksi tulee edelleen ensimmäiset pää- ja aliprojektit, vaikka tiedostoon tallentuukin vaihdetut projektit.

Miten ongelmaa lähteä ratkaisemaan? Videolla laitetaan keskeytyskohta valitutProjektit-tiedoston lukemiseen ja huomataan, että tiedosto on muuttunut tallentamisen jälkeen. Tämä tarkoittaa sitä, että jossakin aiemmassa vaiheessa koodia tiedostoa on sorkittu. Jos muu ei auta, lähde etsimään riviä askel kerrallaan F11 debuggaamalla.

Ongelmakohtaa löytyy tässä tapauksessa pari riviä ylempää, missä paaprojektit-listan ensimmäinen alkio asetetaan pääprojektipudotusvalikon valituksi projektiksi. Poistetaan tämä rivi.

        private void LuoValikko()
        {
            projektitKaikki = File.ReadAllLines(PROJEKTIPOLKU);

            List<string> paaprojektit = HaePaaprojektit(projektitKaikki);
            foreach (string pp in paaprojektit) comboBoxPaaprojektit.Items.Add(pp);
             // POISTA // comboBoxPaaprojektit.SelectedItem = paaprojektit[0];    
            
            if (!File.Exists(NYKPROJPOLKU)) return;
            string[] valitutProjektit = File.ReadAllLines(NYKPROJPOLKU);

            comboBoxPaaprojektit.SelectedItem = valitutProjektit[0];
            comboBoxAliprojektit.SelectedItem = valitutProjektit[1];
        }

Koko koodi tässä vaiheessa

Valittujen projektien tallentaminen tiedostoon.

7. Viimeistely

7.1 Ulkoasullista hienosäätöä

Ohjelmamme avautuvan ikkunan ylälaidassa lukee tällä hetkellä "MainWindow" ja raportissa "RaporttiWindow". Voimme korjata tämän muokkaamalla Window-kontrollin (kontrollihierkarkian korkein kontrolli) Title-ominaisuutta MainWindow.xaml-tiedostossa.

<Window x:Class="Tyoaikaseuranta.MainWindow" ... Title="Työaikaseuranta">

Tehdään sama myös raportti-ikkunalle RaporttiWindow.xaml-tiedostossa.

<Window x:Class="Tyoaikaseuranta.RaporttiWindow" ... Title="Raportti">

Tällä hetkellä pääikkunamme on myös oudon korkuinen. Voimme asettaa korkeuden muokkaamalla ikkunan SizeToContent-ominaisuudeksi korkeus, jolloin ikkunan korkeus skaalautuu automaattisesti sisällön viemän tilan mukaan.

<Window x:Class="Tyoaikaseuranta.MainWindow" ... Title="Työaikaseuranta" SizeToContent="Height">

Ikkunan alalaidassa on kuitenkin vielä tyhjää tilaa. Tämä tyhjä tila on "varattu" gridLopetus-alueelle, sillä sen Visibility on tällä hetkellä Hidden, jolloin kontrolleille varataan ikkunassa niiden viemä tila kuitenkaan näyttämättä niitä käyttäjälle.

Voimme muokata VaihdaNakyvyysalue-aliohjelman alueenpiilotuskohtaa siten, että piilotettavan alueen näkyvyydeksi asetetaan Collapsed, jolloin sille ei varata tilaa.

        private void VaihdaNakyvyysalue(Grid grid)
        {
            foreach (Grid alue in gridit)
            {
                if (alue.Equals(grid)) alue.Visibility = Visibility.Visible;
                else alue.Visibility = Visibility.Collapsed;  // alunp. .Hidden
            }
        }

7.2 Reunuksista

Asetetaan lopuksi vielä reunukset (Margin) hyvännäköisiksi. Tämä on tietenkin mielipidekysymys, joten jokainen voi itse valita miten reunukset säätää. Pääsääntöisesti reunusten kannattaa olla kuitenkin mahdollisimman yhtenevät. Alueiden reunukset sivureunat säädetään videolla samoiksi, ja yksittäistä kontrolleista muokataan keskinäiset etäisyydet sopiviksi reunuksilla. Tämä ei ole ainoa tai välttämättä edes paras tapa.

Margin-ominaisuudelle voi antaa eri määrän parametreja riippuen käyttätavasta.

Neljällä parametrilla reunukset kiertävät kellonsuuntaisesti vasemmasta reunasta alkaen.

        <Grid Margin="1,2,3,4">

Margin vasemmalla 1, ylhäällä 2, oikealla 3 ja alhaalla 4.

Kahdella parametrilla ensimmäinen on molemmat sivureunat ja toinen ylä- ja alareunus.

        <Grid Margin="1,2">

Reunukset vasemmalla ja oikealla 1, ylhäällä ja alhaalla 2.

Yhdellä parametrilla reunus on joka puolella sama.

        <Grid Margin="1">

Margin vasemmalla, ylhäällä, oikealla ja alhaalla 1.

Valmis koodi

Ikkunan nimen vaihtaminen, näkyvyyksien parannus, reunusten hienosäätö.

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.