feat: add line numbers to Production ID input widget

- Integrate tklinenums library for line number display
- Line numbers appear on the left side in black color
- Line numbers are always visible (even with placeholder)
- Line numbers sync with text scrolling
- Add tklinenums>=1.7.0 to requirements.txt

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-02-11 22:10:38 +08:00
parent ee0b4187d7
commit 28d7db26db
2 changed files with 58 additions and 17 deletions

View File

@@ -4,61 +4,80 @@
Production ID 输入控件 Production ID 输入控件
多行文本输入框,用于输入 Production ID 列表。 多行文本输入框,用于输入 Production ID 列表。
支持内容溢出时自动显示滚动条。 支持内容溢出时自动显示滚动条,左侧显示行号
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, font from tkinter import ttk, font
from tklinenums import TkLineNumbers
class ProductionIdInput(ttk.Frame): class ProductionIdInput(ttk.Frame):
"""Production ID 输入控件""" """Production ID 输入控件,带行号和自动滚动条"""
def __init__(self, parent, placeholder="每行输入一个 Production ID", **kwargs): def __init__(self, parent, placeholder="每行输入一个 Production ID", **kwargs):
super().__init__(parent, **kwargs) super().__init__(parent, **kwargs)
self.placeholder = placeholder self.placeholder = placeholder
self._scrollbar_visible = False self._scrollbar_visible = False
self._check_pending = False self._check_pending = False
self._updating_placeholder = False
# 创建文本框 # 创建文本框
self.text_widget = tk.Text(self, wrap=tk.WORD, padx=5, pady=5) self.text_widget = tk.Text(self, wrap=tk.WORD, padx=5, pady=5)
# 创建行号区域的容器
self.linenums_frame = ttk.Frame(self)
# 创建行号控件(使用黑色)
self.line_numbers = TkLineNumbers(
self.linenums_frame,
self.text_widget,
justify="center",
colors=("black", "#f0f0f0"),
bg="#f0f0f0",
width=3
)
self.line_numbers.pack(fill="both", expand=True)
# 创建滚动条 # 创建滚动条
self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text_widget.yview) self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text_widget.yview)
self.text_widget.configure(yscrollcommand=self._on_scroll) self.text_widget.configure(yscrollcommand=self._on_scroll)
# 布局 - 使用 grid 以便动态显示/隐藏滚动条 # 布局:行号 | 文本框 | 滚动条
self.text_widget.grid(row=0, column=0, sticky="nsew") self.linenums_frame.grid(row=0, column=0, sticky="ns")
self.scrollbar.grid(row=0, column=1, sticky="ns") self.text_widget.grid(row=0, column=1, sticky="nsew")
self.scrollbar.grid(row=0, column=2, sticky="ns")
# 配置行列权重 # 配置行列权重
self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1)
# 绑定事件 # 绑定事件
self.text_widget.bind("<FocusIn>", self._on_focus_in) self.text_widget.bind("<FocusIn>", self._on_focus_in)
self.text_widget.bind("<FocusOut>", self._on_focus_out) self.text_widget.bind("<FocusOut>", self._on_focus_out)
# 监听内容变化事件 # 监听内容变化事件
self.text_widget.bind("<KeyRelease>", self._schedule_check) self.text_widget.bind("<KeyRelease>", self._on_content_change)
self.text_widget.bind("<ButtonRelease-1>", self._schedule_check) self.text_widget.bind("<ButtonRelease-1>", self._schedule_check)
self.text_widget.bind("<ButtonRelease-3>", self._schedule_check) self.text_widget.bind("<ButtonRelease-3>", self._schedule_check)
self.text_widget.bind("<Configure>", self._schedule_check) self.text_widget.bind("<Configure>", self._schedule_check)
# 绑定粘贴事件 # 绑定粘贴事件
self.text_widget.bind("<<Paste>>", self._schedule_check) self.text_widget.bind("<<Paste>>", self._schedule_check)
# 初始隐藏滚动条 # 初始隐藏滚动条(行号始终显示)
self.scrollbar.grid_remove() self.scrollbar.grid_remove()
# 显示占位符 # 显示占位符
self._show_placeholder() self._show_placeholder()
# 延迟检查初始状态 # 延迟检查初始状态
self.after(100, self._check_scrollbar_needed) self.after(100, self._check_ui_state)
def _on_focus_in(self, event): def _on_focus_in(self, event):
"""获得焦点时隐藏占位符""" """获得焦点时隐藏占位符"""
if self.text_widget.get("1.0", "end-1c") == self.placeholder: if not self._updating_placeholder and self.text_widget.get("1.0", "end-1c") == self.placeholder:
self.text_widget.delete("1.0", tk.END) self.text_widget.delete("1.0", tk.END)
# 确保文字颜色为黑色
self.text_widget.configure(foreground="black")
def _on_focus_out(self, event): def _on_focus_out(self, event):
"""失去焦点时显示占位符""" """失去焦点时显示占位符"""
@@ -68,15 +87,27 @@ class ProductionIdInput(ttk.Frame):
def _show_placeholder(self): def _show_placeholder(self):
"""显示占位符""" """显示占位符"""
self._updating_placeholder = True
self.text_widget.delete("1.0", tk.END) self.text_widget.delete("1.0", tk.END)
self.text_widget.insert("1.0", self.placeholder) self.text_widget.insert("1.0", self.placeholder)
self.text_widget.configure(foreground="gray") self.text_widget.configure(foreground="gray")
self._updating_placeholder = False
self._schedule_check()
def _hide_placeholder(self): def _hide_placeholder(self):
"""隐藏占位符""" """隐藏占位符"""
self._updating_placeholder = True
if self.text_widget.get("1.0", "end-1c") == self.placeholder: if self.text_widget.get("1.0", "end-1c") == self.placeholder:
self.text_widget.delete("1.0", tk.END) self.text_widget.delete("1.0", tk.END)
self.text_widget.configure(foreground="black") self.text_widget.configure(foreground="black")
self._updating_placeholder = False
def _on_content_change(self, event=None):
"""内容变化时的处理"""
# 如果不是占位符状态,重绘行号
if self.text_widget.get("1.0", "end-1c") != self.placeholder:
self.line_numbers.redraw()
self._schedule_check()
def get(self) -> list[str]: def get(self) -> list[str]:
"""获取 Production ID 列表""" """获取 Production ID 列表"""
@@ -86,18 +117,23 @@ class ProductionIdInput(ttk.Frame):
def set(self, production_ids: list[str]): def set(self, production_ids: list[str]):
"""设置 Production ID 列表""" """设置 Production ID 列表"""
self._updating_placeholder = True
self.text_widget.delete("1.0", tk.END) self.text_widget.delete("1.0", tk.END)
if production_ids: if production_ids:
self.text_widget.insert("1.0", "\n".join(production_ids)) self.text_widget.insert("1.0", "\n".join(production_ids))
self.text_widget.configure(foreground="black") self.text_widget.configure(foreground="black")
self.after_idle(self.line_numbers.redraw)
else: else:
self._show_placeholder() self._show_placeholder()
self._updating_placeholder = False
self._schedule_check() self._schedule_check()
def clear(self): def clear(self):
"""清空内容""" """清空内容"""
self._updating_placeholder = True
self.text_widget.delete("1.0", tk.END) self.text_widget.delete("1.0", tk.END)
self._show_placeholder() self._show_placeholder()
self._updating_placeholder = False
self._schedule_check() self._schedule_check()
def append(self, production_ids: list[str]): def append(self, production_ids: list[str]):
@@ -109,33 +145,37 @@ class ProductionIdInput(ttk.Frame):
self.text_widget.insert(tk.END, "\n" + "\n".join(production_ids)) self.text_widget.insert(tk.END, "\n" + "\n".join(production_ids))
else: else:
self.text_widget.insert("1.0", "\n".join(production_ids)) self.text_widget.insert("1.0", "\n".join(production_ids))
self.after_idle(self.line_numbers.redraw)
self._schedule_check() self._schedule_check()
def apply_font(self, font_family: str, font_size: int): def apply_font(self, font_family: str, font_size: int):
"""应用字体设置""" """应用字体设置"""
font_spec = font.Font(family=font_family, size=font_size) font_spec = font.Font(family=font_family, size=font_size)
self.text_widget.configure(font=font_spec) self.text_widget.configure(font=font_spec)
# 重绘行号以应用字体变化
self.after_idle(self.line_numbers.redraw)
def _on_scroll(self, first, last): def _on_scroll(self, first, last):
"""滚动回调,更新滚动条位置""" """滚动回调,更新滚动条位置和行号"""
self.scrollbar.set(first, last) self.scrollbar.set(first, last)
# 滚动时重绘行号以同步显示
self.line_numbers.redraw()
def _schedule_check(self, event=None): def _schedule_check(self, event=None):
"""调度滚动条检查(防抖)""" """调度 UI 状态检查(防抖)"""
if not self._check_pending: if not self._check_pending:
self._check_pending = True self._check_pending = True
self.after(50, self._check_scrollbar_needed) self.after(50, self._check_ui_state)
def _check_scrollbar_needed(self): def _check_ui_state(self):
"""检查是否需要显示滚动条""" """检查 UI 状态(滚动条"""
self._check_pending = False self._check_pending = False
# 更新界面以确保获取准确的尺寸 # 更新界面以确保获取准确的尺寸
self.text_widget.update_idletasks() self.text_widget.update_idletasks()
# 使用 yview() 返回值判断 # 检查是否需要显示滚动条
first, last = self.text_widget.yview() first, last = self.text_widget.yview()
# last < 1.0 表示内容超出了可见区域(需要滚动条)
needs_scrollbar = last < 1.0 needs_scrollbar = last < 1.0
if needs_scrollbar != self._scrollbar_visible: if needs_scrollbar != self._scrollbar_visible:

View File

@@ -19,3 +19,4 @@ numpy>=1.24.0
python-dateutil>=2.8.0 python-dateutil>=2.8.0
pytz>=2023.0 pytz>=2023.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
tklinenums>=1.7.0