66. Ein fast nützliches Programm I

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.


Markus Demleitner

Copyright Notice