For lang tid siden skrev jeg en artikel om hvordan man kunne tilføje et
bitmap til en form. Jens Borrisholt mente at det var en besværlig måde at
gøre det på, og det har han for så vidt ret i.
Jeg satte mig så ned og besluttede mig for at skrive en komponent som kunne
gøre det i stedet for. Jeg vil i denne artikel beskrive hvordan komponenten
fungerer og derigennem samtidig fortælle lidt om hvordan man skriver
komponenter.
Først og fremmest var det et krav at komponenten skulle være enkel at anvende.
Der skulle ikke være en forfærdentlig masse egenskaber som skulle sættes.
Dernæst skulle jeg have fundet ud af om komponenten skulle understøtte andre
billedformatter end bitmaps.
Endelig skulle jeg jo sørge for at der ikke opstår fejl som kan få programmet
til at gå ned f.eks.
Inklusiv erklæringer blev det til ialt 117 linier kode. Jeg vil i det
efterfølgende gennemgå denne kode, så man kan se hvordan det er gjort.
Det burde være ret let at udvide komponenten til at kunne anvende andre
billedformatter end de to der er her, nemlig Windows bitmaps (.BMP) og
jpeg. Disse to formatter er nemlig understøttet direkte af Delphi og kræver
derfor ikke tredjeparts klasser.
Komponenten kan kun anvendes med forme, og ikke f.eks. frames. Grunden til
dette er at en frame ikke har en Canvas egenskab. Komponenten burde dog let
kunne udvides til at anvendes med andre komponenter som har en Canvas.
Interface delen er jo vigtig idet den fortæller andre units hvad der befinder
sig i denne unit.
unit DueBackground;
interface
// Her fortæller vi hvilke units komponenten skal bruge. En enkelt skal lige
// nævnes. Det er Graphics som rummer understøttelse af bmp billedet, og derudover
// rumme definitionen af TGraphic klassen som er den essentielle del af komponenten.
uses
Forms, SysUtils, Graphics, Classes;
type
TDueBackground = class(TComponent)
private
FExtension : String; // Her gemmes billedets type f.eks. .bmp eller .jpg
FFileName : TFileName; // Filnavn og sti til billedet.
FBackground : TGraphic; // Denne variable er vigtig da den rummer selve billedet.
FMainForm : TForm; // Pegepind til formen som billedet skal vises på.
FOldPaint : TNotifyEvent; // Pegepind til OnPaint hændelsen for formen.
FOldResize : TNotifyEvent; // Pegepind til OnResize hændelsen for formen.
procedure SetFileName(const Value: TFileName); // Her sættes og fjernes billedet
Procedure DoPaint(Sender : TObject); // Her males billedet på formens canvas.
Procedure DoResize(Sender : TObject); // Her sørges for at billedet følger
// formen når denne resizes.
public
Constructor Create(AOwner : TComponent); Override;
Destructor Destroy; Override;
published
// Den eneste egenskab som er nødvendig
Property FileName : TFileName read FFileName write SetFileName;
end;
// Og så selvfølgelig register proceduren. Denne er nødvendig for at Delphi
// kan registrere komponenten.
procedure Register;
Som det ses er selve erklæringen af komponenten ikke særlig omfattende.
Udover diverse pegepinde (pointere) er der kun defineret tre variabler. En
af disse er kun nødvendig at hensyn til overskuelighed af koden og de to
andre er så de vigtige, nemlig
FFileName og
FBackground.
Derudover er der tre metoder som er vigtige.
SetFileName er langt den største metode og bruges til at definere billedet, dets type og starte processen med at tegne det på formen.
SetFileName bliver udført når man sætter komponentens
FileName egenskab.
DoPaint er en såkaldt
TNotifyEvent som anvendes af Delphi og derigennem Windows til at tegne formen.
DoPaint bruges til at tegne billedet på formen, og derefter udføre formens oprindelige
OnPaint event. Mere om dette senere.
DoResize er ligesom
DoPaint en
TNotifyEvent og den fortæller Windows hvad der skal ske med formen når den skal resizes (ændret størrelse). Det er det der sker når man tager fat i et hjørne, eller siden af formen og trækker. Ligesom
DoPaint erstatter
DoResize formens egen
OnResize hændelse for at sørge for at billedet følger med formen, når den bliver resizet.
Nu er vi vist klar til at fortsætte med implementationsdelen.
Implementationsdelen af en unit er den del som rummer selve koden. Udefra kan man ikke se hvad der ligger her, og det er en af de ting som objektorientering handler om. Interface delen fortæller hvad der er af klasser og hvilke metoder disse har, men implementationsdelen rummer selve (ha, ha) implementationen. Grunden til denne adskilles er at man kan ændre på måden en given metode afvikles på, uden at det har betydning for andre dele af et program. I dette tilfælde ville man f.eks. kunne tilføje understøttelse af gif billeder uden at det ville påvirke komponentens udseende for de programmer der anvender den.
implementation
// Her kan du tilføje andre units som f.eks. TGifImage af Anders Melander
// hvis du ønsker at kunne anvende gif billeder med denne komponent.
// Her og nu er der kun understøttelse for jpg billeder.
Uses
jpeg;
// Her fortæller vi Delpi at komponenten TDueBackground skal registreres under
// fanebladet DueComps. Du kan læse om hvad Register kan anvendes til i Delphis
// hjælp.
procedure Register;
begin
RegisterComponents('DueComps', [TDueBackground]);
end;
Nu er vi så nået til implementationen af selve komponenten. Først har vi
konstruktoren og destruktoren som de metoder der anvendes til at skabe og
nedlægge objekter af denne komponent eller klasse.
constructor TDueBackground.Create(AOwner: TComponent);
begin
// Sørg for at udføre andre opgaver som er implementeret i
// forfaderen (ancestor) til komponenten. Husk altid denne
// linie, da der kan være vigtige ting der skal udføre under
// skabelsen af et nyt objekt.
inherited;
// Nu sætter vi pegepindene som nævnt tidligere. Dette er vigtigt da vi
// ellers overskrive vigtige hændelser for formen.
FMainForm := TForm(Owner);
FOldPaint := FMainForm.OnPaint;
FOldResize := FMainForm.OnResize;
// Til sidst sætter vi vores egen hændelser. Dette er nødvendigt for at
// kunne udføres vores opgave, nemlig at tegne et billede på formens baggrund.
with FMainForm do
begin
OnPaint := DoPaint; // Vores egen OnPaint hændelse.
OnResize := DoResize; // Vores egen OnResize hændelse.
DoubleBuffered := TRUE; // Denne linie kan undværes, men så vil billedet
// flimre når vi bla. resizer.
end;
end;
Der er ikke meget der skal gøres i konstruktoren, som det ses sætter vi pegepinde
og hændelser og det er det hele. Nogle gange kan det være nødvendigt med f.eks.
udregninger i konstruktoren, men ikke her.
destructor TDueBackground.Destroy;
begin
// Hvis billedet er sat skal vi have ryddet op efter os, eller kan vi risikere
// at efterlade affald (garbage) som så binder vigtige Windows resourcer.
if assigned(FBackground) then
begin
FBackground.free;
FBackground := nil;
end;
// Så skal vi lige have sat de oprindelige hændelser tilbage. Det kunne jo være
// at de skal bruges.
FMainForm.OnPaint := FOldPaint;
FMainForm.OnResize:= FOldResize;
// Og så tilsidst husk inherited. Der kan jo være yderligere oprydningsopgaver
// skal udføres.
inherited;
end;
Destruktoren er altid vigtig i objektorientering. Den er nødvendig for at kunne
rydde op efter sig. Hvis man ikke tager den med, vil programmet ikke rydde ordenligt op og i værste fald vil Windows stå og mangle resourcer som kunne have været nyttide andre steder. Husker man ikke dette kan man risikere at rende i en besked fra Windows om at systemet er lavt på resourcer, og man bør genstarte. Det er der jo ikke meget sjovt i.
procedure TDueBackground.DoPaint(Sender: TObject);
begin
// Hvis der er defineret et billede ...
if assigned(FBackground) then
// skal det strækkes ud over hele formen og tegnes.
FMainForm.Canvas.StretchDraw(FMainForm.ClientRect, FBackground);
// Hvis formens OnPaint er defineret ...
if assigned(FOldPaint) then
// udfør da denne.
FOldPaint(Sender);
end;
Hvis man ønsker det kan man udvide komponenten med forskellige muligheder. Man kan f.eks. tegne billedet centreret eller tiled, ligesom med baggundsbilledet
i Windows. Jeg har valgt at nøjes med at billedet skal være trukket.
En lille hjemmeopgave:
Udvid komponenten så billedet tegnes centreret istedet
for strukket.
procedure TDueBackground.DoResize(Sender: TObject);
begin
// Her gentegnes billedet når formens størrelse bliver ændret.
FMainForm.Invalidate;
// Hvis formens OnResize er defineret ...
if assigned(FOldResize) then
// udfør da denne.
FOldResize(Sender);
end;
Der er forskellige metoder man kan bruge til at gentegne formen. Jeg har ikke
været i stand til at se nogen forskel på dem, selvom den nok skal være der.
Udover
Invalidate, som forøvrigt anvendes når hele formen skal gentegnes, kan man anvende
Refresh, eller
Repaint. Hvilken en der er bedst at anvende, er efter min bedste viden underordnet. Formålet er under alle omstændigheder at gentegne formen og det gør både
Invalidate, Refresh og
Repaint.
Nu kommer vi så til monster metoden
SetFileName på hele 34 linier.
procedure TDueBackground.SetFileName(const Value: TFileName);
begin
// Hvis det er det samme billede foretager vi os intet. Det er der ingen grund til.
if Value <> FFileName then
begin
// Sæt FFileName variable.
FFileName := Value;
// Hvis den er tom, skal billedet slettes.
if FFileName = '' then
begin
// Hvis den er tildelt ...
if assigned(FBackground) then
begin
// Frigiver vi baggrunden, så den ikke optager resourcer unødigt.
FBackground.free;
FBackground := nil;
end;
end
else
begin
// Find ud af hvad billedet type er.
FExtension := ExtractFileExt(FFileName);
// Hvis der er et jpg billede ...
if SameText(FExtension, '.jpg') or
SameText(FExtension, '.jpeg') then
begin
// Så skab en jpg grafik objekt
FBackground := TJPEGImage.Create;
// Og indlæs billedet.
FBackground.LoadFromFile(FFileName);
end
else
// ellers hvis det er et bitmap ...
if SameText(FExtension, '.bmp') then
begin
// så skab et bmp grafik objekt
FBackground := TBitmap.Create;
// og indlæs billedet.
FBackground.LoadFromFile(FFileName);
end;
end;
// Sørg for at formen bliver gentegnet uanset om billedet er blevet sat,
// eller fjernet.
FMainForm.Invalidate;
end;
end;
end. // vi er færdige med komponenten
SetFileName anvender en særlig teknik som man kun finder i objektorienteringen. Hvis man kigger tilbage på erklæringen af komponenten ser man at
FBackground er defineret som værende af typen
TGraphic.
Alligevel kan objektet oprettes både som
TJPEGImage og som
TBitmap. Hvordan kan det være? Teknikken hedder
polymorfi og det betyder at eftersom både
TJPEGImage og
TBitmap nedarver fra
TGraphic kan
FBackground skabes ud fra begge klasser. Det er nyttigt, når man som her ikke ved hvilken af klasserne man skal bruge, før man skal bruge den...
Hvis man vil tilføje gif understøttelse og
TGifImage klassen nedarver fra
TGraphic skal man bare gøre det samme en gang til. En grundig gennemgang af polymorfi begrebet vil være for meget her, men du kan finde masser af information om både dette og andre objektorienterings emner i bøger og ude på nettet.
Der er iøvrigt nogle udemærkede artikler om objektorienteret programmering i
dette forum, hvis man vil starte et sted.
Komplet kode kan downloades
herfra.