Tkinter Treeview 如何用鼠标正确选择多个项目

Dan*_*son 13 python tkinter

我正在尝试使用鼠标来选择和取消选择多个项目。我有它的工作,但是当用户快速移动鼠标时会出现问题。当鼠标快速移动时,一些项目会被跳过并且根本没有被选中。我一定是走错了路。

更新 1: 我决定使用自己的选择系统,但得到的结果与上述相同。当鼠标快速移动时,某些项目会被跳过,因此它们没有添加正确的颜色标签并保持不变。如果鼠标缓慢移动,所有项目都会被正确选择。下面是新代码和问题的另一个图像。

更新 2:我已经解决了这个问题并发布了工作代码只是为了完整性和将来帮助其他人。我最终使用了我自己的选择系统而不是内置的选择系统。

示例图像

import tkinter as tk
import tkinter.ttk as ttk


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Treeview Demo')
        self.geometry('300x650')
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        tv = self.tv = ttk.Treeview(self)
        tv.heading('#0', text='Name')
        # Populate tree with test data.
        for idx in range(0, 4):
            tv.insert('', idx, f'!{idx}', text=f'Item {idx+1}', tags='TkTextFont', open=1)
            iid = f'!{idx}_!{idx}'
            tv.insert(f'!{idx}', '0', iid, text=f'Python {idx+1}', tags='TkTextFont', open=1)
            for i in range(0, 5):
                tv.insert(iid, f'{i}', f'{iid}_!{i}', text=f'Sub item {i+1}', tags='TkTextFont')

        tv.grid(sticky='NSEW')
        self.active_item = None

        def motion(_):
            x, y = tv.winfo_pointerxy()
            item = tv.identify('item', x - tv.winfo_rootx(), y - tv.winfo_rooty())
            if not item or item == self.active_item:
                return

            if not self.active_item:
                self.active_item = item

            tv.selection_toggle(item)
            self.active_item = item

        def escape(_):
            tv.selection_remove(tv.selection())

        def button_press(_):
            self.bind('<Motion>', motion)

        def button_release(_):
            self.unbind('<Motion>')
            self.active_item = None

        self.bind('<Escape>', escape)
        self.bind('<Button-1>', button_press)
        self.bind('<ButtonRelease-1>', button_release)


def main():
    app = App()
    app.mainloop()


if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

第二次尝试:

示例图像 2

import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Treeview Demo')
        self.geometry('700x650')
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        tv = self.tv = ttk.Treeview(self)
        tv.heading('#0', text='Name')
        tv.tag_configure('odd', background='#aaaaaa')
        tv.tag_configure('even', background='#ffffff')
        tv.tag_configure('selected_odd', background='#25a625')
        tv.tag_configure('selected_even', background='#b0eab2')

        tag = 'odd'
        # Populate tree with test data.
        for idx in range(0, 4):
            tag = 'even' if tag == 'odd' else 'odd'
            tv.insert('', idx, f'!{idx}', text=f'Item {idx+1}', open=1, tags=(tag,))
            tag = 'even' if tag == 'odd' else 'odd'
            iid = f'!{idx}_!{idx}'
            tv.insert(f'!{idx}', '0', iid, text=f'Python {idx+1}', open=1, tags=(tag,))
            for i in range(0, 5):
                tag = 'even' if tag == 'odd' else 'odd'
                tv.insert(iid, i, f'{iid}_!{i}', text=f'Sub item {i+1}', tags=(tag,))

        tv.config(selectmode="none")
        tv.grid(sticky='NSEW')

        dw = tk.Toplevel()
        dw.overrideredirect(True)
        dw.wait_visibility(self)
        dw.wm_attributes('-alpha', 0.2)
        dw.wm_attributes("-topmost", True)
        dw.config(bg='#00aaff')
        dw.withdraw()
        self.selected = False
        self.active_item = None

        def motion(event):
            x, y = self.winfo_pointerxy()
            width = event.x-self.anchor_x
            height = event.y-self.anchor_y
    
            if width < 0:
                coord_x = event.x+self.winfo_rootx()
                width = self.anchor_x - event.x
            else:
                coord_x = self.anchor_x+self.winfo_rootx()
    
            if coord_x+width > self.winfo_rootx()+self.winfo_width():
                width -= (coord_x+width)-(self.winfo_rootx()+self.winfo_width())
            elif x < self.winfo_rootx():
                width -= (self.winfo_rootx() - x)
                coord_x = self.winfo_rootx()
    
            if height < 0:
                coord_y = event.y+self.winfo_rooty()
                height = self.anchor_y - event.y
            else:
                coord_y = self.anchor_y+self.winfo_rooty()
    
            if coord_y+height > self.winfo_rooty()+self.winfo_height():
                height -= (coord_y+height)-(self.winfo_rooty()+self.winfo_height())
            elif y < self.winfo_rooty():
                height -= (self.winfo_rooty() - y)
                coord_y = self.winfo_rooty()

            dw.geometry(f'{width}x{height}+{coord_x}+{coord_y}')

            item = tv.identify('item', coord_x, coord_y-40)
            if not item or item == self.active_item:
                self.active_item = None
                return

            self.active_item = item
            tags = list(tv.item(item, 'tags'))
            if 'odd' in tags:
                tags.pop(tags.index('odd'))
                tags.append('selected_odd')

            if 'even' in tags:
                tags.pop(tags.index('even'))
                tags.append('selected_even')

            tv .item(item, tags=tags)

        def escape(_=None):
            for item in tv.tag_has('selected_odd'):
                tags = list(tv.item(item, 'tags'))
                tags.pop(tags.index('selected_odd'))
                tags.append('odd')
                tv.item(item, tags=tags)

            for item in tv.tag_has('selected_even'):
                tags = list(tv.item(item, 'tags'))
                tags.pop(tags.index('selected_even'))
                tags.append('even')
                tv.item(item, tags=tags)

        def button_press(event):
            if self.selected and not event.state & 1 << 2:
                escape()
                self.selected = False

            dw.deiconify()
            self.anchor_item = tv.identify('item', event.x, event.y-40)
            self.anchor_x, self.anchor_y = event.x, event.y
            self.bind('<Motion>', motion)
            self.selected = True

        def button_release(event):
            dw.withdraw()
            dw.geometry('0x0+0+0')
            self.unbind('<Motion>')

        self.bind('<Escape>', escape)
        self.bind('<Button-1>', button_press)
        self.bind('<ButtonRelease-1>', button_release)


def main():
    app = App()
    app.mainloop()


if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

Dan*_*son 0

工作示例:

经过测试并可在 Windows 和 Linux 上运行

更新:我已经更新了代码,并且在 Windows 和 Linux 中一切正常,尽管在 Windows 中,从右到左调整大小时,蓝色透明窗口会抖动,从左到右则很好。在 Linux 中不会发生抖动,有人知道为什么吗?如果有人能让我知道下面的代码是否适用于 MacOS,那就太好了。

import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkfont


class Treeview(ttk.Treeview):
    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)

        self.root = parent.winfo_toplevel()
        self.selected_items = []
        self.origin_x = \
            self.origin_y = \
            self.active_item = \
            self.origin_item = None

        sw = self.select_window = tk.Toplevel(self.root)
        sw.wait_visibility(self.root)
        sw.withdraw()
        sw.config(bg='#00aaff')
        sw.overrideredirect(True)
        sw.wm_attributes('-alpha', 0.3)
        sw.wm_attributes("-topmost", True)

        self.font = tkfont.nametofont('TkTextFont')
        self.style = parent.style
        self.linespace = self.font.metrics('linespace') + 5

        self.bind('<Escape>', self.tags_reset)
        self.bind('<Button-1>', self.button_press)
        self.bind('<ButtonRelease-1>', self.button_release)

    def fixed_map(self, option):
        return [elm for elm in self.style.map("Treeview", query_opt=option) if elm[:2] != ("!disabled", "!selected")]

    def tag_add(self, tags, item):
        self.tags_update('add', tags, item)

    def tag_remove(self, tags, item=None):
        self.tags_update('remove', tags, item)

    def tag_replace(self, old, new, item=None):
        for item in (item,) if item else self.tag_has(old):
            self.tags_update('add', new, item)
            self.tags_update('remove', old, item)

    def tags_reset(self, _=None):
        self.tag_remove(('selected', '_selected'))
        self.tag_replace('selected_odd', 'odd')
        self.tag_replace('selected_even', 'even')

    def tags_update(self, opt, tags, item):
        def get_items(node):
            items.append(node)
            for node in self.get_children(node):
                get_items(node)

        if not tags:
            return
        elif isinstance(tags, str):
            tags = (tags,)

        if not item:
            items = []
            for child in self.get_children():
                get_items(child)
        else:
            items = (item,)

        for item in items:
            _tags = list(self.item(item, 'tags'))
            for _tag in tags:
                if opt == 'add':
                    if _tag not in _tags:
                        _tags.append(_tag)
                elif opt == 'remove':
                    if _tag in _tags:
                        _tags.pop(_tags.index(_tag))
            self.item(item, tags=_tags)

    def button_press(self, event):
        self.origin_x, self.origin_y = event.x, event.y
        item = self.origin_item = self.active_item = self.identify('item', event.x, event.y)

        sw = self.select_window
        sw.geometry('0x0+0+0')
        sw.deiconify()

        self.bind('<Motion>', self.set_selected)

        if not item:
            if not event.state & 1 << 2:
                self.tags_reset()
            return

        if event.state & 1 << 2:
            if self.tag_has('odd', item):
                self.tag_add('selected', item)
                self.tag_replace('odd', 'selected_odd', item)
            elif self.tag_has('even', item):
                self.tag_add('selected', item)
                self.tag_replace('even', 'selected_even', item)
            elif self.tag_has('selected_odd', item):
                self.tag_replace('selected_odd', 'odd', item)
            elif self.tag_has('selected_even', item):
                self.tag_replace('selected_even', 'even', item)
        else:
            self.tags_reset()
            self.tag_add('selected', item)
            if self.tag_has('odd', item):
                self.tag_replace('odd', 'selected_odd', item)
            elif self.tag_has('even', item):
                self.tag_replace('even', 'selected_even', item)

    def button_release(self, _):
        self.select_window.withdraw()
        self.unbind('<Motion>')

        for item in self.selected_items:
            if self.tag_has('odd', item) or self.tag_has('even', item):
                self.tag_remove(('selected', '_selected'), item)
            else:
                self.tag_replace('_selected', 'selected', item)

    def get_selected(self):
        return sorted(self.tag_has('selected_odd') + self.tag_has('selected_even'))

    def set_selected(self, event):
        def selected_items():
            items = []
            window_y = int(self.root.geometry().rsplit('+', 1)[-1])
            titlebar_height = self.root.winfo_rooty() - window_y
            sw = self.select_window
            start = sw.winfo_rooty() - titlebar_height - window_y
            end = start + sw.winfo_height()

            while start < end:
                start += 1
                node = self.identify('item', event.x, start)
                if not node or node in items:
                    continue
                items.append(node)

            return sorted(items)

        def set_row_colors():
            items = self.selected_items = selected_items()

            for item in items:
                if self.tag_has('selected', item):
                    if item == self.origin_item:
                        continue

                    if self.tag_has('selected_odd', item):
                        self.tag_replace('selected_odd', 'odd', item)
                    elif self.tag_has('selected_even', item):
                        self.tag_replace('selected_even', 'even', item)

                elif self.tag_has('odd', item):
                    self.tag_replace('odd', 'selected_odd', item)
                elif self.tag_has('even', item):
                    self.tag_replace('even', 'selected_even', item)

                self.tag_add('_selected', item)

            for item in self.tag_has('_selected'):
                if item not in items:
                    self.tag_remove('_selected', item)
                    if self.tag_has('odd', item):
                        self.tag_replace('odd', 'selected_odd', item)
                    elif self.tag_has('even', item):
                        self.tag_replace('even', 'selected_even', item)
                    elif self.tag_has('selected_odd', item):
                        self.tag_replace('selected_odd', 'odd', item)
                    elif self.tag_has('selected_even', item):
                        self.tag_replace('selected_even', 'even', item)

        root_x = self.root.winfo_rootx()
        if event.x < self.origin_x:
            width = self.origin_x - event.x
            coord_x = root_x + event.x
        else:
            width = event.x - self.origin_x
            coord_x = root_x + self.origin_x

        if coord_x+width > root_x+self.winfo_width():
            width -= (coord_x+width)-(root_x+self.winfo_width())
        elif self.winfo_pointerx() < root_x:
            width -= (root_x - self.winfo_pointerx())
            coord_x = root_x

        root_y = self.winfo_rooty()
        if event.y < self.origin_y:
            height = self.origin_y - event.y
            coord_y = root_y + event.y
        else:
            height = event.y - self.origin_y
            coord_y = root_y + self.origin_y

        if coord_y+height > root_y+self.winfo_height():
            height -= (coord_y+height)-(root_y+self.winfo_height())
        elif self.winfo_pointery() < root_y + self.linespace:
            height -= (root_y - self.winfo_pointery() + self.linespace)
            coord_y = root_y + self.linespace
            if height < 0:
                height = self.winfo_rooty() + self.origin_y

        set_row_colors()
        self.select_window.geometry(f'{width}x{height}+{coord_x}+{coord_y}')


class App(tk.Tk):
    def __init__(self):
        super().__init__()

        def print_selected_items(_):
            print(tv.get_selected())
            return 'break'

        style = self.style = ttk.Style()

        tv = Treeview(self)
        style.map("Treeview", foreground=tv.fixed_map("foreground"), background=tv.fixed_map("background"))

        tv.heading('#0', text='Name')
        tv.tag_configure('odd', background='#ffffff')
        tv.tag_configure('even', background='#aaaaaa')
        tv.tag_configure('selected_odd', background='#b0eab2')
        tv.tag_configure('selected_even', background='#25a625')

        color_tag = 'odd'
        for idx in range(0, 4):
            # Populating the tree with test data.
            color_tag = 'even' if color_tag == 'odd' else 'odd'
            tv.insert('', idx, f'{idx}', text=f'Item {idx+1}', open=1, tags=(color_tag,))
            color_tag = 'even' if color_tag == 'odd' else 'odd'
            iid = f'{idx}_{0}'
            tv.insert(f'{idx}', '0', iid, text=f'Menu {idx+1}', open=1, tags=(color_tag,))
            for i in range(0, 5):
                color_tag = 'even' if color_tag == 'odd' else 'odd'
                tv.insert(iid, i, f'{iid}_{i}', text=f'Sub item {i+1}', tags=(color_tag,))

            color_tag = 'even' if color_tag == 'odd' else 'odd'
            tv.insert(iid, 5, f'{iid}_{5}', text=f'Another Menu {idx+1}', open=1, tags=(color_tag,))
            iid = f'{iid}_{5}'
            for i in range(0, 3):
                color_tag = 'even' if color_tag == 'odd' else 'odd'
                tv.insert(iid, i, f'{iid}_{i}', text=f'Sub item {i+1}', tags=(color_tag,))

        self.title('Treeview Demo')
        self.geometry('275x650+3000+250')
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        tv.config(selectmode="none")
        tv.grid(sticky='NSEW')

        button = ttk.Button(self, text='Get Selected Items')
        button.grid()
        button.bind('<Button-1>', print_selected_items)


def main():
    app = App()
    app.mainloop()


if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)