Compare commits

...

7 Commits

  1. 42
      README.md
  2. 295
      app.py

42
README.md

@ -11,6 +11,48 @@
pyinstaller --onefile --windowed --name TaskManager app.py
```
```bash
# 创建一个新环境
conda create -n taskMgr python=3.13.9
conda activate taskMgr
# -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install tkcalendar==1.6.1 tksheet==7.5.19 customtkinter==5.2.2
```
当前python环境的conda list
```
# Name Version Build Channel
babel 2.17.0 pypi_0 pypi
bzip2 1.0.8 h2bbff1b_6
ca-certificates 2025.12.2 haa95532_0
customtkinter 5.2.2 pypi_0 pypi
darkdetect 0.8.0 pypi_0 pypi
expat 2.7.3 h885b0b7_4
libexpat 2.7.3 h885b0b7_4
libffi 3.4.4 hd77b12b_1
libmpdec 4.0.0 h827c3e9_0
libzlib 1.3.1 h02ab6af_0
openssl 3.0.18 h543e019_0
packaging 25.0 pypi_0 pypi
pip 25.3 pyhc872135_0
python 3.13.9 h260b955_100_cp313
python_abi 3.13 3_cp313
setuptools 80.9.0 py313haa95532_0
sqlite 3.51.0 hda9a48d_0
tk 8.6.15 hf199647_0
tkcalendar 1.6.1 pypi_0 pypi
tksheet 7.5.19 pypi_0 pypi
tzdata 2025b h04d1e81_0
ucrt 10.0.22621.0 haa95532_0
vc 14.3 h2df5915_10
vc14_runtime 14.44.35208 h4927774_10
vs2015_runtime 14.44.35208 ha6b5a95_10
wheel 0.45.1 py313haa95532_0
xz 5.6.4 h4754444_1
zlib 1.3.1 h02ab6af_0
```
**主要功能:**
1. 添加任务(可设置 类型、优先级、状态、链接、备注等信息)
2. 可筛选、排序,并且可以保存为模板快速应用

295
app.py

@ -1,5 +1,6 @@
import os
import json
import math
import sqlite3
import webbrowser
from datetime import datetime, date, timezone
@ -9,10 +10,11 @@ from tkinter import messagebox, simpledialog
import customtkinter as ctk
from tksheet import Sheet
from tkcalendar import Calendar
import tkinter.font as tkfont
# ----------------------- CONFIG -----------------------
# region 配置
VERSION = "v1.0.0"
VERSION = "v1.0.6"
DB_PATH = "tasks.db" # 数据库文件路径
TEMPLATES_PATH = "templates.json" # 检索模板文件路径
@ -29,7 +31,7 @@ CTK_COLOR_THEME = "blue"
# Options
PRIORITY_OPTIONS = ["SSS", "SS", "S", "A", "B", "C", "D", "E"]
TYPE_OPTIONS = ["Bug", "需求", "其他"]
TYPE_OPTIONS = ["Bug", "需求", "其他", "小问题"]
STATUS_OPTIONS = ["待处理", "进行中", "已完成", "保持跟进", "搁置", "取消"]
# Display sets (icon + background)
@ -47,6 +49,7 @@ TYPE_DISPLAY = {
"Bug": {"icon": "🐞", "bg": "#ed7e7e"},
"需求": {"icon": "", "bg": "#a0e9c4"},
"其他": {"icon": "📝", "bg": "#c1d9fe"},
"小问题": {"icon": "🔔", "bg": "#f6a746"},
}
STATUS_DISPLAY = {
"待处理": {"icon": "", "bg": "#f7e086"},
@ -71,17 +74,17 @@ PROCESSED_DISPLAY = {
# SORT ORDER (Rule A) - used for table-column sorting (integer ranks)
SORT_ORDER = {
"priority": {"SSS": 8, "SS": 7, "S": 6, "A": 5, "B": 4, "C": 3, "D": 2, "E": 1},
"type": {"Bug": 3, "需求": 2, "其他": 1},
"type": {"Bug": 3, "需求": 2, "其他": 1, "小问题" : 1},
"status": {"待处理": 6, "进行中": 5, "保持跟进": 4, "搁置": 3, "取消": 2, "已完成": 1}
}
# COMPOSITE WEIGHTS (Rule B) - used in composite score calculation
COMPOSITE_WEIGHTS = {
"priority": {"SSS": 4.0, "SS": 3.0, "S": 2.0, "A": 1.0, "B": 0.8, "C": 0.6, "D": 0.4, "E": 0.2},
"type": {"Bug": 1.5, "需求": 1.0, "其他": 1.0},
"status": {"待处理": 0.9, "进行中": 1.0, "保持跟进": 0.1, "搁置": 0.1, "取消": 0.0, "已完成": 0.0},
"type": {"Bug": 1.5, "需求": 1.0, "其他": 1.0, "小问题": 1.0},
"status": {"待处理": 0.9, "进行中": 1.0, "保持跟进": 0.15, "搁置": 0.1, "取消": 0.0, "已完成": 0.0},
# age factor multiplier (per day)
"age_factor": 0.1
"age_factor": 0.05
}
# Columns (for sheet) 这里的顺序即列显示顺序
@ -91,6 +94,7 @@ COLUMNS = [
("type", "类型"),
("status", "状态"),
("priority", "优先级"),
("deadline", "截止日期"),
("composite", "综合优先级"),
("title", "标题"),
("brief", "简介"),
@ -115,6 +119,22 @@ COL_START_DATE = COL_INDEX.get("start_date")
COL_LINKS_COUNT = COL_INDEX.get("links_count")
COL_NOTES = COL_INDEX.get("notes")
COL_COMPOSITE = COL_INDEX.get("composite")
COL_DEADLINE = COL_INDEX.get("deadline")
# 截止日期颜色,距离截止日期的时间小于哪个值就是哪个颜色
DEADLINE_COLOR = [
(0, "#fb0000"),
(1, "#ff6f22"),
(2, "#ff904b"),
(3, "#b2ff77"),
(4, "#73bc75"),
(10, "#83d6ff"),
(15, "#bdd8e9"),
]
# 保持跟进、搁置超出日期时的颜色
PAUSE_DEADLINE_COLOR = "#fbffa9"
# 默认颜色
DEFAULT_DEADLINE_COLOR = "#ffffff"
# endregion
@ -141,6 +161,7 @@ class TaskDB:
type TEXT,
status TEXT,
start_date TEXT,
deadline TEXT,
links TEXT,
notes TEXT,
updated_at TEXT
@ -178,6 +199,8 @@ class TaskDB:
adds.append(("status", "TEXT"))
if "start_date" not in cols:
adds.append(("start_date", "TEXT"))
if "deadline" not in cols:
adds.append(("deadline", "TEXT"))
if "links" not in cols:
adds.append(("links", "TEXT"))
if "notes" not in cols:
@ -224,8 +247,8 @@ class TaskDB:
now = datetime.now(timezone.utc).isoformat()
cur = self.conn.cursor()
cur.execute("""
INSERT INTO tasks (title, brief, description, priority, type, status, start_date, links, notes, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO tasks (title, brief, description, priority, type, status, start_date, deadline, links, notes, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
task.get("title", ""),
task.get("brief", ""),
@ -234,6 +257,7 @@ class TaskDB:
task.get("type"),
task.get("status"),
task.get("start_date"),
task.get("deadline"),
json.dumps(task.get("links", []), ensure_ascii=False),
task.get("notes", ""),
now
@ -244,7 +268,7 @@ class TaskDB:
def update_task(self, tid: int, task: dict):
now = datetime.now(timezone.utc).isoformat()
self.conn.execute("""
UPDATE tasks SET title=?, brief=?, description=?, priority=?, type=?, status=?, start_date=?, links=?, notes=?, updated_at=?
UPDATE tasks SET title=?, brief=?, description=?, priority=?, type=?, status=?, start_date=?, deadline=?, links=?, notes=?, updated_at=?
WHERE sid=?
""", (
task.get("title", ""),
@ -254,6 +278,7 @@ class TaskDB:
task.get("type"),
task.get("status"),
task.get("start_date"),
task.get("deadline"),
json.dumps(task.get("links", []), ensure_ascii=False),
task.get("notes", ""),
now,
@ -363,24 +388,43 @@ def compute_composite_score(task_row: dict) -> float:
Compute composite priority score using COMPOSITE_WEIGHTS.
Higher number => more urgent.
"""
# 读取数据
# 任务开始以来的天数
start_date = task_row.get("start_date")
days = 0
days_from_start = 0
try:
if start_date:
dt = datetime.fromisoformat(start_date)
# convert to local naive days
days = (datetime.now(dt.tzinfo or timezone.utc).date() - dt.date()).days
if days < 0:
days = 0
days_from_start = (datetime.now(dt.tzinfo or timezone.utc).date() - dt.date()).days
if days_from_start < 0:
days_from_start = 0
except Exception:
days_from_start = 0
# 距离截止日期的时间
deadline = task_row.get("deadline")
have_deadline = False
days_to_deadline = 0
try:
if deadline:
dt_deadline = datetime.fromisoformat(deadline)
days_to_deadline = (dt_deadline.date() - datetime.now(dt_deadline.tzinfo or timezone.utc).date()).days
have_deadline = True
except Exception:
days = 0
d_score = COMPOSITE_WEIGHTS.get("age_factor", 0.1) * days + 1
have_deadline = False
# 各种因素分数
ddl_score = 1 + math.exp(-(0.3 + days_to_deadline / 3.0)) if have_deadline else 0.9
d_score = COMPOSITE_WEIGHTS.get("age_factor", 0.1) * days_from_start + 1
p_score = COMPOSITE_WEIGHTS["priority"].get(task_row.get("priority"), 1.0)
t_score = COMPOSITE_WEIGHTS["type"].get(task_row.get("type"), 1.0)
s_score = COMPOSITE_WEIGHTS["status"].get(task_row.get("status"), 1.0)
# 跟进和搁置状态不考虑截止日期
state = task_row.get("status")
if state in ["保持跟进", "搁置"]:
ddl_score = 1.0
# 计算综合优先级分数
score = p_score * t_score * s_score * d_score
score = p_score * t_score * s_score * d_score * ddl_score
return round(float(score), 6)
@ -401,6 +445,7 @@ class TaskEditor(ctk.CTkToplevel):
self.title_var = ctk.StringVar()
self.brief_var = ctk.StringVar()
self.start_var = ctk.StringVar(value=date.today().isoformat())
self.deadline_var = ctk.StringVar(value="")
self.priority_var = ctk.StringVar()
self.type_var = ctk.StringVar()
self.status_var = ctk.StringVar()
@ -446,33 +491,37 @@ class TaskEditor(ctk.CTkToplevel):
ctk.CTkLabel(frm, text="Start Date (YYYY-MM-DD)").grid(row=8, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
ctk.CTkEntry(frm, textvariable=self.start_var).grid(row=9, column=0, columnspan=3, sticky="ew", padx=pad)
# deadline
ctk.CTkLabel(frm, text="Deadline (YYYY-MM-DD)").grid(row=10, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
ctk.CTkEntry(frm, textvariable=self.deadline_var).grid(row=11, column=0, columnspan=3, sticky="ew", padx=pad)
# links
ctk.CTkLabel(frm, text="Links (one per line)").grid(row=10, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
ctk.CTkLabel(frm, text="Links (one per line)").grid(row=12, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
self.links_text = ctk.CTkTextbox(frm, height=80)
self.links_text.grid(row=11, column=0, columnspan=3, sticky="nsew", padx=pad)
self.links_text.grid(row=13, column=0, columnspan=3, sticky="nsew", padx=pad)
# logs (processed dates)
ctk.CTkLabel(frm, text="Processed Dates (handling records)").grid(row=12, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
ctk.CTkLabel(frm, text="Processed Dates (handling records)").grid(row=14, column=0, columnspan=3, sticky="w", padx=pad, pady=(10,0))
logs_frame = ctk.CTkFrame(frm)
logs_frame.grid(row=13, column=0, columnspan=3, sticky="ew", padx=pad, pady=(4,0))
logs_frame.grid(row=15, column=0, columnspan=3, sticky="ew", padx=pad, pady=(4,0))
ctk.CTkButton(logs_frame, text="Mark Today", width=120, command=self._mark_today).pack(side="left", padx=6, pady=6)
ctk.CTkButton(logs_frame, text="Add Date...", width=120, command=self._add_date_dialog).pack(side="left", padx=6, pady=6)
ctk.CTkButton(logs_frame, text="Remove Selected", fg_color="#cc4444", hover_color="#aa3333", command=self._remove_selected_date).pack(side="right", padx=6, pady=6)
# list widget for logs (tk Listbox embedded)
self.logs_listbox = tk.Listbox(frm, height=5)
self.logs_listbox.grid(row=14, column=0, columnspan=3, sticky="nsew", padx=pad, pady=(4,0))
self.logs_listbox.grid(row=16, column=0, columnspan=3, sticky="nsew", padx=pad, pady=(4,0))
# buttons
btn_row = ctk.CTkFrame(frm)
btn_row.grid(row=15, column=0, columnspan=3, pady=(12,6))
btn_row.grid(row=17, column=0, columnspan=3, pady=(12,6))
ctk.CTkButton(btn_row, text="Save", command=self._save).pack(side="left", padx=8)
ctk.CTkButton(btn_row, text="Cancel", fg_color="#888", hover_color="#666", command=self.destroy).pack(side="left", padx=8)
# grid weights
frm.grid_rowconfigure(5, weight=1)
frm.grid_rowconfigure(11, weight=0)
frm.grid_rowconfigure(14, weight=0)
frm.grid_rowconfigure(13, weight=0)
frm.grid_rowconfigure(16, weight=0)
frm.grid_columnconfigure(0, weight=1)
frm.grid_columnconfigure(1, weight=1)
frm.grid_columnconfigure(2, weight=1)
@ -489,6 +538,7 @@ class TaskEditor(ctk.CTkToplevel):
self.type_cb.set(row.get("type") or "")
self.status_cb.set(row.get("status") or "")
self.start_var.set(row.get("start_date") or "")
self.deadline_var.set(row.get("deadline") or "")
links = json.loads(row.get("links") or "[]")
self.links_text.delete("1.0", "end"); self.links_text.insert("1.0", "\n".join(links))
# logs
@ -556,6 +606,7 @@ class TaskEditor(ctk.CTkToplevel):
"type": self.type_cb.get() or None,
"status": self.status_cb.get() or None,
"start_date": self.start_var.get().strip() or date.today().isoformat(),
"deadline": self.deadline_var.get().strip() or "",
"links": links,
"processed_dates": getattr(self, "_logs", [])
}
@ -575,6 +626,7 @@ class TaskEditor(ctk.CTkToplevel):
"type": data["type"],
"status": data["status"],
"start_date": data["start_date"],
"deadline": data["deadline"],
"links": data["links"],
"notes": data["notes"]
})
@ -595,6 +647,7 @@ class TaskEditor(ctk.CTkToplevel):
"type": data["type"],
"status": data["status"],
"start_date": data["start_date"],
"deadline": data["deadline"],
"links": data["links"],
"notes": data["notes"],
})
@ -628,6 +681,10 @@ class TaskManagerApp(ctk.CTk):
# guard to prevent duplicate toggles from multiple bound handlers
self._last_header_toggle = None
# 调整单元格宽度所需参数
self._prev_col_widths = {}
self._resized_column = None
# build UI
self._build_ui()
self.refresh_table()
@ -702,30 +759,35 @@ class TaskManagerApp(ctk.CTk):
self.sheet.grid(row=0, column=0, sticky="nsew")
table_frame.grid_rowconfigure(0, weight=1); table_frame.grid_columnconfigure(0, weight=1)
# bind double click using extra_bindings if available
# ===============================
# 各种绑定
try:
self.sheet.extra_bindings([("double_click_cell", self._on_double_click_cell)])
except Exception:
self.sheet.bind("<Double-1>", self._on_double_click_generic, add="+")
# try binding header clicks via extra_bindings if available (try multiple tksheet event names)
try:
self.sheet.extra_bindings([("column_select", self._on_header_click)])
except Exception:
print('[Error] bind _on_header_click failed!!!')
except Exception as e:
print("[Warn] column_select not supported:", e)
try:
self.sheet.extra_bindings([("column_width_resize", self._on_column_resize)])
except Exception as e:
print("[Warn] column_width_resize not supported:", e)
self.bind_all("<ButtonRelease-1>", self._on_column_resize_end, add="+")
# header/column clicks - generic
# primary binding directly on sheet
self.sheet.bind("<Button-1>", self._on_sheet_click_generic, add="+")
# also listen to ButtonRelease as some versions use release for headers
self.sheet.bind("<ButtonRelease-1>", self._on_sheet_click_generic, add="+")
# global binding as fallback to ensure header clicks are caught even if tksheet consumes the event
self.bind_all("<Button-1>", self._on_root_click, add="+")
self.bind_all("<ButtonRelease-1>", self._on_root_click, add="+")
# 指定列居中
align_center_cols = []
for i, (key, _) in enumerate(COLUMNS):
if key in ('sid', 'priority', 'type', 'status', 'start_date', 'processed_today', 'last_processed', 'links_count', 'composite'):
if key in ('sid', 'priority', 'type', 'status', 'start_date', 'processed_today', 'last_processed', 'links_count', 'composite', 'deadline'):
align_center_cols.append(i)
self.sheet.align_columns(columns=align_center_cols, align="center")
@ -758,6 +820,20 @@ class TaskManagerApp(ctk.CTk):
elif col == "processed_today":
today_iso = date.today().isoformat()
rows.sort(key=lambda r: (today_iso in self.db.get_logs_for_task(r["sid"])), reverse=not self.order_asc)
elif col == "deadline":
# Treat empty/null deadlines as greater than any date so they appear last when sorting ascending
def _deadline_key(r):
d = r.get("deadline")
if not d:
# missing deadline -> mark as 'empty' (1) and None for tie
return (1, None)
try:
dt = datetime.fromisoformat(d)
return (0, dt)
except Exception:
# fallback to string compare (ISO-like strings sort correctly)
return (0, d)
rows.sort(key=_deadline_key, reverse=not self.order_asc)
else:
rows.sort(key=lambda r: r.get(col) or "", reverse=not self.order_asc)
@ -801,6 +877,8 @@ class TaskManagerApp(ctk.CTk):
row.append(r.get("last_processed") or "")
elif key == "start_date":
row.append(r.get("start_date") or "")
elif key == "deadline":
row.append(r.get("deadline") or "")
elif key == "links_count":
row.append(str(links_count))
elif key == "notes":
@ -857,6 +935,7 @@ class TaskManagerApp(ctk.CTk):
for r_idx, tid in enumerate(self.displayed_sids):
task = self.db.get_task(tid)
pr = task.get("priority"); ty = task.get("type"); st = task.get("status")
ddl = task.get("deadline")
# check if processed today using processed_map
processed = processed_map.get(tid, False)
if pr and pr in PRIORITY_DISPLAY:
@ -877,6 +956,27 @@ class TaskManagerApp(ctk.CTk):
except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_STATUS,bg)
except Exception: pass
if ddl and st not in ["已完成", "取消"]:
try:
dt_deadline = datetime.fromisoformat(ddl)
days_to_deadline = (dt_deadline.date() - datetime.now(dt_deadline.tzinfo or timezone.utc).date()).days
bg = DEFAULT_DEADLINE_COLOR
for (d, c) in DEADLINE_COLOR:
if days_to_deadline <= d:
bg = c if st not in ["保持跟进", "搁置"] else PAUSE_DEADLINE_COLOR
break
try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg)
except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg)
except Exception: pass
except Exception:
pass
else:
bg = DEFAULT_DEADLINE_COLOR
try: self.sheet.highlight_cells(row=r_idx, column=COL_DEADLINE, bg=bg)
except Exception:
try: self.sheet.set_cell_bg(r_idx,COL_DEADLINE,bg)
except Exception: pass
# processed today column: use COL_PROCESSED_TODAY constant
if processed:
bg = PROCESSED_DISPLAY["yes"]["bg"]
@ -889,10 +989,14 @@ class TaskManagerApp(ctk.CTk):
# try auto row size
try:
# self.sheet.refresh()
self.sheet.set_all_cell_sizes_to_text()
except Exception:
pass
# reset column width cache
self._init_column_width_cache()
def _clear_filters(self):
self.search_var.set("")
self.type_listbox.selection_clear(0, "end")
@ -955,8 +1059,56 @@ class TaskManagerApp(ctk.CTk):
tid = self.displayed_sids[r]
row = self.db.get_task(tid)
links = json.loads(row.get("links") or "[]")
if not links: messagebox.showinfo("No links", "No links found"); return
for u in links: webbrowser.open(u)
if not links:
messagebox.showinfo("No links", "No links found")
return
# Single link: open immediately (preserve original behaviour)
if len(links) == 1:
try:
webbrowser.open(links[0])
except Exception:
messagebox.showerror("Error", "Failed to open link")
return
# Multiple links: show a small window listing them; user selects one to open
win = ctk.CTkToplevel(self)
win.title("Open Link")
win.geometry("640x320")
# header
try:
ctk.CTkLabel(win, text=f"Links for task {tid}").pack(anchor="w", padx=8, pady=(8,0))
except Exception:
tk.Label(win, text=f"Links for task {tid}").pack(anchor="w", padx=8, pady=(8,0))
lb = tk.Listbox(win, height=10)
lb.pack(fill="both", expand=True, padx=8, pady=8)
for u in links:
lb.insert("end", u)
def _open_selected():
sel = lb.curselection()
if not sel:
messagebox.showinfo("Info", "Select a link")
return
url = lb.get(sel[0])
try:
webbrowser.open(url)
except Exception:
messagebox.showerror("Error", "Failed to open link")
win.destroy()
def _open_all():
for u in links:
try: webbrowser.open(u)
except Exception: pass
win.destroy()
lb.bind("<Double-1>", lambda e: _open_selected())
btn_row = ctk.CTkFrame(win)
btn_row.pack(fill="x", padx=8, pady=8)
ctk.CTkButton(btn_row, text="Open All", command=_open_all).pack(side="left", padx=6)
ctk.CTkButton(btn_row, text="Close", fg_color="#888", hover_color="#666", command=win.destroy).pack(side="right", padx=6)
def mark_selected_processed_today(self):
r = self._get_first_selected_row_index()
@ -1018,6 +1170,24 @@ class TaskManagerApp(ctk.CTk):
# swallow - no-op
pass
def _on_column_resize(self, event):
new_w_list = self.sheet.get_column_widths(0)
for col in range(self.sheet.get_total_columns()):
new_w = new_w_list[col]
old_w = self._prev_col_widths[col]
if new_w != old_w:
self._prev_col_widths[col] = new_w
self._resized_column = col
break
def _on_column_resize_end(self, event):
if self._resized_column is None:
return
col = self._resized_column
self._resized_column = None
self._recalc_column_row_heights(col)
# header click generic for sorting
def _on_sheet_click_generic(self, event):
try:
@ -1410,6 +1580,59 @@ class TaskManagerApp(ctk.CTk):
ctk.CTkButton(right, text="Open Selected Task", command=open_task).pack(pady=6)
on_select()
# 调整单元格参数
def _init_column_width_cache(self):
self._prev_col_widths = self.sheet.get_column_widths(0)
def _calc_text_lines(self, text, col_width, tk_font):
if not text:
return 1
lines = 0
for paragraph in str(text).split("\n"):
current_width = 0
for ch in paragraph:
ch_width = tk_font.measure(ch)
if current_width + ch_width > col_width:
lines += 1
current_width = ch_width
else:
current_width += ch_width
lines += 1 # 每个段落至少一行
return max(lines, 1)
def _recalc_column_row_heights(self, col):
font_desc = self.sheet.font()
tk_font = tkfont.Font(font=font_desc)
line_height = tk_font.metrics("linespace")
col_widths = self.sheet.get_column_widths(0)
col_num = self.sheet.get_total_columns()
for row in range(self.sheet.get_total_rows()):
max_height = 0
for col in range(col_num):
text = self.sheet.get_cell_data(row, col)
if not text:
continue
col_width = col_widths[col]
lines = self._calc_text_lines(text, col_width, tk_font)
height = lines * line_height + 6
height = min(max(height, 20), 100000)
if height > max_height:
max_height = height
self.sheet.row_height(row, max_height)
self.sheet.refresh()
# run / refresh
def run(self):
self.mainloop()

Loading…
Cancel
Save