云计算百科
云计算领域专业知识百科平台

Tkinter 全能穿梭框组件 TransferComponent

基于 tkinter 的 TransferComponent,一个功能强大、界面美观、操作流畅的双列表穿梭选择器!无论是用户权限管理、商品分类选择。

TransferComponent 的核心在于其 pick 类方法,它提供了一个简洁的接口来弹出穿梭框并获取用户的选择结果。

1、使用实例

import tkinter as tk
from component import TransferComponent

def main():
root = tk.Tk()
root.geometry("700×550")

def open_transfer():
candidates = [
"周杰伦", "易烊千玺", "王俊凯", "王源", "刘德华", "张学友", "黎明", "郭富城",
"谢霆锋", "张国荣", "梁朝伟", "周星驰", "成龙", "李连杰", "甄子丹", "吴京",
"周迅", "巩俐", "章子怡", "赵丽颖", "杨幂", "迪丽热巴", "刘诗诗", "高圆圆",
"王一博", "肖战", "邓紫棋", "张靓颖", "那英", "韩红", "李宇春",
]
selected = TransferComponent.pick(
master=root,
source_items=candidates,
initial_selected=["周杰伦", "蔡徐坤", "汪峰", "薛之谦", "华晨宇"], # 初始已选
left_title="明星",
right_title="获奖明星",
window_title="明星颁奖选择",
list_width=160,
list_height=200,
)
print("最终选择:", selected)
tk.Button(root, text="打开明星穿梭框", command=open_transfer).pack(pady=20)

root.mainloop()

if __name__ == '__main__':
main()

2、组件参数介绍

pick 方法会创建一个模态的穿梭框窗口,并等待用户操作完成后返回最终选择的列表。它接受以下参数:

  • master (可选): 父窗口,通常是 tk.Tk() 实例。

  • source_items (必填): 初始左侧列表(待选)的所有数据项,类型为 List[str]。

  • initial_selected (可选): 初始右侧列表(已选)的数据项,类型为 Optional[List[str]]。这些项会自动从 source_items 中移除。

  • left_title (可选): 左侧列表的标题,默认为 "待选列表"。

  • right_title (可选): 右侧列表的标题,默认为 "已选列表"。

  • window_title (可选): 穿梭框窗口的标题,默认为 "穿梭框"。

  • list_width (可选): 列表的宽度(像素),默认为 180。

  • list_height (可选): 列表的高度(像素),默认为 220。

3、完整组件代码

将下面代码保存为py文件,在其他位置引入即可。

import tkinter as tk
from typing import List, Dict, Callable, Optional

class _CheckList(tk.Frame):
def __init__(
self,
master=None,
items: Optional[List[str]] = None,
*,
width: int = 180,
height: int = 220,
**kwargs,
):
super().__init__(master, **kwargs)
self.config(highlightbackground="#DDDDDD", highlightcolor="#DDDDDD", highlightthickness=1, bd=0)

self._all_items: List[str] = list(items or [])
self._vars: Dict[str, tk.BooleanVar] = {}

search_frame = tk.Frame(self)
search_frame.pack(fill=tk.X, padx=2, pady=(0, 2))
tk.Label(search_frame, text="🔍").pack(side=tk.LEFT)
self._search_var = tk.StringVar()
entry = tk.Entry(search_frame, textvariable=self._search_var)
entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
entry.bind("<KeyRelease>", lambda e: self._render_items())

self._canvas = tk.Canvas(self, width=width, height=height, highlightthickness=0)
vsb = tk.Scrollbar(self, orient="vertical", command=self._canvas.yview)
self._inner = tk.Frame(self._canvas)
self._inner.bind(
"<Configure>", lambda e: self._canvas.configure(scrollregion=self._canvas.bbox("all"))
)
self._canvas.create_window((0, 0), window=self._inner, anchor="nw")
self._canvas.configure(yscrollcommand=vsb.set)

self._canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
vsb.pack(side=tk.RIGHT, fill=tk.Y)

self._render_items()

def items(self) -> List[str]:
return list(self._all_items)

def add_items(self, items: List[str]):
for it in items:
if it not in self._all_items:
self._all_items.append(it)
self._render_items()

def remove_items(self, items: List[str]):
self._all_items = [it for it in self._all_items if it not in items]
self._render_items()

def get_checked_items(self) -> List[str]:
return [it for it, v in self._vars.items() if v.get()]

def _render_items(self):
for child in self._inner.winfo_children():
child.destroy()
self._vars.clear()

keyword = self._search_var.get().strip()
for idx, txt in enumerate(self._all_items):
if keyword and keyword not in txt:
continue
var = tk.BooleanVar(value=False)
cb = tk.Checkbutton(self._inner, text=txt, variable=var, anchor="w")
cb.pack(fill=tk.X, anchor="w")
self._vars[txt] = var

def set_all_checked(self, status: bool):
for v in self._vars.values():
v.set(status)

def all_selected(self) -> bool:
return bool(self._vars) and all(v.get() for v in self._vars.values())

class TransferComponent(tk.Toplevel):
def __init__(
self,
master=None,
*,
source_items: Optional[List[str]] = None,
target_items: Optional[List[str]] = None,
left_title: str = "待选列表",
right_title: str = "已选列表",
window_title: str = "穿梭框",
list_width: int = 180,
list_height: int = 220,
on_ok: Optional[Callable[[List[str]], None]] = None,
**kwargs,
):
super().__init__(master, **kwargs)
self.title(window_title)
self.resizable(False, False)
self.wm_attributes("-toolwindow", True)

self._on_ok_user = on_ok

src_items = list(source_items or [])
tgt_items = list(target_items or [])
seen = set()
tgt_items = [x for x in tgt_items if not (x in seen or seen.add(x))]
src_items = [it for it in src_items if it not in tgt_items]

body = tk.Frame(self)
body.pack(padx=10, pady=10)

left_frame = tk.Frame(body)
left_frame.grid(row=0, column=0, sticky="nsew")
left_frame.config(highlightbackground="#DDDDDD", highlightcolor="#DDDDDD", highlightthickness=1, bd=0)
self._left_all_var = tk.BooleanVar(value=False)
tk.Checkbutton(
left_frame,
text=left_title,
variable=self._left_all_var,
command=self._toggle_left_all,
anchor="w",
).pack(anchor="w")
self._left_list = _CheckList(left_frame, items=src_items, width=list_width, height=list_height)
self._left_list.pack()

btn_frame = tk.Frame(body)
btn_frame.grid(row=0, column=1, padx=10)
btn_right = tk.Button(btn_frame, text="≫", width=4, command=self._move_right)
btn_left = tk.Button(btn_frame, text="≪", width=4, command=self._move_left)
btn_right.pack(pady=(40, 5))
btn_left.pack(pady=5)

right_frame = tk.Frame(body)
right_frame.grid(row=0, column=2, sticky="nsew")
right_frame.config(highlightbackground="#DDDDDD", highlightcolor="#DDDDDD", highlightthickness=1, bd=0)
self._right_all_var = tk.BooleanVar(value=False)
tk.Checkbutton(
right_frame,
text=right_title,
variable=self._right_all_var,
command=self._toggle_right_all,
anchor="w",
).pack(anchor="w")
self._right_list = _CheckList(right_frame, items=tgt_items, width=list_width, height=list_height)
self._right_list.pack()

bottom = tk.Frame(self)
bottom.pack(fill=tk.X, pady=(5, 10))
tk.Button(bottom, text="取消", width=8, command=self.destroy).pack(side=tk.RIGHT, padx=5)
tk.Button(bottom, text="确定", width=8, command=self._on_ok).pack(side=tk.RIGHT)

self.transient(master)
self.grab_set()
self.protocol("WM_DELETE_WINDOW", self.destroy)

self.update_idletasks()
win_w = self.winfo_width()
win_h = self.winfo_height()
scr_w = self.winfo_screenwidth()
scr_h = self.winfo_screenheight()
pos_x = (scr_w – win_w) // 2
pos_y = (scr_h – win_h) // 2
self.geometry(f"{win_w}x{win_h}+{pos_x}+{pos_y}")

def _move_right(self):
items = self._left_list.get_checked_items()
if not items:
return
self._left_list.remove_items(items)
self._right_list.add_items(items)
self._left_all_var.set(False)
self._right_all_var.set(False)

def _move_left(self):
items = self._right_list.get_checked_items()
if not items:
return
self._right_list.remove_items(items)
self._left_list.add_items(items)
self._left_all_var.set(False)
self._right_all_var.set(False)

def _on_ok(self):
if callable(self._on_ok_user):
self._on_ok_user(self._right_list.items())
self.destroy()

def _toggle_left_all(self):
self._left_list.set_all_checked(self._left_all_var.get())

def _toggle_right_all(self):
self._right_list.set_all_checked(self._right_all_var.get())

@classmethod
def pick(
cls,
master=None,
*,
source_items: List[str],
initial_selected: Optional[List[str]] = None,
left_title: str = "待选列表",
right_title: str = "已选列表",
window_title: str = "穿梭框",
list_width: int = 180,
list_height: int = 220,
) -> List[str]:
res: Dict[str, List[str]] = {"sel": []}

def _ok(items: List[str]):
res["sel"] = items

inst = cls(
master,
source_items=source_items,
target_items=initial_selected,
left_title=left_title,
right_title=right_title,
window_title=window_title,
list_width=list_width,
list_height=list_height,
on_ok=_ok,
)
inst.wait_window()
return res["sel"]

赞(0)
未经允许不得转载:网硕互联帮助中心 » Tkinter 全能穿梭框组件 TransferComponent
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!