0

I'm using the class below to draw scrollable tables with selectable elements. When the user clicks on one of the item I need that entire row to get highlighted (which works). But at the moment you can only select one row at a time. How to modify the code below to allow selection (and highlighting) of multiple rows at a time?

from Tkinter import *
import Tkinter as tk


class MultiListbox_fuse(Frame):
    def __init__(self,master,lists):
        Frame.__init__(self,master,borderwidth=1,relief=SUNKEN)
        self.lists = []
        self.columns=[]
        for l,w in lists:
            frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH)
            Label(frame, text=l, borderwidth=1, relief=RAISED).pack(fill=X)
            lb = Listbox(frame, width=w, height=30, borderwidth=0, selectborderwidth=0,
             relief=FLAT, exportselection=FALSE, selectmode=MULTIPLE)
            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('&lt;Button-4>', lambda e, s=self: s._scroll(SCROLL, 1, PAGES))
            lb.bind('&lt;Button-5>', lambda e, s=self: s._scroll(SCROLL, -1, PAGES))
            lb.bind("<MouseWheel>", self.OnMouseWheel)

            #self.add(frame)
        Label(master, borderwidth=1, relief=FLAT).pack(fill=X)
        sb = Scrollbar(master, orient=VERTICAL, command=self._scroll,borderwidth=1)
        sb.pack(fill=Y,side=RIGHT,expand=NO)
        for l in self.lists:
            l['yscrollcommand']=sb.set
        #self.add(frame)
        self.pack(expand=YES,fill=BOTH)
        self.sortedBy=-1
        self.previousWheel=0


    def _select(self, y,state=16):
        row = self.lists[0].nearest(y)
        if state==16:self.selection_clear(0, END)
        self.selection_set(row)
##        print self.curselection()
        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 clickon(self,e):
        self._sortBy(self.columns.index(e.widget['text']))


    def _sortBy(self, column):
        """ Sort by a given column. """


        if column == self.sortedBy:
                    direction = -1 * self.direction
        else:
            direction = 1

        elements = self.get(0, END)
        self.delete(0, END)
        elements.sort(lambda x, y: self._sortAssist(column, direction, x, y))
        self.insert(END, *elements)

        self.sortedBy = column
        self.direction = direction


    def _sortAssist(self, column, direction, x, y):
        c = cmp(x[column], y[column])
        if c:
            return direction * c
        else:
            return direction * cmp(x, y)

    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)


    def OnMouseWheel(self, event):
        for l in self.lists:
            l.yview("scroll", event.delta,"units")
        # this prevents default bindings from firing, which
        # would end up scrolling the widget twice
        return "break"


root = Tk()
root.minsize(width=650, height=580)
root.maxsize(width=650, height=580)
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
x = (w/2) - 300
y = (h/2) - 250
root.geometry('%dx%d+%d+%d' % (650, 550, x, y-80))
root.wm_title("Results displayed")

tabelka = MultiListbox_fuse(root, (('Costam1',8), ('Costam2',8)))
tabelka.pack()
for a in range(1,100):
    tabelka.insert(END, (str("abc1"), str("def2")))

root.mainloop()
DrOrpheum
  • 23
  • 6
  • This code does not run. Please provide a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve). I tried to guess how I should use this class but came up short. Please provide example of usage as well. – figbeam Jul 17 '18 at 23:06
  • @figbeam - Ok - I have now edited my post to include full code which uses this class – DrOrpheum Jul 17 '18 at 23:37
  • Trying to reduce to minimal example and found that you have both a function and a variable called `index`. They don't seem to interfere but its a bad thing... – figbeam Jul 18 '18 at 00:41
  • Thanks for noticing! I'll be sure to rename one of them! – DrOrpheum Jul 18 '18 at 00:49
  • Added an answer but it's more of a comment. I includet the error message I got and thought an answer had better opportunities for formatting... – figbeam Jul 18 '18 at 00:51
  • You clear the selection on every click before adding the clicked one to the selection: `if state==16:self.selection_clear(0, END)`. Why do you do this? – fhdrsdg Jul 18 '18 at 08:14
  • Thank you @fhdrsdg! This line was my attempt at "deselecting" after a second click on the same row. Clearly this was coded wrong and that's what was causing the problem. Thanks so much! How would I now write "deselecting" the row after a second click on the same item? – DrOrpheum Jul 18 '18 at 09:33

2 Answers2

0

This code was not easy to understand and has almost no comments. The labbdas in the bind statements does not meke it easier.

Getting error when I try to yse the scrollbar as opposed to using the mouse wheel:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\qwerty\AppData\Local\Programs\Python\Python36-32\lib\tkinter\__init__.py", line 1702, in __call__
    return self.func(*args)
  File "C:\Users\qwerty\Documents\Python\test.py", line 58, in _scroll
    apply(l.yview, args)
NameError: name 'apply' is not defined

Here is an attempt:

# Add binding in __init()__ with the other bindings
lb.bind('<Shift-Button-1>', self.more)

# Create the function to handle <Shift-Button-1>
def more(self, event):
    first = self.lists[0].curselection()[0]     # The previous selection
    last = self.lists[0].nearest(event.y)       # Current mouse click
    self.selection_set(first, last=last)        # Set selection

This marks the selection in all lists. I have not taken into account the possibility of first selection not existing etc.

figbeam
  • 7,001
  • 2
  • 12
  • 18
  • Thanks for your time, figbeam! I'm sorry for not providing many comments. This error is nothing I see on my system - works just fine here. I'd preferably like to be able to select multiple rows without any other buttons pressed. – DrOrpheum Jul 18 '18 at 01:17
  • By the way, have you seen the [Tkinter Multi-Column List Demo](https://pyinmyeye.blogspot.com/2012/07/tkinter-multi-column-list-demo.html)? Might be worth a try instead of writing your own... – figbeam Jul 18 '18 at 01:20
  • Thanks for the link - I haven't seen this one. If you think of an easy fix to my class above, I'd prefer that still - I'm deep into a project with this code and wouldn't like to now restructure. – DrOrpheum Jul 18 '18 at 01:55
0

In your _select method, you clear the selection before adding the clicked one:

def _select(self, y,state=16):
    row = self.lists[0].nearest(y)
    if state==16:self.selection_clear(0, END) # This clears the selection
    self.selection_set(row)
    ## print self.curselection()
    return 'break'

If you want to remove the item from the selection if it's already in there do something like:

def _select(self, y,state=16):
    row = self.lists[0].nearest(y)
    if row in self.curselection(): # Check if the row already is in the selection
        self.selection_clear(row) # If it is, remove it
    else:
        self.selection_set(row) # If it isn't, add it
    return 'break'

Because you've also bound this method to <B1-Motion> this does get a bit weird when moving over the same row, because it constantly adds and removes the same row. You could work around that with something like the following, which checks if you've moved to another row when dragging.

self.current_row = None

for l,w in lists:
    ...
    lb.bind('<B1-Motion>', lambda e, s=self: s.drag(e.y))
    ...

def _select(self, y,state=16):
    row = self.lists[0].nearest(y)
    if row in self.curselection():
        self.selection_clear(row)
    else:
        self.selection_set(row)
    self.current_row = row
    return 'break'

def drag(self, y):
    row = self.lists[0].nearest(y)
    if not row == self.current_row:
        self._select(y)
fhdrsdg
  • 10,297
  • 2
  • 41
  • 62