'Echte' Arrays - Grundlagen

Hilfreiche Erklärungen und Tipps zum Lernen von Autohotkey

Moderator: jNizM

just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

'Echte' Arrays - Grundlagen

13 Oct 2015, 06:54

Einführung:

Als 'Array' bezeichnet man im einfachsten Fall einen Speicherbereich, in dem mehrere gleichartige Werte (Elemente) direkt aufeinanderfolgend abgelegt sind. Der gesamte Bereich wird über einen Namen angesprochen. Die einzelnen Werte (Elemente) erreicht man über ihren Index, der die Position des Wertes innerhalb des Bereichs bestimmt. Dieser einfachste Fall ist in AHK bisher nicht realisiert worden. Für den Zugriff auf AHK Arrays sind diese Begriffe aber trotzdem zutreffend.

Im alten AHK 1.0 (AHK Basic) gab es nur die sogenannten 'Pseudo-Arrays'. Dafür wurden einzelne unabhängige Variablen verwendet, deren Namen aus einen gemeinsamen Teil und einer Art Pseudo-Index zusammengesetzt sind, so dass sie sich ähnlich wie ein Array ansprechen lassen. Ein Beispiel dafür ist die Ausgabe des Kommandos StringSplit, man kann sie aber auch händisch erstellen. Die Beispiele zu StringSplit zeigen das Prinzip:

Code: Select all

Farben = rot,grün,blau
StringSplit, FarbArray, Farben, `,
Loop, %FarbArray0%
{
    diese_Farbe := FarbArray%a_index%
    MsgBox, Farbnummer %a_index% ist %diese_Farbe%.
}
Der gemeinsame Namensteil (der Name des Arrays) ist hier FarbArray. Auf die einzelnen Arrayelemente wird in der Schleife durch Anhängen des Durchlaufzählers A_Index (der Pseudo-Index des Arrays) zugegriffen, der dafür in % Zeichen eingeschlossen werden muss: FarbArray%A_Index%. Wenn man einen bestimmten 'Index' nutzen will, kann man den auch direkt an den Arraynamen anhängen, z.B. FarbArray3 für das dritte Element. Für die Nutzung von Pseudo-Arrays finden sich in den Foren unzählige Beispiele.

Bereits im Vorgänger des aktuellen AHK 1.1 (damals AHK_L genannt) wurden zusätzlich 'echte' Arrays (bzw. Objekte, die sich wie echte Arrays nutzen lassen) eingeführt. Die Nähe zu den mysteriösen Objekten und der damit zusammenhängende Gebrauch von Bezeichnungen wir OOP (ObjektOrientierte Programmierung) scheint aber viele Anfänger abzuschrecken, weil sie glauben, dass das 'zu hoch' für sie sei. Ich will mit den folgenden Artikeln versuchen, diese Scheu etwas zu mindern.


Inhalt: Syntax:
  • Weil die innere Sruktur von Arrays und Objekten in AHK identisch ist (d.h. Arrays sind Objekte), gibt es verschiedene Möglichkeiten, um auf Arrays zuzugreifen. Man unterscheidet üblicherweise zwischen Array- und Objektsyntax. Das Merkmal der Arraysyntax sind die eckigen Klammern [...]. Ich werde diese Syntax verwenden, soweit es möglich ist. Die Objektsyntax verwendet stattdessen geschweifte Klammern {...} und den Punkt . als Trennzeichen zwischen Array/Objektnamen und Index/Schlüssel. Wer es möchte, kann z.B stattMeinArray[1] auch MeinArray.1 für den Zugriff auf das erste Element nutzen.
Achtung:
  • Wer sich mit 'echten' Arrays abgibt, begibt sich automatisch in das Land der Ausdrücke. Deshalb sind bei der Programmierung immer deren Regeln zu beachten. Die Wichtigsten sind (wie ich glaube):
    • Für die Zuweisung von Werten an oder aus Arrays muss anstelle des einfachen = Zeichens immer der Operator := verwendet werden.
    • Beim Zugriff auf den Inhalt von Variablen wird der Name in der Regel nicht mit %-Zeichen eingeschlossen.
    • Numerische Werte werden als Zahlen geschrieben.
    • Nicht numerische Zeichenfolgen müssen in "-Zeichen eingeschlossen werden.
  • Außerdem muss man immer im Hinterkopf behalten, dass die Variablen, die dem Array einen Namen geben, nicht die Daten des Arrays sondern nur eine Referenz / einen Verweis auf den Datenbereich enthalten. In Anweisungen wie MsgBox, %MeinArray% wird deshalb nichts (bzw. eine leere Zeichenfolge) ausgegeben.

    Und eine Anweisung wie MeinArray := IrgendEinWert irgendwo im Skript sorgt dafür, dass sowohl der Verweis auf das Array überschrieben wird als auch das Array selbst zerstört bzw. freigegeben wird, wenn MeinArray zu diesem Zeitpunkt ein Array bezeichnet. Das Array ist damit unwiederbringlich futsch.
Last edited by just me on 15 Nov 2015, 04:57, edited 11 times in total.
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

13 Oct 2015, 06:55

'Einfache' Arrays: (Vorläufig abgeschlossen!)

'Einfache' AHK Arrays kommen der in der Einführung enthaltenen Definition des Begriffs Array recht nahe. Sie bestehen aus einer lückenlosen Folge von Werten (Elementen). Lückenlos heißt hier, dass jede Position (jeder Index) des Arrays mit einem Wert belegt ist. Dabei kann der Wert allerdings auch die 'leere Zeichenfolge' "" sein. Der Index ist also eine fortlaufende Nummer.

In einigen Programmier/Skriptsprachen beginnt diese Zählung mit 0. Das hat den Hintergrund, dass diese Sprachen in echten Arrays den Anfang eines Elements durch die Distanz zum Anfang des Gesamtbereichs bestimmen. Weil alle Elemente gleich lang sind, kann man diese Distanz leicht über die Elementlänge und den Elementindex berechnen. Dann ist es praktisch, wenn der Index des ersten Elements 0 ist, denn 0 * Elementlänge = 0, und das ist genau die Distanz des ersten Elements.

In AHK beginnen 'einfache' Arrays aber immer mit dem Index 1, und das entspricht wohl auch eher der menschlichen Denkweise.

Der größte (wirklich der größte) Unterschied zwischen den alten Pseudo-Arrays und den echten Arrays besteht darin, dass echte Arrays als solche deklariert/definiert werden müssen, bevor ein Zugriff auf einzelne Elemente möglich ist. Weil Pseudo-Arrays in Wirklichkeit über einzelne Variablen realisiert werden, kann man jederzeit ein 'Element' mit einer Anweisung wie Array%Index% := 7 erzeugen. Wenn man so etwas mit echten Arrays versucht, bevor das Array deklariert wurde, passiert schlichtweg nichts.

Die Deklaration eines Arrays erfolgt, indem man einer Variablen ein Array zuweist, also mit (alternativ)

Code: Select all

MeinArray := Array()
MeinArray := [] ; die eckigen Klammern definieren ein Array, man spricht hier auch von Array-Syntax
Weil man es, wenn man mit Arrays arbeitet, immer mit Ausdrücken zu tun hat, wird für die Zuweisung zwingend der Operator := benötigt.

Man kann das Array bei der Deklaration auch schon mit Werten versorgen, z.B.

Code: Select all

FarbArray := ["Rot", "Grün", "Blau"]
Hier werden den ersten drei Elementen des Arrays die Zeichenfolgen Rot, Grün und Blau in genau der Reihenfolge zugewiesen, in der sie innerhalb des durch die eckigen Klammern [...] definierten Arrays auftreten.

Die restlichen Unterschiede sind auch recht einfach umzusetzen. So wird beim Zugriff auf ein Element, dessen Index in einer Variablen liegt, aus

Code: Select all

MeinArray%Index% := MeinWert
MeinWert := MeinArray%Index%

Code: Select all

MeinArray[Index] := MeinWert
MeinWert := MeinArray[Index]
d.h. die Prozentzeichen %...% werden durch passende eckige Klammern [...] ersetzt.

Wenn man den Index direkt angeben will, ist das auch nicht viel schwieriger. Hier wird aus

Code: Select all

MeinArray5 := MeinWert
MeinWert := MeinArray5

Code: Select all

MeinArray[5] := MeinWert
MeinWert := MeinArray[5]
d.h. der Namensteil, der den Index bestimmt (hier 5), muss zusätzlich in eckige Klammern eingeschlossen werden [5], und das war's auch schon.

Und jetzt folgen schon die Vorteile, die echte Arrays mit sich bringen.

Im Gegensatz zu Pseudo-Arrays haben echte Arrays einen eingebauten Zähler. So liefert der Ausdruck

Code: Select all

MeinArray.Length()
jederzeit die aktuelle Anzahl der im Array enthaltenen Elemente.

Weiterhin bieten sie eine Methode, um Elemente an das Array anzuhängen. Das erledigt der Ausdruck

Code: Select all

MeinArray.Push(MeinWert)
der den Inhalt der Variablen MeinWert am Ende des Arrays anfügt. Dabei wird auch noch der Index des angehängten Werts zurückgegeben. Braucht man den, schreibt man

Code: Select all

MeinIndex := MeinArray.Push(MeinWert)
Diese Methode hat ein Gegenstück, um das letzte Element auszulesen und dabei aus dem Array zu entfernen:

Code: Select all

LetzterWert := MeinArray.Pop()
Zuletzt noch ein besonderes Schmankerl. Gelegentlich will man auch ein Element aus dem Array entfernen, das nicht am Ende steht, und dabei keine Lücke erzeugen. Mit den Pseudo-Arrays war das ein echtes Problem, weil man dafür alle Variablen mit einem 'Index', der größer ist als der entfernte, umschaufeln musste. Echte Arrays meistern das mit einer Anweisung:

Code: Select all

Inhalt := MeinArray.RemoveAt(5)
entfernt das 5. Element, gibt dessen Wert zurück und verringert alle höheren Indices (so vorhanden) um 1. Aus diesem Grund darf diese Methode aber auch nur in lückenlosen Arrays verwendet werden, weil es sonst zu nicht vorhergesehenen Ergebnissen kommen kann.
Auch dafür gibt es ein Gegenstück, um Werte an beliebiger Stelle in ein Array einzufügen:

Code: Select all

MeinArray.InsertAt(MeinIndex, MeinWert)
fügt z.B. den Inhalt der Variablen MeinWert an der durch MeinIndex bestimmten Position ein und erhöht alle höheren Indices und den des ursprünglich an dieser Position stehenden Wertes um 1. Die Anweisung MeinArray.InsertAt(1, "Bingo!") fügt z.B. den Wert Bingo! am Anfang des Arrays ein und bildet damit auch ein Gegenstück zu MeinArray.Pop("Bingo!"), das am Ende anhängt. Auch hier gilt: Bitte nur in lückenlosen Arrays verwenden.


Und noch etwas Wichtiges, das ich beinahe vergessen hätte. Wie verarbeitet man alle Elemente eines Arrays in einer Schleife? Das geht natürlich mit der bekannten Loop Anweisung. Weil MeinArray.Length() die aktuelle Anzahl der Elemente liefert, sähe das dann z.B. so aus:

Code: Select all

Loop, % MeinArray.Length() {
   MsgBox, 0, Element %A_Index%, % "Inhalt: " . MeinArray[A_Index] ; der dritte Parameter ist wegen des Arrayzugriffs ein Ausdruck!
}
Speziell für Arrays (bzw. Objekte) gibt es aber noch eine andere Schleife, die For-Loop:

Code: Select all

For Index, Inhalt In MeinArray {
   MsgBox, 0, Element %Index%, Inhalt: %Inhalt%
}
Diese Schleife durchläuft alle Elemente des Arrays in aufsteigender Reihenfolge und stellt dabei den Index und den Inhalt des aktuellen Elements in die im Schleifenkopf angegebenen Variablen (hier Index und Inhalt). Die weitere Verarbeitung ist dann über diese 'normalen' Variablen möglich.

Wer hier einen längeren Artikel erwartet hat, wird vielleicht enttäuscht sein, aber es braucht tatsächlich nicht mehr Informationen, um mit echten Arrays arbeiten zu können. Ich hoffe deshalb, dass sich einige Leser dazu entschließen, künftig echte Arrays einzusetzen oder sogar in vorhandenen Skripten Pseudo-Arrays durch echte zu ersetzen. Die Skripte werden dadurch nicht schneller, aber sie sehen (meiner Meinung nach) eleganter aus.
Last edited by just me on 20 Oct 2015, 04:34, edited 6 times in total.
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

13 Oct 2015, 06:55

Assoziative Arrays: (Vorläufig abgeschlossen!)

Unter dem Begriff Assoziatives Datenfeld liefert die deutsche Wikipedia folgende Erläuterung (Auszug):
Das assoziative Datenfeld (englisch map, dictionary oder associative array) ist eine Datenstruktur, die – anders als ein gewöhnliches Feld (engl. array) – nichtnumerische (oder nicht fortlaufende) Schlüssel (zumeist Zeichenketten) verwendet, um die enthaltenen Elemente zu adressieren; ...
und die AHK-Hilfe sagt dazu:
Ein assoziatives Array ist ein Objekt, das mehrere eindeutige Keys (Schlüssel) und mehrere Values (Werte) enthält, die jeweils miteinander verbunden sind. Keys können Strings, Integer oder Objekte sein. Values beliebige Typen.
Wer den Anfang des Tutorials bereits gelesen hat, kann sich vielleicht vorstellen, was das in Bezug auf AHK-Arrays heißt:
  • Für den Zugriff auf die Elemente wird nicht eine absolute Positionsangabe (Index) sondern ein Schlüsselbegriff (Schlüssel / key) verwendet, der allerdings auch numerisch sein darf.
  • Das Array ist in der Regel nicht lückenlos (im Sinne einer linearen Folge von Indices) bzw. kann es bei nichtnumerischen Schlüsseln gar nicht sein.
  • Auch Arrays mit auschließlich numerischen Schlüsseln beginnen oft nicht mit dem Index 1.
  • Letzlich und entscheidend: Das Array ist viel mehr ein Objekt als ein Array im klassischen Sinne.
Für die Arbeit mit assoziativen Arrays wird deshalb oft und gern auf die Objektsyntax mit den geschweiften Klammern {...} zurückgegriffen, es geht aber notfalls auch mit der Arraysyntax [...], nur oft nicht ganz so bequem.

Die Vermischung mit den Objekten hat aber auch andere weitreichende Folgen.

So liefert MeinArray.Length() in diesem Fall nicht die Anzahl der Elemente, sondern nur den Index des größten numerischen Schlüssels zurück. Ist gar kein numerischer Schlüssel vorhanden, wird 0 zurückgegeben, ebenso wenn der größte numerische Schlüssel tatsächlich 0 ist. Ist er negativ, wird sogar ein negatives Ergebnis geliefert. Deshalb ist diese Methode für assoziative Arrays nicht bzw. kaum zu gebrauchen. Das liegt u.a. daran, dass AHK die numerischen (Index) und die nichtnumerischen Schlüssel (Key) bzw. deren Inhalte strikt getrennt verwaltet.

Wenn das Array auch numerische Schlüssel enthält, die nicht lückenlos sind, muss man auch mit dem Gebrauch der Methoden MeinArray.RemoveAt(Index) und MeinArray.InsertAt(Index) äußerst vorsichtig sein, weil beide den Wert anderer numerischer Schlüssel verändern können. Will man das nicht, muss man die Finger davon lassen. Eine Ersatzmethode zum Einfügen gibt es nicht, weil das gezielte Einfügen eines Schlüssels an einer bestimmten Position nicht möglich ist. Die 'Position' wird allein durch den Wert des Schlüssels bestimmt. Zum Löschen von Schlüsseln gibt es aber die Methode MeinArray.Delete("Schlüssel"), die auch numerische Schlüssel entfernt, ohne andere anzutasten.

Für numerische oder gemischte assoziative Arrays sind auch die Methoden MeinArray.MinIndex() bzw. MeinArray.MaxIndex() brauchbar, die den kleinsten bzw. größten numerischen Schlüssel liefern. Außerdem gibt es die nützliche Methode MeinArray.HasKey("Schlüssel"), mit der man prüfen kann, ob ein bestimmter Schlüssel bereits in das Array aufgenommen wurde.

So, und wozu ist das nun gut? Ich will mal versuchen, das am Beipiel der AHK HTML Farbnamen zu verdeutlichen. Diese Tabelle enthält sowohl die Namen einiger Farben, wie sie in einigen Gui-Anweisungen genutzt werden können, als auch ihre hexadezimalen numerischen Werte. Wenn man nun eine Funktion erstellt, die mit Farben arbeitet, ohne dass die Namen der Farben verwendet werden können, und die dann im Forum anderen Usern zur Verfügung stellt, kann man auf die Frage warten: Kann ich da auch HTML-Farbnamen übergeben? Ich habe das in der Vergangenheit so gelöst, dass ich innerhalb der Funktion ein assoziatives Array vorhalte, das die Namen mit den numerischen Werten verbindet. Das kann im einfachsten Fall unter Verwendung der Arraysyntax so aufgebaut werden:

Code: Select all

HTML := []
HTML["AQUA"] := 0x00FFFF
HTML["BLACK"] := 0x000000
HTML["BLUE"] := 0x0000FF
HTML["FUCHSIA"] := 0xFF00FF
HTML["GRAY"] := 0x808080
HTML["GREEN"] := 0x008000
HTML["LIME"] := 0x00FF00
HTML["MAROON"] := 0x800000
HTML["NAVY"] := 0x000080
HTML["OLIVE"] := 0x808000
HTML["PURPLE"] := 0x800080
HTML["RED"] := 0xFF0000
HTML["SILVER"] := 0xC0C0C0
HTML["TEAL"] := 0x008080
HTML["WHITE"] := 0xFFFFFF
HTML["YELLOW"] := 0xFFFF00
Durch die Verwendung der Objektsyntax {...} kann man das drastisch abkürzen:

Code: Select all

HTML := {AQUA: 0x00FFFF, BLACK: 0x000000, BLUE: 0x0000FF, FUCHSIA: 0xFF00FF, GRAY: 0x808080, GREEN: 0x008000
       , LIME: 0x00FF00, MAROON: 0x800000, NAVY: 0x000080, OLIVE: 0x808000, PURPLE: 0x800080, RED: 0xFF0000
       , SILVER: 0xC0C0C0, TEAL: 0x008080, WHITE: 0xFFFFFF, YELLOW: 0xFFFF00}
Der Teil vor dem Doppelpunkt ist der Schlüssel, danach folgt der Wert. Die Schlüssel/Wert Paare werden durch ein Komma getrennt. Wenn der Schlüssel nur aus Buchstaben, Ziffern und dem Unterstrich (_) besteht, braucht er nicht in Anführungszeichen eingeschlossen zu werden. Und weil das für AHK nur eine Zeile ist, kann man so auch statische Variablen in Funktionen initialisieren.

Nun nutzen wir das für eine (zugegebenermaßen recht nutzlose) Funktion. Der wird ein HTML-Farbname übergeben und sie liefert dafür den RGB-Integerwert:

Code: Select all

#NoEnv
Farbe := "Maroon"
RGB := NameToRGB(Farbe)
HexRGB := Format("0x{:06X}", RGB)
MsgBox, 0, Array Test, Der RGB Integerwert von`n`n`t%Farbe%`n`nist %RGB% bzw. %HexRGB%
Farbe := "Bingo"
RGB := NameToRGB(Farbe)
HexRGB := Format("0x{:06X}", RGB)
MsgBox, 0, Array Test, Der RGB Integerwert von`n`n`t%Farbe%`n`nist %RGB% bzw. %HexRGB%
ExitApp

NameToRGB(Name) {
   Static HTML := {AQUA: 0x00FFFF, BLACK: 0x000000, BLUE: 0x0000FF, FUCHSIA: 0xFF00FF, GRAY: 0x808080, GREEN: 0x008000
                 , LIME: 0x00FF00, MAROON: 0x800000, NAVY: 0x000080, OLIVE: 0x808000, PURPLE: 0x800080, RED: 0xFF0000
                 , SILVER: 0xC0C0C0, TEAL: 0x008080, WHITE: 0xFFFFFF, YELLOW: 0xFFFF00}
   If HTML.HasKey(Name) ; ist der übergebene Wert ein gültiger Name / im Array enthalten?
      Return HTML[Name] ; ja, RGB-Wert zurückgeben
   Return "'Das war mal nix!'"
}
Das Ganze funktioniert aber auch umgekehrt. Dafür nehmen wir den (numerischen) Farbwert als Schlüssel und verbinden ihn mit dem Namen. Die Definition in Objektsyntax sähe dann so aus:

Code: Select all

HTML := {0x00FFFF: "AQUA", 0x000000: "BLACK", 0x0000FF: "BLUE", 0xFF00FF: "FUCHSIA", 0x808080: "GRAY", 0x008000: "GREEN"
       , 0x00FF00: "LIME", 0x800000: "MAROON", 0x000080: "NAVY", 0x808000: "OLIVE", 0x800080: "PURPLE", 0xFF0000: "RED"
       , 0xC0C0C0: "SILVER", 0x008080: "TEAL", 0xFFFFFF: "WHITE", 0xFFFF00: "YELLOW"}
Weil die Schlüssel numerisch sind, brauchen sie auch hier keine umschließenden Anführungszeichen, dafür muss aber der Namenstext in Anführungszeichen eingeschlossen werden, weil AHK sonst annimmt, es sei der Name einer Variablen deren Inhalt verwendet werden soll. In der Arraysyntax sähe das so aus:

Code: Select all

HTML := []
HTML[0x00FFFF] := "AQUA"
...
Wie man unschwer erkennen kann, haben wir hier ein lückenhaftes Array mit numerischen Schlüsseln, also ein assoziatives Array. Die numerischen Indices werden von AHK immer als Dezimalzahlen behandelt. Die MethodeHTML.MinIndex() liefert deshalb 0 (= 0x000000). Die Methode HTML.MaxIndex() den Wert 16777215 (=0xFFFFFF) und ebenso die Methode HTML.Length(). Man sollte deshalb nicht versuchen, das Ergebnis von HTML.Length() bei assoziativen Arrays als 'Anzahl der Elemente' in einer Schleifenanweisung wie Loop, % HTML.Length() zu nutzen.

Im folgenden Beispiel gibt die MsgBox den Farbnamen für einen RGB-Wert zurück, so vorhanden:

Code: Select all

#NoEnv
HTML := {0x00FFFF: "AQUA", 0x000000: "BLACK", 0x0000FF: "BLUE", 0xFF00FF: "FUCHSIA", 0x808080: "GRAY", 0x008000: "GREEN"
       , 0x00FF00: "LIME", 0x800000: "MAROON", 0x000080: "NAVY", 0x808000: "OLIVE", 0x800080: "PURPLE", 0xFF0000: "RED"
       , 0xC0C0C0: "SILVER", 0x008080: "TEAL", 0xFFFFFF: "WHITE", 0xFFFF00: "YELLOW"}
FarbWert := 0xC0C0C0
If HTML.HasKey(FarbWert)
   FarbName := HTML[FarbWert]
Else
   FarbName := "Ham wa nich!"
MsgBox, 0, RGB %FarbWert%, Name: %FarbName%
Alternativ kann man den Namen für einen RGB-Wert auch über das zuerst erstellte Array ermitteln. Dazu muss man allerdings in einer Schleife über das Array laufen, bis man den Namen gefunden hat:

Code: Select all

#NoEnv
HTML := {AQUA: 0x00FFFF, BLACK: 0x000000, BLUE: 0x0000FF, FUCHSIA: 0xFF00FF, GRAY: 0x808080, GREEN: 0x008000
       , LIME: 0x00FF00, MAROON: 0x800000, NAVY: 0x000080, OLIVE: 0x808000, PURPLE: 0x800080, RED: 0xFF0000
       , SILVER: 0xC0C0C0, TEAL: 0x008080, WHITE: 0xFFFFFF, YELLOW: 0xFFFF00}
FarbWert := 0xC0C0C0
FarbName := "Ham wa nich!"
For Name, RGB In HTML {
   If (RGB = FarbWert) {
      FarbName := Name
      Break ; wenn der Name / die Farbe gefunden wurde, wird die Schleife beendet
   }
}
MsgBox, 0, RGB %FarbWert%, Name: %FarbName%
Last edited by just me on 18 Oct 2015, 04:16, edited 8 times in total.
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

13 Oct 2015, 10:12

Mehrdimensionale Arrays - Teil 1: (Vorläufig abgeschlossen!)

Und nun die Krönung des Ganzen, mehrdimensionale Array. Von einem mehrdimensionalen Array spricht man dann, wenn die Elemente eines Array aus Arrays bestehen. Um an einen Wert zu kommen, braucht man dann sowohl den Index/Schlüssel des ersten wie auch den des über diesen Index/Schlüssel zurückgegebenen Arrays. Pro benötigtem Index spricht man von einer Dimension. Zwei Indices liefern deshalb die Werte eines zweidimensionalen Arrays.

Das klassische Beispiel für ein zweidimensionales Array ist eine Tabelle, wie man sie z.B. von Excel kennt. Die Tabelle besteht aus Zeilen und die wiederum aus Spalten. Die Werte befinden sich in Feldern, die da stehen, wo sich Zeilen und Spalten kreuzen. Für den Zugriff auf ein Feld bzw. seinen Inhalt braucht man deshalb zwei Indices: Den Zeilenindex (Nummer der Zeile) und den Spaltenindex (Nummer der Spalte). Und daraus ergibt sich, dass man eine solche Tabelle wunderbar in einem zweidimensionalen Array verwalten kann.

Wer schon mit Excel gearbeitet hat, kennt wahrscheinlich auch das einfache Testformat, in dem die Tabellen abgespeichert werden können: Die CSV-Dateien. Dabei werden die Tabellenzeilen hintereinander in eine Datei geschrieben und die Felder innerhalb der Zeilen durch ein eindeutiges Trennzeichen getrennt. Ursprünglich war das mal das Komma (deshalb auch CSV - Comma Separated Values). Es kann aber auch jedes andere Zeichen sein. Ich verwende als Trenner gern das Tabulatorzeichen (Tab), weil dieses Zeichen innerhalb der Feldwerte kaum einmal gebraucht wird. Eine CSV-Datei mit Tabulatoren könnte deshalb so aussehen:

Code: Select all

A	C	H	Ö	H
B	L	M	T	l
V	G	K	X	B
O	W	P	S	Q
Wie gelangt die nun in ein Array?

Wenn die Datei nicht zu riesig ist, kann man sie in einem Stück einlesen:

Code: Select all

FileRead, CSV, DateiName
liest den Dateiinhalt in die Variable CSV. Wir wissen, dass die Zeilen (jedenfalls ist das bei Windows so üblich) mit der Zeichenkombination CRLF (d.h. für AHK `r`n) getrennt sind (zumindest sollte es ein `n sein). Die einzelnen Zeilen können deshalb mit einer Parse-Schleife erreicht werden. Die Hilfe liefert dafür das Beispiel #2. Innderhalb der Zeilen sind die einzelnen Felder mit TAB (d.h. für AHK A_Tab oder `t) getrennt. Auch hier kann man deshalb mit der Parse-Schleife arbeiten. Und das liefert schon einen Hinweis auf die Art des benötigten Arrays: Zwei Schleifen -> zwei Dimensionen.

Code: Select all

#NoEnv
; FileRead, DateiInhalt, C:\Beispiel.csv
DateiInhalt := "
(Join`r`n
A`tC`tH`tÖ`tH
B`tL`tM`tT`tl
V`tG`tK`tX`tB
O`tW`tP`tS`tQ
)"
; Array aufbauen
ZeilenArray := [] ; das Array muss deklariert werden!!!
Loop, Parse, DateiInhalt, `n, `r ; Zeilen einlesen
{
   ZeilenIndex := A_Index ; A_Index speichern, weil er in der folgenden Schleife wiederverwendet wird
   Loop, Parse, A_LoopField, %A_Tab% ; Spalten einlesen
      ZeilenArray[ZeilenIndex, A_Index] := A_LoopField ; zwei Indices -> zwei Dimensionen
}
; Zeilen- und Spaltenzahl ausgeben
MsgBox, 0, ZeilenArray, % "Anzahl der Zeilen: " . ZeilenArray.Length()     ; Anzahl der Elemente in ZeilenArray
MsgBox, 0, ZeilenArray, % "Anzahl der Spalten: " . ZeilenArray[1].Length() ; Anzahl der Elemente im ersten Element von ZeilenArray
ExitApp
Bei im Vergleich zum vorhandenen Hauptspeicher 'richtig' großen Dateien sollte man auf die Read-Schleife ausweichen. Die Anweisung FileReadLine sollte innerhalb von Schleifen nie genutzt werden.

Als Ersatz für die Parse-Schleifen kann man auch die Funktion StrSplit() nutzen. Die gibt fertige einfache Arrays zurück, die direkt zugewiesen werden können. Das Beispiel sähe dann so aus:

Code: Select all

#NoEnv
; FileRead, DateiInhalt, C:\Beispiel.csv
DateiInhalt := "
(Join`r`n
A`tC`tH`tÖ`tH
B`tL`tM`tT`tl
V`tG`tK`tX`tB
O`tW`tP`tS`tQ
)"
; Array aufbauen
ZeilenArray := StrSplit(DateiInhalt, "`n", "`r") ; ZeilenArray muss nicht vorher als Array deklariert werden
For Index, Zeile In ZeilenArray                  ; Schleife über alle Zeilen
   ZeilenArray[Index] := StrSplit(Zeile, A_Tab)  ; Aktuelles ZeilenElement mit dem SpaltenArray aus StrSplit() überschreiben
; Zeilen- und Spaltenzahl ausgeben
MsgBox, 0, ZeilenArray, % "Anzahl der Zeilen: " . ZeilenArray.Length()     ; Anzahl der Elemente in ZeilenArray
MsgBox, 0, ZeilenArray, % "Anzahl der Spalten: " . ZeilenArray[1].Length() ; Anzahl der Elemente im ersten Element von ZeilenArray
ExitApp
Wie man sieht, ist das kürzer, aber es braucht zwischenzeitlich auch mehr Hauptspeicher. Das kann bei großen Dateien ein Nachteil sein.

So, nun haben wir unser zweidimensionales Array. Die erste Dimension wird über den Zeilenindex angesprochen und liefert ein Spaltenarray. Die zweite Dimension (das Spaltenarray) wird über den Spaltenindex angesprochen und liefert den Wert dieser Spalte in der Zeile mit dem gegebenen Zeilenindex. Das sieht dann so aus:

Code: Select all

Zeile := 2
Spalte := 2
; Werte auslesen
Wert := ZeilenArray[Zeile, Spalte]
; Werte zuweisen
ZeilenArray[Zeile, Spalte] := NeuerWert
Komplette Zeilen bzw. Spaltenarrays kann man mit den o.a. Methoden für einfache Arrays bearbeiten:

Code: Select all

SpaltenArray := ZeilenArray[ZeilenIndex]        ; stellt das Spaltenarray an der über ZeilenIndex bestimmten Position in die
                                                ; Variable SpaltenArray.
ZeilenArray.Push(SpaltenArray)                  ; hängt ein neues Spaltenarray an.
SpaltenArray := ZeilenArray.Pop()               ; liefert und löscht das letzte Spaltenarray.
ZeilenArray.RemoveAt(ZeilenIndex)               ; löscht das Spaltenarray an der über ZeilenIndex angegebenen Position aus dem
                                                ; Array und 'korrigiert' alle nachfolgenden Zeilenindices.
ZeilenArray.InsertAt(ZeilenIndex, SpaltenArray) ; fügt ein neues Spaltenarray an der über Zeilenindex bestimmten Position ein und
                                                ; 'korrigiert' die Indices des ursprünglich an dieser Position stehenden 
                                                ; Spaltenarrays und aller nachfolgenden.
ZeilenArray.Length()                            ; liefert die aktuelle Anzahl der Zeilen/Spaltenarrays.
Auf die Spaltenwerte kann man dann über einen einfachen Index mit

Code: Select all

SpaltenWert := SpaltenArray[SpaltenIndex]
zugreifen.

Nachdem wir nun die Datei in ein Array geladen und die Daten des Arrays verarbeitet haben, müssen wir die Änderungen irgendwann in die Datei zurückschreiben. Dazu muss das Array wieder in das ursprüngliche Dateiformat zurückgeführt werden. Dafür wird wieder eine Schleife je Dimension benötigt. Das Ganze könnte z.B. so aussehen:

Code: Select all

ZeilenAnzahl := ZeilenArray.Length()            ; Zeilenanzahl bestimmen (s.u.)
SpaltenAnzahl := ZeilenArray[1].Length()        ; Spaltenanzahl bestimmen (s.u.)
ZeilenEnde := "`r`n"                            ; Zeilenende
FeldTrenner := A_Tab                            ; Feldtrenner
DateiInhalt := ""                               ; leere Variable bereitstellen
For ZeilenIndex, SpaltenArray In ZeilenArray {  ; für jede Zeile im Array
   For SpaltenIndex, Wert In SpaltenArray {     ; für jede Spalte in der aktuellen Zeile
      DateiInhalt .= Wert                       ; Spaltenwert anhängen
      If (SpaltenIndex <> SpaltenAnzahl)        ; bei jeder außer der letzten Spalte
         DateiInhalt .= FeldTrenner             ;    Feldtrenner anhängen
   }
   If (ZeilenIndex <> ZeilenAnzahl)             ; bei jeder außer der letzten Zeile
      DateiInhalt .= ZeilenEnde                 ;    Zeilenende anhängen
}
MsgBox, 0, DateiInhalt, %DateiInhalt%
Der neue Dateiinhalt kann dann in einem Rutsch in die Datei geschrieben werden.

Wenn dabei die Größe zum Problem wird, kann man die Datei auch zeilenweise ausgeben. Ich empfehle dafür das Dateiobjekt, das mit der Funktion FileOpen() erstellt wird:

Code: Select all

If !(DateiObjekt := FileOpen("Beispiel.csv", "w")) ; Datei zum Schreiben öffnen
   MsgBox, 16, Fehler!, Die Datei Beispiel.csv konnte nicht geöffnet werden!
Else {
   ZeilenAnzahl := ZeilenArray.Length()            ; Zeilenanzahl bestimmen (s.u.)
   SpaltenAnzahl := ZeilenArray[1].Length()        ; Spaltenanzahl bestimmen (s.u.)
   ZeilenEnde := "`r`n"                            ; Zeilenende
   FeldTrenner := A_Tab                            ; Feldtrenner
   For ZeilenIndex, SpaltenArray In ZeilenArray {  ; für jede Zeile im Array
      Zeile := ""                                  ; leere Variable für die Zeile bereitstellen
      For SpaltenIndex, Wert In SpaltenArray {     ; für jede Spalte in der aktuellen Zeile
         Zeile .= Wert                             ; Spaltenwert anhängen
         If (SpaltenIndex <> SpaltenAnzahl)        ; bei jeder außer der letzten Spalte
            Zeile .= FeldTrenner                   ;    Feldtrenner anhängen
      }
      If (ZeilenIndex <> ZeilenAnzahl)             ; bei jeder außer der letzten Zeile
         Zeile .= ZeilenEnde                       ;    Zeilenende anhängen
      DateiObjekt.Write(Zeile)                     ; Zeile wegschreiben
   }
   DateiObjekt.CLose()                             ; Datei schließen
}
; Für den Test wieder einlesen und ausgeben
DateiObjekt := FileOpen("Beispiel.csv", "r")       ; hier mal ohne Fehlerprüfung
DateiInhalt := DateiObjekt.Read()
DateiObjekt.CLose()
MsgBox, 0, DateiInhalt, %DateiInhalt%
So, jetzt ist alles wieder gespeichert, und das war'S dann auch schon.
Last edited by just me on 21 Oct 2015, 03:40, edited 6 times in total.
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

20 Oct 2015, 05:40

Mehrdimensionale Arrays - Teil 2: (Vorläufig abgeschlossen!)

OK, der zweite Teil beschäftigt sich mit gemischten mehrdimensionalen Arrays, d.h. Arrays, die sowohl einfache als auch assoziative Arrays enthalten können. Und weil mir kein besseres Beispiel einfällt, will ich dafür das Kommando WinGet, Liste, List, ... nutzen. Die Hilfe liefert ein Beispiel dafür, wie man die Liste mit Loop abarbeiten kann. Wir wollen das Kommando aber in einer Funktion nutzen.

Funktionen haben den Vorteil, dass die in Ihnen verwendeten Variablennamen standardmäßig lokal sind. D.h., sie sind nur innerhalb der Funktion gültig und überschreiben keine Variablen gleichen Namens, die an anderer Stelle im Skript verwendet werden. Sie haben aber auch eine eklatante Beschränkung. Sie können nur genau einen Wert zurückgeben. Braucht man mehrere Werte, kann man die zusätzlichen nur über ByRef-Parameter auslesen, und das ist für eine unbekannte Anzahl von Fenstern für die jeweils mehrere Eigenschaften gebraucht werden, nicht praktikabel. Hier schlägt nun die Stunde der Arrays. Sie werden in AHK durch über genau einen Wert angesprochen, die sogenannte Referenz, die man sich als Zeiger auf den Speicherbereich des Arrays vorstellen kann. Und diesen einen Wert kann eine Funktion zurückgeben.

Nun denn, ans Werk. Die Funktion soll FensterListe heißen und für jedes Fenster die Eigenschaften ID (HWND), Klasse, X- und Y-Position, Breite und Höhe, sowie den Titel in einem zweidimensionalen Array liefern. Wir brauchen dafür die Funktionsdeklaration und darin
  1. eine Arraydeklaration für den Rückgabewert
  2. das WinGet Kommando
  3. die Verarbeitungsschleife
  4. ein paar weitere Win-Kommandos für die Eigenschaften
Das könnte dann so aussehen:

Code: Select all

FensterListe() {
   ReturnArray := [] ; Rückgabearry deklarieren
   WinGet, ID, List, , , Program Manager ; siehe Hilfe
   Loop, %ID% {
      WinID := ID%A_Index%
      WinGetClass, Class, ahk_id %WinID%
      WinGetPos, X, Y, W, H, ahk_id %WinID%
      WinGetTitle, Title, ahk_id %WinID%
      ; ...
   }
}
Wir haben jetzt Alles, was wir brauchen, und müssen die Werte nur noch in das Array stecken und das Array zurückgeben. Das Array braucht zwei Dimensionen. In der ersten wird der Index des Fensters, in der zweiten werden dessen Eigenschaften abgelegt. Dafür wird die Funktion ergänzt:

Code: Select all

FensterListe() {
   ReturnArray := [] ; Rückgabearry deklarieren
   WinGet, ID, List, , , Program Manager ; siehe Hilfe
   Loop, %ID% {
      WinID := ID%A_Index%
      WinGetClass, Class, ahk_id %WinID%
      WinGetPos, X, Y, W, H, ahk_id %WinID%
      WinGetTitle, Title, ahk_id %WinID%
      ReturnArray[A_Index, "WinID"] := WinID
      ReturnArray[A_Index, "X"] := X
      ReturnArray[A_Index, "Y"] := Y
      ReturnArray[A_Index, "W"] := W
      ReturnArray[A_Index, "H"] := H
      ReturnArray[A_Index, "Class"] := Class
      ReturnArray[A_Index, "Title"] := Title
   }
   Return ReturnArray
}
Mit Hilfe der Objektsyntax kann man das noch abkürzen. Es sieht dann so aus:

Code: Select all

FensterListe() {
   ReturnArray := [] ; Rückgabearry deklarieren
   WinGet, ID, List, , , Program Manager ; siehe Hilfe
   Loop, %ID% {
      WinID := ID%A_Index%
      WinGetClass, Class, ahk_id %WinID%
      WinGetPos, X, Y, W, H, ahk_id %WinID%
      WinGetTitle, Title, ahk_id %WinID%
      ReturnArray[A_Index] := {WinID: WinID, X: X, Y: Y, W: W, H: H, Class: Class, Title: Title}
   }
   Return ReturnArray
}
Damit ist die Funktion auch schon fertig. Damit wir sehen, ob das auch tatsächlich funktioniert, bauen wir noch einen Rahmen, der die Funktion aufruft und die Werte der Fenster per MsgBox ausgibt:

Code: Select all

#NoEnv
FensterArray := FensterListe()
For Index, Fenster In FensterArray
   MsgBox, 0, Fenster %Index%, % "WinID: " . Fenster["WinID"] . "`n"
                               . "Titel: " . Fenster["Title"] . "`n"
                               . "Klasse: " . Fenster["Class"] . "`n"
                               . "X: " . Fenster["X"] . "`n"
                               . "Y: " . Fenster["Y"] . "`n"
                               . "W: " . Fenster["W"] . "`n"
                               . "H: " . Fenster["H"]
ExitApp

FensterListe() {
   ReturnArray := [] ; Rückgabearry deklarieren
   WinGet, ID, List, , , Program Manager ; siehe Hilfe
   Loop, %ID% {
      WinID := ID%A_Index%
      WinGetClass, Class, ahk_id %WinID%
      WinGetPos, X, Y, W, H, ahk_id %WinID%
      WinGetTitle, Title, ahk_id %WinID%
      ReturnArray[A_Index] := {WinID: WinID, X: X, Y: Y, W: W, H: H, Class: Class, Title: Title}
   }
   Return ReturnArray
}
Dabei wird das gelieferte zweidimensionale Array mit einer For-Schleife abgearbeitet:

Code: Select all

For Index, Fenster In FensterArray
Innerhalb der For-Schleife beinhaltet die Variable Index den jeweiligen Index der ersten Dimension und die Variable Fenster das jeweilige assoziative Array der zweiten Dimension. Auf die Werte dieses Arrays kann dann per Fenster["Title"] (Arraysyntax) oder Fenster.Title (Objektsyntax) zugegriffen werden.

Und weil die Ausgabe per MsgBox recht primitiv aussieht, gibt es fü Alle, die bis hierher gekommen sind, noch eine elegantere Version, die dafür ein ListView nutzt. Ich verwende hier die Objektsyntax, weil's kürzer ist:

Code: Select all

#NoEnv
Gui, Margin, 20, 20
Gui, Add, ListView, w800 r15 Grid, #|WinID|X|Y|W|H|Klasse|Titel
FensterArray := FensterListe()
For Index, Fenster In FensterArray
   LV_Add("", Index, Fenster.WinID, Fenster.X, Fenster.Y, Fenster.W, Fenster.H, Fenster.Class, Fenster.Title)
LV_ModifyCol()
Gui, Show, , FensterListe
Return
GuiClose:
ExitApp

FensterListe() {
   ReturnArray := [] ; Rückgabearry deklarieren
   WinGet, ID, List, , , Program Manager ; siehe Hilfe
   Loop, %ID% {
      WinID := ID%A_Index%
      WinGetClass, Class, ahk_id %WinID%
      WinGetPos, X, Y, W, H, ahk_id %WinID%
      WinGetTitle, Title, ahk_id %WinID%
      ReturnArray[A_Index] := {WinID: WinID, X: X, Y: Y, W: W, H: H, Class: Class, Title: Title}
   }
   Return ReturnArray
}
Viel Spaß!
Last edited by just me on 15 Nov 2015, 04:55, edited 4 times in total.
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

20 Oct 2015, 10:32

Weitere Threads mit Informationen zum Thema
Last edited by just me on 21 Oct 2015, 02:51, edited 1 time in total.
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

20 Oct 2015, 11:01

Reserve
Umek
Posts: 65
Joined: 06 Oct 2015, 12:40
Location: Germany

Re: 'Echte' Arrays - Grundlagen

22 Oct 2015, 11:25

Oh ich freu mich drauf. Ich habe die nächsten Tage ein bisschen mehr Zeit.

Gruß Umek
just me
Posts: 9407
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: 'Echte' Arrays - Grundlagen

15 Nov 2015, 05:01

Moin,

ich habe das jetzt erst einmal abgeschlossen. Fragen, Anregungen, Fehlerkorrekturen und (wenn's denn sein muss auch) Kritik werden gern entgegengenommen.

Weiterhin viel Spaß mit AHK!

just me
BoBo
Posts: 6564
Joined: 13 May 2014, 17:15

Re: 'Echte' Arrays - Grundlagen

28 Jun 2017, 04:54

R-E-S-P-E-K-T ! :clap: :clap: :clap:
Ich habe zwar mehr als einen anlauf gebraucht um das gezeigte zu "verdauen" (was am thema liegt, und eigentlich bin ich immer noch dabei), doch die art und weise der erklärung lässt doch hoffen, das so auch noobs den zugang zu "richtigen" arrays leichter finden. :thumbup:

Ich habe mich erstmals durch StrSplit() dazu genötigt gefühlt ins thema einzusteigen.
Doch worauf ich immer noch hereinfalle, ist das vergessen der geforderten "Ausdruck-Deklaration" im Loop mittels führendem % bei verwendung von Min-/Max.Index()/Length() :shh:

Danke für das informative Tutorial. Jetzt gehts auf den weg zur OOP show ... :wave:

PS. Zur ausgabe verwendest du in einigen beispielen die MsgBox in der parameter schreibweise. Damit wandern einige infos (zB array element position) in die titelzeile der MsgBox. Dort habe ich diese prompt überlesen (gibt nicht viel her so ein kleiner MsgBox-titel, und ist in der regel so interessant wie ne EULA). IMHO wäre die vereinfachte MsgBox-option (ohne parameter) hier der sache etwas dienlicher. Merci fürs zuhören :)

Return to “Tutorials”

Who is online

Users browsing this forum: No registered users and 16 guests