![]() |
BKY-Analyser |
Version | Anpassungen |
---|---|
1.0 (2025-08-11) | Initiale Version |
2.0 (2025-08-16) | Es können auch Referenzen auf Komponenteneigenschaften ermittelt werden. |
Inhaltsverzeichnis
Definition einer globalen Variablen
Definition eines Event-Handlers
Das ZIP-Archiv AI2-BKY-Analyser zum Download. Das Archiv enthält das Visual-Studio-Projekt und das kompilierte Binary.
Wenn man ein größeres Projekt mit dem App Inventor entwickelt, kann man schnell die Übersicht über die Verwendung einzelner Elemente verlieren. Änderungen oder gar ein Refactoring werden dann schwierig. Mit dem hier vorgestellten Werkzeug kann man sich einen Überblick über die Verwendung der Blöcke in einem AI2-Projekt verschaffen.
Da ich eine grafische Benutzeroberfläche erstellen wollte, habe ich das Programm mit Visual Studio (kostenlose Community Edition) in C# geschrieben. Wesentlichen Elemente lassen sich aber leicht auf eine andere Plattform / Sprache portieren.
Die vom App Inventor exportierte .aia-Datei kann einfach auf das Fenster gezogen werden oder über die Schaltfläche
ausgewählt werden. Danach wird eine
Liste der enthalten
Screen-Blöcke
angezeigt (Liste mit der Überschrift "Screens"). Klickt man einen der Einträge an, wird in der Liste daneben (Überschrift
"Blocks") die enthaltenen Wurzel-Blöcke angezeigt. Klickt man einen der Blöcke an, werden in der der rechten Liste (Überschrift
"Is referenced by") die Blöcke angezeigt, die den selektierten referenzieren. Zur besseren Übersicht werden Blöcke,
die nicht von anderen Blöcken referenziert werden, ein "---" vorangestellt (s. roter Pfeil). Diese Blöcke werden nicht
verwendet und können entfernt werden.
Über die CheckBox Include Events kann ausgewählt werden, ob die Ereignishandler mit in die Liste der Blöcke aufgenommen werden sollen. Sie erscheinen dann auch in der HTML-Tabelle.
Die Schaltfläche Generate HTML Table erzeugt den Code für eine HTML-Tabelle der Blöcke und kopiert diesen in die Zwischenablage.
Die CheckBox Include links for references bestimmt, ob die referenzierenden Blöcke (rechte Liste) einen Link auf die referenzierten Blöcke (mittlere Liste) erhalten sollen. Die Namen der Blöcke werden mit in der HTML-Tabelle mit einem <span>-Tag versehen. Die TextBox Class name legt den Namen der NTML-Klasse (CSS) fest, mit der dieser Tag versehen wird.
Die Tabelle beginnt mit einer Überschriftenzeile:
<table>
<tr>
<th>Type</th>
<th>Name</th>
<th>Usage</th>
<th>Referenced by</th>
</tr>
Für jeden dokumentierten Block wird eine Tabellenzeile erzeugt. Beispiel:
<tr id="refBlockName">
<td>Global</td>
<td><span class="symbolNoWrap">BlockName</span></td>
<td></td>
<td>Get: Procedure: <a href="#refOtherBlockName"><span class="symbolNoWrap">OtherBlockName"</span></a><br>
Set: Procedure: <a href="#refFutherBlockName"><span class="symbolNoWrap">FutherBlockName"</span></a>
</td>
</tr>
Das <tr>-Tag erhält als ID den Blocknamen mit vorangestelltem "ref". Damit kann auf diesen Block per Link verwiesen werden. Die erste Spalte erhält die Bezeichnung des Typs:
Global | Globale Variable |
![]() |
Function | Funktion, eine Methode, die einen Wert zurück liefert |
![]() |
Procedure | Prozedur, eine Methode, die keinen Wert zurück liefert |
![]() |
Event | Eventhandler |
![]() |
In der zweiten Spalte wird der Name des Blocks angegeben. Der Name wird mit einem <span>-Tag versehen. Der Klassenname für dieses Tag kann in der Bildschirmmaske festgelegt werden. Im Beispiel ist dies symbolNoWrap. Auf meinen Seiten ist dafür dieser CSS-Code hinterlegt:
.symbolNoWrap {
font-family: 'Times New Roman';
font-weight: bold;
font-style: italic;
white-space: nowrap;
hyphens: none;
}
Die dritte Spalte ist leer. Sie ist für Kommentierungen vorgesehen.
Die vierte Spalte listet die Blöcke, die den dokumentierten Block referenzieren. Zunächst wird die Art der Referenz angegeben:
Es folgt der Typ des referenzierenden Blocks: Function, Procedure oder Event, gefolgt von dessen Namen. Der Name ist wieder mit einem <span>-Tag versehen und, wenn angefordert, mit einem Link zu der Zeile in der er dokumentiert wird.
Mit einigen zusätzlichen Formatierungsanweisungen für die Tabelle sieht das dann so aus:
App Inventor bietet die Möglichkeit eine Datei mit der Projektdefinition zu exportieren und wieder zu importieren. Die exportierte Datei hat die Endung .aia ist ein ZIP-Archiv. Man kann sie also mit einem entsprechenden Programm öffnen.
Im Verzeichnis src\appinventor\<user>\<projektname> (z.B. src\appinventor\ai_bienonline\ADFC_EIN_Code_SH_HH) findet man den Source-Code für das Projekt. Im Beispiel:
Für jeden Screen gibt es eine Datei mit der Endung .scm, eine Datei im JSON-Format, die Daten für das Designer-Fenster enthält, und eine mit der Endung .bky mit XML-Daten für das Blocks-Fenster. Die .scm-Datei beschreibt also die Oberfläche der App, die .bky-Datei den Code.
Ein App-Inventor-Projekt ist aus vier verschiedenen Wurzel-Blöcken aufgebaut. Die .bky-Datei ist im Wesentlichen eine Liste von XML-Knoten (XmlNode) mit einem Eintrag für jeden dieser Blöcke mit dem Namen <block>. Jeder dieser XML-Knoten hat ein Attribut mit dem Namen type, dass den Typ des Blocks angibt.
<block type="global_declaration" id=")J*K{.OM)m3_hM]z4!xB" x="-970" y="-2190">
Die XML-Nodes für die Definition von Prozeduren haben das type-Attribut global_declaration.
<block type="global_declaration" id=")J*K{.OM)m3_hM]z4!xB"
x="-970" y="-2190">
<field name="NAME">PrintCount</field>
...
Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der globalen Variablen enthält (hier PrintCount).
Die XML-Nodes für die Definition von Prozeduren haben das type-Attribut procedures_defnoreturn.
<block type="procedures_defnoreturn" id="JJ/t,[I9T:0r}S]F_%Nv"
collapsed="true" x="710" y="-2010">
<mutation xmlns="http://www.w3.org/1999/xhtml">
<arg
name="State"/>
</mutation>
<field
name="NAME">SetDialogElements</field>
...
Auch jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der Prozedur enthält (hier SetDialogElements).
Die XML-Nodes für die Definition von Funktionen haben das type-Attribut procedures_defreturn.
<block type="procedures_defreturn" id="iW)k^-]sRQp.7!(gu)@d"
collapsed="true" x="-270" y="-1330">
<field name="NAME">getCode</field>
...
Auch jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der Funktion enthält (hier getCode).
Die XML-Nodes für die Definition von Event-Handlern haben das type-Attribut component_event.
<block type="component_event" id="A:`O78$dOIF1![_A{rdj"
collapsed="true" x="-630" y="-1690">
<mutation xmlns="http://www.w3.org/1999/xhtml"
component_type="Form" is_generic="false"
instance_name="Screen1" event_name="OtherScreenClosed"/>
<field name="COMPONENT_SELECTOR">Screen1</field>
...
Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert NAME, das den Namen der Componente enthält, zu der dieser Event-Handler gehört (hier Screen1). Der Name des Ereignisses in dem Attribut event_name der Child-Node <mutation> codiert (hier OtherScreenClosed).
Referenzen werden ebenfalls durch ein XML-Node mit den Tag <block> dargestellt. Das type-Attribut gibt die Art der Referenz an.
<block type="lexical_variable_set" id="rr6#O08t7p-lB9`@2jOc">
Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut lexical_variable_set. Sie sind einem Wurzel-Block untergeordnet.
...
<block type="lexical_variable_set"
id="rr6#O08t7p-lB9`@2jOc">
<field name="VAR">global
PrintCount</field>
...
Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert VAR, das den Namen der globalen Variablen enthält (hier global PrintCount).
Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut lexical_variable_get. Sie sind einem Wurzel-Block untergeordnet.
...
<block type="lexical_variable_get"
id="2dGj+$lE/@6s)^!dc.L!">
<field name="VAR">global
Today</field>
...
Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert VAR, das den Namen der globalen Variablen enthält (hier global Today).
Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut procedures_callnoreturn. Sie sind einem Wurzel-Block untergeordnet.
...
<block type="procedures_callnoreturn"
id="*9F:y6cE4~l9wWr?)(v`" inline="true">
<mutation xmlns="http://www.w3.org/1999/xhtml"
name="SetDialogElements">
<arg name="State"/>
</mutation>
<field name="PROCNAME">SetDialogElements</field>
...
Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert PROCNAME, das den Namen der globalen Variablen enthält (hier SetDialogElements).
Die XML-Nodes für Blöcke, die globale Variablen beschreiben haben das type-Attribut procedures_callreturn. Sie sind einem Wurzel-Block untergeordnet.
...
<block type="procedures_callreturn"
id="]XYGJ5b|gQ:}):z%3(f~" inline="false">
<mutation xmlns="http://www.w3.org/1999/xhtml"
name="CodeIsNotComplete"/>
<field name="PROCNAME">CodeIsNotComplete</field>
...
Jeder dieser Block-Definitionen hat ein Child-Node <field> mit dem Attribut name und dem Wert PROCNAME, das den Namen der globalen Variablen enthält (hier CodeIsNotComplete).
Die Klasse BKY-Analyser stellt die Methoden zur Analyse einer .aia-Datei bereit. In der Klasse AI2Block werden die notwendigen Informationen zu den gefundenen Wurzel-Blöcken abgelegt. In der Klasse Reference werden die Daten zu den referenzierenden Blöcken gespeichert. Hinzu kommen Enum-Klassen zur Typ-Sicherheit und Listen-Klassen.
BKYException dient zum Erkennen von projektspezifischen Ausnahmen. Die Klasse EmptyNodeList erleichtert die null-Abfrage, wenn XML-Node-Listen abgerufen werden.
Der Code ist mit umfangreichen XML-Dokumentationskommentaren versehen. Daraus wurde eine Help-Datei erstellt (Link zur Help-Datei).
Die Klasse AI2Block speichert die Daten eines Wurzelblocks. Dies sind
Die Methode ToString ist überschrieben als Kombination der Felder Type und Name, z.B. "Global: PrintCount". Objekte dieser Klasse können demnach direkt an eine ListBox übergeben und werden sinnvoll angezeigt.
Die Gleichheitsoperatoren == und != sind überschrieben. Dadurch können Listen von Objekten dieser Klasse einfach durchsucht werden. Verglichen werden die Felder Type und Name.
Implementiert ist die Schnittstelle IComparable<T>. Sie erlaubt das einfache sortieren einer Liste von Objekten dieser Klasse. Verglichen werden die von ToString zurück gegebenen Daten.
Die Klasse Reference speichert Daten zu Blöcken, die andere Blöcke referenzieren. Diese sind:
Eine Liste mit Objekten dieser Klasse wir in der Klasse AI2Block benutzt.
Die Methode ToString ist überschrieben als Kombination der Felder Type und AI2Block.ToString, z.B. "Get: Procedure: ComputeCode". Objekte dieser Klasse können demnach direkt an eine ListBox übergeben und werden sinnvoll angezeigt.
Die Gleichheitsoperatoren == und != sind überschrieben. Dadurch können Listen von Objekten dieser Klasse einfach durchsucht werden. Verglichen werden die Felder Type und ReferencedFrom.
Implementiert ist die Schnittstelle IComparable<T>. Sie erlaubt das einfache sortieren einer Liste von Objekten dieser Klasse. Verglichen werden die von ToString zurück gegebenen Daten.
Die Klasse BKYAnalyser stellt Methoden zur Analyse von .bky-Dateien bereit. Die Methode LoadXml analysiert die XML-Struktur der Datei und erstellt eine Liste von Wurzel-Blöcken (Feld RootBlocks). Diese Liste kann zur Anzeige der Inhalte in einer ListBox verwendet werden. LoadXml liefert ein Objekt dieser Klasse zurück, über das auf die Blockliste zugegriffen werden kann. Die Methode GetTable liefert eine HTML-codierte Tabelle der Blöcke und Referenzen.
Das Analysieren der XML-Datei erfolgt in mehreren Phasen. Die Analyse erfolgt mit Hilfe der XmlDocument-Klasse. Um Elemente in dem XML-Dokument zu selektieren werden Xpath-Abfragen verwendet.
Das in der .bky-Datei enthalte XML-Dokument verwendet Namensräume. Dies muss bei den Xpath-Abfragen berücksichtigt werden. Am einfachsten geht dies, wenn man einen XmlNamespaceManager verwendet. Diesem werden die Bezeichnungen der Namensräume (URI) und ein Kürzel für die spätere Verwendung übergegeben.
// Das .bky-XML-Dokument besitzt einen Namespace (xmlns="https://developers.google.com/blockly/xml")
// Zur Selektion muss deshalb mit einem Namespace-Manager gearbeitet werden.
XmlNamespaceManager nsman = new(bky.NameTable);
nsman.AddNamespace("a", docElement.NamespaceURI);
nsman.AddNamespace("b", "http://www.w3.org/1999/xhtml");
Mit dem XmlNamespaceManager und dem Stamm-XmlElement (XmlDocument.DocumentElement Eigenschaft) wird eine neue Instanz der BKYAnalyser-Klasse erzeugt. Anschließen werden die Wurzel-Blöcke aus dem XML-Dokument und anschließend die Referenzen extrahiert. Die BKYAnalyser-Instanz wird zur weiteren Verwendung zurück geliefert.
var bkyAnalyser = new BKYAnalyser(nsman, docElement);
bkyAnalyser.getRootBlocks();
bkyAnalyser.getReferenzes();
return bkyAnalyser;
getRootBlocks benutzt einen XPath-Ausdruck um alle XML-Wurzelknoten mit dem Namen block zu finden. Diese werden dann in analyzeRootBlock weiter untersucht.
/// <summary>Blöcke auf der obersten Ebene selektieren und analysieren.</summary>
/// <exception cref="BKYException">Das XML-Dokument ist kein wohlformatiertes .bky-Dokument.</exception>
private void getRootBlocks() {
var blockNodes = docElement.SelectNodes("a:block", nsman);
if (blockNodes == null) {
throw new BKYException("invalid structure: Select Blocks");
}
foreach (XmlNode blockNode in blockNodes) {
RootBlocks.Add(analyzeRootBlock(blockNode));
}
RootBlocks.Sort();
}
analyzeRootBlock ermittelt die Typ-Bezeichnung des Blocks ...
// Root-Blocks haben ein Attribut 'type', das angibt, um welchen Typ Block es sich handelt.
string ai2BlockTypeName = ""; // AI2-Bezeichnung des BlockTyps
XmlNode? typeAttr = blockNode.SelectSingleNode("./@type", nsman);
if (typeAttr != null) {
ai2BlockTypeName = typeAttr.InnerText;
}
... den Namen des AI2-Blocks bzw. den Namen der Komponente bei Eventhandlern ...
// Die Root-Block-Nodes haben eine Child-Node mit dem Namen (Tag) 'field' mit dem Attribut 'NAME'
// Dieses Attribut enthält den Namen der Variable, Funktion, Prozedur oder Ereignis.
XmlNode? nodeField = blockNode.SelectSingleNode("child::a:field", nsman);
string blockName = "unknown";
if (nodeField != null) {
blockName = nodeField.InnerText;
}
AI2Block ai2Block = new(blockName, ai2BlockTypeName, blockNode);
... und im Falle eines Eventhandlers den Namen des Ereignisses:
// Ereignisname ermitteln
if (ai2Block.Type == BlockTypes.Event) {
string eventName = "unknown";
XmlNode? nodeMutation = blockNode.SelectSingleNode("child::b:mutation/@event_name", nsman);
if (nodeMutation != null) {
eventName = nodeMutation.InnerText;
}
ai2Block.Name += "." + eventName;
Das suchen der Referenzen erfolgt in vier Schritten. In den gespeicherten XML-Nodes der Wurzelblöcke wird nacheinander nach Knoten mit dem Namen block und den type-Attributen lexical_variable_set, lexical_variable_get, procedures_callnoreturn und procedures_callreturn gesucht. Hier als Beispiel die Suche nach lexical_variable_set. Alle Wurzelblöcke werden durchsucht. Solche, die eine globale Variable repräsentieren, können ignoriert werden. Diese können keine anderen Blöcke referenzieren:
// Prüfen, welche Blöcke der obersten Ebene von welchen Blöcken referenziert werden
// Welcher Root-Block setzt welche globaler Variable
// --------------------------------------------------
foreach (AI2Block rootBlock in RootBlocks) {
if (rootBlock.Type == BlockTypes.Global) // Globale Variablen können keine anderen beschreiben
continue;
Die in der AI2Block-Instanz gespeicherte XML-Node wird per Xpath durchsucht:
// Liste der Nodes in diesem Root-Block, in denen eine globale Variable belegt wird.
var setBlocks = rootBlock.XmlNode.SelectNodes("descendant::a:block[attribute::type='lexical_variable_set']", nsman);
Bei allen gefunden Nodes wird der Name der referenzierten Variablen ermittelt (Innertext der Child-Node field mit dem Attribut name='Var'). Dazu muss geprüft werden, ob es sich um eine globale oder eine lokale Variable handelt.
// Diese Liste abarbeiten
foreach (XmlNode setBlock in setBlocks ?? new EmptyNodeList()) {
// Der Name ist im Child 'field' mit Attribut name = 'VAR'
var referencedVarNode = setBlock.SelectSingleNode("child::a:field[attribute::name='VAR']", nsman);
if (referencedVarNode == null) // Kein solches Feld: Problem!
throw new BKYException();
String variableName = referencedVarNode.InnerText; // Der Name der Variablen
if (!variableName.StartsWith("global")) // Andernfalls lokale Variable
continue;
Zum Schluss muss der referenzierte Block herausgesucht werden, und die Referenz notiert werden:
// Den Root-Block für die globale Variable mit diesem Namen heraussuchen
var globalVariableBlock = RootBlocks.GetBlock(variableName[7..], BlockTypes.Global);
// Dieser Root-Block referenziert diese Variable
globalVariableBlock?.AddReference(new Reference(ReferenceTypes.Set, rootBlock));
}
}
Diese Methode liefert den HTML-Code für eine Tabelle mit den Wurzelblöcken und den zugehörigen Referenzen. Hier sei auf den Abschnitt Aufbau der HTML-Tabelle und die Code-Dokumentation verweiesen.
Die Entschlüsselung der .aia-Datei ist mit der ZipFile-Klasse recht einfach. Wenn man den Dateinamen über den OpenFileDialog oder per Drag-and-Drop erhält, kann man diese öffnen und alle enthaltenen Dateinamen auslesen:
aia = ZipFile.OpenRead(openFileDialog.FileName);
foreach (ZipArchiveEntry entry in aia.Entries) {
if (entry.Name.EndsWith(".bky"))
lbScreens.Items.Add(entry.Name);
}
Hat der Anwender eine .bky-Datei aus der Liste ausgewählt, wird diese in dem ZIP-Archiv gesucht, geöffnet und in eine String-Variable eingelesen. Dieser String wird anschließend durch die Methode BKYAnalyser.LoadXml analysiert.
string screenName = (string)lbScreens.SelectedItem;
foreach (ZipArchiveEntry entry in aia.Entries) {
if (entry.Name == screenName) {
var stream = entry.Open();
StreamReader reader = new StreamReader(stream, System.Text.Encoding.UTF8);
var text = reader.ReadToEnd();
bkyAnalyser = BKYAnalyser.LoadXml(text);