tkinter 自定义组件:多选下拉框

4 阅读3分钟

一、功能概述

Combopicker 是基于 tkinter 的自定义多选下拉框组件,允许用户通过下拉列表选择多个选项,并将选中的内容显示在输入框中。该组件支持滚动、全选、清空等功能。

二、实现原理

  1. 继承组件:

    • Combopicker 继承自 ttk.Combobox 和自定义的 Picker 类。
    • Picker 类负责管理下拉列表的显示、滚动条、复选框等功能。
  2. 核心逻辑:

    • 使用 IntVar 变量绑定每个选项的选中状态。
    • 通过 command 回调函数处理选项的选中和取消操作。
    • 支持鼠标滚轮滚动查看选项。

三、代码

测试代码

import tkinter
from tkinter import *
from app.components.ComBoPicker import Combopicker  # 导入自定义下拉多选框

def get_ces():
    # 获取选中的值
    print(COMBOPICKER.get())

root = Tk()
root.geometry("300x300")

# 创建框架
F = Frame(root)
F.pack(expand=False, fill="both", padx=10, pady=10)
Label(F, text='全选、可滚动:').pack(side='left')

# 初始化 Combopicker
COMBOPICKER = Combopicker(
    F, 
    values=[
        '全选', '项目1', '项目2', '项目3', '项目4', '项目5', 
        '项目11', '项目22', '项目33', '项目44', '项目55', '项目66',
        '项目77', '项目88'
    ]
)
COMBOPICKER.pack(anchor="w")

# 按钮操作
frame = tkinter.Frame(root)
frame.pack()
btn1 = tkinter.Button(frame, text='获取', command=lambda: get_ces())
btn1.pack(side='left')
btn2 = tkinter.Button(frame, text='设置内容', command=lambda: COMBOPICKER.entry_var.set('项目3,项目4'))
btn2.pack(side='left')
btn3 = tkinter.Button(frame, text='清空所有选中', command=lambda: COMBOPICKER.delete(0, 'end'))
btn3.pack(side='left')

root.mainloop()

组件代码ComBoPicker.py

"""
    自定义多选下拉列表
"""
import tkinter.ttk as ttk
from tkinter import *


class Picker(ttk.Frame):#选择器

    def __init__(self, master=None, activebackground='#b1dcfb', values=[], entry_wid=None, activeforeground='black',
                 selectbackground='#003eff', selectforeground='white', command=None, borderwidth=1, relief="solid"):

        self._selected_item = None

        self._values = values

        self._entry_wid = entry_wid

        self._sel_bg = selectbackground
        self._sel_fg = selectforeground

        self._act_bg = activebackground
        self._act_fg = activeforeground

        self._command = command
        self.index = 0
        ttk.Frame.__init__(self, master, borderwidth=borderwidth, height=10, relief=relief)

        self.bind("<FocusIn>", lambda event: self.event_generate('<<PickerFocusIn>>'))
        self.bind("<FocusOut>", lambda event: self.event_generate('<<PickerFocusOut>>'))
        F = LabelFrame(self)
        F.pack(fill='x')
        self.canvas = Canvas(F, scrollregion=(0, 0, 500, (len(self._values) * 23)))


        vbar = Scrollbar(F, orient=VERTICAL)
        vbar.pack(side=RIGHT, fill=Y)

        frame = Frame(self.canvas) #创建框架
        vbar.config(command=self.canvas.yview)

        sbar2 = Scrollbar(F, orient=HORIZONTAL)
        sbar2.pack(side=BOTTOM, fill=X)
        sbar2.config(command=self.canvas.yview)
        # self.canvas.pack(side='left',fill='x',expand=True)
        self.canvas.create_window((0, 0,), window=frame, anchor='nw', tags='frame')

        self.canvas.config(highlightthickness=0)  # 去掉选中边框
        vbar.config(command=self.canvas.yview)
        sbar2.config(command=self.canvas.xview)
        self.canvas.config(width=300, height=150)
        self.canvas.config(yscrollcommand=vbar.set,xscrollcommand=sbar2.set)
        # self.canvas.config(scrollregion=self.canvas.bbox('all'))
        # self._font = tkFont.Font()
        self.dict_checkbutton = {}
        self.dict_checkbutton_var = {}
        self.dict_intvar_item = {}
        for index, item in enumerate(self._values):
            self.dict_intvar_item[item] = IntVar()
            self.dict_checkbutton[item] = ttk.Checkbutton(frame, text=item, variable=self.dict_intvar_item[item],
                                                          command=lambda ITEM=item: self._command(ITEM))
            self.dict_checkbutton[item].grid(row=index, column=0, sticky=NSEW, padx=5)
            self.dict_intvar_item[item].set(0)
            if item in self._entry_wid.get().split(','):
                self.dict_intvar_item[item].set(1)
        self.canvas.pack(side=LEFT, expand=True, fill=BOTH)
        self.canvas.bind("<MouseWheel>", self.processWheel)
        frame.bind("<MouseWheel>", self.processWheel)
        for i in self.dict_checkbutton:
            self.dict_checkbutton[i].bind("<MouseWheel>", self.processWheel)
        self.bind("<MouseWheel>", self.processWheel)



    def processWheel(self, event):
        a = int(-(event.delta))
        if a > 0:
            self.canvas.yview_scroll(1, UNITS)
        else:
            self.canvas.yview_scroll(-1, UNITS)


class Combopicker(ttk.Combobox, Picker):
    def __init__(self, master, values=None, entryvar=None, entrywidth=None, entrystyle=None, onselect=None,
                 activebackground='#ef476f', activeforeground='red', selectbackground='#ffd166',
                 selectforeground='green', borderwidth=1, relief="solid"):

        if values is None:
            values = []
        self.values = values
        self.master = master
        self.activeforeground = activeforeground
        self.activebackground = activebackground
        self.selectbackground = selectbackground
        self.selectforeground = selectforeground

        if entryvar is not None:
            self.entry_var = entryvar
        else:
            self.entry_var = StringVar()

        entry_config = {}
        if entrywidth is not None:
            entry_config["width"] = entrywidth




        if entrystyle is not None:
            entry_config["style"] = entrystyle

        ttk.Entry.__init__(self, master, textvariable=self.entry_var, **entry_config, state="")

        self._is_menuoptions_visible = False

        self.picker_frame = Picker(self.winfo_toplevel(), values=values, entry_wid=self.entry_var,
                                   activebackground=activebackground, activeforeground=activeforeground,
                                   selectbackground=selectbackground, selectforeground=selectforeground,
                                   command=self._on_selected_check)

        self.bind_all("<1>", self._on_click, "+")

        self.bind("<Escape>", lambda event: self.hide_picker())

    @property
    def current_value(self):
        try:
            value = self.entry_var.get()
            return value
        except ValueError:
            return None

    @current_value.setter
    def current_value(self, INDEX):
        self.entry_var.set(str(self.values.index(INDEX)))

    def _on_selected_check(self, SELECTED):
        value = []
        if self.entry_var.get() != "" and self.entry_var.get() is not None:
            temp_value = self.entry_var.get()
            value = temp_value.split(",")

        if str(SELECTED) in value:
            if '全选' == str(SELECTED):
                value.clear()  # 清空选项
            else:
                value.remove(str(SELECTED))
                value.sort()
        else:
            if '全选' == str(SELECTED):
                value = self.values
            else:
                value.append(str(SELECTED))
                value.sort()

        temp_value = ""
        for index, item in enumerate(value):
            if item != "":
                if index != 0:
                    temp_value += ","
                temp_value += str(item)
        self.entry_var.set(temp_value)
        # 可以通过复选框的variable来让勾选中或取消,但下面也行,问题不大
        if '全选' == str(SELECTED):
            self.hide_picker()
            self.show_picker()

    def _on_click(self, event):
        str_widget = str(event.widget)

        if str_widget == str(self):
            if not self._is_menuoptions_visible:
                self.show_picker()
        else:
            if not str_widget.startswith(str(self.picker_frame)) and self._is_menuoptions_visible:
                self.hide_picker()

    def show_picker(self):
        if not self._is_menuoptions_visible:
            self.picker_frame = Picker(self.winfo_toplevel(), values=self.values, entry_wid=self.entry_var,
                                       activebackground=self.activebackground,
                                       activeforeground=self.activeforeground, selectbackground=self.selectbackground,
                                       selectforeground=self.selectforeground, command=self._on_selected_check)

            self.bind_all("<1>", self._on_click, "+")

            self.bind("<Escape>", lambda event: self.hide_picker())
            self.picker_frame.lift()
            self.picker_frame.place(in_=self, relx=0, rely=1, relwidth=1)

        self._is_menuoptions_visible = True

    def hide_picker(self):
        if self._is_menuoptions_visible:
            self.picker_frame.place_forget()  # 不知道为什么这个方式在mac下不起作用,所以就直接销毁这个控件
            # self.picker_frame.destroy()

        self._is_menuoptions_visible = False

四、关键功能说明

1. 多选功能

  • 用户可以通过勾选复选框选择多个选项。
  • 选中的内容以逗号分隔的形式显示在输入框中。

2. 全选功能

  • 提供“全选”选项,点击后自动选中所有项目。

3. 滚动支持

  • 下拉列表支持垂直和水平滚动,方便查看大量选项。

4. 回调函数

  • 通过 command 参数绑定选项选中或取消时的回调函数。

5. 按钮操作

  • 获取选中值:通过 COMBOPICKER.get() 获取当前选中的内容。
  • 设置内容:通过 COMBOPICKER.entry_var.set() 设置默认选中值。
  • 清空选中:通过 COMBOPICKER.delete() 清空所有选中内容。