Sisällys
Tehtävät:
Kuvat:
Malliohjelmat:
Tämä moniste on kirjoitettu alun perin
"Graafisen käyttöliittymien ohjelmointi"
-kurssille syksyllä 1996. Monisteen alkuperäinen tarkoitus on olla
pikakurssi siirtymiseksi C++:sta
Delphi-ohjelmointiin.
Delphi on
Borland Internationalin kehittämä
Visual Basicin tapainen
“visuaalinen” sovelluskehitin. Suurimpana erona
Visual Basiciin on käytetty
ohjelmointikieli: Borland Object Pascal.
Borland nousi aikanaan pinnalle nimenomaan
Turbo Pascal
-kääntäjänsä avulla.
Delphi on Turbo
Pascalista edelleen kehitetty tuote. Erityisen hyvä
Delphi on tietokantoihin liittyvässä
ohjelmoinnissa. Myös yleisenä ohjelmankehitysvälineenä se
on erinomainen. Suurin puute on välineen toimiminen vain yhdessä
laiteympäristössä.
Tätä monistetta ei suinkaan ole
yritettykään tehdä täydelliseksi
Delphi-oppaaksi, vaan ainoastaan
lähtökohdaksi. Suomenkielisenä oppaana voin ehdottomasti
suositella Ari Becksin
Delphi-kirjaa.
Monisteen malliohjelmat on saatavissa
sähköisesti:
Mikroluokka:
hakemisto: N:\KURSSIT\WINOHJ\DELPHI
WWW: URL: http://www.mit.jyu.fi/vesal/kurssit/winohj/delphi
Edellä mainittuun polkuun lisätään aina
monisteessa mainittu polku.
Tässä monisteessa ei tulla puuttumaan
olio-ohjelmoinnin saloihin, vaan tämä tietous pitää hakea
muista oppaista.
Palokassa 4.8.1996 Vesa
Lappalainen
Monisteen uuteen painokseen on korjattu joitakin pieniä
painovirheitä ja vaihdettu muutama komponenttiesimerkki vähän
paremmaksi. Edelleen vikana on mm. WinAapisen
”alkukantainen” ohjelmointityyli. Ohjelma kannattaisi muuttaa
huomattavasti enemmän komponenttipohjaiseksi nykyisestä
proseduraalisesta muodosta.
Palokassa 15.9.1997 Vesa
Lappalainen
Monisteen vuoden 2007 painoksesta on vähennetty
listauksia, koska ne löytyvät kätevämmin netistä.
Lisäksi muutamia lukuja (mm. komponenttien kirjoittaminen) on uudistettu.
Vuosien 1997-2007 välillä monisteen korvasi kirja
”Delphi
4 peruskurssi”. Kirja on kuitenkin loppuunmyyty, joten palataan
taas monisteeseen.
Palokassa 17.8.2007 Vesa
Lappalainen
1. Malliohjelma
Luvun pääaiheet:
- yleistä
Delphistä
- 1.
malliohjelma: autolaskuri - ohjelma joka sisältää graafisen
käyttöliittymän peruskomponentit
1.1 Yleistä
Delphistä
Delphi
on olio-pohjainen visuaalinen sovelluskehitin. Ohjelman kehittäminen on
pitkälle erilaisten komponenttien sijoittelua lomakkeille ja sitten
komponentteihin liittyvien tapahtumien kirjoittamista.
Kukin komponentti, kuten esimerkiksi nappula, menu jne., on
itsessään olio.
Kun komponentteja laitetaan
lomakkeelle saadaan uusi olio. Komponenttioliolla on ominaisuuksia,
attribuutteja, kuten esimerkiksi komponentin sijainti lomakkeella
(
Left, Top) komponentin koko
(
Width, Height) jne. Osaa komponenttien ominaisuuksista
voidaan muuttaa jo suunnitteluaikana ja osaa vasta ajon aikana.
Kuva
1.1
Delphi,
esimerkkiohjelman suunnittelu
1.2 Autolaskuri
Ensimmäisenä
malliohjelmana teemme “autolaskurin”, jossa on kaksi nappia, joita
painamalla voi lisätä joko henkilöautojen tai kuorma-autojen
lukumäärää.
- Käynnistä
Delphi.
- Vaihda
lomakkeen nimeksi (Object
Inspectorissa
name-ominaisuus)
Autolaskuri.
1.2.1 Nappuloiden
lisääminen
- Valitse
Standard-työkaluvalikosta nappula
(Button, ): klikkaa kuvaketta kerran
- Lisää
nappula lomakkeeseen: vie hiiri sinne minne haluat nappulan vasemman
yläkulman ja paina vasen nappi alas. Rahaa oikea alakulma paikalleen ja
päästä hiiren nappi ylös. “Kahvoihin”
tarttumalla voit vielä vaihtaa nappulan kokoa.
- Anna
nappulalle nimeksi ButtonHA.
- Vaihda
nappulan tekstiksi
(caption-ominaisuus)
&Henkilöautoja. Tässä
&-merkki tarkoittaa sitä, että seuraava kirjain tulee
alleviivatuksi ja sama toiminto voidaan suorittaa painamalla
Alt-H tai joissakin
tapauksissa jopa pelkästään H
(jollei H muuten voi merkitä lomakkeella mitään).
- Monista
nappula: valitse nappula ja paina Ctrl-Ins. Paina
Shift-Ins ja raahaa uusi nappula oikealle
kohdalleen.
- Nimeä
uusi nappula ButtonKA ja vaihda tekstiksi
&Kuorma-autoja.
1.2.2 Laskuriruutujen
lisääminen
- Lisää
lomakkeelle “laskuriruutu” nappulan alapuolelle käyttäen
vakiotekstiä (Label, ).
- Laita
tekstin AutoSize-ominaisuus
epätodeksi: Tuplaklikkaa
True -sanaa. Näin laskuriruutu saadaan
pysymään aina samankokoisena. Joissakin tapauksissa on toisaalta
mukavaa että ruutu muuttuu tekstin koon mukaan.
- Kun
olet lisännyt tekstin, saat sen saman levyiseksi kuin nappulankin
valitsemalla molemmat aktiiviseksi (shift+klikkaus
vasemmalla kumpaankin) ja sitten hiiren oikeasta näppäimestä
aukeavasta menusta Size ja ruksi esim. ruutuun
Grow to
largest leveyden
(Width) kohdalla.
- Nimeä
teksti LabelHA ja tekstiksi
(caption) 0.
- Laitetaan
teksti ruudun oikeaan reunaan:
Alignment-ominaisuus:
taRightJustify
- Vaihda
tekstin väriksi vaikkapa
clAqua.
- Vaihda
tekstin fontiksi vaikkapa Arial Bold
18: tuplaklikkaa
(Tfont)-tekstiä.
- Muuta
tekstiruudun korkeus oikeaksi.
- Monista
tekstiruutu ja siirrä uusi ruutu Kuorma-autoja
-nappulan alle.
- Nimeä
uusi tekstiruutu:
LabelKA.
1.2.3 Talletus
- Lisää
vielä nappula, jonka nimeksi annat ButtonNollaa ja
tekstiksi &Nollaa.
- Talleta
projekti: Alt-F v. Kysymykseen
Save Unit As vastataan esim.
autolask (tämä tulee sen tiedoston
nimeksi, jossa itse ohjelmakoodi on).
Save Project As vastataan
esim.
autol
(projekti tallentuu nyt nimellä
autol.dpr ja itse käännetystä
ohjelmasta tulee
autol.exe).
1.2.4 Kääntäminen
ja
ajaminen
Ohjelma on nyt toimintakuntoinen, mutta se ei vielä tee
mitään. Voimme kuitenkin kokeilla miltä valmis ohjelma
näyttäisi:
- Käännä
ja aja ohjelma: F9
1.2.5 Ohjelmakoodin
lisääminen
Kun olemme laittaneet komponentteja lomakkeelle,
Delphi on lisännyt koko ajan tiedostoon
autolask.pas ohjelmakoodia lomakkeen
Autolaskuri-olion
määrittelevään
TAutolaskuri-luokkaan
(
class).
Nappuloiden toiminnallisuutta vastaavan koodin
lisääminen on ohjelmoijan tehtävä. Onneksi
Delphi tekee tästäkin suurimman
osan:
- Tuplaklikkaa
Henkilöautoja-nappulaa. Nyt aukeaa
koodi-ikkuna, jossa on valmiina
Pascal-kielinen
tapahtumankäsittelijän
esittely tapahtumalle, joka tulee kun painetaan nappulaa nimeltä
ButtonHA:
procedure TAutolaskuri.ButtonHAClick(Sender: TObject);
begin
_
end;
- Kursori
on valmiina paikassa, johon oma koodi kirjoitetaan. Me haluamme että
nappulaa painettaessa LabelHA:ssa oleva lukema
lisääntyy yhdellä. Tämä voitaisiin kirjoittaa:
LabelHA.Caption := LabelHA.Caption + 1;
mutta valitettavasti LabelHA.Caption on tekstiä
eikä sitä voi numeerisesti lisätä (kuten
Visual Basicin
Variant-tyyppiä
voi). Siispä kirjoitamme
koodin:
LabelHA.Caption := IntToStr(StrToInt(LabelHA.Caption)+1);
- Koodi
kannattaa saman tien laittaa leikekirjaan, koska sehän tulee lähes
samanlaisena nappulaan ButtonKA.
- Lisää
vastaava koodi oikein muutettuna nappulaan ButtonKA.
Huom! Jos et edellä huomannut laittaa koodia leikekirjaan, löytyy
edellinen koodi samasta koodi-ikkunasta hieman ylempää ja voit hakea
sen kuin missä tahansa editorissa.
- Lisää
vielä koodi nappulaan ButtonNollaa. Nyt koodiksi
riittää
LabelHA.Caption := '0';
LabelKA.Caption := '0';
- Käännä
ja aja ohjelma.
1.2.6 Valmis
ohjelma
Seuraavana vielä täydelliset listaukset valmiin
malliohjelman eri tiedostoista (hakemistossa
autol). Itse kirjoitetut tai muutetut osat on
varjostettu:
autol.dpr
- projetitiedosto
program Autol;
uses
Forms,
Autolask in 'AUTOLASK.PAS' {Autolaskuri};
{$R *.RES}
begin
Application.CreateForm(TAutolaskuri, Autolaskuri);
Application.Run;
end.
autolask.pas
- autolaskuri-lomakeluokan määrittely ja toteutus
unit Autolask;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
type
TAutolaskuri = class(TForm)
ButtonHA: TButton;
ButtonKA: TButton;
LabelHA: TLabel;
LabelKA: TLabel;
ButtonNollaa: TButton;
procedure ButtonHAClick(Sender: TObject);
procedure ButtonNollaaClick(Sender: TObject);
procedure ButtonKAClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Autolaskuri: TAutolaskuri;
implementation
{$R *.DFM}
procedure TAutolaskuri.ButtonHAClick(Sender: TObject);
begin
LabelHA.Caption := IntToStr(StrToInt(LabelHA.Caption)+1);
end;
procedure TAutolaskuri.ButtonKAClick(Sender: TObject);
begin
LabelKA.Caption := IntToStr(StrToInt(LabelKA.Caption)+1);
end;
procedure TAutolaskuri.ButtonNollaaClick(Sender: TObject);
begin
LabelHA.Caption := '0';
LabelKA.Caption := '0';
end;
end.
Seuraavassa
lomakkeen listauksessa tummennetut osat ovat niitä, joita on muutettu
Object Inspectorissa. Luonnollisesti kunkin
komponentin paikkaa ja kokoa on muutettu oletuksesta, mutta tämä on
tehty siirtämällä komponenttia hiirellä.
autolask.dfm
- autolaskuri-lomake, komponenttien ominaisuudet
object Autolaskuri: TAutolaskuri
Left = 190
Top = 90
Width = 435
Height = 300
Caption = 'Autolaskuri'
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'System'
Font.Style = []
PixelsPerInch = 96
TextHeight = 16
object LabelHA: TLabel
Left = 40
Top = 104
Width = 145
Height = 29
Alignment = taRightJustify
AutoSize = False
Caption = '0'
Color = clAqua
Font.Color = clBlack
Font.Height = -24
Font.Name = 'Arial'
Font.Style = [fsBold]
ParentColor = False
ParentFont = False
end
object LabelKA: TLabel
Left = 208
Top = 104
Width = 145
Height = 29
Alignment = taRightJustify
AutoSize = False
Caption = '0'
Color = clAqua
Font.Color = clBlack
Font.Height = -24
Font.Name = 'Arial'
Font.Style = [fsBold]
ParentColor = False
ParentFont = False
end
object ButtonHA: TButton
Left = 40
Top = 32
Width = 145
Height = 49
Caption = '&Henkilöautoja'
TabOrder = 0
OnClick = ButtonHAClick
end
object ButtonKA: TButton
Left = 208
Top = 32
Width = 145
Height = 49
Caption = '&Kuorma-autoja'
TabOrder = 1
OnClick = ButtonKAClick
end
object ButtonNollaa: TButton
Left = 96
Top = 168
Width = 193
Height = 57
Caption = '&Nollaa'
TabOrder = 2
OnClick = ButtonNollaaClick
end
end
Lisää ohjelmaan myös polkupyörien
laskeminen.
1.3 Näkymättömät
komponentit
Edellisessä
esimerkissä lisättiin vain näkyviä komponentteja.
Delphissä on myös suuri joukko
näkymättömiä komponentteja.
1.3.1 Timer
- ajastetut tapahtumat
Lisätään
vaikkapa aluksi auton kuva, joka ajaa ruudun vasemmasta laidasta oikeaan
laitaan.
- Lisää
Image-komponentti
ruudun vasempaan alalaitaan (löytyy additional
sivulta).
- Valitse
Image-komponentin
Picture-ominaisuus ja lataa siihen vaikkapa kuvaksi
hauto.bmp.
(n:\kurssit\winohj\delphi\autol\hauto.bmp).
- nyt
kannattaa laittaa AutoSize-ominaisuus
päälle
- vaihda
komponentin nimeksi vaikkapa
ImageHA.
Jotta
auton kuva liikkuisi, pitäisi
ImageHA-olion paikkaa
muuttaa tietyn välein. Tietyn aikavälein tapahtuvia tapahtumia
saadaan
Timer-komponentilta.
- Lisää
lomakkeelle mihin tahansa kohtaan Timer-komponentti
(löytyy system sivulta). Paikalla ei ole
väliä, koska komponentti EI ole näkyvissä ohjelman ajon
aikana.
- vaihda
nimeksi vaikkapa TimerHA
- vaihda
tapahtumaväliksi esim. 100 (=100 ms).
- tuplaklikkaa
ajastinta ja lisää koodi-ikkunaan
koodi:
procedure TAutolaskuri.Timer1Timer(Sender: TObject);
begin
ImageHA.Left := ImageHA.Left + 1;
end;
Muuta ohjelmaa siten, että auton kuva kulkee
ruudussa edestakaisin.
1.3.2 Menut
Lisätään ohjelmaan vielä
päämenu.
- Lisää
lomakkeelle MainMenu-komponentti
(standard
-sivu). Paikalla ei jälleenkään ole väliä, sillä
menu tulee aina lomakkeen yläreunaan.
- tuplaklikkaa
Menu-oliota
- Kirjoita
seuraava
menusysteemi:
&File &Options H&elp
E&xit &Colors &About
- Laita
vielä lisäksi Exit-kohtaan pikavalinta
Ctrl-X.
- tuplaklikkaa
Exit-valintaa ja lisää tapahtumaksi
koodi
procedure TAutolaskuri.Exit1Click(Sender: TObject);
begin
Close;
end;
Miksi laitoimme menuun
H&elp
eikä
&Help?
Mikä tässäkin valinnassa on huonoa?
1.3.3 Valmiit
lomakkeet
Delphissä on valmiina
joukko yleisimpiä dialogeja
: tiedoston avaus ja
talletus, fonttien valinta, värin valinta, tulostaminen sekä
etsintä ja korvaus.
Lisäämme seuraavaksi mahdollisuuden
taustavärin vaihtamiseksi.
- Lisää
lomakkeelle väridialogi
(dialogs-sivu).
Paikalla ei ole väliä.
- Laita
dialogin nimeksi vaikkapa ColorDialogTausta.
- Lisää
menun Colors-kohdan tapahtumaksi
koodi:
procedure TAutolaskuri.Colors1Click(Sender: TObject);
begin
ColorDialogTausta.Color := Color; // tai := self.Color
if ( not ColorDialogTausta.Execute ) then exit;
Color := ColorDialogTausta.Color;
end;
Tehtävä
1.4 Muidenkin komponenttien värin vaihto
Muuta ohjelmaa siten, että voit muuttaa kaikkien
muidenkin komponenttien värin (voit käyttää samaa dialogia
kaikille komponenteille).
1.3.4 Omat
dialogit
Oikeassa
ohjelmassa on harvoin vain yksi ikkuna. Lisäämme esimerkin vuoksi
vielä ohjelmaamme itse tehdyn
About-dialogin:
- Luo
uusi lomake (File|New form| Blank form).
- Vaihda
lomakkeen nimeksi FormAbout ja otsikoksi
Tietoja autolaskurista.
- Lisää
vakioteksti (Label) jonka nimeksi vaikkapa
LabelAbout ja WordWrap -ominaisuus
todeksi. Tekstiksi laitetaan sitten mikä tahansa ohjelman toimintaa yms.
kuvaava teksti.
- Lisää
vielä haluamiasi koristeita, kuten esim. bittikarttoja (vrt. liikkuvan
auton lisääminen).
- Lisää
vielä nappula, jonka nimeksi ButtonOK ja tekstiksi
OK sekä Default- ominaisuus todeksi.
- Lisää
OK-nappulan
koodiksi:
procedure TFormAbout.ButtonOKClick(Sender: TObject);
begin
Close;
end;
Lomake on nyt valmis, mutta siihen ei viitata varsinaisesta
lomakkeesta.
- Talleta
About-lomakkeen tiedosto nimelle
about.pas
- Lisää
varsinaisen ohjelman menunvalintaa About seuraava
koodi:
procedure TAutolaskuri.About1Click(Sender: TObject);
begin
FormAbout.Show;
end;
- Kokeile
ajaa ohjelmaa. Todennäköisesti saat
virheilmoituksen:
Error 3: Unknown identifier
- Kursori
on sanan FormAbout alussa. Tämä johtuu
siitä, ettei Autolaskuri-lomakkeen toteutuksessa ole kerrottu
mitään About-lomakkeesta. Korjataan vielä
tämä vika.
- Siirry
autolask.pas -tiedostossa aivan alkuun.
Sieltä löytyy uses-lause. Lisää
tämän lauseen loppuun tieto siitä että
käytetään myös
About-lomaketta.
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ExtCtrls, Menus, About;
- Kokeile
nyt
ohjelmaa.
Tehtävä
1.5 Modaalinen dialogi
Muuta rivi
FormAbout.Show;
muotoon FormAbout.ShowModal; Mitä eroa
on nyt ohjelman toiminnassa?
Tehtävä
1.6 Liikkuva auto myös toisessa dialogissa
Lisää liikkuva auto myös
About-dialogiin.
1.4 Ohjelmakoodin
korjailu
Ohjelmakoodi
on aivan tavallista tekstiä ja sitä voidaan muokata kuten millä
tahansa tekstieditorilla. Seuraavat seikat on kuitenkin syytä
pitää mielessä:
- Muuta
komponenttien nimiä vain Object
Inspectorissa. Muuten lomake ja ohjelmakoodi eivät pysy
synkronissa.
- Jos
haluat hävittää jonkin tapahtuman käsittelijän, poista
begin-end -parin välissä oleva koodi.
Delphi hävittää sitten loput
seuraavan talletuksen tai käännöksen yhteydessä. Kukin
metodi on nimittäin esitelty sekä luokan määrittelyssä,
että itse koodissa.
1.5 Muut
tapahtumat
1.5.1 Saman
tapahtuman käyttö toisessa komponentissa
Olemme voineet kirjoittaa tapahtuman käsittelijän
koodin tuplaklikkaamalla komponenttia. Näin voimme kirjoittaa kuitenkin
vain komponentin oletustapahtuman käsittelijän. Kullakin
komponentilla on lukuisia muitakin tapahtumia. Nämä muut tapahtumat
löytyvä
Object Inspectorissa
Events-sivulta. Seuraavassa esimerkki
ButtonHa:n mahdollisista
tapahtumista:
Tapahtuma päästään kirjoittamaan
tuplaklikkaamalla tapahtuman nimeä (nimen paikkaa jos nimi on tyhjä).
Tapahtuma voidaan laittaa myös samaksi jonkin toisen tapahtuman kanssa
jolla on sama parametrilista.
Tehtävä
1.7 Laskenta
tapahtumaan myös laskurista
Muuta ohjelmaa (kirjoittamatta lisää koodia)
siten, että myös laskurikentän
LabelHA
tai
LabelKA
painaminen lisää vastaavaa laskuria. Laita vielä liikkuvan kuvan
painaminen lisäämään henkilöautojen
lukumäärää.
1.5.2 Vedä
ja pudota (drag and
drop,
DaD)
“Nykyaikaisessa”
suorakäyttöliittymässä olion vetäminen ja pudottaminen
on muotia. Lisätään omaan ohjelmaamme vielä ominaisuus,
jossa käyttäjä voi “tarttua” henkilöauton kuvaan
ja pudottaa sitten sen jomman kumman laskuri-ikkunan päälle.
Tällöin vastaava laskuri lisääntyy vaikkapa
10:llä.
- Muuta
henkilöauton kuvan DragMode -ominaisuus arvoon
dmAutomatic.
- Kokeile
ajaa ohjelmaa. Nyt voi tarttua henkilöauton kuvaan, muttei sitä voi
vielä pudottaa mihinkään.
- Lisää
LabelHA:han tapahtuma
(DragOver),
jolla hyväksytään siihen
pudottaminen:
procedure TAutolaskuri.LabelHADragOver(Sender, Source: TObject; X,
Y: Integer; State: TDragState; var Accept: Boolean);
begin
Accept := True;
end;
- Kokeile
ajaa ohjelmaa. Nyt pudottaminen on sallittua LabelHA:n
päälle, muttei LabelKA:n päälle.
- Laita
sama tapahtuma LabelKA:n DragOver
tapahtumaksi (kirjoittamatta koodia).
- Seuraavan
vaiheen helpottamiseksi kirjoitetaan aliohjelma, jonka avulla
tekstikenttää voidaan lisätä yhdellä. Samalla vanhat
laskut voidaan muuttaa käyttämään tätä
aliohjelmaa. Koodi kirjoitetaan vaikkapa ennen kaikkia tapahtuman
käsittelijöitä:
procedure lisaa(lab:TLabel; n:integer);
begin
lab.Caption := IntToStr(StrToInt(lab.Caption)+n);
end;
procedure TAutolaskuri.ButtonHAClick(Sender: TObject);
begin
lisaa(LabelHA,1);
end;
- Lisää
käsittelijä LabelHA:n pudotuksen
vastaanottamiselle
(DragDrop):
procedure TAutolaskuri.LabelHADragDrop(Sender, Source: TObject; X,
Y: Integer);
begin
lisaa(Sender as TLabel,10);
end;
- Laita
sama tapahtuma LabelKA:n DragDrop
tapahtumaksi (kirjoittamatta koodia).
- Jos
laskurikentät tulevat vielä hyväksymään muistakin
komponenteista tulevia pudotuksia, voidaan em. koodi
modifioida:
procedure TAutolaskuri.LabelHADragDrop(Sender, Source: TObject; X,
Y: Integer);
begin
if ( Source = ImageHA ) then lisaa(Sender as TLabel,10);
end;
Tehtävä
1.8 Muitakin lisäysmääriä
Lisää ohjelmaan joukko numeroikkunoita (esim.
1,2,3,4,5), joihin kuhunkin voidaan tarttua ja vetää sitten
laskuri-ikkunan päälle. Kun numero pudotetaan, lisääntyy
laskuri-ikkunan arvoa vastaavalla numerolla (itse kirjoitettua ohjelmakoodia n.
1 rivi edelliseen lisää).
Tehtävä
1.9 Liikkuvan kuvan siirto toiseen paikkaan
Lisää ohjelmaan ominaisuus: jos tartut
henkilöauton kuvaan ja pudotat sen lomakkeen päälle, niin auto
siirtyy tähän kohtaan lomaketta (ohjelmakoodia korkeintaan 4 itse
kirjoitettua riviä).
Tehtävä
1.10 Fontin ja värin vaihto
Lisää ohjelmaan
PopupMenu
kullekin nappulalle ja laskurille, jotta voit vaihtaa nappuloiden ja laskureiden
fonttia ja laskureiden väriä. (Olennaisesti 6 erilaista itse
kirjoitettua riviä lisää, käytännössä 9,
koska nappuloiden fontti ja laskureiden fontti tarvitsee oman koodinsa).
Tehtävä
1.11 Komponentin paikan vaihtaminen
Lisää ohjelmaan mahdollisuus tarttua
komponenttiin (esim. nappulaan) ja siirtää se uuteen paikkaan.
2. Object
Pascalin ja C++:n eroja
Luvun pääaiheet:
- Delphin
Object Pascalin perusrakenne
- parametrin
välitys
- silmukat
- taulukot
- class
- RTTI
- ajonaikainen tyypin tunnistus
- dynaamisesti
luotavat kontrollit
- säikeet
- tämän
luvun esimerkit ovat alihakemistossa
moniste\esimerki
2.1 Perusrakenne
Jatkossa käytämme
Delphin Object
Pascal
-kielestä pelkästään nimeä
Pascal. Tästä huolimatta
tämän monisteen tekstiä ei tule sotkea
standardi-Pascaliin,
johon
Delphin Object Pascalissa on
lisätty huomattavasti omia lisäpiirteitä, mm:
- merkkijonot
- luokat
- moduulit
(unit)
Oletamme että
lukija tuntee suhteellisen hyvin vähintään C-kielen, mieluummin
perusteet C++:stakin.
Aloitetaan erojen selvittäminen lyhyellä konsoli
-esimerkkiohjelmalla, joka lukee kaksi kokonaislukua ja tulostaa niistä
suuremman, lukujen keskiarvon ja luvut suuruusjärjestyksessä. Aluksi
sama pääohjelma
C++:lla ja
Delphi 6.0:lla.
esim1.cpp
- C++ pääohjelma
#include <iostream.h>
#include "ali.hpp"
int main(void)
{
int a,b;
cout << "Anna kaksi lukua välilyönnillä"
" erotettuna>";
cin >> a >> b;
cout << "Suurempi luvuista on "
<< bigger(a,b) << endl;
cout << "Lukujen keskiarvo on "
<< average(a,b) << endl;
if ( a > b ) {
swap(a,b);
cout << "Luvut järjestyksessä ovat "
<< a << " " << b << endl;
}
else
cout << "Luvut olivat järjestyksessä"
<< endl;
return 0;
}
|
esim1.dpr
- Delphi 6.0 pääohjelma
program Esim1;
uses Ali;
var a,b:integer;
begin
write('Anna kaksi lukua välilyönnillä ',
'erotettuna>');
readln(a,b);
writeln('Suurempi luvuista on ',
bigger(a,b));
writeln('Lukujen keskiarvo on ',
average(a,b):5:2);
if ( a > b ) then begin
swap(a,b);
writeln('Luvut järjestyksessä ovat ',
a,' ',b);
end
else
writeln('Luvut olivat järjestyksessä');
end.
|
2.1.1 Peruserot
- ei
erotella isoja ja pieniä kirjaimia begin = Begin =
BEGIN
- pääohjelma
(moduuli) alkaa aina sanalla
program
- pääohjelma
loppuu aina pisteeseen (
end.)
- lohkot
suljetaan
begin-end
-sanojen väliin
- muuttujat
esitellään lohkojen ulkopuolella
- muuttujien
esittely alkaa
var
-sanalla
- muuttujien
esittelyssä tulee ensin muuttujan nimi (nimet) ja sitten muuttujan
tyyppi
- merkkijonot
suljetaan yksinkertaisiin lainausmerkkeihin
- if
-lauseeseen kuuluu
then
-sana
- if
-lauseen ehto ei välttämättä tarvitse kaarisulkuja,
paitsi yhdistetyssä
ehdossa
if ( 0 < a ) and ( a < 10 ) then
- else
-sanan edessä EI saa olla puolipistettä
- aliohjelmakirjastot
esitellään kääntäjälle
uses
-lauseella
- em.
käännöksessä etsitään automaattisesti
ali.pas -tiedostoa, joka tarvittaessa
käännetään ja linkitetään
mukaan
Tehtävä
2.12 Sama ohjelma C-kielellä
Kirjoita vastaava ohjelma C-kielellä.
2.1.2 Parametrin
välitys
Edellisen
esimerkin aliohjelmat on kirjoitettu omaan tiedostoonsa, joka
C++:ssa pitää muistaa
linkittää mukaan. Otsikkotiedostossahan on tiedot vain
kääntämistä varten.
Pascalin unitissa ovat
sekä otsikkotiedot päämoduulin kääntämiseksi,
että varsinainen toteutus. Tarvittaessa voidaan tietysti jakaa
käännettyä moduulia (
ali.dcu,
Delphi Compiled Unit).
esim1.hpp
- C++ otsikkotiedosto
#ifndef ALI_HPP
#define ALI_HPP
double average(int a, int b);
int bigger(int a, int b);
void swap(int &a,int &b);
#endif
ali.cpp
- C++ aliohjelmat
#include "ali.hpp"
double average(int a, int b)
{
return (a+b)/2.0;
}
int bigger(int a, int b)
{
if ( a > b ) return a;
return b;
}
// Vaihdetaan luvut keskenään
void swap(int &a,int &b)
{
int t;
t = a; a = b; b = t;
}
|
ali.pas
- Pascal aliohjelmat
unit Ali;
interface
function average(a,b:integer):real;
function bigger(a,b:integer):integer;
procedure swap(var a,b:integer);
implementation
function average(a,b:integer):real;
begin
Result := (a+b)/2.0;
end;
function bigger(a,b:integer):integer;
begin
Result := b;
if ( a > b ) then Result := a;
end;
{ Vaihdetaan luvut keskenään }
procedure swap(var a,b:integer);
var t:integer;
begin
t := a; a := b; b := t;
end;
end. { Unitin lopetus }
|
- aliohjelmamoduulissa
(unit) on
interface
-osa, jonka alla on lueteltu moduulin ulkopuolelle näkyvät
määritykset
- implementation
-osassa on varsinainen toteutus
- moduuli
lopetetaan
end.
(huom. piste)
- moduulissa
voi olla alustusosa
initialization
alustus;
end.,
joka suoritetaan kun moduuli ladataan muistiin
- voi
lisäksi olla
finalization
-osa, joka suoritetaan kun moduuli poistuu muistista
- aliohjelman
otsikkorivi loppuu AINA
puolipisteeseen ;
- sijoitusoperaattorina
on
:=
- yhtäsuuruusvertailu
tehdään operaattorilla
=
- kommenttisulkuina
on {}
tai
vaihtoehtoisesti
(* ... *)
- rivikommenttina
toimii myös
//
- aliohjelmia
on kaksi tyyppiä:
procedure
ja
function.
- procedure
vastaa C:n
void -funktioita
- parametrilistan
erotin on puolipiste
;
- parametrilistoissa
voidaan kirjoittaa useita muuttujia pilkulla erotettuna ennen parametrin tyypin
ilmaisemista
- reaalilukutyyppi
on
real,
voidaan käyttää myös
double.
- funktion
paluuarvo sijoitetaan funktion nimeen. Sijoituksia voi olla useita, joista
viimeisenä tehty jää voimaan.
- paluuarvolle
on myös lokaali muuttuja
Result,
jolle voidaan sijoittaa ja jota voidaan käyttää lausekkeen
oikeallakin
puolella
function bigger(a,b:integer):integer;
begin
Result := b;
if ( a > Result ) then Result := a;
end;
- funktio
ja aliohjelma voidaan lopettaa
exit-lauseella,
jolloin funktion arvoksi jää viimeisen sijoituksen arvo
- aliohjelmilla
on kahta eri parametrityyppiä:
arvoparametrit
(C:ssä on vain näitä,
call by value) ja
muuttujaparametrit
(call by reference).
Muuttujaparametrit ovat samoja kuin C:ssä
parametrin välitys osoittimien avulla.
C++:ssa vastine on referenssi
(&).
Muuttujaparametrit ilmaistaan parametrilistassa
var
-sanalla.
- parametritonta
aliohjelmaa kutsutaan ilman sulkuja, esimerkiksi: writeln;
Tosin Delphissä saa myös käyttää sulkuja jos
sillä haluaa korostaa että kyseessä on aliohjelmakutsu.
- aliohjelmien
sisään voidaan kirjoittaa omia "apu"aliohjelmia, jotka voivat
käyttää "isäntänsä" muuttujia ilman parametrin
välitystä
2.1.3 Silmukat
ja taulukot
Seuraavassa esimerkissä on ohjelma, joka ensin tekee
viisipaikkaisen kokonaislukutaulukon:
Sitten ohjelma laskee, montako taulukon alkiota voidaan
ottaa mukaan, ilman että summa ylittää vielä 10. Lopuksi
tulostetaan ko. alkiot takaperin:
3 lukua mahtuu alle 10 näiden summa on 9
Luvut on: 6 3 0
silmu.cpp
- esimerkki silmukoista
#include <iostream.h>
const int TKOKO=5;
const int RAJA=10;
void alusta(int luvut[], int n, int kasvu)
/* Alustetaan taulukko sarjalla
0,kasvu,2*kasvu... */
{
int i,luku=0;
for (i=0; i<n; i++) {
luvut[i] = luku;
luku += kasvu;
}
}
int montako_mahtuu(const int luvut[], int n,
int raja)
/* Mihin asti lukujen summa ei ylitä rajaa */
{
int i=0,summa=0;
do {
summa += luvut[i];
} while ( summa < raja && ++i < n );
return i;
}
int summaa(const int luvut[], int n)
{
int i=0,summa=0;
while ( i < n ) {
summa += luvut[i];
i++;
}
return summa;
}
void tulosta(ostream &os,
const int luvut[],int n)
/* Tulostaa taulukon nurinpäin */
{
int i;
for (i=n-1; i>=0; i--)
os << luvut[i] << " ";
os << endl;
}
int main(void)
{
int luvut[TKOKO],n;
alusta(luvut,TKOKO,3);
n = montako_mahtuu(luvut,TKOKO,RAJA);
cout << n << " lukua mahtuu alle " << RAJA
<< " näiden summa on "
<< summaa(luvut,n) << endl;
cout << "Luvut on: ";
tulosta(cout,luvut,n);
return 0;
}
|
silmu.dpr
- esimerkki silmukoista
program Silmu;
const TKOKO=5;
const RAJA=10;
{ Seuraavalla korvataan C:n ++i }
function esi_lisaa(var i:integer):integer;
begin inc(i); esi_lisaa := i; end;
procedure alusta(var luvut:array of integer;
n,kasvu:integer);
{ Alustetaan taulukko sarjalla
0,kasvu,2*kasvu... }
var i,luku:integer;
begin
luku := 0;
for i:=0 to n-1 do begin
luvut[i] := luku;
inc(luku,kasvu);
end;
end;
function montako_mahtuu(const luvut:array of
integer; n,raja:integer):integer;
{ Mihin asti lukujen summa ei ylitä rajaa }
var i,summa:integer;
begin
i := 0; summa := 0;
repeat
summa := summa + luvut[i];
until ( summa >= raja ) or
( esi_lisaa(i) >= n );
montako_mahtuu := i;
end;
function summaa(const luvut:array of integer;
n:integer):integer;
var i,summa:integer;
begin
i := 0; summa := 0;
while ( i < n ) do begin
inc(summa,luvut[i]);
inc(i);
end;
summaa := summa;
end;
procedure tulosta(var f:textfile;
const luvut:array of integer; n:integer);
{ Tulostaa taulukon nurinpäin }
var i:integer;
begin
for i:=n-1 downto 0 do
write(f,luvut[i],' ');
writeln(f);
end;
{ Pääohjelma: }
var luvut : array[0..TKOKO-1] of integer;
n : integer;
begin
alusta(luvut,TKOKO,3);
n := montako_mahtuu(luvut,TKOKO,RAJA);
writeln(n,' lukua mahtuu alle ',RAJA,
' näiden summa on ',summaa(luvut,n));
write('Luvut on: ');
tulosta(output,luvut,n);
end.
|
Huomioita silmukoiden ja taulukoiden eroista:
- while
do kuten
C:ssä
while
- do-while:n
tilalle
repeat
until, jossa on lopettamisehto, ei
jatkamisehto kuten C:ssä, eli ehto
vastaavan C-ehdon negaatio
- for
-silmukka on huomattavasti rajoittuneempi kuin
C:ssä. Silmukkalaskurin arvo ei ole
välttämättä tunnettu silmukan jälkeen, eikä
laskurin arvon muuttaminen kesken silmukan välttämättä
katkaise silmukkaa
- for-to
osaa lisätä vain yhdellä
- alaspäin
laskemista varten on oma
for-downto
- taulukoiden
esittelyssä esitellään
alaraja..yläraja
- itse
asiassa taulukon indeksityppinä voi olla mikä tahansa ordinaalityyppi
(numeroituva tyyppi)
- moniulotteinen
taulukko on taulukko
taulukoista:
var T: array[Boolean] of array[1..10] of array[3..6] of Real;
{ on sama kuin: }
var T: array[Boolean,1..10,3..6] of Real;
{ Alkioon viitataan joko }
r := T[True][3][5];
{tai}
r := T[True,3,5];
- avoimen
taulukon luvut:array of integer koko saataisiin itse
asiassa selville
kutsuilla:
Low(luvut); { aina 0 riippumatta varsinaisen taulukon alarajasta }
High(luvut); { 4, eli alkuperäisen taulukon koko-1 }
SizeOf(luvut); { 10 tai 20 riippuen onko 16 vai 32-bittinen kääntäjä }
- esimerkissä
kuitenkin käytetään parametrina tuotua
lukumäärää; näin voidaan laskea tuloksia myös
osataulukoille. Tietysti pitäisi aina testata onko
n <= High(luvut)+1, esimerkissä on
pyritty vain matkimaan mahdollisimman paljon viereistä C-koodia.
- avointa
taulukkoa (open array) voidaan kutsua myös
vakiotaulukolla:
tulosta(output,[1,2,4,8,16],5);
- jollei
olisi tehty apufunktiota esi_lisaa, olisi
repeat -silmukka pitänyt kirjoittaa
esimerkiksi:
repeat { repeat edelleen väärin, koska jos n=0, viitataan lait.alkioon }
summa := summa + luvut[i];
if ( summa >= raja ) then break;
i := i + 1;
until ( i >= n );
- break
-proseduurissa on sama vika kuin C:n
break -lauseessakin: vain yksi taso voidaan katkaista.
Tätä voidaan kiertää joko aliohjelman
exit
-lauseella tai goto
-lauseella
Tehtävä
2.13 Avoimen taulukon ylärajan tarkistus
Muuta
silmu.dpr
-ohjelmaa siten, että kussakin aliohjelmassa tarkistetaan,
ettei taulukon ylärajaa ylitetä.
Miten em. muutoksen jälkeen kutsu
tulosta(output,[1,2,4,8,16],5);
voitaisiin korvata laskematta itse vakiotaulukon kokoa?
2.1.4 case-lause
- case
-lause poikkeaa hieman C-kielen
switch
lauseesta, tavallisimmassa tapauksessa se on jopa helpompi
käyttää:
caseof.c
- esimerkki switch -lauseesta
#include <stdio.h>
int main(void)
{
int tunnit;
for ( tunnit=1; tunnit<=24; tunnit++ ) {
printf("%2d: ",tunnit);
switch ( tunnit ) {
case 1: case 2: case 3: case 4: case 5:
case 6: printf("Nukutaan\n"); break;
case 7: printf("Herätys\n"); break;
case 8: printf("Töihin\n"); break;
case 9: case 10: case 11:
case 13: case 14: case 15:
case 16: printf("Tehdään töitä\n");
break;
case 12:
case 18: printf("Syödään\n"); break;
default: printf("Huilaillaan\n"); break;
}
}
return 0;
}
|
caseof.dpr
- esimerkki case-of -lauseesta
program Caseof;
{ Pääohjelma: }
var tunnit : integer;
begin
for tunnit:=1 to 24 do begin
write(tunnit:2,': ');
case tunnit of
1..6 : writeln('Nukutaan');
7 : writeln('Herätys');
8 : writeln('Töihin');
9..11,
13..16: writeln('Tehdään töitä');
12,18 : writeln('Syödään');
else writeln('Huilaillaan');
end; { case:lle oma end! }
end;
end.
|
- ei
tarvita (eikä ole)
break
-lauseita kunkin vaihtoehdon jälkeen (C:n
ilman breakiä olevaa tapausta ei voi edes toteuttaa
case-lauseella)
- samassa
vaihtoehdossa voi olla myös väli
..
ilmoitettuna
- useita
vaihtoehtoja voidaan erottaa pilkulla
- valitsimena
voi olla mikä tahansa ordinaalityyppi (numeroituva
tyyppi)
Muuta
C-esimerkkiä
casof.c
siten, että klo 8:sta tulostetaan sekä
Töihin
että
Tehdään
töitä. Kokeile tehdä vastaava muutos
caseof.pas
tiedostoon.
2.2 Olio-ominaisuudet
Seuraavassa esimerkissä toteutetaan luokkahierarkia
(käytämme unkarilaista nimeämistapaa, missä
c=class ja
a=abstract):
Kuva
2.1 Esimerkkiohjelman oliohierarkia
piste.cpp
- esimerkki perinnästä
#include <stdio.h>
// Esimerkki ehdollisesta kaantamisesta:
#define RTTI
// RTTI = Run Time Type Information,
// toimii esim. BC++ 4.5 alkaen
#ifdef RTTI
#include <typeinfo.h>
#endif
//-------------------------------------------
class caGraafinenOlio {
protected:
int x,y;
int nakyy;
int paikka(int nx,int ny)
{ x = nx; y = ny; return 0; }
public:
caGraafinenOlio(int ix=0, int iy=0)
{ paikka(ix,iy); nakyy = 0; }
virtual ~caGraafinenOlio() { }
virtual int piirra() const = 0;
int nakyvissa() const { return nakyy; }
int sammuta();
int sytyta();
int siirra(int nx, int ny) {
if ( !nakyvissa() ) return paikka(nx,ny);
sammuta(); paikka(nx,ny); return sytyta();
}
virtual int tulosta(const char *s="")
const {
# ifdef RTTI
printf("%-10s: ",typeid(*this).name());
# endif
printf("%-10s (%02d,%02d)",s,x,y);
return 0;
}
}; // caGraafinen olio
int caGraafinenOlio::sammuta()
{
if ( !nakyvissa() ) return 1;
printf("Sammutettu: ");
nakyy = 0;
return piirra();
}
int caGraafinenOlio::sytyta()
{
if ( nakyvissa() ) return 1;
printf("Sytytetty: ");
nakyy = 1;
return piirra();
}
//-------------------------------------------
class caSateellinenOlio :
public caGraafinenOlio {
protected:
int r;
int koko(int nr) { r = nr; return 0; }
public:
caSateellinenOlio(int ix=0,int iy=0,
int ir=1) :
caGraafinenOlio(ix,iy), r(ir) {}
virtual int tulosta(const char *s="")
const {
caGraafinenOlio::tulosta(s);
printf( " r=%d",r);
return 0;
}
int muuta_koko(int nr) {
if ( !nakyvissa() ) return koko(nr);
sammuta(); koko(nr); return sytyta();
}
}; // caSateellinen olio
//-------------------------------------------
class cPiste : public caGraafinenOlio {
public:
cPiste(int ix=0, int iy=0) :
caGraafinenOlio(ix,iy) {}
virtual ~cPiste() { sammuta(); }
virtual int piirra() const {
tulosta("Piste"); printf("\n"); return 0;}
};
//-------------------------------------------
class cYmpyra : public caSateellinenOlio {
public:
cYmpyra(int ix=0, int iy=0, int ir=1) :
caSateellinenOlio(ix,iy,ir) {}
virtual ~cYmpyra() { sammuta(); }
virtual int piirra() const {
tulosta("Ympyra"); printf("\n"); return 0;}
};
int main(void)
{
caGraafinenOlio *p;
caGraafinenOlio *kuvat[10];
int i;
cPiste p1,p2(10,20);
cYmpyra y1(1,1,2);
p1.sytyta(); p2.sytyta();
p1.siirra(7,8);
y1.sytyta(); y1.muuta_koko(5);
// Esimerkki polymorfismista
p = new cYmpyra(9,9,9);
p->sytyta(); p->siirra(8,8);
// p->muuta_koko(4); ei laillinen
# ifdef RTTI
// if ( typeid(*p) == typeid(cYmpyra) )
caSateellinenOlio *ps =
dynamic_cast<caSateellinenOlio *>(p);
if ( ps ) ps->muuta_koko(4);
# else
((caSateellinenOlio *)p)->muuta_koko(4);
# endif
delete p;
// Esimerkki polymorfismista
kuvat[0] = new cYmpyra(10,10,100);
kuvat[1] = new cPiste(11,11);
kuvat[2] = new cYmpyra(12,12,102);
kuvat[3] = NULL;
for (i=0; kuvat[i];i++) kuvat[i]->sytyta();
for (i=0; kuvat[i];i++) delete kuvat[i];
return 0;
}
|
piste.dpr
- esimerkki perinnästä
program piste;
{ Esimerkki ehdollisesta kaantamisesta: }
{$DEFINE RTTI }
uses
SysUtils;
{-------------------------------------------}
type caGraafinenOlio = class
protected
x,y:integer;
nakyy:boolean;
procedure paikka(nx,ny:integer);
public
constructor Create;
constructor Create2(ix,iy:integer);
destructor Destroy; override;
procedure piirra; virtual; abstract;
function nakyvissa:boolean;
procedure sammuta;
procedure sytyta;
procedure siirra(nx,ny:integer);
procedure tulosta(s:string); virtual;
end;
procedure caGraafinenOlio.paikka(
nx,ny:integer);
begin x := nx; y := ny; end;
constructor caGraafinenOlio.Create2(
ix,iy:integer);
begin
inherited Create;
paikka(ix,iy);
nakyy := False;
end;
constructor caGraafinenOlio.Create;
begin Create2(0,0); end;
destructor caGraafinenOlio.Destroy;
begin sammuta;
inherited Destroy;
end;
function caGraafinenOlio.nakyvissa:boolean;
begin Result := nakyy; end;
procedure caGraafinenOlio.siirra(
nx,ny:integer);
begin
if ( not nakyvissa ) then begin
paikka(nx,ny); exit;
end;
sammuta; paikka(nx,ny); sytyta;
end;
procedure caGraafinenOlio.tulosta(s:string);
begin
{$IFDEF RTTI}
write(format('%-10s: ',[ClassName]));
{$ENDIF}
write(format('%-10s (%02d,%02d)',[s,x,y]));
end;
procedure caGraafinenOlio.sammuta;
begin
if ( not nakyvissa ) then exit;
write('Sammutettu: ');
nakyy := False;
piirra;
end;
procedure caGraafinenOlio.sytyta;
begin
if ( nakyvissa ) then exit;
write('Sytytetty: ');
nakyy := True;
piirra;
end;
{-------------------------------------------}
type caSateellinenOlio =
class(caGraafinenOlio)
protected
r:integer;
procedure koko(nr:integer);
public
constructor Create3(ix,iy,ir:integer);
constructor Create2(ix,iy:integer);
constructor Create;
procedure tulosta(s:string); override;
procedure muuta_koko(nr:integer);
end;
procedure caSateellinenOlio.koko(nr:integer);
begin r := nr; end;
constructor caSateellinenOlio.Create3(
ix,iy,ir:integer);
begin
inherited Create2(ix,iy);
koko(ir);
end;
constructor caSateellinenOlio.Create2(
ix,iy:integer);
begin Create3(ix,iy,1); end;
constructor caSateellinenOlio.Create;
begin inherited Create; koko(1); end;
procedure caSateellinenOlio.tulosta(s:string);
begin
inherited tulosta(s);
write(' r=',r);
end;
procedure caSateellinenOlio.muuta_koko(
nr:integer);
begin
if ( not nakyvissa ) then begin
koko(nr); exit;
end;
sammuta; koko(nr); sytyta;
end;
{-------------------------------------------}
type cPiste = class(caGraafinenOlio)
public
procedure piirra; override;
end;
procedure cPiste.piirra;
begin tulosta('Piste'); writeln; end;
{-------------------------------------------}
type cYmpyra = class(caSateellinenOlio)
public
procedure piirra; override;
end;
procedure cYmpyra.piirra;
begin tulosta('Ympyra'); writeln; end;
{-------------------------------------------}
var p : caGraafinenOlio;
kuvat : array[0..9] of caGraafinenOlio;
p1,p2 : cPiste;
y1 : cYmpyra;
i : integer;
begin
p1 := cPiste.Create;
p2 := cPiste.Create2(10,20);
y1 := cYmpyra.Create3(1,1,2);
p1.sytyta; p2.sytyta;
p1.siirra(7,8);
y1.sytyta; y1.muuta_koko(5);
(* Esimerkki polymorfismista *)
p := cYmpyra.Create3(9,9,9);
p.sytyta; p.siirra(8,8);
{ p.muuta_koko(4); ei laillinen }
{$IFDEF RTTI }
if ( p is caSateellinenOlio ) then
{$ENDIF}
(p as caSateellinenOlio).muuta_koko(4);
p.Free;
(* Esimerkki polymorfismista *)
kuvat[0] := cYmpyra.Create3(10,10,100);
kuvat[1] := cPiste.Create2(11,11);
kuvat[2] := cYmpyra.Create3(12,12,102);
kuvat[3] := NIL;
i:=0;
while ( kuvat[i] <> NIL ) do begin
kuvat[i].sytyta; inc(i);
end;
i:=0;
while ( kuvat[i] <> NIL ) do begin
kuvat[i].Free; inc(i);
end;
y1.Free; p2.Free; p1.Free;
end.
|
Mikäli vakio RTTI on
määritelty, tulostavat molemmat ohjelmat:
Sytytetty: cPiste : Piste ( 0, 0)
Sytytetty: cPiste : Piste (10,20)
Sammutettu: cPiste : Piste ( 0, 0)
Sytytetty: cPiste : Piste ( 7, 8)
Sytytetty: cYmpyra : Ympyra ( 1, 1) r=2
Sammutettu: cYmpyra : Ympyra ( 1, 1) r=2
Sytytetty: cYmpyra : Ympyra ( 1, 1) r=5
Sytytetty: cYmpyra : Ympyra ( 9, 9) r=9
Sammutettu: cYmpyra : Ympyra ( 9, 9) r=9
Sytytetty: cYmpyra : Ympyra ( 8, 8) r=9
Sammutettu: cYmpyra : Ympyra ( 8, 8) r=9
Sytytetty: cYmpyra : Ympyra ( 8, 8) r=4
Sammutettu: cYmpyra : Ympyra ( 8, 8) r=4
Sytytetty: cYmpyra : Ympyra (10,10) r=100
Sytytetty: cPiste : Piste (11,11)
Sytytetty: cYmpyra : Ympyra (12,12) r=102
Sammutettu: cYmpyra : Ympyra (10,10) r=100
Sammutettu: cPiste : Piste (11,11)
Sammutettu: cYmpyra : Ympyra (12,12) r=102
Sammutettu: cYmpyra : Ympyra ( 1, 1) r=5
Sammutettu: cPiste : Piste (10,20)
Sammutettu: cPiste : Piste ( 7, 8)
Jos vakiota RTTI ei ole
määritelty, on tulostus muuten sama, mutta puuttuu sarake, jossa on
cPiste ja cYmpyra.
Delphissä on kaksi
tapaa tehdä luokkia:
- object,
jolla voidaan tehdä myös staattisia olioita
- class,
jolla voidaan tehdä vain dynaamisia
olioita
Tässä monisteessa
käytetään pääasiassa vain
class-määreellä
esiteltyjä luokkia
- class-esitellyt
luokat periytyvät aina
TObject-luokasta
- kaikki
oliot ovat (class-esittelyn jälkeen) dynaamisia
(käytännössä osoittimia), eli ne pitää itse luoda
ennen käyttöä (=kutsua konstruktoria) ja poistaa käytön
jälkeen (kutsua destruktoria)
- this-osoitinta
Delphissä vastaa
self
- ei
inline-kirjoitusmahdollisuutta metodeille
- ei
funktioiden oletusparametreja
- ei
funktioiden lisämäärittelyä (function
overloading), eli ei voi olla kahta
samannimistä funktiota tai kahta samannimistä metodia samassa oliossa.
Esimerkissä on tämän takia kirjoitettu useita konstruktoreita:
Create - 0 parametria, Create2 - 2
parametria, Create3 - 3 parametria. Eri olioissa voi
tietenkin olla samojakin metodien nimiä (ja usein pitääkin
olla).
- ei
operaattoreiden lisämäärittelyä
(operator overloading)
- ei
moniperintää (kaikki eivät pidä tätä
miinuksena)
- olion
sijoittaminen toiseen "olioon" tekee aliasing-ongelman: molemmat "oliot"
viittaavat samaan olion esiintymään
- konstruktori
periytyy, eli jos isäluokan konstruktori kelpaa sellaisenaan, ei tarvitse
kirjoittaa kontruktoria joka pelkästään kutsuu isäluokan
kontruktoria
- kannattaa
ehkä aina kirjoittaa yksi
Create
-niminen parametriton konstruktori, koska tällainen on kantaluokassa ja
käyttäjä voi vanhasta tottumuksesta luoda olion esiintymän
käyttäen tätä konstruktoria
- myös
destruktori näyttäisi periytyvän, eli sitä ei tarvitse
kirjoittaa joka luokkaan uudelleen. Destruktorin aikana olio on omaa
luokkaansa, eli "luokka ei pienene" kuten
C++:ssa. Näin jos destruktorissa
kutsutaan jotakin virtuaalista tai välillisesti virtuaalista metodia,
käytetään purettavan luokan virtuaalitaulua vaikka kutsu olisikin
jonkin kantaluokan destruktorissa
- vaikka
destruktoreitakin voi kirjoittaa useita eri nimisiä, kannattaa ehkä
kirjoittaa aina vain
destroy
-niminen destruktori, koska valmiit oliot tuhotaan
free
-metodilla, joka taas kutsuu
destroy:ta
(TObject-luokassa)
- dynaamisuuden
takia joudutaan aina kutsumaan itse eksplisiittisesti olion esiintymän
luomiseksi jotakin konstruktioria ja lopuksi olion hävittämiseksi
jotakin destruktoria, käytännössä varminta ehkä kutsua
Borlandin ohjeiden mukaisesti
Free -metodia.
- C:ssä
kannattaa joskus tehdä ylimääräisten lauselohkojen
välttämiseksi
int-funktioita,
vaikkei paluuarvoa
tarvitakaan:
// Vertaa:
int siirra(int nx, int ny) {
if ( !nakyvissa() ) return paikka(nx,ny);
sammuta(); paikka(nx,ny); return sytyta();
}
// Tai:
void siirra(int nx, int ny) {
if ( !nakyvissa() ) { paikka(nx,ny); return; }
sammuta(); paikka(nx,ny); sytyta();
}
- Pascalissa
vastaava ei hyödytä mitään, tosin ei juuri lisää
koodiakaan:
{ Vertaa: }
function caGraafinenOlio.siirra(nx,ny:integer):integer;
begin
if ( not nakyvissa ) then begin siirra := paikka(nx,ny); exit; end;
sammuta; paikka(nx,ny); siirra := sytyta;
end;
{ Tai: }
procedure caGraafinenOlio.siirra(nx,ny:integer);
begin
if ( not nakyvissa ) then begin paikka(nx,ny); exit; end;
sammuta; paikka(nx,ny); sytyta;
end;
- RTTI
(=Run Time Type Information)
on ehkä helpompi käyttää
Delphissä kuin
C++:ssä
Tehtävä
2.15 C++
ilman
inline-funktoita
Kirjoita
piste.cpp
käyttämättä
inline-funktioita
Tehtävä
2.16 Neliö ja suorakaide
Lisää kumpaankin esimerkkiin
(piste.cpp
ja
piste.dpr)
luokka
cNelio.
Entä
cSuorakaide?
Tehtävä
2.17 Väri ja suunta
Mieti miten luokkahierarkiaa muutetaan, mikäli
kuvioista halutaan värillisiä ja eri asennossa olevia.
2.3 Säikeet
ja dynaamiset kontrollit
Säikeet
(=
threads)
eivät oikeastaan kuulu kieleen, vaan
käyttöjärjestelmään. Koska
Delphitoimii kuitenkin vain
Windowsin alla, käsitellään
tässä säikeet ikään kuin kieleen kuuluvana asiana.
Säikeet toimivat
Win95:sta alkaen.
Vertailukohdaksi otetaan
Borland C++ 5.0,
OWL 5.0 ja
AppExpertillä osittain tehty
sovellus.
Seuraava esimerkki luo dialogi-ikkunassa (lomakkeessa,
form) valmiiksi olevan
Käynnistä-näppäimen
lisäksi joukon laskureita (
Delphissä
tekstikenttiä ja
C++:ssa
Tbutton-nappuloita). Kun
Käynnistä-nappia painetaan,
käynnistetään kutakin laskuri-kenttää varten oma
prosessi (säie), joka pyörittää kentässä lukuja
0:sta ylöspäin. Kenttää painamalla voidaan
ko. säie "tappaa". Kummastakin ohjelmasta on
jätetty listaamatta sekä pääohjelma että
resurssitiedosto.
saie\saiedemo.cpp - esimerkki
säikeistä
#include <owl/button.h>
#include <classlib/thread.h>
#include "saieapp.rh" // Def of all resources
const int SAIKEITA = 20; // Säikeiden lkm
const int PAIVITYS = 100000;// Minkä väl.päiv.
const int KIERROKSIA = 1000000;
class cLaskuri : public TThread
// Laskurisäie perit.yleisestä säikeestä
// ja siihen lisätään omat erikoispiirt.
{
TControl *Text; //Mihin teksti-ikkunaan
int n; //Sis. laskurin arvo
int raja; //Mihin asti lasketaan
protected:
void count(); //Yhden laskuaskeleen suor
//Perit.luokan Run korvataan omalla
int Run();
public:
//Rakentaja, joka alustaa mm. sis. muut.
cLaskuri(TControl *oLabel,int r) :
Text(oLabel), raja(r), n(0) {}
~cLaskuri() { ;}
};
//{{TDialog = TFormSaieDemo}}
class TFormSaieDemo : public TDialog {
// Lomake,jossa laskureita pyöritetään
TControl *Labels[SAIKEITA];
cLaskuri *saikeet[SAIKEITA];
public:
TFormSaieDemo(TWindow* parent,
TResId resId = IDD_SAIEDEMO,
TModule* module = 0);
virtual ~TFormSaieDemo();
//{{TFormSaieDemoVIRTUAL_BEGIN}}
public:
virtual bool Create();
virtual TResult EvCommand(uint id,
THandle hWndCtl, uint notifyCode);
virtual bool CanClose();
//{{TFormSaieDemoVIRTUAL_END}}
//{{TFormSaieDemoRSP_TBL_BEGIN}}
protected:
void BNKaynnistaClicked();
//{{TFormSaieDemoRSP_TBL_END}}
DECLARE_RESPONSE_TABLE(TFormSaieDemo);
}; //{{TFormSaieDemo}}
saie\saiedemo.cpp
- esimerkki säikeistä
#include <owl/pch.h>
#include "saiedemo.h"
#define ALKU_ID 3000
//-------------------------------------------
// cLaskuri =================================
//-------------------------------------------
//-------------------------------------------
void cLaskuri::count()
{
n++;
if ( n % PAIVITYS == 0 ) {
char s[30];
wsprintf(s,"%d",n);
Text->SetWindowText(s);
}
}
//-------------------------------------------
int cLaskuri::Run()
// Tämä suorittaa varsinaisen säikeen
// toimenpiteet. Kun tämä metodi loppuu,
// pysähtyy säie.
{
const char *mes="";
while ( n < raja ) {
if ( ShouldTerminate() ) {
mes = "T"; break;
}
count();
}
char s[30];
wsprintf(s,"%d%s",n,mes);
Text->SetWindowText(s);
return 0;
}
//-------------------------------------------
// TFormSaieDemo ============================
//-------------------------------------------
//-------------------------------------------
DEFINE_RESPONSE_TABLE1(TFormSaieDemo,TDialog)
//{{TFormSaieDemoRSP_TBL_BEGIN}}
EV_BN_CLICKED(IDKAYNNISTA,
BNKaynnistaClicked),
//{{TFormSaieDemoRSP_TBL_END}}
END_RESPONSE_TABLE;
//{{TFormSaieDemo Implementation}}
//-------------------------------------------
TFormSaieDemo::TFormSaieDemo(TWindow* parent,
TResId resId, TModule* module)
: Tdialog(parent, resId, module)
{
}
//-------------------------------------------
TFormSaieDemo::~TFormSaieDemo()
{
Destroy();
}
//-------------------------------------------
static bool Clear(cLaskuri * &saie)
// Tämä funktio tarkistaa onko säie jo
// siivottu, eli se on tuhottu.
// Mikäli säie ei ole tuhottu, mutta se
// on valmis, tuhotaan säie.
{
if ( saie == NULL ) return true;
if ( saie->GetStatus() !=
TThread::Finished ) return false;
delete saie;
saie = NULL;
return true;
}
//-------------------------------------------
void TFormSaieDemo::BNKaynnistaClicked()
{
for (int i=0; i<SAIKEITA; i++) {
if ( !Clear(saikeet[i]) ) continue;
saikeet[i] = new cLaskuri(Labels[i],
KIERROKSIA);
saikeet[i]->Start();
}
saikeet[0]->SetPriority(
THREAD_PRIORITY_ABOVE_NORMAL);
}
//-------------------------------------------
TResult TFormSaieDemo::EvCommand(uint id,
THandle hWndCtl, uint notifyCode)
// Jos laskuria klik., "tapetaan vast.säie"
{
TResult result=TDialog::EvCommand(id,
hWndCtl,notifyCode);
int i = id-ALKU_ID;
if ( 0 <= i && i < SAIKEITA )
if ( !Clear(saikeet[i]) )
saikeet[i]->Terminate();
return result;
}
//-------------------------------------------
bool TFormSaieDemo::Create()
// Luodaan laskurit alekkain näytölle
{
bool result;
int y = 10, dy = 20;
for (int i=0; i<SAIKEITA; i++) {
saikeet[i] = NULL;
Labels[i] = new Tbutton(this,ALKU_ID+i,
"Terve",10,y,70,dy);
y += dy;
}
result = Tdialog::Create();
TRect rc = this->GetWindowRect();
rc.top = 0; rc.bottom = rc.top + y + 50;
this->MoveWindow(rc,TRUE);
return result;
}
bool TFormSaieDemo::CanClose()
{
bool result = TDialog::CanClose();
for (int i=0; i<SAIKEITA; i++) {
if ( Clear(saikeet[i]) ) continue;
result = false;
saikeet[i]->Terminate();
}
return result;
}
|
saie\saiedemo.pas - esimerkki
säikeistä
unit saiedemo;
interface
uses
Windows, Messages, SysUtils, Classes,
Graphics, Controls, Forms, Dialogs,
StdCtrls;
const SAIKEITA = 20; // Säikeiden lkm
PAIVITYS = 100000;// Minkä välein päiv.
KIERROKSIA = 1000000;
type
//-----------------------------------------
cLaskuri = class(TThread)
// Laskurisäie perit.yleisestä säikeestä
// ja siihen lisätään omat erikoispiirt.
private
Text : TLabel; //Mihin teksti-ikkunaan
n:integer; //Sis. laskurin arvo
raja:integer; //Mihin asti lasketaan
protected
procedure count; //Yhden laskuaskeleen su
//Perit.luokan Execute korvataan omalla
procedure Execute; override;
public
//Rakentaja, joka alustaa mm. sis. muut.
constructor Create(oLabel:TLabel;
r:integer);
end;
//-----------------------------------------
TFormSaieDemo = class(TForm)
// Lomake,jossa laskureita pyöritetään
ButtonKaynnista: TButton;
procedure ButtonKaynnistaClick(
Sender: TObject);
procedure ThreadDone(Sender:TObject);
procedure FormCreate(Sender: TObject);
procedure LabelsClick(Sender: TObject);
private
{ Private declarations }
Labels: Array [0..SAIKEITA-1] of TLabel;
saikeet:Array[0..SAIKEITA-1] of cLaskuri;
public
{ Public declarations }
end;
//-------------------------------------------
// Globaalit muuttujat
//-------------------------------------------
var
FormSaieDemo: TFormSaieDemo;
implementation
//-------------------------------------------
// cLaskuri =================================
//-------------------------------------------
//-------------------------------------------
procedure cLaskuri.count;
begin
inc(n);
if ( n mod PAIVITYS = 0 ) then
Text.Caption := IntToStr(n);
end;
//-------------------------------------------
procedure cLaskuri.Execute;
// Tämä suorittaa varsinaisen säikeen
// toimenpiteet. Kun tämä metodi loppuu,
// pysähtyy säie. Säie häviää autom.
// jos FreeOnTerminate := True
var mes:String;
begin
mes := '';
while ( n < raja ) do begin
if terminated then begin
mes := 'T'; Break;
end;
count;
end;
Text.Caption := IntToStr(n)+mes;
end;
//-------------------------------------------
constructor cLaskuri.Create(oLabel:TLabel;
r:integer);
begin
inherited Create(False);
Text := oLabel;
raja := r;
n := 0;
FreeOnTerminate := True;
end;
{$R *.DFM}
//-------------------------------------------
// TformSaieDemo ============================
//-------------------------------------------
//-------------------------------------------
procedure TFormSaieDemo.ThreadDone(
Sender:TObject);
// Tämä tapahtuma, kun jokin säie valmistuu.
var i:integer;
begin
for i:=0 to SAIKEITA-1 do // Etsitään säie
if ( Sender = saikeet[i] ) then
saikeet[i] := NIL;
end;
//-------------------------------------------
procedure TFormSaieDemo.ButtonKaynnistaClick(
Sender: TObject);
var i:integer;
begin
// Luodaan uudet säikeet niiden tilalle
// joita ei ole
for i:=0 to SAIKEITA-1 do
if ( saikeet[i] = NIL ) then begin
saikeet[i] :=
cLaskuri.Create(Labels[i],KIERROKSIA);
saikeet[i].OnTerminate := ThreadDone;
end;
saikeet[0].Priority := tpHigher;
end;
//-------------------------------------------
procedure TFormSaieDemo.LabelsClick(
Sender:TObject);
// Jos laskuria klik., "tapetaan vast.säie"
var i:integer;
begin
i := (Sender as TLabel).Tag;
if ( saikeet[i] <> NIL ) then
saikeet[i].Terminate;
end;
//-------------------------------------------
procedure TFormSaieDemo.FormCreate(
Sender: TObject);
// Luodaan laskurit alekkain näytölle
var i,y,dy:integer;
begin
y := 10; dy := 20;
for i:=0 to SAIKEITA-1 do begin
saikeet[i] := NIL;
Labels[i] := TLabel.Create(Self);
Labels[i].AutoSize := False;
Labels[i].top := y;
Labels[i].left := 10;
Labels[i].Width := 70;
Labels[i].Caption := 'Terve';
Labels[i].Parent := Self;
Labels[i].Tag := i;
Labels[i].OnClick := LabelsClick;
y := y + dy;
end;
Top := 0; Height := y + 50;
end;
end.
|
- Delphissä
säikeen loppumisesta saadaan selvä viesti. Tämä helpottaa
pysähtyneiden säikeiden poistamista tai muuta valmistuneen prosessin
jatkokäsittelyä
- Delphissä
ohjelman voi kokeilujen perusteella turvallisesti sammuttaa vaikka
säikeiden suoritus olisikin kesken.
C++:ssa täytyy sammuttaminen
estää, mikäli säikeitä on käynnissä ja
suorittaa sammutus loppuun vasta kun säikeet ovat loppuneet
- Delphissä
on helpompi laittaa säie käyntiin jo konstruktorissa, koska
tätä explisiittisesti kuitenkin kutsutaan:
C++:ssa voi olla myös staattinen olio ja
tällöin säie lähtisi käyntiin jo staattisen olion
esittelyn yhteydessä
- Delphissä
dynaamisten kontrollien tekeminen on jossain
määrin helpompaa, kun viestinkäsittelijä voidaan sijoittaa
kontrolliin jo luontivaiheessa
- vaikka
dynaamiset kontrollit onkin itse luotu, häviävät ne
automaattisesti lomakkeen poistuessa, koska kontrollit ovat lomakkeen
lapsia
- luotaessa
kontrolleja dynaamisesti, kannattaa kontrollin
Tag-ominaisuuteen laittaa jokin kontrollia hyvin kuvaava
arvo. Edellisessä esimerkissä tämä on ollut kontrollin
järjestysnumero
- Delphissä
on huomattavasti helpompi muuttaa kontrollin tiettyä ominaisuutta.
Toisaalta jos muutetaan kerralla esim. paikka ja kokoa, on
C++:n suorakaiteen välittäminen
järkevämpää, koska jokaisesta yksittäisestä
sijoituksesta tulee periaatteessa muutostarve näytöllä. Tosin
Delphissä voidaan käyttää
SetBounds -metodia.
- Delphin
merkkijonoja on huomattavasti helpompi käyttää kuin
C:n perusmerkkijonoja, joiden päälle
Windows ja myös
OWL-luokkakirjasto on suurelti
rakennettu
Tehtävä
2.18
TLabel =>
TButton
Muuta
saiedemo.pas-esimerkissä
laskurikentät nappuloiksi.
2.3.1 Delphi
ei
ole ”thread safe”
Ainakaan Delphi 3.0 ei ole
vielä ”thread safe”.
Tällä tarkoitetaan mm. Sitä, että vain ohjelman
pääsäie saa käyttää tiettyjä VCL-kirjaston
kutsuja. Itse asiassa edellisen esimerkkiohjelman ei tämän mukaan
kuuluisi toimia lainkaan!
Jokaisen säikeen tulisi synkronoitua
pääsäikeeseen jos säikeen tarvitsee mm.
päivittää näyttöä. Näin säikeestä
tulee hetkellisesti osa pääsäiettä, jolla on VCL:än
käyttöoikeus. Edellisessä esimerkissä tämä
tehtäisiin esimerkiksi siten, että
Text.Caption := IntToStr(n)+mes;
siirrettäisiin esimerkiksi omaksi metodikseen
nimeltä UpdateDisplay ja metodia
kutsuttaisiin:
Synchronize(UpdateDisplay);
Synkronointikutsuun kelpaa vain parametriton säikeen
metodi. Mahdollinen parametrin välitys (edellä
mes) pitää valitettavasti hoitaa olion
attribuuttien avulla.
Esimerkkiohjelma tosin toimii synkronoituna huomattavasti
huonommin, mutta esimerkiksi Canvasta
käsittelevät säikeet on pakko synkronoida.
Muuta
saiedemo.pas-esimerkissä
näytön päivitys synkronoiduksi. Kokeile muuttaa säikeiden
prioriteettia.
Tehtävä
2.20 Erillinen näytön päivitys
Toinen tapa hoitaa näytön päivitys
”siististi” olisi esimeriksi seuraava: Lisää
cLaskuri-luokkaan:
attribuutti: DispText -
näytössä oleva teksti
metodi: IsUpdated -
palauttaa tiedon onko laskuri päivitetty
metodi: UpdateDisplay -
päivittää näytön
Pääohjelmassa voi sitten esimerkiksi
”kellokeskeytysten” avulla kysellä mitkä laskurit
tarvitsevat päivitystä ja sitten päivittää ne. Tai
UpdateDisplay
voi suoraan olla sellainen, ettei se turhaan päivitä
näyttöä. Tällä menetelmällä säikeet
saavat rauhassa keskittyä laskemiseen ja pääohjelma hoitelee
näyttöä. Vaikka metodit ovatkin säikeen sisällä,
ne suoritetaan pääohjelman säikeessä, mikäli
pääohjelma niitä kutsuu.
3. Tietokantojen
käyttö
Luvun pääaiheet:
- tietokantataulujen
käyttö
Delphissä
- paneelit
auttamassa komponenttien sijoittelua
- SQL-kyselyt
- dynaamisesti
luotavat tietueen kentät
- luvun
esimerkit hakemistossa puh
Eräs
Delphin vahvimmista puolista on mahdollisuus
vähällä ohjelmoimisella käsitellä valmiita, jopa muussa
koneessa olevia tietokantoja.
3.1 Tietokantakomponentit
Seuraavassa kuvassa on esitelty
Delphiin tietokantakomponenttien
toimintaperiaate
Kuva
3.1 Tietokantakomponenttien toimintaperiaate
3.2 Yhden
taulun esimerkki
Aloitamme aivan yksinkertaisella
puhelinluettelo-esimerkillä. Seuraava ohjelma syntyy kirjoittamatta
riviäkään ohjelmakoodia:
Kuva
3.2 "Valmis" puhelinluettelo ilman koodaamista
3.2.1 Tietokantataulun
luominen
- Käynnistä
aluksi Delphin mukana tullut
Database Desktop
(DBD).
- Tarkista
että työhakemisto on oikea - vaihda tarvittaessa
- Luo
uusi taulu. Jos mitään ihmeominaisuuksia ei tulla tarvitsemaan,
kannattaa ehkä taulun tyypiksi valita Paradox
3.5. Määrittele esim. seuraavat
kentät:
Kuva
3.3 Tietokantataulun kenttien määrittely
- Talleta
taulu vaikkapa nimelle puh
- Avaa
edellä luotu taulu puh ja täytä
aluksi muutama henkilö:
- Sulje
DBD.
Kuva
3.4 Valmis tietokantataulu
3.2.2 Delphi-sovellus,
joka käyttää valmista taulua
Nyt kun meillä on valmis tietokantataulu, voimme
tehdä Delphi-sovelluksen, jolla taulua
käsitellään. Tämä voitaisiin tehdä vielä
helpommin käyttämällä valmista tietokanta-experttiä,
mutta asian ymmärtämiseksi teemme vähän enemmän
käsityötä:
- Luo
uusi tyhjä Delphi-sovellus
- Muuta
lomakkeen nimi ja otsikko sopivasti
- Aluksi
tarvitaan yhteys itse tietokantaan. Tämä voidaan tehdä joko
taulukko-komponentilla tai SQL-komponentilla
Query
(Structured Query Language), jotka
löytyvät Data Access -sivulta.
Valitsemme yleiskäyttöisyyden vuoksi
SQL-komponentin. Sijoita komponentti mihin
tahansa lomakkeella (näkymätön komponentti).
- Laita
ensin nimi ja SQL-ominaisuus ja sen jälkeen muut
ominaisuudet:
Name = QueryPuh
SQL.Strings = select * from puh
Active = True
RequestLive = True
- Yhteys
tauluun on valmis. Seuraavaksi tarvitaan komponentti, joka käsittelee
yhtä tietuetta (=taulun yksi rivi). Lisää
DataSource-komponentti johonkin ja muuta
ominaisuudet:
Name = DataSourcePuh
DataSet = QueryPuh
- Lopuksi
tarvitaan vielä jokin komponentti, joka näyttää tietueen
käyttäjälle. Näiksi voidaan Data Controls
-sivulta valita joko yksittäisiä kenttiä tai jopa koko
taulun näyttävä komponentti DBGrid.
Tämä on näkyvä komponentti, joten sijoita se siten kuin
haluat taulun näkyvän näytöllä. Muuta
ominaisuudet:
name = DBGridPuh
DataSource = DataSourcePuh
- Nyt
voit jopa kokeilla ohjelman toimintaa!
- Lisätään
vielä komponentti, jolla on helppo liikkua taulukossa edestakaisin:
DBNavigator:
name = DBNavigatorPuh
DataSource = DataSourcePuh
- Lopuksi
komponentti, joka näyttää isolla kohdalla olevan henkilön
nimen: DBEdit
name = DBEditNimi
Color = clYellow
DataSource = DataSourcePuh
DataField = 'Nimi'
Font.Height = 18
Font.Name = MS Sans Serif
Font.Style = [fsBold]
Tehtävä
3.21 Database form
Luo edellisen esimerkin sovellus seuraavasti:
File/New/Forms/Database
Form (Delphi 2.0) tai
Options/Gallery/Database
form (Delphi 1.0).
Kokeile vielä saman
"expertin" avulla
lisätä lomake, jossa onkin "lomakemuotoinen näkyvä"
tietueeseen.
3.3 Paneelit
Edellisessä ohjelmassa on se huono puoli, että
mikäli ikkunan kokoa suurennetaan, ei taulukosta näy yhtään
enempää. Useilla komponenteilla on kuitenkin
Align-ominaisuus,
jolla voidaan säätää miten komponentti muuttaa kokoaan sen
isä-ikkunan muuttaessa kokoaan. Eli lisäämällä sopiva
määrä "ylimääräisiä" isä-ikkunoita,
saadaan sovelluksen komponentit toimimaan halutulla tavalla. Valitettavasti
Delphissä ei voi suoraan
siirtää komponenttia toisen ikkunan omistukseen (?), mutta
tämä voidaan onneksi kiertää leikekirjan kautta.
3.3.1 Ikkunan
jakaminen kahteen osaan
Tavoitteena on nyt aluksi jakaa sovellusikkuna kahteen
loogiseen osaan, joista ylemmässä (PanelNimi),
aina samankorkuisena pysyvässä, on henkilön nimi ja alemassa
(PanelGrid) ikkunan koon mukaan muuttuva
tietokantataulu:
Ominaisuudet paneeleille laitetaan seuraavasti
name = PanelNimi
Height = 40
Align = alTop
Caption = ''
name = PanelGrid
Align = alClient
Caption = ''
Tässä Align-ominaisuus
määrää miten komponentti - tässä tapauksessa
paneeli - muuttaa kokoaan.
- alTop
tarkoittaa, että paneeli menee aina ylimpään mahdolliseen
paikkaan korkeutensa säilyttäen, leveys muuttuu, täyttäen
kaiken tilan (mikä isä-ikkunan sisällä on
käytettävissä). Mikäli kaksi alTop
paneelia on päällekkäin, menee ylempi isä-ikkunansa
yläreunaan ja alempi alkaa ylemmän alareunasta. Tällöin
kumpikin säilyttää korkeutensa.
- alClient
tarkoittaa, että komponentti täyttää kaiken
isä-ikkunasta jäljelle jäävän tilan.
- alBottom
on alTopin vastakohta.
- alLeft
ja alRight
säilyttävät komponentin leveyden, mutta täyttävät
korkeussuunnassa kaiken mahdollisen tilan.
3.3.2 Ikkunan
jakaminen kolmeen osaan
Mikäli esim.
alTop ja
alRight kohtaavat, voittaa
alTop
kiistelyn tilan leveydestä. Tämän takia usein joutuu laittamaan
ylimääräisiä paneeleja jakamaan tila ensin pystysuorasti ja
sitten näiden sisään paneeleja jakamaan vaakasuorasti.
Seuraavassa esimerkissä ikkuna on ensin jaettu pystysuorasti kahtia
Panel1 ja
Panel2.
Panel2 säilyttää aina leveytensä
(
alRight). Sitten
Panel1 on
laitettu täyttämään koko jäljelle jäänyt tila
(
alClient) ja kun
Panel1 on
aktiivinen, niin sen sisälle on lisätty
Panel3,
joka aina säilyttää korkeutensa (
alTop).
Isyys määrätään siis sillä, mikä komponentti
on aktiivinen kun uusi komponentti lisätään. "Isänä"
voi toimi mm.
Form,
GroupBox,
RadioGroup,
TabbedNotebook ja
Panel.
Kuva
3.6 Ikkunan jako kolmeen osaan
Tehtävä
3.22 Ikkunan jakaminen 9 osaan
Kokeile mitkä paneelit pitää sijoittaa,
jotta saat seuraan jaon ikkunoilla: A, B, C ja D säilyttävä aina
sekä korkeutensa ja leveytensä ja E muuttaa sekä korkeutta
että leveyttä ikkunan koon muuttuessa:
3.3.3 Paneelien
näkyminen
Paneelien väliset rajat voidaan tietysti tarpeen mukaan
tehdä näkyviksi tai näkymättömiksi. Kokeile!
3.3.4 Suhteellisen
koon säilyttäminen
Mikäli ikkuna halutaan jakaa esim. 3 yhtäsuureen
osaan korkeussuunnassa, joudutaan itse ohjelmoimaan miten paneelien koot
muuttuvat. Tämä voidaan kirjoittaa esim.
FormResize-tapahtumaan:
{ Panel1.Align = alTop }
{ Panel2.Align = alTop }
{ Panel3.Align = alClient }
procedure TFormPanelDemo.FormResize(Sender: TObject);
var korkeus:integer;
begin
korkeus := ClientHeight div 3;
Panel1.Height := korkeus;
Panel2.Height := korkeus;
end;
Tehtävä
3.23 Ikkunan jakaminen 9 osaan ¼ suhteessa
Muuta edellistä yhdeksään osaan jaettua
ohjelmaa siten, että ikkunoiden A, B, C ja D leveydet ja korkeudet ovat
aina ¼ koko ikkunan leveydestä ja korkeudesta
3.3.5 Paneelien
lisääminen jälkikäteen
Olisi tietysti mainiota jos paneelit osattaisiin sijoittaa
etukäteen. Usein paneelien tarpeen kuitenkin huomaa vasta
jälkeenpäin ja paneeleja pitää laittaa olemassa olevien
komponenttien "alle". Näinhän meidän
puhelinluettelo-esimerkissäkin:
Lisätään ensin
PanelNimi
- Leikkaa
leikekirjaan (Cut)
DBEditNimi
- Lisää
PanelNimi ja laita Align =
alTop
- Varmista
että PanelNimi on valittuna.
- Liimaa
(Paste) DBEditNimi paneeliin
ja siirrä siten oikealle
kohdalleen
Seuraavaksi lisätään
PanelGrid. Tähän tulee kaksi
komponenttia:
- Valitse
ensin vaikkapa DBNavigatorPuh ja laita
Shift pohjaan ja valitse
DBGridPuh. Varmista että molemmat ja ainoastaan
nämä on valittuna (valinta näkyy harmaana).
- Leikkaa
molemmat komponentit leikekirjaan (Cut).
- Klikkaa
lomaketta tyhjältä kohdalta, jottei seuraava paneeli mene vahingossa
PanelNimi-paneelin sisälle
- Lisää
PanelGrid ja laita Align =
alClient
- Varmista
että PanelNimi on valittuna.
- Liimaa
(Paste) leikekirjan sisältö paneeliin ja
siirrä siten komponentit oikealle kohdalleen.
- Laita
DBNavigatorPuhiin Align = alTop
- Laita
DBGridPuhiin Align = alClient
- Kokeile
muuttaa ikkunan kokoa ja tarkista että komponenttien koot muuttuvat
halutulla tavalla
3.3.6 Muiden
komponenttien koon automaattinen koon muutos
Kaikkien komponenttien
Align-ominaisuutta ei voi muuttaa suunnitteluaikana (ei
ole tietoa miksi ei voi!). Tarpeen vaatiessa
Align-ominaisuus voidaan kuitenkin muuttaa ajon aikana
sijoituksella sopivassa tapahtumassa, esimerkiksi lomakkeen luonnissa:
ButtonNollaa.Align := alBottom;
Tehtävä
3.24
Nappulat nurkissa
Muuta 9-osaan ¼ -suhteessa jaettua ohjelmaa siten,
että kussakin nurkassa on nappula, joka on koko nurkkapaneelin
kokoinen.
3.4 SQL-kyselyt
SQL-komponenttiin laitettiin hakuehto
select * from puh
Tämä tarkoittaa, että valitaan kaikki
kentät (*) taulusta puh.
Muutetaan ohjelmaa siten, että hakuehto voidaan kirjoittaa itse.
- Lisää
komponentit Edit ja Button
paneeliin PanelNimi:
TEdit:
name = EditHakuehto
Text = 'select * from puh'
TButton:
name = ButtonHae
Caption = '&Hae'
Default = True
- Laita
Hae-nappulan
tapahtumaksi:
procedure TFormPuh.ButtonHaeClick(Sender: TObject);
begin
QueryPuh.Close;
QueryPuh.SQL.Clear;
QueryPuh.SQL.Add(EditHakuehto.Text);
QueryPuh.Open;
end;
- Kokeile
ajaa ohjelmaa esim.
SQL-hakuehdoilla:
1: select nimi from puh
2: select nimi,postinumero from puh
3: select * from puh where nimi > "Bond"
4: select * from puh where nimi like "%p%"
5: select * from puh where Upper(nimi) like "%P%"
6: select nimi,osoite from puh order by osoite
7: select osoite from puh
Huomattakoon ettei järjestetyn haun
(
order by) tulosta voida muokata!
Delphi
1.0:ssa rajoitukset ovat vielä voimakkaammat, esim.
like-hakuehdolla haettuakaan taulua ei voi
editoida.
Tehtävä
3.25 Haku nimen alkuosan perusteella
Hakuehdolla
select * from puh where upper(substring(nimi from 7 for 2))
="AK"
löydettäisiin mm.
Ankka
Aku. Lisää ohjelmaan
Edit-ikkuna,
johon käyttäjä voi kirjoittaa nimeä alusta päin. Kun
käyttäjä on kirjoittanut
a,
haetaan kaikki
a:lla
alkavat nimet, kun käyttäjä painaa vielä lisäksi
n,
haetaan kaikki an
-alkavat nimet jne. Haku tapahtuu heti kun kirjainta on painettu.
Onko tehtävän alussa annettu vinkki sopiva tähän
tarkoitukseen, vai olisiko joku vielä yksinkertaisempi hakuehto
käytettävissä. Jos haun tulos halutaan korjailtavaksi, tulee
tämä "ilmaiseksi"
Delphi2.0:ssa,
mutta
1.0:ssa
joutuu koko haun tekemään toisella tavalla.
(idean alku
Delphi
1.0:aa varten :
"A"<=nimi
and nimi <"B")
3.5 Tietueen
ominaisuuksien selvittäminen
Hakuehto "select osoite from puh"
päättyy virheeseen. Miksi? Koska
DBEditNimi-komponentti tarvitsee
nimi-kenttää ja ao. haun tuloksena
nimikenttää ei saada. Voisimme muuttaa ohjelmaa siten, että
DBEditNimi-komponentissa olisikin aina tietueen 1.
kentän tiedot:
- Lisää
Hae-nappulan tapahtuman
alkuun:
DBEditNimi.DataField := '';
- ja
tapahtuman
loppuun:
DBEditNimi.DataField := QueryPuh.Fields[0].FieldName;
- Kokeile
nyt esim.
hakuehtoa:
select puh,osoite from puh
Voisimme vielä muuttaa
ohjelmaa siten, että "isossa" kenttäikkunassa näkyy aina
aktiivisen kentän arvo:
- Lisää
käsittelijä taulukon sarakkeen
vaihtumiselle
procedure TFormPuh.DBGridPuhColEnter(Sender: TObject);
begin
DBEditNimi.DataField := DBGridPuh.SelectedField.FieldName;
end;
3.6 Dynaamisesti
luotavat data-kontrollit
DataForm-Expertillä
syntyi mukavasti lomake, jossa on kaikki tietueen kentät
näkyvissä. Huono puoli tässä on se, että mikäli
tehdään hakuja, joissa rajoitetaan kenttien
lukumäärää, ei kaikkia tarvittavia lomakkeeseen olekaan
saatavilla.
Muutetaan vielä ohjelmamme sellaiseksi, että
siinä on kaksi sivua:
- sivu,
jolla näkyy taulu taulumuotoisena
- sivu,
jolla aktiivinen tietue näkyy lomakemuotoisena, lomake generoidaan haun
perusteella dynaamisesti
3.6.1 TabbedNotebook
Lisätään aluksi
DBGridPuh:in tilalle "lärpsykkäkirja":
- Aktivoi
DBGridPuh ja leikkaa se leikekirjaan
- Aktivoi
PanelGrid ja laita sille
TabbedNotebook-komponentti:
name = TabbedNotebookPuh
Align = alClient
Pages = '&Taulu','&Lomake' (tuplaklikataan Pages ominaisuutta)
- Valitse
hiiren oikealla näppäimellä
TabbedNotebookPuh ja valitse sivu
Taulu.
- Liimaa
leikekirjassa oleva TabbedNotebookPuh tälle
sivulle
- Valitse
sivu Lomake ja lisää tälle sivulla
ScrollBox:
name = ScrollBoxLomake
Align = alClient
- Kokeile
ajaa ohjelmaa.
3.6.2 PageControl
Delphi
6.0:ssa on TabbedNotebookkia parempi komponentti:
PageControl.
Kokeile
edellinen esimerkki PageControlin avulla.
3.6.3 Apuluokka
cKentat
Kirjoitetaan ensin apuluokka cKentat,
joka hoitelee kontrollien lisäämisen:
dynkent.pas
- luokka dynaamisten kenttien tekemiseen
unit dynkent;
{
Purpose: Dynaamisesti lisätä taulun kenttiä paneelin sisälle
Author: Vesa Lappalainen
Date: 21.08.1996
Usage: Alustus:
olion varaus: kentat.cKentat
kentat := cKentat.Create(MyPanel);
Täyttö:
kentat.LisaaKaikki(MyDataSource);
Poisto:
kentat.Free
}
interface
uses
DB, Classes, StdCtrls, DBCtrls, Controls;
const MAX_KENTTIA = 20;
C_KENTAN_KORKEUS = 0;
C_NIMEN_LEVEYS = 60;
C_EDIT_LEVEYS = 200;
type
{----------------------------------------------------------------------------}
cKentta = object { Pari: otsikko - tietueen kenttä }
nimi : TLabel;
edit : TDBEdit;
index : integer;
end;
{----------------------------------------------------------------------------}
cKentat = class { Luokka, joka tallettaa monta tieueen kentää esim. Paneliin }
private
kentat : array [0..MAX_KENTTIA] of cKentta; { Taulukko kentistä }
panel : TWinControl; { Panelin, johon kentät luod. }
n : integer; { Kenttien lukumäärä }
kentan_korkeus,
nimen_leveys,
edit_leveys:integer;
procedure Paikka(var kentta:cKentta); { Vaihtaa kentän paikanja koon}
procedure MuutaKoko; { Vaihtaa kaikk kenttien koot }
public
constructor Create(p:TWinControl);
destructor Destroy; override;
procedure Siivoa; { Poistaa kaikki kentät }
procedure Lisaa(s:string;data:TDataSource); { Lisää yhden kentän }
procedure LisaaKaikki(data:TDataSource); { Lisää kaikki kentät }
procedure SetEditLeveys(l:integer);
procedure SetNimenLeveys(l:integer);
procedure SetKentanKorkeus(h:integer);
end;
implementation
{-----------------------------------------------------------------------}
constructor cKentat.Create(p:TWinControl);
begin
inherited Create;
n := 0;
kentan_korkeus := C_KENTAN_KORKEUS;
nimen_leveys := C_NIMEN_LEVEYS;
edit_leveys := C_EDIT_LEVEYS;
panel := p;
end;
{-----------------------------------------------------------------------}
procedure cKentat.Siivoa;
var i:integer;
begin
for i:=0 to n-1 do begin
kentat[i].nimi.Free; kentat[i].nimi := NIL;
kentat[i].edit.Free; kentat[i].edit := NIL;
end;
n := 0;
end;
{-----------------------------------------------------------------------}
destructor cKentat.Destroy;
begin
Siivoa;
inherited Destroy;
end;
{-----------------------------------------------------------------------}
procedure cKentat.Paikka(var kentta:cKentta);
var korkeus:integer;
begin
korkeus := kentan_korkeus;
if ( korkeus <= 0 ) then korkeus := kentta.Edit.Height+1;
with ( kentta.nimi ) do begin
Left := 10;
Width := nimen_leveys;
Top := 10 + kentta.index*korkeus;
end;
with ( kentta.edit ) do begin
Left := 10 + kentta.nimi.Left + nimen_leveys;
Width := edit_leveys;
Top := kentta.nimi.Top;
end;
end;
{-----------------------------------------------------------------------}
procedure cKentat.MuutaKoko;
var i:integer;
begin
for i:=0 to n-1 do
Paikka(kentat[i]);
end;
{-----------------------------------------------------------------------}
procedure cKentat.Lisaa(s:string;data:TDataSource);
begin
if ( n >= MAX_KENTTIA ) then exit;
kentat[n].nimi := TLabel.Create(panel);
with ( kentat[n].nimi ) do begin
Caption := s;
Parent := panel;
end;
kentat[n].edit := TDBEdit.Create(panel);
with ( kentat[n].edit ) do begin
DataSource := data;
DataField := s;
Parent := panel;
end;
kentat[n].index := n;
Paikka(kentat[n]);
inc(n);
end;
{-----------------------------------------------------------------------}
procedure cKentat.LisaaKaikki(data:TDataSource);
var i:integer;
begin
Siivoa;
for i:=0 to data.DataSet.FieldCount-1 do
Lisaa(data.DataSet.Fields[i].FieldName,data);
end;
{-----------------------------------------------------------------------}
procedure cKentat.SetEditLeveys(l:integer);
begin
edit_leveys := l;
MuutaKoko;
end;
{-----------------------------------------------------------------------}
procedure cKentat.SetNimenLeveys(l:integer);
begin
nimen_leveys := l;
MuutaKoko;
end;
{-----------------------------------------------------------------------}
procedure cKentat.SetKentanKorkeus(h:integer);
begin
kentan_korkeus := h;
MuutaKoko;
end;
end.
3.6.4 Data-kontrollien
lisääminen dynaamisesti
Seuraavaksi lisätään luokan käyttö
omaan lomakkeeseemme, harmaalla on esitetty koodiin edellisen kerran
jälkeen tulleet muutokset:
puh.pas
- muutokset dynaamisten kenttien käyttämiseksi
unit puh;
interface
uses ...
dynkent;
type
TFormPuh = class(TForm)
...
ScrollBoxLomake: TScrollBox;
procedure ButtonHaeClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure ScrollBoxLomakeResize(Sender: TObject);
private
{ Private declarations }
kentat : cKentat;
procedure TeeHaku(s:string);
public
{ Public declarations }
end;
var
FormPuh: TFormPuh;
implementation
{$R *.DFM}
procedure TFormPuh.TeeHaku(s:string);
begin
kentat.Siivoa;
DBEditNimi.DataField := '';
QueryPuh.Close;
QueryPuh.SQL.Clear;
QueryPuh.SQL.Add(s);
QueryPuh.Open;
DBEditNimi.DataField := QueryPuh.Fields[0].FieldName;
kentat.LisaaKaikki(DataSourcePuh);
end;
procedure TFormPuh.ButtonHaeClick(Sender: TObject);
begin
TeeHaku(EditHakuehto.Text);
end;
procedure TFormPuh.FormCreate(Sender: TObject);
begin
kentat := cKentat.Create(ScrollBoxLomake);
TeeHaku(EditHakuehto.Text);
end;
procedure TFormPuh.FormDestroy(Sender: TObject);
begin
kentat.free;
end;
procedure TFormPuh.ScrollBoxLomakeResize(Sender: TObject);
begin
kentat.SetEditLeveys(ScrollBoxLomake.ClientWidth-100);
end;
end.
Kuva
3.8
Dynaamisesti luodut DBEdit-kentät
3.7 Useiden
tietokantataulujen yhdistäminen
Oikeassa ohjelmassa on lähes aina kyseessä
relaatiotietokannat ja näin ollen useammasta taulusta saatavan tiedon
yhdistäminen. Meidänkin esimerkissämme esimerkiksi
postiosoite on itseään toistavaa tietoa ja
kannattaisi ehkä käytännössä tehdä oma taulu,
jossa olisi pelkkiä
postinumero-postiosoite -pareja.
Varsinaisessa päätaulussa ei sitten olisi
postiosoitetta lainkaan, vaan postiosoite haettaisiin
postinumeron avulla (relaatio)
postiosoite-taulusta.
Tehtävä
3.26 Relaation hallitseminen
Delphillä
Kirjoita
DataFormExpertin
avulla ohjelma, joka hakee
postiosoitteen
postinumeron
avulla
postisosoite-taulusta.
Tehtävä
3.27 Monta puhelinnumeroa
Oikeastaan ei ole järkevää tehdä
taulua, jossa on kentät
puh1,
puh2
jne.. Voihan jollakin ihmisellä olla jopa 10 numeroa, mistä
häntä pitää tavoitella. Suunnittele relaatiotietokanta,
jossa jokaisella ihmisellä on mahdollisuus 0-n puhelinnumeroon. Pohdi
ratkaisun tilankäyttöä normaalitilanteessa verrattuna meidän
alkuperäiseen ratkaisuun. Kokeile toteuttaa taulut
DBD:llä
ja niitä käyttävä ohjelma
Delphillä.
3.8 Raportointi
Raportointi hoidetaan erillisillä komponenteilla, jotka
ovat suuresti vaihdelleet eri Delphi-versioiden välillä.
Kokeile raportin tekemistä
käyttämälläsi
Delphin versiolla.
3.9 XML-tietokannat
Jos sovelluksen tietokanta on pieni ja on
todennäköistä ettei siihen liity monia asiakkaita yhtä
aikaa, kannattaa tietokanta tehdä ehkä XML-tiedostoksi. Etuna on
silloin, että sovelluksen levittämisessä ei tarvitse jakaa
raskasta BDE tms. tietokantaa lainkaan. Midas.dll
on ainoa lisätiedosto sovelluksen ja XML-tiedoston lisäksi, joka
pitää toimittaa käyttäjälle.
Alla on esimerkki puhelinluettelosta tehtynä
TClientDataSet-komponenttia käyttäen.
PuhluForm.pas - tietokanta
XML-pohjaiseksi
unit PuhluForm;
{
Esimerkki siitä, kuinka käytetään tekstitiedostoa
ClientDataSet-komponentin avulla.
1) Lisää tarvittavat komponentit, erityisesti ClientDataSet
(ks. alla)
2) Liitä nyt tai myöhemmin DataSource ClientDataSet-komponenttiin
ja näkyvät komponentit DataSourceen.
3) Jos sinulla on valmis XML tms- tiedosto, niin laita se
ClientDataSet-komponentin (jatkossa cds) FileNameen.
Jos olet luomassa uutta, niin kirjoita FileNameen
vaikka puh.xml (mielellään ei absoluuttista polkua).
4) Lisää cds:n FieldDefs-kohdasta haluamasi kentät.
Laita kenttien nimeksi selväkielisiä nimiä. Jopa
skandit kelpaavat.
5) Kun olet luonut kaikki kentät, paina cds:ää oikealla napilla
ja valitse Create DataSet.
6) Laita ohjelma käyntiin ja lisäile tietueita.
7) Voit halutessasi pistää cds:n Active := false ja jopa
poistaa nyt kenttien määritelmät jolloin dfm-tiedosto
pienenee. Tällöin laita FormCreateen
ClientDataSetPuh.Active := true;
8) Toimita ohjelman mukana puh.xml-tiedosto käyttäjille.
Vesa Lappalainen 16.9.2006
}
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, DBCtrls, Grids, DBGrids, ExtCtrls, DB,
DBClient;
type
TFormPuhlu = class(TForm)
PanelOtsikko: TPanel;
DBGridPuhlu: TDBGrid;
DBNavigatorPuhlu: TDBNavigator;
DBTextNimi: TDBText;
DataSourcePuh: TDataSource;
ClientDataSetPuh: TClientDataSet;
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
FormPuhlu: TFormPuhlu;
implementation
{$R *.dfm}
procedure TFormPuhlu.FormCreate(Sender: TObject);
begin
ClientDataSetPuh.Active := true;
end;
end.
Esimerkkitiedosto:
<?xml version="1.0" standalone="yes"?>
<DATAPACKET Version="2.0">
<METADATA>
<FIELDS>
<FIELD attrname="Nimi" fieldtype="string" WIDTH="30"/>
<FIELD attrname="Puh" fieldtype="string" WIDTH="20"/>
<FIELD attrname="Puh2" fieldtype="string" WIDTH="20"/>
<FIELD attrname="Puh3" fieldtype="string" WIDTH="20"/>
<FIELD attrname="Fax" fieldtype="string" WIDTH="20"/>
<FIELD attrname="Osoite" fieldtype="string" WIDTH="30"/>
<FIELD attrname="Postinumero" fieldtype="string" WIDTH="5"/>
<FIELD attrname="Postiosoite" fieldtype="string" WIDTH="25"/>
<FIELD attrname="Lisatietoja" fieldtype="string" WIDTH="50"/>
</FIELDS>
<PARAMS LCID="1053"/>
</METADATA>
<ROWDATA>
<ROW Nimi="Ankka Aku" Puh="12-12324" Osoite="Ankkakuja 13"
Postinumero="12345" Postiosoite="ANKKALINNA"/>
<ROW Nimi="Ankka Tupu" Puh="12-12324" Osoite="Ankkakuja 13"
Postinumero="12345" Postiosoite="ANKKALINNA"/>
<ROW Nimi="Bond James" Puh="007" Osoite="Jamesstreet 007"
Postinumero="007" Postiosoite="BOND"/>
<ROW Nimi="Ponteva Veli" Puh="111-222" Osoite="Pontevankuja 1"
Postinumero="12555" Postiosoite="PERÄMETSÄ"/>
<ROW Nimi="Susi Sepe" Puh="111-111" Osoite="Sepesudentie 13"
Postinumero="12555" Postiosoite="PERÄMETSÄ"
Lisatietoja="Jahtaa possuja"/>
<ROW Nimi="Veli Huilu"/>
<ROW Nimi="Ääliö Älä lyö" Lisatietoja="ööliä läikkyy"/>
</ROWDATA>
</DATAPACKET>
Vikana siis tässä ratkaisussa on se, että jos
samaa ”tietokantaa” haluaa päivittää useampi ohjelma
yhtä aikaa, niin se ei kunnolla onnistu. Mutta jos kyseessä on
todellakin yhden käyttäjän yksi ohjelma, niin tämä on
hyvä ja kevyt ratkaisu, joka oikein tehtynä on helppo myöhemmin
tarvittaessa muuttaa käyttämään oikeita tietokantoja.
Toki yhdenkin ohjelman sisällä voi tulla tarvetta
esittää samasta datasta useita eri näkymiä.
Tällöin menetellään niin, että jokaista taulua kohti
luodaan yksi ”master”
TClientDataSet-komponentti ja sitten jokaista erilaista
näkymää kohti luodaan oma
TClientDataSet-komponentti, joka kloonataan
käyttämään pääkomponenttiaan.
...
DSKokoPuhlu : TClientDataSet;
DSPostinmeroNakyma : TClientDataSet;
...
DSPostinumeroNakyma.CloneCursor(DSKokoPuhlu);
DSPostinumeroNakyma.Filter := ...
/// editoinnin jälkeen
DSKokoPuhlu.MergeChangeLog();
DSKokoPuhlu.SaveToFile();
...
Tällöin osanäkymä aina tiedottaa
muutoksista pääkomponentilleen ja pääkomponentti jokaisella
mahdolliselle muulle osanäkymälle. Tiedoston käsittely
pitää muistaa hoitaa pääkomponentin kautta.
Mikäli käytössä on oikea
tietokantapalvelin, niin silloin
TClienDataSet-komponentti liitetään
käyttämään tietokantapalvelinta pitemmän ketjun
kautta:
object SQLConnectionPuh: TSQLConnection
/// Tästä yhteys oikeaan tietokantapalvelimeen
...
object SQLDataSetPuh: TSQLDataSet
SQLConnection = SQLConnectionPuh
CommandText = 'select * from PUH'
...
object DataSetProviderPuh: TDataSetProvider
DataSet = SQLDataSetPuh
...
object ClientDataSetPuh: TClientDataSet
ProviderName = 'DataSetProviderPuh'
4. Multimediaa
Delphillä
Luvun pääaiheet:
- valmiiden
.wav tiedostojen soittaminen
- kuvan
näyttäminen, .bmb, .wmf
- beta-version
koodit waapi/beta
- valmiin
version koodit
waapi-hakemistossa
Tämän monisteen viimeisenä malliohjelmana
toteutamme 1½-4 vuotiaille sopivan kirjainten opetteluohjelman:
WinAapinen.
4.1 Lähtökohta
Ensimmäinen tavoite on tehdä ohjelma, joka
painettaessa esimerkiksi kirjainta A, sanoo
ääneen AUTO, näyttää ruudulla isoilla kirjoitetun
tekstin AUTO, sekä lisäksi vielä
auton kuvan. Ohjelman pitää kestää "jumiintumatta"
1½-vuotiaan käyttöä!
Toteutus on helpointa aloittaa siitä, mistä
saataisiin puhuttavat äänet. Jos olisi käytössä
puhesyntetisaattoriohjelma, voitaisiin äänet ottaa sieltä.
Käytännössä on ehkä kuitenkin helpointa
äänittää äänet itse. Usein äänikortin
mukana tulee ainakin jokin ohjelma, jolla voidaan äänittää
.wav-tiedostoja. Äänitetään
aluksi muutama ääni jotta pääsemme alkuun ohjelman teossa.
Esimerkiksi
auto.wav,
aiti.wav jne.
Kuvat järjestetään vastaavasti tiedostoihin
auto.bmp, aiti.bmp
jne. Myös .wmf
(=Windows MetaFile) tiedostojen
näyttäminen tulee onnistumaan.
4.1.1 Tietokanta
Miten sitten yhdistämme kirjaimet vastaaviin kuviin ja
ääniin. Yksi mahdollisuus olisi tekstitiedosto, jossa olisi
rivi:
A |auto.wav |auto.bmp |auto
M |mummo.wav| |mummo
O |ovi.wav |ovi.bmp |ovi
Ä |aiti.wav | |äiti
Toisaalta saamme etsimisen "ilmaiseksi" jos annamme
tietokantahaun tehdä sen. Luomme siis
DBD:llä tietokantataulun
waapi.db:
Field Name Type Size Key
-----------------------------------------------------
Kirjain A 3 *
Ääni A 20
Kuva A 20
Sana A 20
Taulu voidaan aluksi täyttää vaikkapa em.
tietueilla.
4.2 Äänen
soittaminen
4.2.1 Lyhyt
esimerkki
Ennen kuin yhdistämme tietokantataulun ohjelmaamme,
teemme pienen kokeen:
- Avaa
uusi projekti
- Laita
lomakkeelle:
Button:
Name = ButtonSoita
Caption = '&Soita'
MediaPlayer: (system-sivu)
Name = MediaPlayerWav
AutoOpen = True
FileName = auto.wav
- Kirjoita
Soita-nappulan
tapahtumaksi:
procedure TForm1.ButtonSoitaClick(Sender: TObject);
begin
MediaPlayerWav.Play;
end;
- Kokeile
ohjelman toimintaa.
Siinä kaikki! Periaatteessa ainoa mitä
meidän siis tarvitsee tehdä, on sijoittaa
MediaPlayer -komponentin FileName
ominaisuuteen sen tiedoston nimi, jonka haluamme soittaa. Samalla komponentilla
voimme soittaa myös .mid (midi-musiikki),
.avi (audiovideo) yms. tieostoja. Kokeile!
4.2.2 Tietokantataulun
lisääminen
Yhdistetäänpä tietokantataulu edelliseen
esimerkkiin.
- Ota
nappula pois.
- Nimeä
lomake Aapinen ja otsikoksi
WinAapinen
- Talleta
nimelle: unit: aapinen.pas ja projekti:
waapi.dpr
- Lisää
seuraavat komponentit:
Query:
Name = QueryAapinen
SQL.Strings = 'select * from waapi'
Active = True
DataSource:
Name = DataSourceAapinen
DataSet = QueryAapinen
DBEdit:
Name = DBEditSana
Width = ... iso ...
CharCase = ecUpperCase
Color = clYellow
DataField = Sana
DataSource = DataSourceAapinen
Font.Height = ... iso ...
Font.Name = 'Arial'
Font.Style = fsBold
4.2.3 Painetun
kirjaimen tunnistaminen
Mistä tahansa lomakkeella painetusta kirjaimesta
saadaan viesti OnKeyPress. Kuitenkin
Windows saattaa käsitellä suuren
osan näppäinten painalluksista. Siksi pitääkin ensin
muuttaa lomakkeen Aapinen ominaisuus
KeyPreview = True. Näin kaikki painallukset tulevat
ENSIN omalle ohjelmallemme ja vasta sitten joku muu saa käsitellä
niitä.
4.2.4 Taulusta
haetun äänen soittaminen
- Kirjoitetaan
lomakkeelle tapahtuman
käsittelijä:
procedure TFormAapinen.FormKeyPress(Sender: TObject; var Key: Char);
var nimi,c:string; lkm:integer;
begin
c := AnsiUpperCase(Key);
QueryAapinen.Close;
QueryAapinen.SQL.Clear;
QueryAapinen.SQL.Add('select * from WAapi where kirjain = "' + c +'"');
QueryAapinen.Open;
lkm := QueryAapinen.RecordCount;
if ( lkm = 0 ) then exit;
nimi := QueryAapinen.Fields[1].AsString;
if ( nimi = '' ) then exit;
MediaPlayerWav.Filename := nimi;
try
MediaPlayerWav.Open;
MediaPlayerWav.Play;
except
on EMCIDeviceError do ;
end;
end;
- Kokeile
ohjelmaa
4.2.5 Tietueen
kentän hakeminen nimen perusteella
Edellinen toteutus olisi arka sille, jos
ääni-kenttä olisikin jokin muu kuin tietueen toinen kenttä
(kenttien indeksit 0,1,2,3). Voitaisiin käyttää myös
lausetta:
nimi := QueryAapinen.FieldByName('Ääni').AsString;
4.2.6 Erillisen
kenttä-komponentin käyttö
Toisaalta jo ohjelman suunnitteluvaiheessa voitaisiin luoda
oma kenttä-komponentti:
- Tuplaklikkaa
QueryAapinen komponenttia
- Klikkaa
aukeavaa dialogia hiiren oikealla näppäimellä
- Valitse
Add Fields
- Valitse
kaikki kentät aktiiviseksi.
- Katso
Object Inspectorista: olet saanut 4 uuttaa
komponenttia:
QueryAapinenKirjain
QueryAapinenKuva
QueryAapinenni (Ääni muuttui ni)
QueryAaapinenSana
- Muuta
QueryAapinenni nimeksi
QueryAapinenAani.
- Nyt
äänitiedoston nimi saataisiin
kutsulla:
nimi := QueryAapinenAani.AsString;
4.2.7 Suora
hyppy taulun oikealle riville
Olisi vielä eräs mahdollisuus oikean
äänen löytämiseksi: Ei suoriteta tietokantahakua uudelleen,
vaan siirrytään aina kaikki tietueet sisältävässä
taulussa oikealle riville:
- Jos
QueryAapinen-komponentin tilalla
käytettäisiinkin TableAapinen-komponenttia,
niin voitaisiin hakuehdon muodostamisen sijasta vain
siirtyä:
TableAapinen.FindKey([c]);
Valitettavasti tämä ei toimi suoraan
Query-komponentille. Toimintoa voidaan kuitenkin
jäljitellä Queryn
Filter-ominaisuuden avulla.
Toteuta äänen soittaminen
siirtymällä taulukon oikealle riville.
4.3 Kuvan
esittäminen
Ohjelmasta puuttuu vielä kuvan piirto:
- Lisää
komponentti:
Image ( Additional-sivu )
Name = ImageBmp
koko = ... mahdollisiman iso ...
- Lisää
painetun näppäimen käsittelykoodin
loppuun:
nimi := QueryAapinenKuva.AsString;
if ( nimi = '' ) then exit;
ImageBmp.Picture.LoadFromFile(nimi);
- Aja
ohjelma :-)
Ohjelmassa on vielä se vika,
että kuva ei aina välttämättä mahdu sille varattuun
tilaan. Toisaalta pienet kuvat ovat todella pieniä. Asiaa voitaisiin
auttaa laittamalla päälle ominaisuus
ImageBmp.Stretch. Kokeile! Nyt kuitenkin kuvat ovat
vääränmuotoisia. Ongelma voidaan korjata muuttamalla
ImageBmp-komponentin kokoa samassa suhteessa, mikä
on kuvan alkuperäinen suhde. Nämä laskut on tehty ohjelman
versiossa 1.0, joka löytyy monisteen lopussa olevasta
liitteestä.
4.4 WinAapinen
1.0
Ohjelman "valmis" versio on hieman monimutkaisempi:
- kutakin
kirjainta kohti voi olla useita sanoja, painettua kirjainta vastaava sana
arvotaan näistä
- kirjaimilla
tasonumerot, arvonta ottaa mukaan vain valitun tasonumerovälin
täyttävät sanat
- myös
kuvaikkunassa voidaan soittaa .avi-elokuvia =>
tyypin mukaan pitää valita joko MediaPlayer tai
Image
- mahdollisuus
"pelata" myös käänteisesti, eli ohjelma arpoo
välilyöntiä painamalla sanan ja kysyy esimerkiksi: "Mistä
tulee mummo". Tämän jälkeen pitää painaa
m ennen kuin voi jatkaa.
- voidaan
valita mitä tulee näyttöön kun painetaan kirjainta ja
mitä tulee näyttöön kun painetaan
välilyöntiä
- asetusten
tallettaminen .ini-tiedostoon.
- mahdollisuus
soittaa CD-levyjä taustamusiikkina
- fonttikoon
vaihtaminen
- systeemimenu
jätetty pois
- esto,
jonka aikana ei voi painaa uutta näppäintä, mutta jonka
jälkeen näppäimen painaminen katkaisee meneillään
olevan soiton
- erillinen
editointi-ohjelma WMuokkaa tietokannan
ylläpitoon
5. Omien
komponenttien tekeminen
Luvun pääaiheet:
- yksinkertainen
mallikomponentti
Tlaskuri
- muutama
muu esimerkkikomponentti:
TEditPanel,
TColorChange
- koostamalla
käytettävä luokka:
TIniSave
- koodit
comps/nimi-hakemistossa
Lopuksi otetaan vielä pikainen katsaus omien
komponenttien tekemiseen.
5.1 Miksi
omia komponentteja
Visual Basicin
vahvin puoli on erittäin helppo valmiiden
komponenttien käyttäminen. Tämä ominaisuus on myös
Delphissä.
Visual Basicissa omien komponenttien tekeminen
on todella työlästä (ennen .Net) ja vaatii
Windowsin perus-API -ohjelmointia.
Delphissä omien
komponenttien tekeminen on "lähes" samanlaista
kuin Object Pascalin luokkien tekeminen ja
voidaan siis tehdä ja testata
Delphillä.
Hyvin tehtyjen ja testattujen komponenttien avulla
ohjelmointi on helppoa ja koodin kirjoittamisen tarve vähenee radikaalisti.
Oikeastaan aina kun tekee uuden luokan, kannattaa miettiä olisiko
siitä uudelleen käytettäväksi komponentiksi.
5.2 Yksinkertainen
TLaskuri
Seuraavassa tehdään ensin yksinkertaistettu
autolaskuriin kelpaavasta komponentista.
Kirjoittamisvinkki Delphi 4.0:
1) Lisää ensin rivi: property Count : integer;
2) Paina Shift+Ctrl+C (Complete Class at Cursor)
3) Täydennä syntynyt SetCount -metodi.
4) Kirjoita rivi: constructor Create(...
5) Paina Shift+Ctrl+C
6) Täydennä Create-metodi
7) Täydennä puuttuvat virtual -määritykset
unit laskuri;
interface
uses SysUtils, Classes, Controls, Graphics, StdCtrls;
type
TLaskuri = class(TLabel)
private
FCount : integer;{ Ominaisuuksien attribuutit yleensä F-alkuisiksi}
protected
procedure SetCount(const Value: integer); virtual;
public
constructor Create(AOwner:TComponent); override;
function Inc(i:integer) : integer; virtual;
published { Published declarations } { Yleensä vain ominaisuudet. }
{ Ominaisuuksiin ilmoitetaan miten niitä luetaan ja miten asetetetaan }
{ mahdollinen oletusarvo. Oletusarvo on ainoastaan ohje siitä, ettei }
{ arvoa talleteta resurssitiedostoon, jos arvo on sama kuin oletus. }
{ Luku/kirjoitusohje voi olla saman tyyppisen attribuutin nimi tai }
{ aliohjelma asettamiseen ja funktio lukemiseen }
property Count:integer read FCount write SetCount default 0;
end;
procedure Register;
implementation
constructor TLaskuri.Create(AOwner:TComponent);
begin
inherited Create(AOwner); { Muista tämä!!! Muuten käy huonosti!!!}
Count := 0;
end;
procedure TLaskuri.SetCount(const Value: integer);
begin
FCount := Value;
inherited Caption := IntToStr(Count)+' '; { Välilyönti lop. on par.näk}
end;
function TLaskuri.Inc(i:integer) : integer;
begin
Count := Count + i;
Result := Count;
end;
procedure Register;
{ Komponentit pitää rekisteröidä komponenttisivulle }
begin
RegisterComponents('Samples', [TLaskuri]);
end;
end.
5.3 Väärinkäytön
poistaminen:
Komponentissamme on vielä pieni vika: voidaan
tehdä esimerkiksi sijoitus:
lask.Caption := 10;
ja kun tämän jälkeen lisätään
laskurin arvoa, ei seuraava arvo olekaan 11, vaan yksi suurempi kuin
FCount-attribuuttiin on jäänyt edelliseltä
kerralta.
Miten vika voidaan poistaa? Suositeltavin vaihtoehto
olisikin periä luokka TCustomLabel. Ero
TLabel-luokkaan on siinä, että
TCustomLabelissa on kaikki samat ominaisuudet kuin
TLabelissa, mutta
protected-osassa, eli niitä ei voi ulkopuolinen
käyttää. Tällöin em. sijoitus ei olisi mahdollista.
Kuitenkin moni muukin TLabelin kiva ominaisuus jäisi
suojatuksi. Ominaisuuksia voidaan siirtää kyllä julkaistulle
puolelle kirjoittamalla:
published // ks mallia TLabel-komponentin lähdekoodista StdCtrl.pas
property Align;
property Alignment;
property Anchors;
property AutoSize;
property BiDiMode;
// property Caption; // Tätä ei julkaista
property Color;
. . .
Yleensä kaikista komponenteista on ensin
TCustom... -versio joka on tarkoitettu
perittäväksi.
Toinen mahdollisuus on tehdä uusi
Caption-ominaisuus, joka korvaa
TLabelin
Caption-ominaisuuden:
Lisää
published
. . .
property Caption:string read GetCaption write SetCaption stored false;
ja tee vastaavat metodit (käytä
luokkatäydennintä):
function TLaskuri.GetCaption: string;
begin
Result := Trim(inherited Caption); { Varo rekursiota!!! }
end;
procedure TLaskuri.SetCaption(const Value: string);
begin
Count := StrToIntDef(Trim(Value),0);
end;
Huomaa inherited avainsanan
käyttö edeltäjäluokan ominaisuuden
käyttämiseksi.
5.4 Oletusarvojen
muuttaminen
Jos haluamme laskurimme tulevan valmiiksi isommalla fontilla
oikeaan reunaan, voimme tehdä vielä pieniä parannuksia:
Lisää valmiiden ominaisuuksien uudet
oletusarvot:
published
. . .
property Alignment default taRightJustify;
property AutoSize default False;
property Color default clAqua;
property ParentColor default false;
Tämä on vasta tieto siitä, että jos
ominaisuuden arvo sattuu olemaan sama kuin oletus, niin arvoa ei tarvitse tilan
säästämisen vuoksi tallettaa lomakkeelle. Varsinainen arvon
asetus pitää tehdä itse:
constructor TLaskuri.Create(AOwner:TComponent);
begin
inherited Create(AOwner); { Muista tämä!!! Muuten käy huonosti!!!}
Count := 0; { Defaulteissa luvatut arvot: }
Alignment := taRightJustify;
Color := clAqua;
ParentColor := False;
Font.Height := -32;
Font.Style := [fsBold];
AutoSize := False;
end;
5.5 Komponentin ikonin
piirtäminen
Jotta komponentti olisi tunnistettavissa
työkalupalkissa, sille kannattaa piirtää ikoni:
- Avaa
Tools/Image
editor
- Tee
uusi .dcr-tiedosto (Dynamic
Component Resource)
- Valitse
Contents oikealla näppäimellä ja tee uusi
24x24 pxl-kokoinen bitmap.
- Anna
kuvalle nimeksi TLASKURI (kirjoita nimi isoilla!)
- piirrä
ikoni (tuplaklikkaa kuvan nimeä)
- talleta
tiedosto nimelle laskuri.dcr (olettaen että
komponentti on nimellä
laskuri.pas)
5.6 Komponentin
lisääminen komponenttikirjastoon
Komponentti voidaan lisätä kirjastoon
seuraavasti (Delphi 3.0-):
- Kopioi
laskuri.dcr samaan paikkaan kuin
laskuri.pas (tänne on tehty
komponenttisivulle tuleva bittikartta TLASKURI, 24x24, Huom! nimi isolla)
- Valitse
Component/Install Component
- a)
Jos haluamasi kirjasto on jo olemassa, valitse "Into Existing package" ja sitten
Yes
- b)
Jos kirjastoa ei ole, valitse "Into New Package", kirjoita kirjaston nimi (esim.
lask) ja kuvaus ja OK
- Jos
kirjastoa (Package) pitää korjailla,
valitse esim. Component/Install Packages ja valitse
kirjasto ja Edit. Korjausten jälkeen paina
Compile ja tarvittaessa
Install
Tehtävä
5.30 Uusi uljas autolaskuri
Tee uusi
Autolaskuri-ohjelma
ja käytä siinä
Laskuri-komponentteja.
Tehtävä
5.31
TLisaaButton
Tee uusi komponentti
TLisaaButton,
jolla on
ominaisuutena
Laskuri
:
TLaskuri
- laskuri, jonka arvoa lisätään aina,
kun nappia painetaan
Lisays
:
Integer
- paljollako laskuria lisätään
Tehtävä
5.32 Autolaskuri vähällä koodaamisella
Tee autolaskuri em. komponenteilla niin, että
tarvitsee kirjoittaa vain yksi rivi koodia:
Nollaa-nappulalle
Tee vielä komponentti
TNollaa,
joka nollaa kaikki samalla lomakkeella olevat laskurit (ks.
ComponentCount
ja
Components).
Tehtävä
5.34 Autolaskuri
koodaamatta
Tee autolaskuri kirjoittamatta riviäkään
koodia. Jos lisätään uusi laskuri-lisää -pari, niin
tarvitaanko vieläkään koodia?
Tehtävä 5.35 Uusi
TNollaa
*Modifioi
TNollaa
-komponenttia siten, että sille voidaan antaa ominaisuutena
kaikki komponentit, jotka halutaan nollata.
Tehtävä
5.36 Laskuri
panelista
Peri
TLaskuri
panelista ja aseta
oletuksena sopivat reunukset päälle.
Tehtävä
5.37 Laskuri
TCustom???-komponentista
Peri
TLaskuri
TCustomLabelista
tai
TCustomPanelista
ja pidä huoli ettei
Caption-ominaisuutta
voi käyttää .
Tehtävä
5.38
LiikkuvaAuto-komponentti
Tee uusi komponentti
LiikkuvaAuto,
jolla on oma nopeus ja joka tutkii itse, milloin tulee seinä vastaan.
6. Tapahtumapohjainen
ohjelmointi
Edellisessä luvussa saimme tehtyä ensimmäisen
oman komponentin. Komponentin teko ei ollut kovin vaikeaa. Mutta komponentteja
tehdessä pitää ehkä sittenkin huomioida vähän
enemmänkin. Itse asiassa HYVÄN komponentin tekeminen on jo taidetta
sinänsä. Pitäisi pystyä ennakoimaan mitä mahdolliset
komponentin käyttäjät haluavat komponentiltamme. Tai
pitäisi ainakin jättää mahdollisuus periä
komponenttiamme siten, että halutut muutokset on mahdollista toteuttaa
itse. Esimerkki hyvästä komponenttihierarkiasta on
Delphin VCL-kirjasto.
Sen lähdekoodeihin kannattaa tutustua.
Tehdäänpä autolaskuriin pieni lisäys:
Summa-laskuri, jossa on kaikkien muiden laskureiden
summa. Tietysti ensin lisäämme
Autolaskuri-lomakkeelle:
LaskuriSumma : TLaskuri
seuraavaksi voi tulla mieleen tehdä nappuloihin
painamisiin muutoksia:
procedure TFormAutolaskuri.ButtonHAClick(Sender: TObject);
begin
LaskuriHA.Inc(1);
LaskuriSumma.Count := LaskuriHA.Count + LaskuriKA.Count;
end;
Tämä tietysti toimii, mutta sama koodimuutos
pitää tehdä kuorma-auton nappulaan ja myös
Nollaa-nappulaan. Ensimmäisessä
autolaskurissamme lisäsimme lopulta myös Drag
and Drop -ominaisuuksia. Näihinkin pitäisi tehdä em.
koodimuutos. Asiaa voidaan tietysti kiertää tekemällä
lisäämistä ja muuttamista varten sopivia aliohjelmia (tai
metodeja), jotka pitävät huolta myös summalaskurista. Mutta
ennenpitkää koodiin tulee kuitenkin jokin muutos,
jossa LaskuriSumma unohtuu
päivittää.
Mikä on parempi tapa hoitaa ongelma? Jos laskurit itse
kertoisivat omasta muutoksestaan, voitaisiin summalaskuria
päivittää aina kun jokin laskuri muuttuu. Siispä
toimeen:
Lisätään
TLaskuri-komponentin koodiin tapahtuma:
TLaskuri = class(TLabel)
private
. . .
FOnChange: TNotifyEvent;
. . .
published
. . .
property OnChange : TNotifyEvent read FOnChange write FOnChange;
end;
Tämä tapahtuma tulee komponentin
Events-välilehdelle, koska ominaisuuden
tyyppinä on metodi. Tämä lisäys ei vielä yksin
riitä. Meidän täytyy myös pitää huoli siitä,
että tapahtuman käsittelijää kutsutaan siellä
missä on tarkoituskin. Jos TLaskuri komponentti on
tehty hyvin, ei laskurin arvoa muuteta muualla kuin yhdessä paikassa.
Onneksi meidän kohdallamme näin on:
procedure TLaskuri.SetCount(const Value: integer);
begin
FCount := Value;
inherited Caption := IntToStr(Count)+' '; { Välilyönti lop. on par.näk}
if ( Assigned(OnChange) ) then OnChange(self);
end;
Eli jos joku on halunnut tapahtuman
käsiteltäväksi (Assigned(OnChange)),
kutsutaan sitä metodia, johon OnChange
-metodiosoitin osoittaa (OnChange(self)).
Itse autolaskuri toimii vielä kuten ennenkin, mutta jos
klikkaamme (muista kääntää komponentti uudelleen)
LaskuriHA:n
Events-välilehdellä
OnChange -tapahtumaa ja kirjoitamme koodin:
procedure TFormAutolaskuri.LaskuriHAChange(Sender: TObject);
begin
LaskuriSumma.Count := LaskuriHA.Count + LaskuriKA.Count;
end;
niin johan rupesi laskemaan. Sijoitetaan sama tapahtuma
vielä LaskuriKA:n OnChange
-tapahtumaksi.
Saavutettu hyöty on nyt siinä, että
muutettiinpa laskureita millä tavalla tahansa, niin muutos tulee aina
käsiteltyä. Pienenä miinuksena on se, että jos
lisätään esim. LaskuriPP, niin
tällöin summan laskukaavaa pitää muuttaa.
Voisimme tehdä myös erilaisen tapahtuman.
Tapahtuman, jossa kerrotaan itse laskurin lisäksi muutoksen suuruus.
Tällaista tapahtumaosoitinta ei ole tietenkään valmiina, mutta
voimme sellaisen tehdä itsekin:
type
TLaskuri = class;
TLaskuriEvent = procedure (sender:TLaskuri; diff:integer) of object;
TLaskuri = class(TLabel)
private
FCount : integer;
FOnChange: TLaskuriEvent;
. . .
published
. . .
property OnChange : TLaskuriEvent read FOnChange write FOnChange;
end;
Ja tietysti muutos itse laskurin asettamiseen:
procedure TLaskuri.SetCount(const Value: integer);
var diff : integer;
begin
diff := Value - Count;
FCount := Value;
inherited Caption := IntToStr(Count)+' '; { Välilyönti lop. on par.näk}
if ( Assigned(OnChange) ) then OnChange(self,diff);
end;
Nyt autolaskurissamme riittäisi koodi:
procedure TFormAutolaskuri.LaskuriHAChange(sender: TLaskuri;
diff: Integer);
begin
LaskuriSumma.Inc(diff);
end;
ja kun kaikki summaan halukkaat laskurit ilmoittavat saman
tapahtuman, niin summalaskuri pysyy ajantasalla vaikka lisäisimme kuinka
monta laskuria.
Nollautuuko
summalaskuri kuin painetaan
Nollaa-nappulaa?
Miksi?
Lisää laskuriin yläraja-ominaisuus ja
tapahtuma kun yläraja ylitetään.
Tehtävä
6.41
OnBeforeChange
Lisää laskuriin
OnBeforeChange
-tapahtuma, joka suoritetaan,
ennenkuin arvoa
muutetaan. Parametrina tapahtumalle on mm.
var
Accept:boolean,
johon tapahtumankäsittelijän tulee sijoittaa arvo, sen mukaan
sallitaanko muutos vai ei.
6.1 TLaskuri
unit laskuri;
interface
uses SysUtils, Classes, Controls, Graphics, StdCtrls;
type
TLaskuri = class;
TLaskuriEvent = procedure (sender:TLaskuri; diff:integer) of object;
TLaskuri = class(TLabel)
private
FCount : integer;
FOnChange: TLaskuriEvent;
function GetCaption: string;
procedure SetCaption(const Value: string);
protected
procedure SetCount(const Value: integer); virtual;
public
constructor Create(AOwner:TComponent); override;
function Inc(i:integer) : integer; virtual;
published { Published declarations } { Yleensä vain ominaisuudet. }
property Count:integer read FCount write SetCount default 0;
property Caption:string read GetCaption write SetCaption
stored false;
property OnChange : TLaskuriEvent read FOnChange write FOnChange;
{ Seuraavat ominaisuudet isältä, ainoastaan oletusarvot erilaiset }
property Alignment default taRightJustify;
property AutoSize default False;
property Color default clAqua;
property ParentColor default false;
end;
procedure Register;
implementation
constructor TLaskuri.Create(AOwner:TComponent);
begin
inherited Create(AOwner); { Muista tämä!!! Muuten käy huonosti!!!}
Count := 0; { Defaulteissa luvatut arvot: }
Alignment := taRightJustify;
Color := clAqua;
ParentColor := False;
Font.Height := -32;
Font.Style := [fsBold];
AutoSize := False;
end;
procedure TLaskuri.SetCount(const Value: integer);
var diff : integer;
begin
diff := Value - Count;
FCount := Value;
inherited Caption := IntToStr(Count)+' '; { Välilyönti lop. on par.näk}
if ( Assigned(OnChange) ) then OnChange(self,diff);
end;
function TLaskuri.Inc(i:integer) : integer;
begin
Count := Count + i;
Result := Count;
end;
procedure Register;
{ Komponentit pitää rekisteröidä komponenttisivulle }
begin
RegisterComponents('Samples', [TLaskuri]);
end;
function TLaskuri.GetCaption: string;
begin
Result := Trim(inherited Caption); { Varo rekursiota!!! }
end;
procedure TLaskuri.SetCaption(const Value: string);
begin
Count := StrToIntDef(Trim(Value),0);
end;
end.
Becks,
Ari:
Delphi, Sovelluksentekijän
opas, Suomen ATK-kustannus OY, 1996
Borland:
Delphi manuals - Borland International Inc, 1996
Borland:
Component Writer's Guide - Borland International Inc, 1996
Cantù,
Marco
: Mastering Delphi 6, SYBEX,
2001
Järvinen Jani,
Piispa Juha: Delphi, Sovellusten
Opas - Docendo 2000