Wir wollen einen kleinen Editor schreiben, der verschiedene Encodings kann und
erlaubt, experimentell zu bestimmen, in was für einem Encoding eine Datei geschrieben ist. Dazu brauchen wir
zunächst einen Text mit Scrollbalken. Kombinationen von Widgets packt man meist in einen Frame.
Also:
class ScrollableText(Tkinter.Frame):
def __init__(self, master, *args, **kwargs):
Tkinter.Frame.__init__(self, master,
*args, **kwargs)
self.encoding = "iso-8859-1"
self.textField = Tkinter.Text(self, width=60,
height=20, wrap=Tkinter.NONE)
self.scrollVert = Tkinter.Scrollbar(self,
command=self.textField.yview)
self.scrollHorz = Tkinter.Scrollbar(self,
command=self.textField.xview,
orient=Tkinter.HORIZONTAL)
self.textField.config(xscrollcommand
=self.scrollHorz.set,
yscrollcommand=self.scrollVert.set)
self.textField.grid(row=0, col=0, sticky
=Tkinter.N+Tkinter.S+Tkinter.W+Tkinter.E)
self.scrollVert.grid(row=0, col=1,
sticky=Tkinter.N+Tkinter.S)
self.scrollHorz.grid(row=1, col=0,
sticky=Tkinter.W+Tkinter.E)
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self._doBindings()
Anmerkungen:
-
Wir verwenden zum Aufruf des Konstruktors der Elterklasse das oben vorgestellte Pattern.
- Tkinter.Text hat
haufenweise Optionen, wir stellen hier die Anfangsgröße und das Verhalten bei zu langen Zeilen ein. Da wir auch
horizontal Scrollen können, soll gar nicht umgebrochen werden.
- Die Scrollbars haben einen Callback command. Wenn
an ihnen rumgeschoben wird, rufen sie diesen Callback auf, um die Änderungen dem von ihnen gesteuerten Widget
mitzuteilen. In unserem Fall hat Text schon Methoden, die genau so gemacht sind, wie Scrollbar das braucht, nämlich
yview und xview.
- Umgekehrt muss auch der Text die Möglichkeit haben, den Scrollbars zu sagen, wenn
sich was am dargestellten Ausschnitt ändert, etwa, weil mit der Tastatur gescrollt wurde oder weil der
Text sich geändert hat. Dafür dienen die .scrollcommand-Methoden, die wir im Nachhinein durch
config setzen.
- Wir verwenden hier den grid-Geometriemanager. Dessen sticky-Option dient etwa dem
gleichen Zweck wie die fill-Option des pack-Managers, nur dass hier angegeben werden kann, an
welchen Grenzen einer Zelle das Widget “kleben” soll, und zwar nach Himmelsrichtung. Ein Widget, das
mit sticky=Tkinter.N gegridet wurde, wird immer mit der Oberkante seiner Zelle abschließen.
- Ein
Äquivalent der expand-Option des pack-Managers hat grid nicht – das ginge auch nicht, weil ja alle
Zellen einer Spalte bzw. Zeile in gleicher Weise wachsen müssen. Daher kann das Wachstum auch nur
zeilen- oder spaltenweise festgelegt werden. Genau das tun row- bzw. columnconfigure. Hier legen wir
einfach fest, dass das ganze Wachstum des Containers auf Spalte und Zeile 0 gehen soll, eben dort,
wo das Text ist.
- Wir wollen das Verhalten des Standard-Text-Widgets noch ändern. Dazu werden wir
Bindings verwenden, wollen den Code dazu aber aus dem Konstruktor draußen haben und lagern ihn in die
Funktion _doBindings aus.
- Außerdem soll unser Text-Widget gegenüber dem Text-Widget aus Tkinter
auch um Encodings wissen – letzteres nimmt an, dass es (im Groben) Unicode-Strings bekommt. Dazu
machen wir uns ein Attribut encoding, in dem wir das augenblicklich verwendete Encoding speichern.
Wir delegieren das Holen und Setzen der Texte in unserem Widget an Text und kümmern uns ums
Encoding:
def getText(self):
return self.textField.get(1.0,
Tkinter.END).encode(self.encoding)
def setText(self, tx):
utx = tx.decode(self.encoding)
self.textField.delete(1.0, Tkinter.END)
self.textField.insert(1.0, utx)
def setEncoding(self, encoding):
tx = self.getText()
oldEnc = self.encoding
try:
self.encoding = encoding
self.setText(tx)
except UnicodeDecodeError:
self.encoding = oldEnc
raise
Anmerkungen:
- Hier verwenden wir unser Encoding-Attribut, um zwischen dem vom Text-Widget verwendeten Unicode
und dem von der einbettenden Anwendung verwendeten Encoding (das wir auf den in Westeuropa sicheren
Fallback iso-8859-1 gesetzt haben) zu übersetzen. Das ist nicht ganz ungefährlich, weil nicht alles in allem
kodiert werden kann. Die encode- und decode-Methoden können Exceptions werfen. Diese geben wir
hier einfach an die einbettende Anwendung weiter, die das irgendwie behandeln sollte (wir tun das im
Beispielprogramm nicht). Dadurch, dass wir zunächst dekodieren und dann erst den alten Text löschen,
vermeiden wir, dass, wenn das Dekodieren nicht möglich sein sollte, gar kein Text mehr im Widget steht.
- In
setEncoding müssen wir Dekodierungsfehler aber selbst behandeln. Wenn nämlich nicht dekodiert werden
kann, steht der Text immer noch im alten Encoding im Widget. Deshalb müssen wir das alte Encoding
speichern und es restaurieren, wenn der Wechsel nicht geklappt haben sollte. Die Exception müssen wir aber
trotzdem an die einbettende Anwendung weitergeben – in unserem Beispiel müsste dann die Anzeige des
Encodings auf den alten Wert gesetzt werden. Ich haben das nicht gemacht. Probiert es selbst (ihr braucht
dafür wahrscheinlich eine Methode getEncoding von ScrollableText; nützlich dabei ist, dass ihr
useEncoding setzen könnt und die Radiobuttons automatisch den neuen Zustand reflektieren, siehe unten).
- Methoden wie get, insert oder delete des Text-Widgets von Tkinter können auch nur Teile des Textes
bearbeiten. Deshalb nehmen sie Argumene wie 1.0 (“Erste Zeile, Nulltes Zeichen”, also Anfang des
Textes) oder Tkinter.END, das sich, egal wie viel Text da ist, immer auf das Ende des Textes bezieht.
Die doBindings-Methode soll hier – nur als Beispiel – das Scrollen mit dem Mausrädchen unter X (wo
die Bewegung des Rädchens in Mausklicks mit den imaginären Maustasten 4 und 5 übersetzt wird)
aufsetzen:
def _doBindings(self):
self.textField.bind("<Button-4>", lambda ev,
self=self: self.textField.yview(
Tkinter.SCROLL, -1, Tkinter.UNITS))
self.textField.bind("<Button-5>", lambda ev,
self=self: self.textField.yview(
Tkinter.SCROLL, 1, Tkinter.UNITS))
Anmerkung: Es ist nicht immer ganz einfach, zu sehen, an welche Widgets Bindings kommen sollen, und
in der Tat sind die Regeln, wer alles Events zum Prozessieren vorgelegt bekommt, nicht einfach. Für
Maus-Events ist das in aller Regel das Widget, das gerade “direkt” unter dem Mauszeiger liegt, nicht aber
eventuelle Container. Für Tastaturevents hat wenigstens X einen Focus, eben das Widget, das diese
Events bekommt. Wie das funktioniert, will ich hier nicht erklären, die Frage selbst ist jedoch unter
Umständen sehr relevant. In der Tkinter-Doku erfährt man einiges dazu u.a. im Kapitel über Dialog Windows.