• GDI+ Tutorial

    Inhaltsverzeichnis

    Einleitung
    Grundlegendes Zeichnen
    Linie
    Rechteck
    Kreis / Ellipse
    Farben
    Farben mischen
    Füllen
    Erweitertes Zeichnen
    Weichzeichnen
    Text
    Bilder
    Techniken
    Doublebuffer
    FPS-Anzeige
    WM_PAINT
    Animationen und ihre Mathematik
    Simple Animation
    Vollsynchrone Kreisbewegungen
    Synchrone Kreisbewegungen mittels Vorzeichenwechsel
    Synchrone Kreisbewegungen mittels Periodenverschiebung
    Gleichmäßige Geradenbewegungen
    Gebremste Geradenbewegungen
    Realitätsnahe Geradenbewegung
    Schlusswort


    Einleitung

    So, hier stehen wir, am Anfang eines neuen Tutorials. Wir wollen uns hier mit den Grundlagen der "modernen" GDI+-Programmierung in AutoIt beschäftigen. Um es gleich vorweg zu nehmen: Ich bin kein "Meister" auf diesem Gebiet. Wenn ich mir anschaue, was Mars, eukalyptus oder UEZ so schreiben... Nein, da kann ich nicht mithalten. :D
    Ich kann aber mit GDI+ umgehen, sogar vergleichsweise gut und ordentlich. Ich weiß, wie es funktioniert und wo ich etwas sinnvoll einsetzen kann. Leider gibt es kaum verständliche Anleitungen für diesen Teil der AutoIt-Programmierung, mal abgesehen von einem Uralt-Tutorial von Ubuntu aus dem Jahre 2010, in dem (meiner Meinung nach) ein paar wichtige Dinge unzureichend oder gar nicht angesprochen werden. Was jetzt natürlich keine direkte Kritik an dem Tutorial darstellen soll. ^^ Ich dachte nur, es wäre Zeit, mal eine ordentliche Neuauflage zu produzieren.
    Inhaltlich werden wir uns mit den grundlegenden GDI+-Funktionen auseinandersetzen, ein wenig "Good Practice" und "Bad Practice" anschauen und – als kleines Schmankerl – das ganze ein wenig mit der normalen Win32-API verbinden. Da die Code-Darstellung hier im Forum zur Zeit nicht ganz so optimal ist, sind einerseits alle Skripte in [ code ]-Tags gesetzt (bei AutoIt-Tags steht alles in einer Reihe), andererseits hängen alle Skripte nochmal als ZIP-Datei im Anhang. Damit sollten alle klarkommen.
    Und so stehen wir hier, am Anfang eines neuen Tutorials. ;)

    Download der Skripte: Skripte.zip

    Grundlegendes Zeichnen

    Wir beschränken uns am Anfang zunächst auf die simpelsten Zeichenaktionen, die es gibt. Dazu ist aber natürlich erstmal ein Grundgerüst notwendig! Nun, um etwas unter Windows "sichtbar" zu machen, benötigt man in den meisten Fällen zunächst ein Fenster. Wir erstellen uns also ein kleines Fenster mit dazugehörigem Main-Loop:
    Skript 1.1

    Nun, das sieht doch schon ganz gut aus. Wenn wir unser Skript jetzt ausführen, haben wir eine schöne kleine Zeichenoberfläche. Nun, jetzt brauchen wir nur noch GDI+, nicht war? Dazu inkludieren wir die Datei GDIPlus.au3 in unser Skript. Um die Grafikengine GDI+ auch wirklich benutzen zu können, muss sie zunächst gestartet werden. Dabei wird dann die GDI+-DLL geöffnet und geladen, die auf jedem modernen Windows-System vorhanden sein müsste. Dazu nutzen wir die Funktion _GDIPlus_Startup(). An dieser Stelle könnten wir theoretisch den Pfad zur besagten DLL-Datei angeben, in der Praxis ist das aber eigentlich nicht relevant.
    Skript 1.2

    So, wir wollen direkt lernen, ordentlich mit GDI+ umzugehen. Dazu gehört auch das sogenannte Resourcen-Managment. Konkret heißt das, dass wir innerhalb der GDI+-Funktionen die Möglichkeit haben, bestimmte Objekte oder Resourcen zu erstellen. Die entsprechenden Befehle tragen fast immer ein "Create" im Namen. Auch Funktionen, die eine Resource laden, erstellen prinzipiell solch ein Objekt. Der ordentliche Programmierer muss lernen, mit diesen Resourcen umzugehen und sie richtig zu verwalten. Alles andere kann sich negativ auf die Gesamtleistung des Skriptes auswirken. Konkret heißt das: Was man öffnet, muss man auch wieder schließen.
    Wir fangen damit an, dass wir beim Beenden des Skriptes die GDI+-DLL wieder freigeben. Um den Überblick nicht zu verlieren, werden wir unsere "Aufräumarbeiten" in eine Funktion auslagern, die vor dem Beenden aufgerufen wird. In dieser Funktion steht dann erstmal nur der Aufruf von _GDIPlus_Shutdown. Ganz simpel. ;)
    Skript 1.3

    Bisher haben wir aber nur eine GUI erstellt und GDI+ gestartet. Um etwas zeichnen zu können, müssen wir zunächst die GUI zu unserer "Leinwand" machen. Im GDI+-Slang nennt man diese Leinwand Graphics. Wenn wir eine Leinwand aus einem Fenster erzeugen wollen, nutzen wir dafür die Funktion _GDIPlus_GraphicsCreateFromHWND. Dieser Funktion übergeben wir natürlich das Handle unserer GUI, also den Rückgabewert von GUICreate. Wir speichern das ganze dann in der Variable $hGraphics. Da wir ja ordentlich programmieren, fügen wir auch gleich den Freigabebefehl zu unserer Aufräum-Funktion hinzu.
    Wichtig ist, dass die Funktion _GDIPlus_GraphicsDispose vor der Freigabe der GDI+-DLL mittels _GDIPlus_Shutdown aufgerufen wird. Sonst wird der Vorgang fehlschlagen!
    Skript 1.4


    Linie

    So, jetzt wollen wir mal den ersten richtigen Schritt tun, nämlich etwas zeichnen!
    Dazu muss man noch wissen, dass wir uns auf unserer "Leinwand" mittels eines Koordinatensystems bewegen. Unsere GUI hat eine Größe von 400x400 Pixeln, die Ecke oben links (nicht unten links!!!) hat die Koordinaten (0|0). Daraus ergibt sich, dass die untere rechte Ecke die Koordinaten (399|399) besitzt. Dies sollte man bei seinen Berechnungen berücksichtigen. Wir widmen uns unserer Aufgabe: Eine Linie von der oberen linken Ecke in die Mitte zeichnen!
    Wir machen uns zunächst klar, welche beiden Punkte wir denn mit der Linie verbinden wollen. Nun, zunächst natürlich den Punkt A mit den Koordinaten (0|0), also die linke obere Ecke. Doch wo liegt jetzt der Mittelpunkt? Man könnte meinen, dass der Mittelpunkt bei (200|200) liegt. Dem ist aber nicht so! Und warum? Das ist einfach erklärt: Wir nehmen einmal an, unser Koordinatensystem würde nicht auf beiden Achsen von 0 bis 399 laufen, sondern von 1 bis 400. In diesem Koordinatensystem würde der Mittelpunkt tatsächlich bei (200|200) liegen, unsere Linie würde von (1|1) nach (200|200) laufen. Wir wissen aber, dass unser eigentliches Koordinatensystem um genau eine Einheit verschoben ist, daher ziehen wir von jeder Koordinate genau 1 ab. Und schon landen wir bei den Werten (0|0) und (199|199). ;)
    Diese beiden Punkte können wir mittels der Funktion _GDIPlus_GraphicsDrawLine verbinden. Zuerst übergeben wir die Referenz zu der "Leinwand", also $hGraphics. Dann nacheinander die Koordinaten, in der Reihenfolge X1, Y1, X2 und Y2. Den letzten Parameter lassen wir erst einmal außen vor.
    Skript 1.5

    Nun, eine Linie ist zwar ganz schön, aber doch irgendwie langweilig. GDI+ kennt noch ein paar weitere, komplexere Formen. Dazu zählen: Rechtecke, Ellipsen, Ellipsensegmente und unregelmäßige Polygone. Darüber hinaus gibt es auch noch eine Reihe von nicht geschlossenen Formen: Bögen, Kurven und Bezier-Kurven. Außerdem sind noch zwei Sonderformen vorhanden: Bilder und Texte. Eine ganze Reihe also! Wir werden uns exemplarisch mit Rechtecken, Ellipsen, Bildern und Texten außeinandersetzen. Bleiben wir zunächst im Reich der simplen Formen.

    Rechteck

    Das Rechteck wird ein wenig anders gehandhabt als die Linie. Anstatt zwei Koordinaten anzugeben, müssen wir hier nur eine Koordinate (die obere linke Ecke des Rechtecks) und jeweils Breite und Höhe angeben. Das eigentliche Zeichnen erledigt der Befehl _GDIPlus_GraphicsDrawRect, die Parameterreihenfolge ist weitestgehend logisch. Den letzten Parameter ignorieren wir wieder.
    Zeichnen wir doch mal in unsere quadratische GUI mittig ein weiteres Quadrat mit den halben Seitenlängen, also 200x200 Pixel. Dazu zeichnen wir vom Startpunkt (99|99) aus.
    Unsere altehrwürdige Linie können wir auch gleich wieder löschen, die brauchen wir nicht mehr. ;)
    Skript 1.6

    Ist es nicht wunderschön? Nun, es geht noch schöner!

    Kreis / Ellipse

    Nun wollen wir unserem Quadrat den letzten Schliff verpassen – wir packen einen Kreis in die Mitte! Dazu nutzen wir die Funktion _GDIPlus_GraphicsDrawEllipse.
    Man könnte meinen, die Funktion würde die Koordinaten des Kreismittelpunktes erwarten. In der klassischen Mathematik wird ein Kreis schließlich über den Mittelpunkt und den Radius definiert. Hier aber ist eine andere Vorgehensweise gefragt. Da auch Ellipsen gezeichnet werden können müssen, reicht ein einfacher Radius nicht. Die Technik ist eigentlich ganz simpel: Man stelle sich ein Rechteck vor, welches genau um die Ellipse (bzw. Kreis) herum aufgebaut ist. Unser Kreis wird über dieses Rechteck definiert. Keine Angst, du wirst gleich sehen, was ich meine. Wir wollen den Kreis genau in das innere unseres Rechtecks zeichnen, wir können die Koordinaten und Größenangaben also 1:1 übernehmen.
    Skript 1.7

    Beim Ausführen wird ganz schnell klar, was mit dem umgebenden Rechteck gemeint war, denke ich. Wenden wir uns nun dem unterdrückten, letzten Parameter der ...Draw-Funktionen zu!

    Farben

    Bisher sehen alle unsere Formen so aus, als ob sie mit einem dünnen Filzstift gezogen worden wären. Allesamt schwarz und 1 Pixel dick. Das wollen wir nun ändern. Innerhalb der GDI+-Engine haben wir nicht die Möglichkeit, die Farbe und Dicke beim Zeichnen direkt anzugeben. Diese Farben und Dicken werden zentral verwaltet, nämlich in Form von... Stiften, oder eben auf Englisch: Pens. So einen Stift erstellen wir ganz fix mit _GDIPlus_PenCreate. Der erste Parameter stellt die Farbe des Stiftes im AARRGGBB-Format dar. Wenn du nicht weißt, was für eine Farbe 0xFFFF00FF ist, dann solltest dich im Internet mal ein wenig über Farbdarstellungen mittels des Hexadezimalsystems schlau machen. Der zweite Parameter der Funktion gibt die Dicke des Stiftes in Pixel an. Der dritte Parameter wäre prinzipiell dafür gedacht, für die Breitenangabe eine andere Einheit wählen zu können. Mit der Standardeinstellung Pixel ist man aber eigentlich ganz gut bedient. Wir erstellen direkt zwei Stifte, denn wir wollen Kreis und Quadrat unterschiedlich färben: Der Kreis wird rot, das Quadrat blau! Dazu wählen wir noch eine ordentliche Dicke von 5 Pixeln. Die Rückgabewerte der Erstell-Funktionen übergeben wir schließlich als letzten Parameter an unsere jeweiligen Zeichenfunktionen.
    ABER STOP!
    Na, hast du dran gedacht? Wir haben gerade zwei Stifte erstellt, nun müssen wir auch dafür sorgen, dass sie am Ende wieder freigegeben werden. Dazu nutzen wir _GDIPlus_PenDispose.
    Skript 1.8

    Nun, wie man sieht, übermalen wir mit dem später gezeichneten Kreis einen Teil von dem Quadrat. Um den nächsten Schritt besser kenntlich zu machen, werden wir zuerst den Hintergrund unserer Leinwand wirklich weiss färben müssen. Um eine komplette Leinwand in einer Farbe einzufärben, können wir einfach die Funktion _GDIPlus_GraphicsClear benutzen. Diese Funktion akzeptiert ausnahmsweise direkt den Farbwert. Dieses Einfärben muss vor dem Zeichnen der Formen erfolgen, ansonsten würden wir diese komplett übermalen. Als Farbe nutzen wir ein komplett reines Weiss: 0xFFFFFFFF.
    Skript 1.10


    Farben mischen

    So, wir wollen jetzt vermeiden, dass das Rechteck an den Schnittstellen komplett vom Kreis überdeckt wird. Dazu verändern wir einfach den Alpha-Wert unserer Farbstifte. Wenn dieser nicht auf 100% steht, dann überdecken sich die Farben nur teilweise, sprich: Sie vermischen sich. Um den Effekt besser kenntlich zu machen, vergrößern wir auch direkt unsere Stiftbreite von 5 Pixel auf 15 Pixel.
    Skript 1.11


    Füllen

    Nun, bisher haben wir nur leere Kreise und Quadrate gezeichnet. Wir werden jetzt beides mit ein wenig Farbe füllen. GDI+ bietet dazu eine ganz neue Reihe von Funktionen an, die nicht mit _GDIPlus_GraphicsDraw... beginnen, sondern mit _GDIPlus_GraphicsFill..., was auch irgendwie logisch ist. Die Parameter der beiden Funktionsklassen unterscheiden sich kaum, nur kann man eben nicht mit einem Stift zeichnen! Dazu verlangt GDI+, dass wir den Pinsel benutzen, auf Englisch Brush. Dieser kennt natürlich keine Breite, denn Füllen ist Füllen. Somit akzeptiert _GDIPlus_BrushCreateSolid nur ein Argument, nämlich die Farbe im AARRGGBB-Format. Demnach müssen wir auch unsere Freigabe-Funktion ein wenig anpassen:
    Skript 1.12

    Wunderschön, nicht wahr?

    Erweitertes Zeichnen

    Weichzeichnen

    Wenn man sich das Ergebnis des letzten Skriptes mal genau anschaut, dann entdeckt man, dass der Kreis ein wenig... Eckig aussieht. Er wirkt relativ rau gezeichnet. Das ist natürlich nicht ganz das, was wir uns wünschen. Das, was uns fehlt, ist das weiche Zeichnen. Dieses weiche Zeichnen können wir mittels _GDIPlus_GraphicsSetSmoothingMode aktivieren. Wir nutzen dafür den Smoothing Mode Nummer 2.
    Skript 1.13

    Der Kreis sieht nun weich aus, wie Butter. ;)

    Text

    Nachdem wir auch diese Hürde gemeistert haben, wollen wir nun eine kleine Botschaft übermitteln, d.h. ein wenig Text schreiben. Auch, wenn sich das nicht so schwer anhört, ist das Zeichnen von Text eine relativ aufwändige Sache. Wir beschränken uns zunächst auf einen simplen Text in schwarz in der oberen linken Ecke. Wir lassen auch die Standardschrift (Arial, Größe 10) so, wie sie ist. Die entsprechende Funktion trägt den Namen _GDIPlus_GraphicsDrawString.
    Skript 1.14

    Nun, das war noch einfach. Allerdings erlaubt _GDIPlus_GraphicsDrawString nur rudimentäre Textzeichenoperationen. Die wirklich interessanten Sachen – Farbe, Ausrichtung, Endgröße – kann man nur mit _GDIPlus_GraphicsDrawStringEx kontrollieren. Dabei gelangen wir dann aber schon zu einer Funktion, die sehr aufwändig aufgerufen wird. Als Beispiel sollte man sich einmal die Definition von _GDIPlus_GraphicsDrawString anschauen, denn diese Funktion ruft letztendlich auch nur die erweiterte Variante auf. Dieses Wissen machen wir uns im Weiteren zunutze. Wir bewegen den Mauscursor in SciTE auf _GDIPlus_GraphicsDrawString und drücken Strg + J. Zumindest in der erweiterten SciTE-Variante springt man so zur Funktionsdefinition. Hier kopieren wir uns die Funktion und fügen sie in unserem eigenen Skript wieder ein. Nun haben wir einen Codeschnipsel, den wir als Grundlage benutzen können.
    Skript 1.15A

    Es gilt nun, diese Funktion so zu verändern, dass wir eine Farbe, eine umschließende Box und eine (horizontale und vertikale) Ausrichtung angeben können. Die Farbe ist leicht gemacht, wir ersetzen den Parameter $iFormat durch den Parameter $iColor, als Standardwert wählen wir 0xFF000000 (volles schwarz). Diese Variable setzen wir direkt bei _GDIPlus_BrushCreateSolid ein. Damit könnten wir jetzt schon die Farbe verändern. Widmen wir uns der umschließenden Box. Das kann man sich in etwa wie bei den GUICtrlCreate-Funktionen vorstellen. Bei einem Label hat man auch die Möglichkeit, die Größe des umgebenden Kastens anzugeben. Wird er leer gelassen, wird die Größe automatisch angepasst. Das passiert hier auch, und zwar in der Zeile mit _GDIPlus_RectFCreate. Wir erweitern die Funktion um die Parameter $nW und $nH mit den Standardwerten 0.0. Zum Schluss fehlt noch die Ausrichtung, welche durch _GDIPlus_StringFormatSetAlignment (horizontal) bzw. _GDIPlus_StringFormatSetLineAlignment (vertikal) beeinflusst werden kann. Wieder fügen wir zwei Parameter hinzu. Diese Funktion müsst ihr noch nicht zur Gänze verstehen, es reicht, wenn ihr sie anwenden könnt.
    Skript 1.15B

    Wenn wir die Funktion nun anwenden, sieht das ganze so aus:
    Skript 1.15

    Wie man sieht, haben wir einen wunderbar zentrierten, grünen Text in unserem Quadratkreis.
    Und ja, mir ist bewusst, dass ich das Thema ziemlich gekürzt habe. Aber mit dieser Funktion kommt man eigentlich immer gut klar. ^^

    Bilder

    Zum Ende des Kapitels wollen wir noch schnell ein Bild laden und zeichnen. GDI+ unterstützt dabei laut Dokumentation eine Fülle an Bildformaten: BMP, DIB, RLE, JPG, JPEG, JPE, JFIF, GIF, EMF, WMF, TIF, TIFF, PNG und ICO. Wir werden uns mit dem Windows-7-Standardbild von einem Koala, welches im JPG-Format vorliegt, begnügen. Zunächst muss das Bild mittels _GDIPlus_ImageLoadFromFile geladen werden. Und jetzt muss man natürlich wieder an das Freigeben des Bildes denken, ein _GDIPlus_ImageDispose und das Problem ist erledigt. Nun, das Bild ist geladen, doch wie zeichnen wir es? Die Funktion _GDIPlus_GraphicsDrawImage erlaubt es uns, unter Angabe des Graphics-Handels, des Image-Handles und der oberen linken Koordinate das Bild zu zeichnen.
    Skript 1.16

    Wie man ganz klar sehen kann, wird das Bild in Originalgröße gezeichnet. Klar, wir haben ja auch keine Größenangabe gemacht. So erkennt man unseren Koala aber leider nicht. Dazu wiederum gibt es die Funktion _GDIPlus_GraphicsDrawImageRect, mit welcher wir ein umgebendes Rechteck angeben können. Wir füllen einmal unsere ganze GUI mit dem Koala:
    Skript 1.17

    Wenn man das Seitenverhältnis beibehalten wollte, könnte man es zuerst mit _GDIPlus_ImageGetWidth bzw. _GDIPlus_ImageGetHeight ermitteln, um es dann auf unsere 400 Pixel Breite zu beziehen.


    Techniken
    Buffer

    Vielleicht ist es dem ein oder anderen schon aufgefallen, vielleicht auch nicht... Jedenfalls, sobald man das Fenster einer unserer bisherigen Zeichnungen aus dem Bildschirm raus schiebt und zurückholt, fehlt ein Teil des Bildes. Auch beim Mini- und wieder Maximieren des Bildes tritt dieser Effekt auf. Wir können diesen unschönen Nebeneffekt vermeiden, indem wir das Bild immer wieder neu zeichnen. Dazu nutzen wir die Hauptschleife, wechseln aber der Einfachkeit halber in den OnEvent-Mode.
    Skript 2.1

    Nun, jetzt können wir das Fenster beliebig verschieben und minimieren, das Bild bleibt uns erhalten. Allerdings flackert das Bild jetzt, wenn man ein wenig darauf achtet. Das liegt daran, dass der von uns durchgeführte Zeichenvorgang zu aufwändig ist. Wenn man ihn zu schnell wiederholt, sieht man irgendwann den Zeichenvorgang. Das wollen wir natürlich vermeiden. Die bekannteste Technik, dies zu erreichen, trägt den Namen Buffering. Dabei zeichnen wir unser Bild in ein zweites, unsichtbares Graphics, welches dann ganz am Ende mit einem Schlag in das sichtbare Graphics kopiert wird. Konkret nutzen wir zuerst die Funktion _GDIPlus_BitmapCreateFromGraphics, um von unserem vorhandenen Graphics eine Kopie als Bitmap zu erhalten. Dabei müssen wir zuerst (!) die Bildgröße angeben (400x400), danach das Handle des Graphics. Von unserer erzeugten Bitmap holen wir uns dann mittels _GDIPlus_ImageGetGraphicsContext wieder einen Graphics-Kontext. Somit können wir auf die Kopie von unserer Bitmap zeichnen. Perfekt!
    Von nun an müssen sich alle Zeichenoperationen auf diese Kopie beziehen, außerdem muss am Schleifenende die Bitmap auf unser Haupt-Graphics gezeichnet werden. Dazu nutzen wir das bereits bekannte _GDIPlus_GraphicsDrawImage. Natürlich dürfen wir auch nicht vergessen, dass wir die erzeugte Bitmap sowie das zweite Graphics wieder freigeben müssen.
    Skript 2.2

    FPS-Anzeige
    Sicherlich kennen die meisten von euch die FPS-Anzeige aus manchen Spielen. Wir werden jetzt in das Bild vom blauen Kreis eine FPS-Anzeige implementieren. "FPS" heißt eigentlich nur "Frames Per Second", sprich: Wie viele Bilder pro Sekunde gezeichnet werden. In unserem Fall heißt das, dass wir rausfinden müssen, wie oft in der Sekunde unsere Hauptschleife durchlaufen wird. In AutoIt wird das typischerweise mit AdlibRegister umgesetzt. Wir lassen in der Schleife eine Variable (FramesPerSecond) oben links auf unser Graphics zeichnen. Weiterhin addieren wir pro Schleifendurchlauf 1 zu einer anderen Variable (CurrentFrameCount). Einmal jede Sekunde lassen wir dann pro AdlibRegister den aktuellen Wert von CurrentFrameCount in FramesPerSecond übertragen und setzen CurrentFrameCount zurück auf 0. So einfach funktioniert unsere FPS-Anzeige. ;)
    Dabei muss aber gesagt werden, dass das Sleep(10), welches die CPU-Last begrenzen soll, auch unseren FPS-Wert beeinflusst bzw. begrenzt. Auf meinem Rechner schafft das Skript 100 FPS, wer jetzt ein wenig rumrechnet, erkennt, dass 100 FPS * 10ms = 1 Sekunde aufgeht. Das heißt, dass das Zeichnen an sich extrem wenig Zeit in Anspruch nimmt.
    Skript 2.3


    WM_PAINT
    Nun, wir haben gesehen, dass wir das Verschwinden unserer Zeichnung durch Neuzeichnen in einer Schleife verhindern können. Das ist auch die Lösung, die man meistens in der AutoIt-Szene sieht. Allerdings kommt jetzt das große ABER! Sofern wir ein bewegtes Bild zeichnen, ist diese Technik voll und ganz berechtigt, denn jedes Frame ist anders. Wenn wir aber ein unbewegtes Bild zeichnen, wie in Skript 2.2, dann verschwenden wir damit nur Rechenpower, weil unser Bild andauernd neugezeichnet wird, obwohl es noch vorhanden ist. Auch, wenn es gar nicht mehr zu sehen ist, wird weitergezeichnet. Das ist natürlich alles andere als optimal. Deshalb stellt Windows uns auch eine Möglichkeit zur Verfügung, um dieses Problem zu umgehen. Windows teilt jedem Fenster gewisse Events über die Fensternachrichten (Window Messages) mit, auf die jenes dann reagieren kann. Eine dieser Nachrichten trägt den Namen WM_PAINT. Das Betriebssystem sendet diese Nachricht an ein Fenster, wenn ein Neuzeichnen nötig ist. Das hört sich ja schon wesentlich besser an, nicht? AutoIt gibt uns eine einfache Funktion an die Hand, mit der wir WM_PAINT leicht abfangen können. Sie trägt den Namen GUIRegisterMsg. Wir registrieren für die Konstante $WM_PAINT (WindowsConstants.au3) eine Callback-Funktion, die aufgerufen wird, sobald die Nachricht empfangen wird. Diese Funktion muss bestimmte Merkmale erfüllen, hinsichtlich der Anzahl der Parameter und dem Rückgabewert. Durch die Rückgabe von $GUI_RUNDEFMSG (GUIConstants.au3) wird die Nachrichtenverarbeitung fortgesetzt, sprich der interne Handler für WM_PAINT greift nach unserer Funktion. Das ist für ein fehlerfreies Arbeiten unseres Programmes extrem wichtig.
    In unserer WM_PAINT-Funktion führen wir jetzt einen Teil aus unserem eigentlichen Zeichenvorgang aus, nämlich das Übertragen des Buffers auf das Haupt-Graphics. Den restlichen Zeichenvorgang verlegen wir aus der Schleife heraus vor die Schleife. Somit wird unser Bild einmal komplett aufgebaut und gezeichnet, danach nur noch kopiert, wenn es nötig ist. Ganz einfach, nicht? Damit unsere WM_PAINT-Funktion auch fehlerfrei arbeitet, ist es wichtig, dass das Registrieren von WM_PAINT erst nach dem Zeichnen unseres Hauptbildes geschieht. Was soll sonst kopiert werden? Um uns einen unnötigen Aufruf zu ersparen, machen wir das Fenster erst nach dem Registrieren sichtbar. So wird gewährleistet, dass unsere Funktion schon beim ersten Zeichnen des Fensters greift.
    Skript 2.5

    Bewegungen und ihre Mathematik
    Simple Bewegung
    Nun, wenn wir schon eine FPS-Anzeige haben, wollen wir doch auch mal unseren Kreis bewegen. Am einfachsten lässt sich eine simple, gleichmäßige Kreisbewegung realisieren. Ich gehe davon aus, dass jeder Leser mit den trigonometrischen Funktionen vertraut ist. Ich hoffe es zumindest. Ich schreibe trotzdem nochmal die vollendeten Formeln auf:
    x = x[Drehpunkt] – Kreisradius + Drehradius * sin(alpha)
    y = y[Drehpunkt] – Kreisradius + Drehradius * cos(alpha)
    alpha ist dabei der Winkel, der bestimmt, an welchem Punkt der Drehung wir uns momentan befinden. Dieser Winkel wird fortlaufend erhöht und bei 360° wieder auf 0° zurückgesetzt.
    Der Drehradius ist der Radius, mit dem der Kreis um den Mittelpunkt rotieren soll.
    Der Kreisradius ist der bekannte Radius des Kreises.
    Die Drehpunktkoordinate ist die jeweilige Koordinate des Drehpunktes.
    Außerdem ist es nötig, den Winkel vor der Rechnung mit Sinus und Cosinus ins Bogenmaß (Radiant) umzurechnen, da die AutoIt-Funktionen Winkelangaben in Grad (Degree) nicht akzeptieren. Diese Umrechnung folgt der Formel rad = deg * (PI / 180).
    Skript 3.1

    Und voilá, unsere Kugel bewegt sich kreisförmig. Bei mir erreicht dieses Skript immer noch 100 FPS. Wer mag, kann testweise das Sleep(10) im Schleifenkopf durch ein True ersetzen. Somit wird die Framerate nur noch durch die Leistung des Computers begrenzt. Mein Mittelklassensystem erreicht so immerhin noch 480 FPS. :D
    Vollsynchrone Kreisbewegungen
    So, wir haben einen großen, sich bewegenden, blauen Kreis. Wir wollen daraus jetzt einen blauen Kreis mit einem schwarzen Rand machen. Dieser Rand muss sich natürlich vollsynchron mit dem Kreis bewegen. Dazu nutzen wir unsere GDI+-Grundlagen und zeichnen an den selben Koordinaten mittels _GDIPlus_GraphicsDrawEllipse einen schwarzen, hohlen Kreis mit derselben Größe. Damit wir einen 3px dicken Rand bekommen, müssen wir natürlich noch einen neuen Stift erstellen. Dazu lagern wir dann auch unsere Koordinatenberechnungen in eine Variable aus.
    Skript 3.2

    Siehe da, es hat funktioniert. Theoretisch bewegen sich jetzt schon zwei Objekte in unser Animation, Rahmen und Füllung. Machen wir weiter.
    Synchrone Kreisbewegungen mittels Vorzeichenwechsel
    So, nun wollen wir zwei Objekte unterschiedlich bewegen. Wir wählen dafür – um keine neue Berechnung aufstellen zu müssen – zwei Kreise, die in je eine Richtung kreisen. Dazu behalten wir unseren blauen Kreis bei, fügen aber noch einen roten hinzu. Die Berechnung können wir fast 1:1 übernehmen, für die Bewegung in die andere Richtung ist lediglich ein Vorzeichenwechsel in der jeweiligen trigonometrischen Funktion nötig: Aus sin(x) wird also sin(-x).
    Weil der Effekt so schöner ist, schrauben wir die Deckkraft der Farben noch ein wenig runter. Nehmen wir 0xA7FF0000 und 0xA70000FF.
    Skript 3.3

    Synchrone Kreisbewegungen mittels Periodenverschiebung
    Unser Ziel ist es nun, drei (!) Kugeln mit immer gleichem Abstand zueinander in einer Richtung auf unserer Kreisbahn zu bewegen. Prinzipiell könnte man jetzt einfach mehrere Zählervariablen a la $iAngle verwenden. Das ist aber nicht die feine englische Art. Wir bedienen uns einfach der Mathematik der trigonometrischen Funktionen. Für den einen oder anderen könnte diese Skizze hier bei den folgenden Erklärungen eine Hilfe sein.
    Wir wissen, dass die Sinuskurve eine simple Kreisbewegung darstellt. Dieses Wissen haben wir uns auch schon zur Nutze gemacht. Den Bereich von -5 bis 5 auf der verlinkten Skizze nennen wir eine Periode. Nach dieser Periode wiederholt sich dasselbe immer wieder. Eine Periode ist also eine komplette Drehung um 360° oder im Bogenmaß um 2PI. Unser Ziel ist es nun, diese Periode ein wenig zu verschieben. Das heißt, wir müssen unsere Drehfunktionen so anpassen, dass sie auf diesem Graphen versetzt beginnen. Da wir einen gleichmäßigen Abstand haben wollten, müssen wir den ersten Graphen einfach bei 0° beginnen lassen, also keine Veränderung vornehmen. Den zweiten Graphen verschieben wir dann durch die Addition von 120° ein wenig nach rechts, der Dritte wird dann demnach um 240° verschoben.
    Kleine Bemerkung nebenbei, dem geneigten Elektrotechniker wird diese Verschiebung durchaus bekannt vorkommen. Die Phasen des Drehstroms sind auch jeweils um 120° verschoben. Daher findet man hier auch eine anschauliche Darstellung der Verschiebung. Das ganze Skript sieht dann so aus:
    Skript 3.4

    Ich erwähnte schon öfters mal das Bogenmaß... Anstatt unsere Kugeln um je 120° zu verschieben, hätten wir auch einfach eine Verschiebung von 2/3*PI nehmen können. Dann spart man sich bei den AutoIt-Internen Funktionen das Umrechnen mittels $nDeg2Rad.
    Gleichmäßige Geradenbewegungen
    Das, was die Bewegung auf einer Geraden von der Kreisbewegung unterscheidet, ist, dass es sich nicht um eine geschlossene Bewegung handelt. An einem gewissen Punkt müssen wir die Bewegung umkehren oder beenden. Wir wollen einmal einen blauen Ball auf dem Boden auf- und abhüpfen lassen. Den Boden symbolisieren wir ganz einfach mit einer schwarzen Linie. Als Laufvariable benutzen wir diesmal einfach den Abstand zum Startpunkt, sodass sich die folgende Gleichung ergibt:
    X = X[start] + Abstand
    Jetzt müssen wir natürlich noch die Richtungsänderung implementieren. Bisher haben wir zu unser Laufvariable immer eins dazu addiert und beim Überschreiten eines gewissen Wertes wieder auf 0 gesetzt. Jetzt müssen wir beim Überschreiten eines gewissen Grenzwertes veranlassen, dass sich unsere Laufvariable um eins verringert. Konkret heißt das, dass wir eine neue Variable einführen, die die Bewegungsrichtung angibt. Dabei steht +1 für unten und -1 für oben, denn der Koordinatenursprung ist ja oben links.
    Skript 3.5

    Und siehe da, unser Ball springt!
    Gebremste Geradenbewegungen
    Wir schauen nochmal kurz zu unserem springenden Ball zurück... Das ist ja schon ganz gut geworden, sieht aber nicht allzu realistisch aus. Und warum? Na, der Ball wird in der Luft beim Höhepunkt der Bewegung sicherlich nicht bei voller Geschwindigkeit einfach nach unten fallen. Er wird zunächst langsamer. Wir müssen also unsere gleichmäßige Beschleunigung durch etwas anderes ersetzen. Es gäbe an dieser Stelle einige Möglichkeiten, die physikalisch richtige Variante wäre, eine Parabelgleichung nachzubilden. Wir wählen jedoch eine weniger aufwändige Variante, die genau so gut aussieht... Man stelle sich folgendes vor: Eine Kreiskontur, auf welcher sich ein Punkt im Uhrzeigersinn bewegt. Diese Kreiskontur drehen wir jetzt um 90° und betrachten das ganze von der Seite. Theoretisch sähe man jetzt nur noch eine Linie... Stellt es euch vor, als ob man die Kante einer Münze betrachten würde. Wenn wir jetzt unsere Kreiskontur verschwinden lassen, bleibt nur noch der Punkt, der auf- und abhüpft. Doch diese Bewegung erfolgt nicht gleichmäßig! Sie wird kurz vor dem Umkehren auf beiden Seiten jeweils langsamer. Genau das, was wir gesucht haben. ;)
    Wir ersetzen also unsere Distanzberechnung durch die Formel, die wir schon einmal rausgearbeitet hatten:
    x = x[Drehpunkt] – Kreisradius + Drehradius * sin(alpha)
    Hier müssen noch ein paar Sachen verändert werden...
    x = x[Startpunkt] + Drehradius * (1 + sin(alpha))
    Woher das +Drehradius kommt, ist einfach erklärt: In der Originalformel ist unser Basiswert der Drehpunkt, also der Mittelpunkt der Drehung. Wir wollen jedoch eine Geradenbewegung simulieren, bei der wir eigentlich nicht den Mittelpunkt der Bewegung angeben wollen, sondern den Startpunkt. Um vom Startpunkt auf den Mittelpunkt zu kommen, muss man eben nochmal den Drehradius addieren. Der Kreisradius ist hier einfach nicht relevant und beträgt 0.
    Skript 3.6

    Im oberen Bereich sieht das ganze jetzt schon relativ realistisch aus. Im unteren Bereich dagegen wird unsere Bewegung auch langsamer, was ja eigentlich nicht sehr realistisch ist.
    Realitätsnahe Geradenbewegung
    Im Vergleich zum Vorgängerskript wollen wir nun den unteren Teil der Bewegung ein wenig verschnellern, die Bremsung also rausnehmen. Sicherlich könnte man hier jetzt mit einer quadratischen Gleichung arbeiten, wir bleiben aber erstmal bei dem bekannten Sinus. Wir schauen uns nochmal den Graphen des Sinus' an. Im Moment findet unsere Bewegung von -5 bis +5 statt, wir stellen also eine komplette Periode dar. Dabei ist der Tiefpunkt des Graphen der Hochpunkt unseres Wurfes und demnach umgekehrt der Hochpunkt des Graphen der Tiefpunkt des Wurfes, also der Moment, wo Bodenkontakt herrscht. An diesen Stellen erkennen wir anhand der flachen Steigung, dass die Geschwindigkeit sehr gering wird.
    An dieser Stelle benutzen wir einen einfachen Trick: Wir durchlaufen die Sinus-Funktion nicht immer wieder von 0° bis 360°, sondern nur von 0° bis 180°! Somit haben wir nur noch einen Punkt, an dem die Geschwindigkeit gering wird. Praktisch bedeutet das, dass wir unsere Bewegungslänge verdoppeln, aber nur noch die Hälfte darstellen. Dazu muss unsere Bewegungsgleichung ein wenig angepasst werden:
    x = x[Startpunkt] + 2 * Drehradius * (1 + sin(alpha+180°))
    Das *2 des Drehradius' ist die Verdopplung der Bewegungslänge, die +180° sagen nur aus, dass der zweite Teil der Bewegung dargestellt wird. Ansonsten würde unser Ball durch den Boden durchhüpfen, ihr könnt es ja mal ausprobieren. Zudem legen wir die Bedingung zum Zurücksetzen der Laufvariable auf >=180°.
    Skript 3.7

    Für mein Empfinden ist das Hüpfen jetzt schon ziemlich realistisch. Wer mag, kann pro Schleifendurchlauf die Laufvariable auch nur um 0.5 erhöhen, siehe Skript 3.7A im Anhang. Das lässt die Bewegung langsamer, aber sauberer wirken.
    Natürlich könnte man jetzt auch noch die plastische Verformung nachstellen, die auftritt, sobald der Ball den Boden berührt, aber das ist dann ein ganz anderes Thema. :D
    Man kann den meisten Bewegungen mit der Verwendung der Sinus-Funktion einen natürlicheren Touch verpassen, das klappt meistens. Wie man den Sinus konkret einsetzt, kommt natürlich auf den Anwendungsfall an. ^^


    Schlusswort

    So, jetzt bin ich einmal durch, nach knapp 6 Stunden. Das sind die imho wichtigsten Themen im Bezug auf GDI+. Damit sollte sich eigentlich ganz gut arbeiten lassen. Falls ich dennoch trotz sorgfältiger Überlegung einen Punkt vergessen oder gar falsch erklärt haben sollte, meldet euch. Wie immer ist Lob und (konstruktive) Kritik ausdrücklich erwünscht. ;)
    chesstiger

  • Du bezeichnest den Einfachpuffer als Doppelpuffer.
    Ein Doppelpuffer besteht aus 2 unsichtbaren Bitmappuffern die jeweils ein eigenes GFX (unabhängig vom GFX des GUI) haben. Ein Anwendungsbeispiel ist z.B. eine Zeichenfunktion die sehr viel Zeit in Anspruch nimmt. Tritt ein WM_PAINT während dieses Vorgangs auf, so wird der bisher nicht komplett aufgebaute Puffer in die GUI gebeamt -> Unvollständiges Ergebnis. Ein Doppelpuffer behält immer ein bereits fertiges Bild in der Hinterhand, ist der langwierige Zeichenvorgang abgeschlossen, so werden die Pufferadressen von Backbuffer 1 und Backbuffer 2 getauscht (klassiker -> kein Zeitverlust), sodass ein ggf Auftretendes WM_PAINT IMMER ein vollständig aufgebautes Bild zur Verfügung hat.

    Edit:
    Das klingt jetzt eventuell zu negativ. Das soll konstruktive Kritik sein ;)

    M

  • DANKE !!!

    Lieben Gruß,
    Alina

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Geheime Information: ;)
    OuBVU5ebLhHu5QvlnAyQB4A7SzBrvWulwL7RLl2BdH5tI6sIYspeMKeXMSXl

  • Freut mich, dass es euch gefällt. ^^
    Ich habe gerade den Teil mit den Bewegungen noch ein wenig um gängige Bewegungsformen und ihre Berechnung erweitert, parabelförmige Bewegungen kommen auch noch.
    Ich werde wohl früher oder später auch noch etwas über Transformationen usw schreiben. ^^

    Gruß

  • Ach ja, ich hatte das "Smoothing" aus dem Funktionsnamen einfach übersetzt, "smooth" => "weich". Damit war nicht das eigentliche Weichzeichnen gemeint, sondern weiches Zeichnen. Der Begriff Kantenglättung kam mir gerade nicht in den Sinn, deshalb hatte ich ein Synonym gesucht. Sobald ich wieder am PC bin, korrigiere ich das. ^^

  • Es wird auch nicht so gezeichnet. Der große Nachteil an GDI+ ist, dass es eben nicht kantengeglättet zeichnet, sonderm im Nachhinein einen einfachen Box-Filter drüberlegt. Verhindert auch das Pixelgenaue positionieren von Elementen.

  • Sehr schönes und verständliches Tutorial :)
    Alles gut zusammengefasst und erklärt - Und mir hat es sehr geholfen, da ich nun keine eckigen Kreise mehr habe ^^
    Danke :)

    Spoiler anzeigen

    Überraschung!


    MfG Donkey

  • Hallo zusammen,

    ich weiß der Thread ist schon älter, würde den trotzdem gerne hochkramen. ;)

    chesstiger erstmal möchte ich mich bei dir für das Tutorial bedanken.

    Ich bin gerade an meinen ersten GDIPlus+ geh versuchen dran.

    Was ich schon mal umgesetzt habe ist ein eigenes Viereck mit "X" drin, das meinen bisherigen Close Button für die GUI ersetzen soll (ich nutze $WS_POPUP + $WS_SYSMENU für meine GUI). :party:

    Jetzt würde ich gerne noch weiter gehen und hoffe das ist möglich.

    Ich würde gerne von meiner GUI einen transparenten Background haben, wenn es nicht zu rechenaufwändig ist am liebsten sogar mit Blur Effekt.

    Aber nur auf den Hintergrund beschränkt, sämtliche andere Elemente sollten 100% sichtbar bleiben.

    Also egt genau das, was Win10 auch macht.

    https://www.imagebanana.com/s/1245/IA6RTEDw.html


    Ist soetwas mit GDIPlus+ möglich?

    Am liebsten ausgehend von diesem Script, das ich von oben leicht angepasst entnommen habe.


    Vielen Dank schon mal. ^^

    VG

    borsTiHD

  • Ist soetwas mit GDIPlus+ möglich?

    Na klar, nennt sich Gaussian Blur und ist schon in Gdi+ direkt eingebaut _GDIPlus_EffectCreateBlur.

    Hier mal ein kleines Beispiel:

  • Ah, das ist ja schon mal super. ^^

    Das wäre dann abgehakt.

    Geht das auch mit dem "durchsichtigen" Hintergrund?

    In meinem Beispiel oben, hätte ich gerne das rote Viereck durchsichtig, das soll später die gesamte Fläche meiner GUI einnehmen (letzendlich so, dass der Hintergrund von Windows selbst, oder worüber die GUI auch immer liegt verschwommen durchscheint).

  • _GDIPlus_DwmEnableBlurBehindWindow vielleicht? Das Viereck ist in dem 1. Beispiel nicht geblurrt, im 2. Beispiel aber schon.

  • Hm... sehen die Beispiele bei dir so aus?

    Oder sollten die anders aussehen? Ich glaube bei mir siehts nicht wie von dir gewollt aus?

    Dein erstes: https://www.imagebanana.com/s/1245/6sB2cbgO.html

    Dein zweites: https://www.imagebanana.com/s/1245/yQia4t71.html

    Ansonsten... beim ersten Bild wäre egt schon der gewünschte Effekt (bis auf den fehlenden Blur).

    Das würde ich statt rot mit meiner eigentlichen Hintergrundfarbe füllen und dann über die gesamte GUI größe ziehen.

    Auf dieser Fläche sollen dann zuletzt noch meine ganzen Buttons/Labels, etc drauf.

    €dit: Selbst das example von der Hilfe Seite sieht bei mir anders aus wie es wohl gewollt wäre?!:

    https://www.autoitscript.com/autoit3/docs/l…ehindWindow.htm

    https://www.imagebanana.com/s/1245/JB7D61zx.html

    Einmal editiert, zuletzt von borsTiHD (18. November 2018 um 14:13)

  • Ah, ich bin gerade am googlen.

    Scheint wohl an Win10 zu liegen. :|

    Ich suche mal weiter.

    Aber vielen Dank nochmal. ;)

    So wie es bei dir aussieht, genau das wollte ich erreichen. ^^

    €dit: Perfekt ^^ https://www.autoitscript.com/forum/files/fi…for-windows-10/

    €dit 2: Da mit _WinAPI_DwmEnableBlurBehindWindow10() keine Hintergrundfarbe gesetzt werden kann, habe ich mit _GDIPlus_GraphicsFillRect() die gesamte GUI Hintergrundfläche "angemalt". Der genutzte Brush brauch nur einen Alpha Wert. So hab ich quasi das Viereck vom GDIPlus "über" den Blur Effekt gelegt und habe eine durchsichtige Hintergrundfarbe meiner wahl. :)

    3 Mal editiert, zuletzt von borsTiHD (18. November 2018 um 14:51)