TreeCtrl + Panel + expat XML Parser

Plattformunabhängige GUIs mit wxWidgets.
Antworten
m0ps1234
User
Beiträge: 40
Registriert: Freitag 13. März 2009, 08:57

Hallo zusammen,
ich möchte mit dem expat Parser ein XML parsen, dass dann einige Element in einem Baum, andere in einem Panel darstellt.

Das XML sieht Schemahaft so aus:

Code: Alles auswählen

<ueberblick>
    <test name="a">
        <testresult name="result Test a">OK</testresult>
        ...weitere Elemente
    </test>
    <testGroup name="b">
        <test name="c">
             ...weitere Elemente
        </test>
        <test name="d">
             ...weitere Elemente        
        </test>
    </testGroup>
...
</ueberblick>
Es gibt Tests und es gibt Testgruppen mit mehreren Tests. Einzelne Tests sind auf der gleichen Ebene wie Testgruppen. Tests enthalten weitere Elemente, welche sich aber von der Anzahl unterscheiden. Also Test a hat beispielsweise 5 weitere Elemente mit Unterelementen, wobei Test c nur 3 Elemente ohne Unterelemente hat.

Ich Parse die Datei mit dem expat Parser und möchte das ganze gerne so machen, dass mir bis zu der Ebene der Testgruppentests ein Baum erstellt wird.
In diesem Fall also:

Baum:

Code: Alles auswählen

-Überblick
   -a
   -b
      -c
      -d
Die weiteren Inhalte sollen dann neben dem Baum in einem Panel angezeigt werden. Also am Beispiel von Test a sollte bei einem Klick darauf im Panel dann folgendes erscheinen:

result Test a: OK
...

Den Parser mit den Start-/End-Element- & Character-Handler habe ich schon, welcher mir das komplette XML als Baum darstellt:

Code: Alles auswählen


"""
Build a GUI Tree (wxWindows) from an XML file using pyExpat
"""

import sys,string
from xml.parsers import pyexpat

from wxPython.wx import *

class MyFrame(wxFrame):
	def __init__(self, parent, id, title):
		wxFrame.__init__(self, parent, id, title, wxPoint(100, 100), wxSize(160,
100))
		menu = wxMenu()
		menu.Append (1001,"Open")
		menu.Append (1002,"Close")
		menu.Append (1003,"Exit")
		menubar = wxMenuBar()
		menubar.Append (menu,"File")
		self.SetMenuBar(menubar)

class MyApp(wxApp):
	def OnInit(self):
		self.frame = MyFrame(NULL, -1, "Tree View of XML")
		self.tree = wx.wxTreeCtrl (self.frame, -1)
		EVT_MENU(self, 1001, self.OnOpen)
		EVT_MENU(self, 1002, self.OnClose)
		EVT_MENU(self, 1003, self.OnExit)
		self.frame.Show(true)
		self.SetTopWindow(self.frame)
		return true

	def OnOpen(self,event):
		f = wxFileDialog(self.frame,"Select a file",".","","*.xml",wxOPEN)
		if f.ShowModal() == wxID_OK:
			LoadTree (f.GetPath())
		
	def OnClose(self,event):
		self.tree = wx.wxTreeCtrl (self.frame, -1)
		pass
		
	def OnExit(self,event):
		self.OnCloseWindow(event)

	def OnCloseWindow(self, event):
		self.frame.Destroy()


NodeStack = []

# Define a handler for start element events
def StartElement( name, attrs ):
	global NodeStack
	NodeStack.append (app.tree.AppendItem (NodeStack[-1],name))

def EndElement( name ):
	global NodeStack
	NodeStack = NodeStack[:-1]

def CharacterData ( data ):
	global NodeStack
	if string.strip(data):
		app.tree.AppendItem (NodeStack[-1],data)


def LoadTree (f):
	print f
	# Create a parser
	Parser = pyexpat.ParserCreate()

	# Tell the parser what the start element handler is
	Parser.StartElementHandler = StartElement
	Parser.EndElementHandler = EndElement
	Parser.CharacterDataHandler = CharacterData

	# Parse the XML File
	ParserStatus = Parser.Parse( open(f,'r').read(), 1)
	if ParserStatus == 0:
		print "oops!"
		raise SystemExit

app = MyApp(0)
NodeStack = [app.tree.AddRoot ("Root")]


app.MainLoop()
raise SystemExit


Leider weiß ich überhaupt nicht, wie ich da vorgehen soll. Oder gibt es vielleicht eine bessere Möglichkeit als den expat? Bin noch recht neu auf dem Gebiet.
Freu mich auf Antworten.

Grüße
Matthias
BlackJack

@m0ps1234: Der Quelltext sieht nach einem sehr alten Tutorial für `wxPython` aus. Es gibt einiges was man heute anders lösen würde. Das fängt beim Importieren an. Man importiert in modernem `wxPython`-Quelltext nur noch ganz schlicht `wx`. Die Namen in diesem Modul haben alle keinen wx-Pfäfix mehr, weil man dafür dann ja schon den Modulnamen hat. Man schreibt dann also `wx.Frame` statt `wxFrame`.

Bei Grössenangaben braucht man nicht mehr explizit `wx.Point` oder `wx.Size` Exemplare erstellen, sondern kann dort einfach Tupel mit Zahlen übergeben. Statt `NULL` wird `None` verwendet, statt `true` und `false` die Python-Standardnamen `True` und `False`, und wenn man eine beliebige ID möchte, sollte man aus Gründen der Lesbarkeit die Konstante `wx.ID_ANY` statt einer literalen -1 verwenden.

Für eine Menge Standardoperationen gibt es schon vorgefertigte ID-Konstanten, wie zum Beispiel für Deine Menüpunkte (`wx.ID_OPEN`, `wx.ID_CLOSE`, `wx.ID_EXIT`). Die sollte man eigenen Zahlen vorziehen. Da kann man dann auch den Text weglassen, denn plattformabhängige Icons, Texte, und Tastaturkürzel bekommt man frei Haus bei diesen IDs. Auch sonst sollte man keine IDs für Menüs erfinden sondern `wx.ID_ANY` verwenden und sich das Ergebnis des `Append()`-Aufrufs merken. Da bekommt man nämlich ein `wx.MenuItem`-Exemplar, welches man auch verwenden kann, um Aktionen damit zu verbinden.

Zum Binden von Aktionen verwendet man nicht mehr die `EVT_*`-Funktionen, sondern die `Bind()`-Methode auf `wx`-Objekten.

Das `wx.TreeCtrl` würde ich wiederverwenden, also kein neues erstellen wenn eine Datei geschlossen wird, sondern beim Schliessen den alten Bauminhalt entsorgen.

Die Datenhaltung ist sehr unsauber gelöst. Das würde ich unabhängig von der GUI erst einmal ordentlich lösen -- das XML in passende Objekte parsen. Ohne die Verwendung von globalen Namen und dem ``global`` Schlüsselwort!

Wenn Du die Datenobjekte hast, kannst Du Dich daran machen eine GUI-Komponente zu schreiben, die den Baum und die Details darstellt. Du müsstest dann auf Ereignisse des `wx.TreeCtrl` reagieren wenn der Benutzer dort etwas auswählt, um dann im `wx.Panel` die passenden Details anzuzeigen.

Das `string`-Modul sollte man nicht für Funktionen verwenden, die es als Methoden auf Zeichenketten gibt. Das `strip()` in `CharacterData()` ist allerdings sowieso falsch, da diese Funktion auch mit Teilen von dem Inhalt eines Textknotens aufgerufen werden kann, und dann würdest Du Leerzeichen *innerhalb* von Texten entfernen.

Dateien die man öffnet, sollte man auch selbst wieder schliessen. Am besten mit der ``with``-Anweisung.

``raise SystemExit`` ist eine recht ungewöhnliche Art `sys.exit()` zu schreiben. Und ganz am Ende des Quelltextes ist das nicht nötig, da endet das Programm ja sowieso, wenn der Programmfluss diese Stelle im Code erreicht.

Wie bist Du auf `expat` gekommen? Ich würde eher `ElementTree` oder `lxml` zum Parsen von XML empfehlen. Das ist eine wesentlich angenehmere API.

Du gehst übrigens ziemlich uneinheitlich mit Leerzeichen im Quelltext um.
m0ps1234
User
Beiträge: 40
Registriert: Freitag 13. März 2009, 08:57

Vielen Danke erstmal für deine schnelle Antwort!
Ich bin mit Python noch nicht sonderlich vertraut. Mit dem Quelltext hast du recht, er stammt aus einem alten Tutorial.
Werde deine Verbesserungsvorschläge aber zu Kenntnis nehmen und versuchen diese umzusetzen.

Auf expat kam ich durch dieses Tutorial. Ich bin erst seit 2 Tagen dabei mich mit Python zu beschäftigen. Deswegen kannte ich noch nicht wirklich Alternativen. Aber ich werde mir die APIs mal anschauen. Danke!

Ansonsten würde mir noch helfen, wenn ihr einen Tipp zur Umsetzung hättet. Hab es mir jetzt so gedacht, dass ich den Baum bis zu den Tests aufbaue. Parst er im XML in den Test hinein, möchte ich gerne die Daten irgendwie speichern, die zu diesem test gehören, um diese dann mit einem EVENT abrufen zu können. Jedoch sollten sie in der Reihenfolge bleiben. Dachte da an ein Dictionary. Oder gibt es da etwas besseres?

Grüße
Matthias
BlackJack

@m0ps1234: Sowohl `ElemenTree` -- seit Python 2.5 in der Standardbibliothek enthalten -- als auch `lxml` haben die gleiche "Grund-API". `lxml` kann ein bisschen mehr, aber das braucht man hier wahrscheinlich nicht unbedingt. Wenn das XML nicht gerade den Hauptspeicher sprengt, würde ich nicht imkrementell parsen, sondern die XML-Datei erst einmal komplett parsen lassen.

Wenn Du unbedingt willst, kannst Du natürlich daraus direkt die Daten für die GUI ziehen. Die zusätzlichen Daten für das Panel kannst Du direkt an die GUI-Objekte für die Baumelemente hängen. Dafür gibt es das `data`-Argument beim `AppendItem()` und `wx.TreeCtrl.GetPyData()` um die Daten wieder abzurufen. Die nötige ID wird beim Event mitgeliefert. Hier stellt sich wieder die Frage ob man die XML-Knoten dort hinterlegt, oder nicht doch besser das XML unabhängig von der GUI erst einmal eine passende Objekthierarchie überführt.
Antworten