Sortierbare Multilistbox

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
Mr_Snede
User
Beiträge: 387
Registriert: Sonntag 8. Februar 2004, 16:02
Wohnort: D-Dorf, Bo

Ich habe hier die MultiListbox von Brent Burley mit zwei Erweiterungen versehen.
1. Mousewheel fix (für Linux?) wie in der Diskussion unter dem ASPN Rezept vorgeschlagen
2. SortableTable(Screenshot unten auf der Seite) um die Spaltenköpfe klickbar zu machen.

Viel Spass damit.

Code: Alles auswählen

#http://tkinter.unpythonic.net/wiki/SortableTable
#based on http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52266 by Brent Burley's excellent widget

from Tkinter import *

class MultiListbox(Frame):
   def __init__(self, master, lists):
      Frame.__init__(self, master)
      self.lists = []
      self.colmapping={}
      self.origData = None
      for l,w in lists:
         frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH)
         b = Button(frame, text=l, borderwidth=1, relief=RAISED)
         b.pack(fill=X)
         b.bind('<Button-1>', self._sort)
         self.colmapping[b]=(len(self.lists),1)
         lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0,
                        relief=FLAT, exportselection=FALSE)
         lb.pack(expand=YES, fill=BOTH)
         self.lists.append(lb)
         lb.bind('<B1-Motion>', lambda e, s=self: s._select(e.y))
         lb.bind('<Button-1>', lambda e, s=self: s._select(e.y))
         lb.bind('<Leave>', lambda e: 'break')
         lb.bind('<B2-Motion>', lambda e, s=self: s._b2motion(e.x, e.y))
         lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
         lb.bind('<Button-4>', lambda e, s=self: s._scroll(SCROLL, -1, UNITS))
         lb.bind('<Button-5>', lambda e, s=self: s._scroll(SCROLL, 1, UNITS))
      frame = Frame(self); frame.pack(side=LEFT, fill=Y)
      Label(frame, borderwidth=1, relief=RAISED).pack(fill=X)
      sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
      sb.pack(expand=YES, fill=Y)
      self.lists[0]['yscrollcommand']=sb.set

   def _sort(self, e):
      # get the listbox to sort by (mapped by the header button)
      b=e.widget
      col, direction = self.colmapping[b]

      # get the entire table data into mem
      tableData = self.get(0,END)
      if self.origData == None:
         import copy
         self.origData = copy.deepcopy(tableData)

      rowcount = len(tableData)

      #remove old sort indicators if it exists
      for btn in self.colmapping.keys():
         lab = btn.cget('text')
         if lab[0]=='[': btn.config(text=lab[4:])

      btnLabel = b.cget('text')
      #sort data based on direction
      if direction==0:
         tableData = self.origData
      else:
         if direction==1: b.config(text='[+] ' + btnLabel)
         else: b.config(text='[-] ' + btnLabel)
         # sort by col
         def colsort(x, y, mycol=col, direction=direction):
            return direction*cmp(x[mycol], y[mycol])

         tableData.sort(colsort)

      #clear widget
      self.delete(0,END)

      # refill widget
      for row in range(rowcount):
         self.insert(END, tableData[row])

      # toggle direction flag
      if(direction==1): direction=-1
      else: direction += 1
      self.colmapping[b] = (col, direction)

   def _select(self, y):
      row = self.lists[0].nearest(y)
      self.selection_clear(0, END)
      self.selection_set(row)
      return 'break'

   def _button2(self, x, y):
      for l in self.lists: l.scan_mark(x, y)
      return 'break'

   def _b2motion(self, x, y):
      for l in self.lists: l.scan_dragto(x, y)
      return 'break'

   def _scroll(self, *args):
      for l in self.lists:
         apply(l.yview, args)
      return 'break'

   def curselection(self):
      return self.lists[0].curselection()

   def delete(self, first, last=None):
      for l in self.lists:
         l.delete(first, last)

   def get(self, first, last=None):
      result = []
      for l in self.lists:
          result.append(l.get(first,last))
      if last: return apply(map, [None] + result)
      return result

   def index(self, index):
      self.lists[0].index(index)

   def insert(self, index, *elements):
      for e in elements:
         i = 0
         for l in self.lists:
            l.insert(index, e[i])
            i = i + 1

   def size(self):
      return self.lists[0].size()

   def see(self, index):
      for l in self.lists:
         l.see(index)

   def selection_anchor(self, index):
      for l in self.lists:
         l.selection_anchor(index)

   def selection_clear(self, first, last=None):
      for l in self.lists:
         l.selection_clear(first, last)

   def selection_includes(self, index):
      return self.lists[0].selection_includes(index)

   def selection_set(self, first, last=None):
      for l in self.lists:
         l.selection_set(first, last)

if __name__ == '__main__':

   employees = [
      ('alex',  'john',  'Alex Kellman'),
      ('alan',  'john',  'Alan Adams'),
      ('andy',  'peter', 'Andreas Crawford'),
      ('doug',  'jeff',  'Douglas Bloom'),
      ('jon',   'peter', 'Jon Baraki'),
      ('chris', 'jeff',  'Chris Geoffrey'),
      ('chuck', 'jeff',  'Chuck McLean'),
      ('alexx',  'johxn',  'Alexx Kellxman'),
      ('alaxn',  'jxohn',  'Alan Adaxms'),
      ('andxy',  'petxer', 'Andreas Crxawford'),
      ('douxg',  'jefxf',  'Douglasx Bloom'),
      ('jonx',   'petxer', 'Joxn Baraki'),
      ('chxris', 'jexff',  'Chrxis Geoffrey'),
      ('chuxck', 'jxeff',  'Chuck MxcLean')
      ]

   tk = Tk()
   Label(tk, text='MultiListbox').pack()
   mlb = MultiListbox(tk, (('Head1', 40), ('Head2', 20), ('head3', 10)))
   for zeile in employees:
      mlb.insert(END, zeile)
      mlb.pack(expand=YES,fill=BOTH)
   tk.mainloop()
Benutzeravatar
Mr_Snede
User
Beiträge: 387
Registriert: Sonntag 8. Februar 2004, 16:02
Wohnort: D-Dorf, Bo

alternativer "if __name__ == '__main__'" Block für ein paar mehr Spalten:

Code: Alles auswählen

if __name__ == '__main__':

   tk = Tk()
   Label(tk, text='MultiListbox').pack()
   mlb = MultiListbox(tk, (('Head1', 5), ('Head2', 10), ('Head3', 20)))
   for zeile in xrange(2000):
      mlb.insert(END, (zeile,  'john',  'Alex Kellman'))
      mlb.pack(expand=YES,fill=BOTH)
   tk.mainloop()
Benutzeravatar
Mr_Snede
User
Beiträge: 387
Registriert: Sonntag 8. Februar 2004, 16:02
Wohnort: D-Dorf, Bo

Ich konnte es nicht lassen und habe die einzelnen Spalten auf ein Tkinter PanedWindow Widget gepackt.
Die Spalten sind somit in der Breite veränderbar!

Um die einzelnen Elemente besser zu kennzeichnen habe ich in den Farbtopf gegriffen und einige Rahmen eingeschaltet.

Zusätzlich habe ich das Mausrad auch an den Rollbalken geknüpft.
Jetzt wird die Liste auch gerollt, wenn sich die Maus über dem Rollbalken befindet.

Edit:
Dadurch, dass ich nun einen Button anstelle eines Labels verwende, ist der Bereich über dem Rollbalken nun genauso hoch wie die Zeile mit den Spaltenköpfen.
- auf dem Unteren der Screenshots sieht man das Problem.

Code: Alles auswählen

#http://tkinter.unpythonic.net/wiki/SortableTable
#based on http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52266 by Brent Burley's excellent widget,
#then copy over the following methods to make it sortable by clicking on column header
from Tkinter import *

class MultiListbox(Frame):
   def __init__(self, master, lists):
      Frame.__init__(self, master)
      self.lists = []
      self.colmapping={}
      self.origData = None

      m = PanedWindow(self)#(orient=VERTICAL)
      m.config(handlesize=0, sashrelief=SUNKEN, borderwidth=0)
      m.pack(side=LEFT,fill=BOTH, expand=1)

      for l,w in lists:
         frame = Frame(m)
         frame.config(bg="red", padx=5, pady=5, relief=RAISED, bd=4)
         frame.pack(side=LEFT, expand=YES, fill=BOTH)

         b = Button(frame, text=l, borderwidth=1, relief=RAISED)
         b.pack(fill=X)
         b.bind('<Button-1>', self._sort)
         self.colmapping[b]=(len(self.lists),1)
         lb = Listbox(frame, width=w, borderwidth=4, selectborderwidth=0,
                        relief=RAISED, exportselection=FALSE)
         lb.pack(expand=YES, fill=BOTH)
         self.lists.append(lb)

         m.add(frame)

         lb.bind('<B1-Motion>', lambda e, s=self: s._select(e.y))
         lb.bind('<Button-1>', lambda e, s=self: s._select(e.y))
         lb.bind('<Leave>', lambda e: 'break')
         lb.bind('<B2-Motion>', lambda e, s=self: s._b2motion(e.x, e.y))
         lb.bind('<Button-2>', lambda e, s=self: s._button2(e.x, e.y))
         lb.bind('<Button-4>', lambda e, s=self: s._scroll(SCROLL, -1, UNITS))
         lb.bind('<Button-5>', lambda e, s=self: s._scroll(SCROLL, 1, UNITS))
      frame = Frame(self)
      frame.config(bg="green", padx=5, pady=5, relief=RAISED, bd=4)
      frame.pack(side=LEFT, fill=Y)

      b = Button(frame, text="", borderwidth=1, relief=RAISED).pack(fill=X)
      
      sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll)
      sb.pack(expand=YES, fill=Y)
      self.lists[0]['yscrollcommand']=sb.set

      sb.bind('<Button-4>', lambda e, s=self: s._scroll(SCROLL, -1, UNITS))
      sb.bind('<Button-5>', lambda e, s=self: s._scroll(SCROLL, 1, UNITS))

   def _sort(self, e):
      # get the listbox to sort by (mapped by the header button)
      b=e.widget
      col, direction = self.colmapping[b]

      # get the entire table data into mem
      tableData = self.get(0,END)
      if self.origData == None:
         import copy
         self.origData = copy.deepcopy(tableData)

      rowcount = len(tableData)

      #remove old sort indicators if it exists
      for btn in self.colmapping.keys():
         lab = btn.cget('text')
         if lab[0]=='[': btn.config(text=lab[4:])

      btnLabel = b.cget('text')
      #sort data based on direction
      if direction==0:
         tableData = self.origData
      else:
         if direction==1: b.config(text='[+] ' + btnLabel)
         else: b.config(text='[-] ' + btnLabel)
         # sort by col
         def colsort(x, y, mycol=col, direction=direction):
            return direction*cmp(x[mycol], y[mycol])

         tableData.sort(colsort)

      #clear widget
      self.delete(0,END)

      # refill widget
      for row in range(rowcount):
         self.insert(END, tableData[row])

      # toggle direction flag
      if(direction==1): direction=-1
      else: direction += 1
      self.colmapping[b] = (col, direction)

   def _select(self, y):
      row = self.lists[0].nearest(y)
      self.selection_clear(0, END)
      self.selection_set(row)
      return 'break'

   def _button2(self, x, y):
      for l in self.lists: l.scan_mark(x, y)
      return 'break'

   def _b2motion(self, x, y):
      for l in self.lists: l.scan_dragto(x, y)
      return 'break'

   def _scroll(self, *args):
      for l in self.lists:
         apply(l.yview, args)
      return 'break'

   def curselection(self):
      return self.lists[0].curselection()

   def delete(self, first, last=None):
      for l in self.lists:
         l.delete(first, last)

   def get(self, first, last=None):
      result = []
      for l in self.lists:
          result.append(l.get(first,last))
      if last: return apply(map, [None] + result)
      return result

   def index(self, index):
      self.lists[0].index(index)

   def insert(self, index, *elements):
      for e in elements:
         i = 0
         for l in self.lists:
            l.insert(index, e[i])
            i = i + 1

   def size(self):
      return self.lists[0].size()

   def see(self, index):
      for l in self.lists:
         l.see(index)

   def selection_anchor(self, index):
      for l in self.lists:
         l.selection_anchor(index)

   def selection_clear(self, first, last=None):
      for l in self.lists:
         l.selection_clear(first, last)

   def selection_includes(self, index):
      return self.lists[0].selection_includes(index)

   def selection_set(self, first, last=None):
      for l in self.lists:
         l.selection_set(first, last)

if __name__ == '__main__':

   tk = Tk()
   Label(tk, text='MultiListbox').pack()
   mlb = MultiListbox(tk, (('Head1', 5), ('Head2', 10), ('Head3', 20)))
   for zeile in xrange(100):
      mlb.insert(END, (zeile,  'john',  'Alex Kellman'))
      mlb.pack(expand=YES,fill=BOTH)
   tk.mainloop()
Microp
User
Beiträge: 1
Registriert: Donnerstag 20. Januar 2011, 12:22

Hi! Erstmal danke, bin gerade dabei mich an Python heranzuwagen, der Beitrag ist ja nun immerhin von 2006, kann ich die Tabellen einfach so übernehmen?
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Was genau meinst du mit "übernehmen" ?

Also "aktuell" ist er noch, also immer noch unter < Python 3.x funktionstüchtig (für 3.x müssten ein wenig geändert werden), allerdings müsste an einigen Stellen etwas aufgeräumt werden. zB.: würden die Scrollevents nicht unter Windows funktionieren.
Wenn du hoffst das der Autor sich nochmal meldet hast du wohl schwarze Karten, da er hier im Forum schon eine Weile nicht mehr aktiv ist.

Und Achtung: Das ist keine Tabelle sondern eine MultiListBox, also ein kleiner Unterschied.
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Benutzeravatar
Mr_Snede
User
Beiträge: 387
Registriert: Sonntag 8. Februar 2004, 16:02
Wohnort: D-Dorf, Bo

Ich bin noch da ;-), wenn auch in letzter Zeit nur als Leser.

Bei mir hat's gerade eben auf einem aktuellen Ubuntu funktioniert. Auf Windows habe ich es nie getestet.

Spiel ruhig damit rum. Wenn du magst kannst du es ja verbessern.
Zum Beispiel könntest du den "Sternchen-Import":

Code: Alles auswählen

from Tkinter import *
durch Modulaufrufe ersetzen oder unter Windows testen und eine Rückmeldung hier schreiben.

[Edit]
Zum rumspielen taugt das noch, aber zum Lernen (abgesehen vom Verbessern) eher nicht.
Lies dir auch mal die Diskussionen auf Active State dazu durch.
Xynon1
User
Beiträge: 1267
Registriert: Mittwoch 15. September 2010, 14:22

Oha, wie lange war dein letztes Posting her :D
Mr_Snede hat geschrieben:Warum würden die Scrollevents so nicht unter Windows funktionieren?
Weil du unter Windows die Button 4 und 5 als solches nicht gibt, dort muss man das Event "MouseWheel" erfassen und das event.delta auslesen. Ich mach es immer so hier:

Code: Alles auswählen

def _yscroll(self, event):
    if event.num == 5 or event.delta < 0:
        self.canvas.yview_scroll(1, "unit")
    elif event.num == 4 or event.delta > 0:
        self.canvas.yview_scroll(-1, "unit")

widget.bind("<MouseWheel>", self._yscroll)       
widget.bind("<Button-4>", self._yscroll)
widget.bind("<Button-5>", self._yscroll)
Mr_Snede hat geschrieben:Spiel ruhig damit rum. Wenn du magst kannst du es ja verbessern.
Zum Beispiel könntest du den "Sternchen-Import":
Das ist nur eins was mich etwas stört, aber andere Kleinigkeiten wie, "import copy" mitten im Quelltext oder die Build-In Funktion "apply"(gut die war damals warscheinlich noch nicht deprecated).
Wie schon gesagt halt ein paar Kleinigkeiten, ansonsten netter Script.
Traue keinem Computer, den du nicht aus dem Fenster werfen kannst.
Xynon auf GitHub
Benutzeravatar
Mr_Snede
User
Beiträge: 387
Registriert: Sonntag 8. Februar 2004, 16:02
Wohnort: D-Dorf, Bo

Keine Ahnung wie lange ich nix mehr geschrieben habe. War kopfmäßig nie richtig weg ;-)

Grrr, da haste mich ja erwischt, wie ich die Frage zum Scrollevent unter Windows wieder zurückgezogen habe. Hatte ich mittlerweile aus dem Thread bei Activestate herausgelesen.

Mich ärgert zudem noch das Verhalten der Spalten / Zellen, wenn das Hauptfenster in der Größe verändert wird. Hauptsächlich das Verkleinern mag ich nicht. ZB. verschwindet als erstes der Rollbalken.
Da bin ich bei Tkinter noch nie hinter gestiegen.
Antworten