diff --git a/gui/widgets/production_id_input.py b/gui/widgets/production_id_input.py index 1687dca..d7f2beb 100644 --- a/gui/widgets/production_id_input.py +++ b/gui/widgets/production_id_input.py @@ -4,61 +4,80 @@ Production ID 输入控件 多行文本输入框,用于输入 Production ID 列表。 -支持内容溢出时自动显示滚动条。 +支持内容溢出时自动显示滚动条,左侧显示行号。 """ import tkinter as tk from tkinter import ttk, font +from tklinenums import TkLineNumbers class ProductionIdInput(ttk.Frame): - """Production ID 输入控件""" + """Production ID 输入控件,带行号和自动滚动条""" def __init__(self, parent, placeholder="每行输入一个 Production ID", **kwargs): super().__init__(parent, **kwargs) self.placeholder = placeholder self._scrollbar_visible = False self._check_pending = False + self._updating_placeholder = False # 创建文本框 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.text_widget.configure(yscrollcommand=self._on_scroll) - # 布局 - 使用 grid 以便动态显示/隐藏滚动条 - self.text_widget.grid(row=0, column=0, sticky="nsew") - self.scrollbar.grid(row=0, column=1, sticky="ns") + # 布局:行号 | 文本框 | 滚动条 + self.linenums_frame.grid(row=0, column=0, 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_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) # 绑定事件 self.text_widget.bind("", self._on_focus_in) self.text_widget.bind("", self._on_focus_out) # 监听内容变化事件 - self.text_widget.bind("", self._schedule_check) + self.text_widget.bind("", self._on_content_change) self.text_widget.bind("", self._schedule_check) self.text_widget.bind("", self._schedule_check) self.text_widget.bind("", self._schedule_check) # 绑定粘贴事件 self.text_widget.bind("<>", self._schedule_check) - # 初始隐藏滚动条 + # 初始隐藏滚动条(行号始终显示) self.scrollbar.grid_remove() # 显示占位符 self._show_placeholder() # 延迟检查初始状态 - self.after(100, self._check_scrollbar_needed) + self.after(100, self._check_ui_state) 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.configure(foreground="black") def _on_focus_out(self, event): """失去焦点时显示占位符""" @@ -68,15 +87,27 @@ class ProductionIdInput(ttk.Frame): def _show_placeholder(self): """显示占位符""" + self._updating_placeholder = True self.text_widget.delete("1.0", tk.END) self.text_widget.insert("1.0", self.placeholder) self.text_widget.configure(foreground="gray") + self._updating_placeholder = False + self._schedule_check() def _hide_placeholder(self): """隐藏占位符""" + self._updating_placeholder = True if self.text_widget.get("1.0", "end-1c") == self.placeholder: self.text_widget.delete("1.0", tk.END) 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]: """获取 Production ID 列表""" @@ -86,18 +117,23 @@ class ProductionIdInput(ttk.Frame): def set(self, production_ids: list[str]): """设置 Production ID 列表""" + self._updating_placeholder = True self.text_widget.delete("1.0", tk.END) if production_ids: self.text_widget.insert("1.0", "\n".join(production_ids)) self.text_widget.configure(foreground="black") + self.after_idle(self.line_numbers.redraw) else: self._show_placeholder() + self._updating_placeholder = False self._schedule_check() def clear(self): """清空内容""" + self._updating_placeholder = True self.text_widget.delete("1.0", tk.END) self._show_placeholder() + self._updating_placeholder = False self._schedule_check() 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)) else: self.text_widget.insert("1.0", "\n".join(production_ids)) + self.after_idle(self.line_numbers.redraw) self._schedule_check() def apply_font(self, font_family: str, font_size: int): """应用字体设置""" font_spec = font.Font(family=font_family, size=font_size) self.text_widget.configure(font=font_spec) + # 重绘行号以应用字体变化 + self.after_idle(self.line_numbers.redraw) def _on_scroll(self, first, last): - """滚动回调,更新滚动条位置""" + """滚动回调,更新滚动条位置和行号""" self.scrollbar.set(first, last) + # 滚动时重绘行号以同步显示 + self.line_numbers.redraw() def _schedule_check(self, event=None): - """调度滚动条检查(防抖)""" + """调度 UI 状态检查(防抖)""" if not self._check_pending: 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.text_widget.update_idletasks() - # 使用 yview() 返回值判断 + # 检查是否需要显示滚动条 first, last = self.text_widget.yview() - # last < 1.0 表示内容超出了可见区域(需要滚动条) needs_scrollbar = last < 1.0 if needs_scrollbar != self._scrollbar_visible: diff --git a/requirements.txt b/requirements.txt index 53827ff..f95887d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ numpy>=1.24.0 python-dateutil>=2.8.0 pytz>=2023.0 python-dotenv>=1.0.0 +tklinenums>=1.7.0