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ä
- Työaikaraportin suunnitelmasivu
- Työaikaraportin tutoriaalisivu
- Tämän ohjelman (Työaikaseuranta) suunnitelmasivu
- Työaikaseurannan esittelysivu (täällä vaihtoehdot WPF:lle)
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 ajat.txt
-tiedostoon, mikäli ajat ovat hyväksyttävässä muodossa. Ajan voi myös "unohtaa" -painikkeella, tai ajanottoa jatkaa -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.
—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 :-)
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.
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
—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 StackPanel
in 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
-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*"
.)
Tässä vaiheessa saatetaan huomata, että taulukon sisällä oleva 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 + + .)
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>
TextBox
eissa käytetty Grid.ColumnSpan="2"
"venyttää" kontrollin kahden kolumnin yli.
Vaihdetaan vielä lopuksi taustan väri.
<StackPanel Background="Gray">
...
(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
-painikkeelle myös tapahtumakäsittelijä, joka tekee päinvastaista kuin . (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>
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];
}
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 -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
-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.
—4.3 Muiden painikkeiden toiminta
Peruuta_Click
-aliohjelmaan.
buttonAloita.IsEnabled = false;
buttonLopeta.IsEnabled = true;
Lisätään Poista_Click
-aliohjelmaa.
textBlockIlmoitusteksti.Text = "Aikaa ei tallennettu";
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();
}
5. Raportin näyttäminen
5.1 Menupalkki
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"/>
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 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];
}
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.
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.