Ausgewählte Artikel zur Programmierung mit Delphi

Werner Voigt

 für 


Testbild unter der Lupe 4/2002

Autor: Dipl.-Math. Werner Voigt

veröffentlicht in Toolbox
Copyright 2003 © Werner Voigt und Toolbox
Bitte beachten Sie auch die Hinweise zur kommerziellen Nutzung

Problem:

Beschreibung eines für Umsteiger (oder Einsteiger) verständliches und leicht pflegbares Programm. Dieses Programm soll Lösungen zu einigen grundsätzlichen Programmierproblemen zeigen.

Lösung:

Ausgangspunkt für diesen Artikel sind die von einem Leser im Heft 3/2002 S.102 ff aufgeworfenen Fragen. Ursprünglich wollte ich im vorgelegten Quelltext nur klärend eingreifen, um einige Fragen des Autors zu beantworten.

Hier will ich den Versuch unternehmen, ein für Umsteiger (oder Einsteiger) verständliches und leicht pflegbares Programm zu (be-)schreiben. Dieses Programm soll Lösungen zu einigen grundsätzlichen Programmierproblemen zeigen. Deshalb beschreibe ich zuerst meine Lösung, um dann die noch offenen Fragen zu beantworten.

Wir machen es wie Egon Olsen. Am Anfang muß ein Plan her. Wir brauchen einen PC mit Monitor, Delphi und eine Aufgabenstellung.

Hier ist die Aufgabe:

Es soll ein Programm entstehen, mit dem man die Bildwiedergabequalität eines Monitors statisch begutachten kann. Das Programm soll die Lösung einiger typischer Programmieraufgaben zeigen.

  1. Dazu sollen verschiedene Testmuster (Gitter, Farbbalken, Kreise, ...) gezeichnet werden.
  2. Deren räumliche Anordnung und Größe soll das Programm passend zu den vorgefundenen Bildschirmparametern beim Start festlegen.
  3. Alle Eigenschaften der Testmuster sollen in einem Dialogfenster beliebig veränderbar sein.
  4. Die Werte sollen in Dateien gespeichert werden können.
  5. Der Dialog soll eine Vorschaumöglichkeit enthalten.
  6. Beim Programmstart soll der Programmbenutzer eine solche Einstellungsdatei aktivieren können.
  7. Zur genauen Ortung von Bildfehlern wird bei Bedarf die aktuelle Mausposition angezeigt. Weil bei TFT-Monitoren einzelne Pixel fehlerhaft sein könnten, brauchen wir speziell dafür einen Test. Ein Pixel kann fälschlich ständig leuchten oder dunkel bleiben. Dies kann eine Grundfarbe oder mehrere betreffen. So ein Pixel fällt auf, wenn der gesamte Bildschirm einfarbig (weiß, rot, blau, grün, schwarz) ist.
  8. Die Anzeige von Bildschirmparameter und Mausposition soll mit der Maus verschiebbar sein.
  9. Testgegenstand ist der gesamte Bildschirm, deshalb muß unser Programm auch die Taskbar und andere Startleisten überdecken können.

Nach dem die Aufgabe klar ist, überlegen wir jetzt, wo es Codierungsprobleme geben könnte, weil wir nicht wissen, wie irgendwas funktioniert.

Ein Kernpunkt des Programms sind Berechnungen der Testmustergeometrien bei den einzelnen Auflösungen. Es soll ja immer (nach der Meinung des Programmierers) zweckdienlich sein. Offensichtlich ist es sinnvoll, von der aktuellen Bildschirmauflösung auszugehen. Dann ist zu klären, wie sich alle anderen Größen daraus ableiten lassen. Einige Positionen kann man sofort bestimmen (z.B. Bildmitte). Bei anderen Angaben hilft vielleicht etwas Prozentrechnung weiter. Sollen verschiedene Objekte auf dem Bildschirm etwas gemeinsam haben, erhält das ein Objekt (aus meiner Sicht das Gitter) wiederum Priorität. Die Zentren der Kreise dann auf Gitterpunkte zu legen ist sicher nicht schwierig. Und die Farbbalken werden genau zwischen Gitterlinien eingepaßt. Deshalb sollte man schon bei der Gitterbemessung an dessen Vorreiterrolle denken.

In der Standardeinstellung sollen möglichst alle darstellbaren Elemente gezeigt werden. Also sind deren Eigenschaften zueinander passend festzulegen. Ausgangspunkt ist ein Gitternetz mit annähernd quadratischen Maschen. Gehen wir von 16 senkrechten Linien bei 800x600 Pixel aus, so ergibt sich daraus ein Abstand von 40 Pixel. Netterweise läßt sich auch 600 durch 40 teilen. Um bei anderen Auflösungen keine großen Probleme zu haben, wäre es schön, wenn die Rechnung immer so glatt aufginge. Tut sie aber vermutlich nicht. Entweder wir befassen uns jetzt mit Primfaktorzerlegung & Co, oder wir gehen von unseren Wünschen aus.

Dazu stellen wir uns das Ziel, mindestens eine Linienzahl (LzX) von minimal 16 bis maximal 48 für die senkrechten Linien so zu finden, daß wir bei der aktuellen Auflösung sich ein quadratisches Gitter erhalten. In UTestBild.pas findet sich in TMainForm.CalcParms eine entsprechende Berechnung.

Ein Fehler von einem Pixel ist dabei nicht zu vermeiden. Wenn z.B. die Folge 0,64, 128,... als Linienpositionen genommen wird, dann ist die letzte Linie bei 640er Auflösung bei 576, d.h. es ist alles korrekt, aber links steht eine Linie auf 0 und ihr Partner ist ein Pixel im Nirwana (auf 640). Damit muß man leben, es sei denn, es gelingt die Auflösung auf 641 Pixel zu setzen. Nur so würde ein Abstand von 64 Pixel zu einem exakt symmetrischen Bildaufbau führen. Hinweis: dieser Sachverhalt hat nichts mit der Zählung ab 0 oder 1 zu tun.

Problematisch ist es nur, wenn die Auflösungen nicht im Verhältnis 4:3 stehen (oder 16:9 falls jemand ein modernes Fernsehgerät als Monitor nutzt?). Diesen Fall mag betrachten, wer dieses Problem hat.

Hier nur soviel: man sollte die Einstellungen noch um eine manuelle Eingabe des Seitenverhältnisses erweitern. Etwas mehr zu rechnen ist dann natürlich auch. Für beide Bildschirmachsen läuft es dann getrennt. In diesem Fall können auch Kreise nicht über Mittelpunkt und Radius definiert werden. Hier kommt dann der Nutzen der Canvas-Methode Ellipse mit dem umschreibenden Rechteck voll zum Tragen. In diesem Beispielprogramm ist soetwas nicht enthalten.

Ein Programm mit einer zu fest programmierte Berechnung hat auch Nachteile. Ohne Eingriffsmöglichkeit würde dem Programmbenutzer die Sichtweise des Programmierers aufgedrängt. Und das Rechengebäude bricht zusammen, wenn man dem Benutzer die Möglichkeit einräumt, das Testbild interaktiv zu ändern. Deshalb halte ich den Rechenaufwand für die Standardeinstellung klein und biete lieber dem Programmbenutzer soviel Freiheit, wie ich sie selbst an seiner Stelle gern hätte. Programme sind primär nicht für ihren Autor da, selbst wenn er davon lebt!

Also schließen wir einen Kompromiß der seinen Namen verdient. Es wird beim Programmstart berechnet, dann ggf. das Ergebnis durch gespeicherte Daten ersetzt und danach sieht der Benutzer in beiden Fällen ein Testbild.

Fundamental für unser Programm ist die Frage nach der Zeichentechnik. An verschiedenen Stellen war schon zu lesen, wie man direkt auf dem Desktop zeichnet. Das funktioniert zwar, hat aber den großen Nachteil, daß Windows nichts davon weiß, weil die Zeichenaktion ja nicht vom Besitzer des Desktop durchgeführt wird. Betrachten wir folgende kleine Routine:

 
procedure TMainForm.DeskTopPaint;
var
  DeskTopDC: hDC;   
  C: TCanvas;
begin
{ Gerätekontext des Desktop ermitteln }
  DeskTopDC := GetDC(0); 
{ und daraus was für Delphi machen }
  C := TCanvas.Create;
  C.Handle := DeskTopDC;
  with C
  do begin { beliebige Zeichenaktionen auf C }
    Pen.Color := clRed;
    MoveTo(0,0);
    LineTo(Screen.Width,Screen.Height);
  end;
  ReleaseDC(DeskTopDC);
end;
    

Wenn man die Taskbar übermalt hat, dort aber die Uhr aktiv ist, sieht man sie auch in Kürze wieder. Will man solche Effekte sicher ausschließen, wird dies komplizierter als das ganze Programm. Schließlich erlaubt Windows mehr als nur eine Taskbar und Animationen könnten auch noch irgendwo ihr Unwesen treiben.

Dieser Ausflug war aber nicht umsonst. Denn mit dieser Technik kann man leicht einen Screenshot realisieren und das ist wieder gut so! Details in finden sich in TMainForm.ScreenshotClick.

Für unsere Zeichenabsichten sehen wir uns woanders um. Die Information können wir aus der Delphi-Hilfe beziehen. Mit Delphi zeichnet man immer mittels Canvas-Eigenschaft. Also wird in der Hilfe wird im Register "Index" einfach nach Canvas gesucht. Dort ist neben vielen anderen Komponenten auch TCustomForm, der Vorgänger von TForm aufgeführt. TForm enthält also auch eine (geerbte) Canvas-Eigenschaft. Sie wird nur selten direkt genutzt, ist für diese Aufgabe aber wichtig, denn damit werden alle Probleme klein. Mit einem maximierten Fenster ohne Titelleiste sind wir dann dem Ziel recht nah. Zur Bedienung bauen wir nur ein Kontextmenü ein. Bis auf SaveDialog1 (für die BMP-Datei zum Screenshot), verzichten wir auf weitere Controls.

Kernstück des Programms werden eigenständige Zeichenroutinen für die einzelnen Testmuster. Hier kann es leicht zu Seiteneffekten kommen. Weil alle TMainForm.Canvas gemeinsam nutzen, stellen die einzelnen Routinen die benutzten Canvas-Untereigenschaften (Pen.Color, Brush.Style,...) immer wieder so her, wie sie vorgefunden wurden. Als Alternative könnte auch jede Routine einfach alles so einstellen, wie es von ihr gebraucht wird. Das unterstellt aber eigentlich Kenntnis darüber, was andere Routinen abweichend von Standard hinterlassen haben. Dazu müßte man dann den ganzen Quelltext im Kopf haben. Eines von beiden Verfahren sollte man konsequent anwenden, wenn man mystisches Fehlverhalten (wie z.B. wenn die Farbbalken an sind, kommt die Mausposition plötzlich in weiß) vermeiden will. Die Ursache ist nicht immer so schnell gefunden.

Dem Paint-Ereignis von TMainForm ordnen wir eine Methode Allpaint zu, die unser Fenster vollständig neu zeichnet. Weil im Normalfall nur die Mausposition neu darzustellen ist, wird diese Darstellung ausgelagert. Sonst benötigt jede Mausbewegung ein Paint-Ereignis und schafft beste Voraussetzungen für eifriges Bildschirmflackern.

In der Prozedur Allpaint werden einstellungsabhängig Zeichenroutinen der aktuell benötigten Testmuster aufgerufen. Damit kann man leicht Programmpflege (ein- oder Ausbau von Testmustern) betreiben.

Die Arbeit mit den vielen Einstellungswerten verlagern wir in ein separates Formular namens Options. Damit müssen wir uns nun auch Gedanken über den Informationsaustausch zwischen den Objekten MainForm und Options und der Namensgültigkeit/Sichtbarkeit von Werten in beiden Units machen.

Aus meiner Sicht sollten wir eine Record-Struktur TParms definieren, die alle einstellbaren Größen aufnimmt. Wir benötigen sowohl in TMainForm wie auch in TOptions eine Variable vom Typ TParms. Deshalb kommt die Deklaration in den Interface-Teil von UOptions und UOptions wird in der Uses-Anweisung des Interface-Teils von UTestbild aufgeführt.

Zusätzlich deklarieren wir eine zweite Record-Struktur TGlobal zur Aufnahme beim Programmstart ermittelter Werte der Bildschirmparameter (Breite, Höhe und wer will x/y-Verhältnis,...).

Um den Zeichenroutinen für die Testmuster die aktuellen Einstellungen zur Verfügung zu stellen, definieren wir die private-Eigenschaft Parms vom Typ TParms bei TMainForm. Ihr Gegenstück ist sowohl Parms als auch POld als public-Eigenschaft von TOptions. Der Status public ist nötig, um von TMainForm aus zugreifen zu können.

Beim Aufruf des Einstelldialogs werden die Werte vom MainForm.Parms nach Options.Parms kopiert. Diese Zustandsbeschreibung wird wiederum in TOptions.Show nach POld gesichert. Im Normalfall verläßt man den Dialog via Ok-Button. Dann wird MainForm.Parms mit den Werten aus Options.Parms versorgt. Im Falle eines Abbruchs dagegen wird MainForm.Parms mit den Werten aus POld versorgt. Damit wird die alte Lage auch dann wieder hergestellt, wenn zwischendurch mittels Vorschau-Button MainForm.Parms zeitweilig verändert wurde.

Nachdem all diese Vorüberlegungen erledigt sind, dürften uns fast keine Überraschungen mehr begegnen und wir können nun auch am Quelltext schreiben.

Im Folgenden werden einige Erläuterungen zum Zusammenwirken einiger Programmabschnitte gegeben und einige Details erläutert. Ansonsten verweise ich auf die Kommentare im beigelegten Quelltext.

1. Unit UTestbild (TMainForm)

Im Objektinspektor stellen wir ein:

Align = alNone
BorderIcons = []
BorderStyle = bsNone
PopupMenu = PopupMenu1
Position = poScreenCenter
WindowState = wsMaximized
OnCreate = FormCreate
OnActivate = FormActivate
OnKeyPress = FormKeyPress
OnMouseMove = FormMouseMove
OnPaint = AllPaint
    

Damit startet unser Programm als Vollbildfenster ohne Titelzeile. Wenn jemand glaubt während eines Monitortests noch viele andere wichtige Programme laufen lassen zu müssen, könnte auch noch die Zuweisung FormStyle = fsStayOnTop nötig werden.

Ein Teil der FormCreate zugedachten Aufgaben wurde nach FormActivate verlagert, weil darin ein Bezug auf Options.Parms enthalten ist. Da zu diesem Zeitpunkt

Application.CreateForm(TOptions, Options); 
    

noch nicht ausgeführt wurde, wäre dies ein illegaler Speicherzugriff.

In diesem Programm wird im Hauptfenster auf nachgeordnete Controls verzichtet. Daher müssen wir auch an einigen Stellen das Fahrrad neu erfinden. So sind die "Unterfenster" zur Anzeige der Mausposition und der Bildschirmauflösung in der üblichen Weise mit der Maus zu verschieben. Der zugehörige Code sieht für die Mausposition so aus:

  if Parms.Mauspositionzeigen and not MovingSW
     and PtInRect(MausWin,Point(x,y)) and (ssLeft in Shift)
  then begin
    Canvas.Pen.Mode := pmnotXOR;
    Canvas.Brush.Color := clYellow;
    if MovingMW
    then begin
      Canvas.Rectangle(MausWin); { alten Ort restaurieren }
      { MausWin verschieben }
      OffsetRect(MausWin,x-MovePos.x,y-MovePos.y);
    end;
    MovingMW := true;
    MovePos := Point(x,y);
    Canvas.Rectangle(MausWin);   { neuen Ort zeigen }
    Canvas.Pen.Mode := pmCopy;
  end
  else begin
    if MovingMW
    then begin
      { Operation beendet, jetzt komplett neu zeichnen }
      Invalidate;
      MovingMW := false;
      MovePos := Point(-1,-1);
    end;
  end;
  { sonst normale Anzeige der Mausposition }
  if Parms.Mauspositionzeigen and not MovingMW
  then MausPos(mx, my);
    

PtInRect und OffsetRect gehören zum Windows API. MovingMW ist true, wenn MausWin geschoben werden soll. MovingSW erfüllt die gleiche Funktion für ScreenWin. Es kann natürlich immer nur eines der beiden "Fenster" bewegt werden. In MovePos wird immer der letzte Standort der Maus aufbewahrt, um den Offset bestimmen zu können. Während des Schiebens wird nur ein Rechteck mit pmNotXOR gezeichnet.

Bemerkenswert ist vielleicht noch das Zusammenspiel zwischen AllPaint, FarbtestClick und und FormKeyPress. Der Abschnitt am Anfang von AllPaint, in dem der Bildschirm vor den anderen Zeichenaktionen gelöscht wird, dient noch einem anderen Zweck. Im Modus Farbtest wird auf einfache Weise statt der Hintergrundfarbe via Leertaste eine Farbliste abgearbeitet. Statt des Tag-Feldes hätte genauso natürlich auch eine (privat-) Variable benutzt werden können.

2. Unit UOptions (TOptions)

Diese Unit ist für die Einstellungen zuständig. Weil ich schon oft riesenhaft aufgeblähte Registry-Dateien gesehen habe, scheint es mir sinnvoll, dazu keinen Beitrag zu leisten, sondern für alle Daten, die nur eine Anwendung betreffen, eine anwendungsspezifische ini-Datei zu verwenden. Schließlich hängt die Effizienz eines Windows-Systems stark von einem schnellen Registry-Zugriff ab. Außerdem reduziert sich die Programminstallation auf einen einfachen Kopiervorgang. Das vereinfacht die "rückstandsfreie" Desinstallation.

Bei Aufruf des Programms mit einem Parameter wird dieser als vollständiger Name einer ini-Datei gedeutet. Diese Datei wird als Quelle für die Einstellungen genutzt.

Diese Unit enthält in der Hauptsache ein TPageControl, dessen einzelne Registerseiten inhaltlich zusammengehörige Controls enthalten. Die Controls heißen wie die zugehörigen Werte in TParms. Damit ist der Zusammenhang deutlich. Aufgrund der unterschiedlichen Typen (z.B. TCheckBox und Boolean) fällt eine Verwechslung schon bei der Syntaxprüfung auf.

Ansonsten machen die einzelnen Methoden das, was ihr Name verspricht. Sie sind im Quelltext ausreichend dokumentiert.

Die Click-Methoden einzelner Controls dienen der notwendigen Aktualisierung von Anzeigen. So wird ein Panel als Schalter zum Auslösen von ColorDialog benutzt, weil man diesen "Schalter" besonders einfach färben kann. In anderen Click-Methoden wird eine Abhängigkeit von Controls erzwungen. So das Beachten des Seitenverhältnisses beim Gitter, oder die Sperrung von nachgeordneten Controls.

3. Antworten auf noch offene Fragen aus Toolbox 3/2002 S.102ff

Application.ProcessMessages ist eine in bestimmten Situationen sehr nützliche Sache. Innerhalb von lange laufenden Programmschleifen ist ein maßvoller(!) Einsatz die einzige Möglichkeit dem Benutzer eine nicht zu sehr zeitverzögerte Interaktion mit dem Programm zu erlauben. In einer Schleife, in der eigentlich fast nichts zu tun ist, wirkt der unnötige Aufruf von Application.ProcessMessages dagegen natürlich nur als Bremse.

Programm-Icon kontra Formular-Icon. Es gibt ein Icon welches dem Gesamtprogramm zugeordnet ist. Dieses Icon repräsentiert die laufende Anwendung in der Tastbar und dient außerdem im Windows-Explorer (sowie ggf. auf dem Desktop) als Programmsymbol. Dieses Icon ist über Application.Icon ansprechbar. Delphi-Hilfe: "Mit der Eigenschaft Icon können Sie bestimmen, durch welches Symbol die minimierte Anwendung in der Windows-Task-Leiste repräsentiert wird." Außerdem hat jedes Formular ein eigenes Icon. Dieses wird in der Titelzeile des Fensters benutzt. Dieses Icon ist ein Bestandteil von TForm. So könnte man der Application ein Icon durch Nachladen zuordnen:

Application.Icon.LoadFromFile(MyDirectory+'TB.ICO');
    

Ohne "Application." geht's an das aktuelle Formular, aber mangels Titelleiste ist dann davon wenig zu sehen. Dagegen sorgt

Application.Icon := Icon; 
    

für die Übernahme des via Objektinspektor beim Formular eingestellten Icons an Application.Icon.

Ich habe das Icon von Herrn Schlange dem Programm so zugeordnet: In der IDE Menüpunkt Projekt, Optionen, dort das Register Anwendung wählen und das für die Exe-Datei gewünschte Icon laden. Dann bedarf es keiner Icon-Datei als Zugabe zur Exe.

Mit der Routine Format lassen sich parametrisierte Zeichenketten mit den verschiedensten Formatierungsvorschriften (Delphi-Hilfe: Format-Strings/String-Formatierung) leicht herstellen. Die korrekte Ausrichtung einer (Text-)Ausgabe setzt nur die vorherige Kenntnis des Platzbedarfs voraus. Dazu gibt es die Canvas-Methoden TextExtent, TextHeight, TextWidth. Damit ist sowohl die horizontale, wie auch die vertikale Ausrichtung leicht zu berechnen. Schwierig wird es erst, wenn in einer Zeile unterschiedlich hohe Schriften auf einer Grundlinie stehen sollen.

Probleme bei der Umwandlung zwischen pChar und string haben ihre Ursache in der Verwendung der falschen Werkzeuge. Mit StrPCopy & Co ist es leicht zu machen. Ansonsten ist das Konzept der Long-Strings für den Programmierer recht bequem und vor allem sehr leistungsfähig. Ohne Anstrengung kann man beispielsweise den vollständigen Inhalt eines Memofeldes über die Eigenschaft Text als einen String hantieren.

4. Zur Programmiertechnik

Jeder hat natürlich seinen eigenen Stil. Aber es gibt Dinge, die ich mir im Verlauf von 35 Jahren Programmierung (vielfach auch als Anlaufstelle für die Programmier-Nöte der Kollegen) angewöhnt habe.

Nach jedem deutlichen Erkenntniszuwachs (und somit mehr oder weniger umfangreichen Änderungen) wandert der Quelltext in ein Archiv (wenn sein erreichter Zustand das wert ist) und wird danach konsequent entrümpelt/wieder sauber strukturiert. Damit bleibt der Quelltext pflegbar. Und gemachte Fehler sollte man nur in Ausnahmefällen der Nachwelt erhalten.

Die Lesbarkeit des Quelltextes hat oberste Priorität. Deshalb empfiehlt sich die auf den ersten Blick elegante Nutzung der Tag-Werte zur Unterscheidung der einzelnen Menüpunkte in PopUpClick nicht. Besser lesbar ist m.E.

if Sender=Balken then ...
    

auch wenn man dann keine case-Anweisung verwenden kann. Die Benutzung des Tag-Feldes zur Identifikation verschleiert nur den Bezug zum Menüpunkt. Eine gemeinsame Routine macht es aus meiner Sicht nur Sinn, wenn auch ausreichend gemeinsamer Code enthalten ist. In Bildtest.pas steht der zusätzliche Aufwand (tag, case-Anweisung, IF item IN [4..8]) gegen ein einsames Formshow(Sender).

In der gewählten Form der Implementation der "Circle"-Prozedur liegt eine Ursache für die Ungenauigkeit in der Kreisdarstellung. Da die Mittelpunktkoordinaten indirekt über den Ausdruck (size div 2) bestimmt werden, wird nur bei geraden Werten für Size das erwartete Ergebnis erzielt. Besser ist es, wie beim schon beim BGI-Vorbild unter Turbo-Pascal als Parameter die Mittelpunktkoordinaten und den Radius zu verwenden. Also:

Ellipse(x0-Radius,y0-Radius,x0+Radius,y0+Radius);    

Bei Pen.Width<>1 gibt es noch mehr zu bedenken.

Konstruktionen wie

IF fBalken
THEN Graue.enabled := TRUE
ELSE Graue.enabled := FALSE;
    

schreibt man besser kurz und schmerzlos (auch schon vor Delphi) als

Graue.enabled := fBalken;    

Auch wenn ich nicht so ganz von der Sinnhaftigkeit eines mehr oder weniger passiven Testbildprogramms überzeugt bin, so hoffe ich doch etwas Licht auf den Monitor gebracht zu haben.

zurückzurück
Copyright © 2003 by  EDVoigt  (Werner Voigt).