import tkinter as tkfrom tkinter import ttkfrom datetime import datetime,timedelta# 悬浮提示类class CanvasHoverTooltip:"""一个为Canvas项目添加悬浮提示的通用类参数:canvas (tk.Canvas): 目标Canvas对象hover_delay (int): 显示提示前的延迟毫秒数(默认300ms)bg (str): 提示背景色(默认"lightyellow")fg (str): 提示文字颜色(默认"black")font (tuple): 提示字体(默认("Arial", 10))bd (int): 提示边框宽度(默认1)relief (str): 提示边框样式(默认"solid")offset_x (int): 水平偏移量(默认10)offset_y (int): 垂直偏移量(默认10)"""def __init__(self, canvas, hover_delay=300, bg="lightyellow", fg="black",font=("Arial", 10), bd=1, offset_x=10, offset_y=10):self.canvas = canvasself.hover_delay = hover_delayself.bg = bgself.fg = fgself.font = fontself.bd = bdself.offset = (offset_x, offset_y)# 存储所有项目的提示信息 {item_id: {"text": str, "after_id": str}}self.tooltips = {}self.current_tooltip = None # 当前显示的提示窗口# 绑定Canvas的鼠标事件self.canvas.bind("<Motion>", self._on_motion)self.canvas.bind("<Leave>", self._on_leave)self.canvas.bind("<Button-1>", self._on_click)# 鼠标左键def _on_click(self, _):passdef add_tooltip(self, item_id, text):"""为指定Canvas项目添加提示文本"""self.tooltips[item_id] = {"text": text,"after_id": None # 用于存储after事件ID}def remove_tooltip(self, item_id):"""移除指定项目的提示"""if item_id in self.tooltips:# 取消可能存在的延迟显示after_id = self.tooltips[item_id]["after_id"]if after_id:self.canvas.after_cancel(after_id)del self.tooltips[item_id]def _show_tooltip(self, text, x, y):"""显示提示窗口"""if self.current_tooltip:self.current_tooltip.destroy()self.current_tooltip = tk.Toplevel(self.canvas)self.current_tooltip.wm_overrideredirect(True) # 无边框# 计算屏幕坐标x_root = self.canvas.winfo_rootx() + x + self.offset[0]y_root = self.canvas.winfo_rooty() + y + self.offset[1]self.current_tooltip.wm_geometry(f"+{x_root}+{y_root}")label = tk.Label(self.current_tooltip,text=text,bg=self.bg,fg=self.fg,font=self.font,bd=self.bd,relief="solid")label.pack()def _hide_tooltip(self):"""隐藏提示窗口"""if self.current_tooltip:self.current_tooltip.destroy()self.current_tooltip = Nonedef _on_motion(self, event):"""鼠标移动事件处理"""# 获取鼠标下的项目x = self.canvas.canvasx(event.x) # 用画布的虚拟坐标y = self.canvas.canvasy(event.y) # 用画布的虚拟坐标item_ids = self.canvas.find_overlapping(x, y, x + 1, y + 1)found = Falsefor item_id in item_ids:if item_id in self.tooltips:found = True# 如果已经设置了延迟显示,则取消if self.tooltips[item_id]["after_id"]:self.canvas.after_cancel(self.tooltips[item_id]["after_id"])# 设置新的延迟显示self.tooltips[item_id]["after_id"] = self.canvas.after(self.hover_delay,lambda: self._show_tooltip(self.tooltips[item_id]["text"],event.x,event.y))breakif not found:self._hide_tooltip()def _on_leave(self, event):"""鼠标离开Canvas事件处理"""self._hide_tooltip()# 甘特图类class Gantt:def __init__(self, parent, json_project:dict, gantt_conf):self.json_project = json_projectself.parent = parentself.canvas = Noneif gantt_conf is not None:self.rect_height = gantt_conf.height # 矩形高度self.line_spacing = gantt_conf.spacing # 进度条之间的间隔self.interval = gantt_conf.interval # 时间刻度间隔天数self.width = gantt_conf.width # 甘特图画布宽度self.init_y = gantt_conf.init_y # 进度条起始y坐标self.progress_color = getattr(gantt_conf, 'progress_color', '#4CAF50') # 进度条颜色self.parent_progress_color = getattr(gantt_conf, 'parent_progress_color', '#2196F3') # 父任务进度条颜色self.critical_progress_color = getattr(gantt_conf, 'critical_progress_color', '#FF6B6B') # 关键任务进度条颜色else:self.rect_height = 17 # 矩形高度self.line_spacing = 3 # 进度条之间的间隔self.interval = 7 # 时间刻度间隔天数self.width = 600 # 甘特图画布宽度self.init_y = 45 # 进度条起始y坐标self.progress_color = '#4CAF50' # 默认绿色self.parent_progress_color = '#2196F3' # 默认蓝色self.critical_progress_color = '#FF6B6B' # 默认粉红色self.last_x = 0 # 最晚的进度条x坐标self.last_y = 0 # 最晚的进度条y坐标self.init_x = 10 # 进度条起始x坐标self.total_day = 0 # 项目总工期self.px_per_day = 0 # 每天占多少个像素,根据项目总工期、时间刻度来定self.tooltip_manager = None# 初始化组件self.init_component()# 画图self.draw_gantt()# 清除当前悬浮提示def delete_hover(self):self.tooltip_manager._hide_tooltip()# 画图def draw_gantt(self):# 创建工具提示管理器tooltip_manager = CanvasHoverTooltip(self.canvas,hover_delay=100, # 延迟显示,防止抖动bg="#FFFFE0", #font=("宋体", 10, "bold") # 使用中文字体)self.tooltip_manager = tooltip_manager# 获取项目的起始日期date_init = datetime.strptime(self.json_project[0].get('startdate'), '%Y-%m-%d')date_last = datetime.strptime(self.json_project[0].get('endate'), '%Y-%m-%d')self.total_day = (date_last - date_init).days + 1self.px_per_day = (self.width-self.init_x)/ self.total_dayrow_num = 1for task_id, task_info in self.json_project.items():if task_id == 0:continue# 获取开始日期date_start = datetime.strptime(task_info['startdate'], '%Y-%m-%d')date_end = datetime.strptime(task_info['endate'], '%Y-%m-%d')start = (date_start - date_init).daysdays = (date_end - date_start).days + 1# 判断是否是关键任务if task_info['critical']:# 使用配置的关键任务进度条颜色color = getattr(self, 'critical_progress_color', '#FF6B6B')else:# 判断是否是父任务 (通过WBS_no判断:任务的WBS_no加上".1"后新的WBS_no是否在所有任务中存在)is_parent = Falsecurrent_wbs = task_info.get('WBS_no')if current_wbs:# 构建潜在的子任务WBS编号potential_child_wbs = f"{current_wbs}.1"# 检查是否存在对应的子任务for _, other_task in self.json_project.items():if other_task.get('WBS_no') == potential_child_wbs:is_parent = Truebreakif is_parent:color = getattr(self, 'parent_progress_color', '#2196F3')else:color = getattr(self, 'progress_color', '#4CAF50')ret_itemid = self.draw_progress(row=row_num, startday=start, days=days, color=color)row_num += 1# 增加悬浮提示task_name = task_info['name']task_dur = task_info['duration']task_start = task_info['startdate']task_end = task_info['endate']hover_info = f'{task_name}\n工期:{task_dur}\n开始日期:{task_start}\n结束日期:{task_end}'tooltip_manager.add_tooltip(ret_itemid, hover_info)# 画时间刻度self.draw_startend(date_init.strftime('%Y-%m-%d'), date_last.strftime('%Y-%m-%d'), self.interval)def on_mousewheel(self, event):# Windows 和 Mac 使用不同的事件属性if event.num == 4 or event.delta > 0:self.canvas.yview_scroll(-1, "units") # 向上滚动elif event.num == 5 or event.delta < 0:self.canvas.yview_scroll(1, "units") # 向下滚动# 初始化画布组件def init_component(self):self.canvas = tk.Canvas(self.parent, bg="white")v_scroll = ttk.Scrollbar(self.parent, orient="vertical", command=self.canvas.yview)h_scroll = ttk.Scrollbar(self.parent, orient="horizontal", command=self.canvas.xview)self.canvas.configure(yscrollcommand=v_scroll.set,xscrollcommand=h_scroll.set,scrollregion=(0, 0, 1000, 1000))# 布局self.canvas.grid(row=0, column=0, sticky="nsew")v_scroll.grid(row=0, column=1, sticky="ns")h_scroll.grid(row=1, column=0, sticky="ew")self.canvas.bind("<MouseWheel>", self.on_mousewheel)self.parent.grid_rowconfigure(0, weight=1)self.parent.grid_columnconfigure(0, weight=1)# 在画布上画进度条def draw_rounded_rect_v2(self, canvas, x1, y1, x2, y2, **kwargs):"""通过计算顶点绘制精确的圆角矩形"""ret_itemid = canvas.create_rectangle(x1, y1, x2, y2, **kwargs)return ret_itemid# 画进度条,根据天数,起始的第几天def draw_progress(self, row, startday, days, color):x1 = self.init_x+startday*self.px_per_day # 矩形左上角x坐标y1 = self.init_y + (self.rect_height + self.line_spacing) * (row-1) # 矩形左上角y坐标x2 = x1 + days * self.px_per_day # 矩形的长度取决于工期y2 = y1 + self.rect_heightself.last_x = max(x2, self.last_x)self.last_y = max(y2, self.last_y)ret_itemid = self.draw_rounded_rect_v2(self.canvas,x1, y1, x2, y2,fill=color,outline="grey",width=2)return ret_itemid# 画项目开始、完成时间def draw_startend(self, start, end, every):canvas = self.canvaslast_x = self.last_xlast_y = self.last_ytotal_day = self.total_daypx_per_day = self.px_per_day# 将字符串日期转成datedate_start = datetime.strptime(start, "%Y-%m-%d").date()date_end = datetime.strptime(start, "%Y-%m-%d").date()# 开始canvas.create_text(10, 10, text=start, font=("Times New Roman", 10), fill="black", anchor="nw")# 完成canvas.create_text(last_x, 10, text=end,font=("Times New Roman", 10), fill="black", anchor="ne" )# 画开始和完成日期的刻度线# canvas.create_line(8, 40, 8, last_y+10, dash=(5, 2)) # 5px线段 + 2px间隔canvas.create_line(7, 40, 7, last_y+10, dash=(5, 2)) # 5px线段 + 2px间隔# canvas.create_line(last_x+1, 40, last_x+1, last_y+10, dash=(5, 2)) # 5px线段 + 2px间隔canvas.create_line(last_x+2, 40, last_x+2, last_y+10, dash=(5, 2)) # 5px线段 + 2px间隔# 根据参数每多少天画一根刻度线# 计算要画多少根线nums = int(total_day/every)for i in range(1, nums+1):# 计算日期date_draw = date_start + timedelta(days=every*i-1)date_str = date_draw.strftime('%y-%m-%d')date_strall = date_draw.strftime('%Y-%m-%d')# 如何时间是项目完成时间,则不写日期if date_strall != end:canvas.create_line(i * px_per_day * every + self.init_x, 40, i * px_per_day * every + self.init_x,last_y + 10, dash=(1, 1))canvas.create_text(i*px_per_day*every+self.init_x, last_y+20, text=date_str, font=("Times New Roman",10), fill="black", anchor="center")# if __name__ == '__main__':# # 使用示例# root = tk.Tk()# root.geometry("600x600+50+50")# f1 = tk.LabelFrame(root, text='甘特图')# f1.pack(fill='both', expand=True)## project_json = {}# date_start = datetime.today()# dur = 9# date_end = date_start + timedelta(days=dur-1)# project_json[0] = {# 'WBS_no': '0',# 'name': f'task0',# 'duration': dur,# 'startdate': date_start.strftime('%Y-%m-%d'),# 'endate': date_end.strftime('%Y-%m-%d'),# 'remark': None,# 'critical': True,# 'preceding_list': []# }# for i in range(1, 9):# date_start = date_start + timedelta(days=1)# project_json[i] = {# 'WBS_no': f'{i}',# 'name': f'task{i}',# 'duration': 1,# 'startdate': date_start.strftime('%Y-%m-%d'),# 'endate': date_start.strftime('%Y-%m-%d'),# 'remark': None,# 'critical': False,# 'preceding_list': []# }## Gantt(f1, project_json, GanttConfig({'height':10,'interval':1,'width':600,'spacing':5}))## root.mainloop()
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。