一、背景
在Python制作排班小工具【二】中,我们实现了GUI界面元素的展示。本文将介绍按钮对应的事件及文本框的展示。以下是界面事件和按钮流程图的详细描述:
1.打开程序时显示上一次的输入记录。如果没有记录,输入框为空。
2.姓名输入以顿号“、”隔开。
3.姓名排序和序号相对应,即第一个姓名序号为1。
4.每日值班人数和值班组数量不能为空且不能为非整数。
5.排班表展示框将展示生成的排班表。
6.开启【删除人员配置开关】后,方可点击【删除人员配置】按钮。
7.【查看记录】标签页将展示所有生成的排班表。
主要按钮的流程图如下:
二、完整代码
import datetime
import os.path
import re
import time
from tkinter.font import Font
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import DISABLED, NORMAL
from Scheduling import yaml_read, scheduling, yaml_write, yaml_clear
class GUI:
def __init__(self, master):
"""排排班GUI"""
self.root = master
self.name = ttk.StringVar() # 生成StringVar对象,以保存输入框中的内容--参与排班人员姓名
self.each_group_num = ttk.StringVar() # 每日值班人数
self.group_num = ttk.StringVar() # 值班组数量
self.variable = ttk.BooleanVar() # 删除人员配置开关设置--布尔值
self.create_page() # 显示页面元素
self.update_time() # 更新时间
self.show_last_input_record() # 展示最近一次输入记录
self.tab_idx = ttk.IntVar() # tab标签页的下标
self.bind_notebook_event() # 绑定tab标签页切换事件
def create_page(self):
"""创建页面元素"""
# # ------------------------------------标签页控件------------------------------------#
# 创建标签页控件
self.tabcontrol = ttk.Notebook(self.root)
# 【生成排班】标签
tab1 = ttk.Frame(self.tabcontrol)
self.tabcontrol.add(tab1, text='生成排班')
# 【查看记录】标签
tab2 = ttk.Frame(self.tabcontrol)
self.tabcontrol.add(tab2, text="查看记录")
# 打包标签控件
self.tabcontrol.pack(expand=1, fill=BOTH)
# # ------------------------------------标签页控件------------------------------------#
# # ------------------------------------生成排班------------------------------------#
# 参与排班人员姓名标签
name_label = ttk.Label(tab1, text="参与排班人员姓名:", bootstyle=PRIMARY)
name_label.grid(row=0, column=0, pady=5) # 使用grid布局(网格布局方式)
# 参与排班人员姓名文本框
self.name_input = ttk.Text(tab1, width=23, height=2, wrap=WORD)
self.name_input.grid(row=0, column=1, columnspan=3, pady=5)
# 每日值班人数标签
each_group_num_label = ttk.Label(tab1, text="每日值班人数:", bootstyle=PRIMARY)
each_group_num_label.grid(row=1, column=0, pady=5, sticky=E)
# 每日值班人数输入框
each_group_num_input = ttk.Entry(tab1, textvariable=self.each_group_num, width=5)
each_group_num_input.grid(row=1, column=1, pady=5)
# 值班组数量标签
group_num_label = ttk.Label(tab1, text="值班组数量:", bootstyle=PRIMARY)
group_num_label.grid(row=1, column=2, pady=5, padx=5, sticky=E)
# 值班组数量输入框
group_num_input = ttk.Entry(tab1, textvariable=self.group_num, width=5)
group_num_input.grid(row=1, column=3, pady=5)
# 生成排班按钮
self.generate_button = ttk.Button(tab1, text="生成排班", bootstyle=OUTLINE, command=self.generate)
self.generate_button.grid(row=2, column=0, pady=5)
# 删除人员配置开关设置按钮
switch_button = ttk.Checkbutton(tab1, variable=self.variable, offvalue=False, onvalue=True,
command=self.delete_switch, bootstyle="round-toggle")
switch_button.grid(row=2, column=1, pady=5)
# 删除人员配置按钮
self.del_button = ttk.Button(tab1, text="删除人员配置", bootstyle="danger-outline", command=self.delete,
state=DISABLED) # 默认不可点击
self.del_button.grid(row=2, column=2, columnspan=2, pady=5)
# 生成的排班表展示文本框
self.show_text = ttk.Text(tab1, wrap=WORD, width=39, height=10, state=DISABLED) # 默认text文本框不可编辑
self.show_text.grid(row=3, column=0, columnspan=4)
self.show_text_style(self.show_text) # 设置展示样式
# # ------------------------------------生成排班------------------------------------#
# # ------------------------------------查看记录------------------------------------#
# 生成的排班记录展示文本框
self.show_record_text = ttk.Text(tab2, wrap=WORD, width=39, height=16, state=DISABLED)
self.show_record_text.pack(expand=1, fill=BOTH)
self.show_text_style(self.show_record_text) # 设置展示样式
# # ------------------------------------查看记录------------------------------------#
# # ------------------------------------当前时间------------------------------------#
# 当前时间标签
self.current_time_label = ttk.Label(self.root, bootstyle=PRIMARY)
self.current_time_label.pack()
# # ------------------------------------当前时间------------------------------------#
def generate(self):
"""
点击【生成排班】按钮时对应的事件
"""
# 获取UI界面输入的内容
people_str = self.name_input.get(1.0, END) # 获取输入的参与排班人员列表
each_group_num_str = self.each_group_num.get() # 获取输入的每日值班人数
group_num_str = self.group_num.get() # 获取输入的值班组数量
people_list = [i for i in (re.split(r"、|\n", people_str)) if i != ''] # 将输入的姓名字符串分割为列表
people_num = len(people_list) # 获取参与排班人员数量
# 有输入记录
if os.path.exists("last_input_record.yaml"):
input_record = yaml_read("last_input_record.yaml") # 读取输入记录Yaml文件
# 获取参与排班人员列表记录
old_people_str = input_record['people']
old_people_list = [i for i in (re.split(r"、|\n", old_people_str)) if i != '']
old_people_num = len(old_people_list)
if people_num != old_people_num: # 若修改了参与排班人员的数量
Messagebox.show_info(title="没整对!", message='参与排班人员发生变化,请【删除人员配置】后再重新排班!')
else: # 未修改参与排班人员的数量
# 输入异常时提示,无异常时执行
self.execute(people_str, people_list, people_num, each_group_num_str, group_num_str)
# 无输入记录
else:
# 输入异常时提示,无异常时执行
self.execute(people_str, people_list, people_num, each_group_num_str, group_num_str)
def delete_switch(self):
"""
点击【删除人员配置开关设置】按钮时对应的事件
"""
switch = self.variable.get() # 获取开关值
if switch is True: # 开关打开时,【删除人员配置】按钮设置为可点击
self.del_button.config(state=NORMAL)
else: # 开关关闭时,【删除人员配置】按钮设置为不可点击
self.del_button.config(state=DISABLED)
def delete(self):
"""
点击【删除人员配置】按钮时对应的事件
"""
msg = Messagebox.okcancel("删除人员配置后将不再根据历史记录生成排班!")
if msg == '确定': # 点击确定后执行
if os.path.exists('random_list.yaml'): # 若文件存在则删除并提示
os.remove('random_list.yaml') # 删除文件
Messagebox.show_info("删除成功!") # 提示
self.variable.set(False) # 【删除人员配置开关】置为关闭
self.del_button.config(state=DISABLED) # 【删除人员配置】按钮设置为不可点击
self.root.focus() # 移除焦点
else: # 若文件不存在则提示
Messagebox.show_info("文件不存在,请勿重复操作!")
def update_time(self):
"""
更新当前时间和本年第几周
"""
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
week_num = int(time.strftime("%W")) + 1
self.current_time_label.config(text=f"{current_time} 第{week_num}周")
self.current_time_label.after(1000, self.update_time)
def execute(self, people_str, people_list, people_num, each_group_num_str, group_num_str):
"""
输入异常时提示,无异常时执行
"""
# 输入异常时提示,无异常时执行
if self.exception_input(people_str, people_num, each_group_num_str, group_num_str):
# 生成排班序号列表并格式化处理
head, body = self.format_content(people_list, people_num, each_group_num_str, group_num_str)
# 保存排班记录并更新输入记录
self.save_and_update_record(head, body, people_str, each_group_num_str, group_num_str)
# 将排班记录插入到展示框中
self.show_scheduling(head, body)
def exception_input(self, people_str, people_num, each_group_num_str, group_num_str):
"""
输入异常时弹窗提醒,无异常时返回True
"""
# 输入异常
if (people_str == '\n') or not self.is_separated_by_pause(people_str): # 未输入参与排班人员姓名或不是以”、“分隔
Messagebox.show_info(title="没整对!", message='请输入参与排班人员姓名,以"、"隔开!')
elif each_group_num_str == '' or group_num_str == '': # 未输入每日值班人数或值班组数量
Messagebox.show_info(title="没整对!", message='请输入每日值班人数和值班组数量!')
elif not each_group_num_str.isdigit() or not group_num_str.isdigit(): # 输入的每日值班人数或值班组数量不是整数
Messagebox.show_info(title="没整对!", message='每日值班人数和值班组数量请输入整数!')
elif each_group_num_str.isdigit() and people_num < int(each_group_num_str): # 输入的参与排班人员数量小于每日值班人数
Messagebox.show_info(title="没整对!", message='参与排班人员数量不能小于每日值班人数')
elif group_num_str.isdigit() and int(group_num_str) > 7: # 输入的值班组数量大于7
Messagebox.show_info(title="没整对!", message='值班组数量不能大于7!')
# 输入无异常
else:
return True
@staticmethod
def is_separated_by_pause(target):
"""
判断字符串是否以顿号“、”分隔
"""
return target.find('、') != -1 and target.find('、') != 0 and target.rfind('、') != len(target) - 1
def format_content(self, people_list, people_num, each_group_num_str, group_num_str):
"""
调用scheduling方法生成排班序号列表并格式化处理展示内容
:return: head--时间和本年第几周;body--星期几和值班人员姓名
"""
each_group_num, group_num = int(each_group_num_str), int(group_num_str) # 将每日值班人数和值班组数量转换为整型
scheduling_list = scheduling(people_num, each_group_num, group_num) # 调用scheduling方法生成排班序号列表
cur_time, week_num, today_week_num = self.get_time_and_week() # 获取当前时间、周数和星期
week_list = [i for i in self.get_week_list(today_week_num - 1, group_num)] # 以今天为起点生成连续的星期列表
watchkeepers = ["、".join([people_list[j - 1] for j in i]) for i in scheduling_list] # 拼接值班人员姓名列表
head = f"{cur_time} 【第{week_num}周】" # 拼接时间和本年第几周
body = "\n".join([f"{i}: {j}" for i, j in zip(week_list, watchkeepers)]) # 将星期列表和值班人员姓名列表组合成字符串
return head, body
@staticmethod
def get_time_and_week():
"""
获取当前时间、周数和星期
:return: cur_time--当前时间;week_num--本年周数;today_week_num--今天星期数
"""
# 当前时间
cur_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 本年的第几周
week_num = int(time.strftime("%W")) + 1
# 今天的日期
today = datetime.date.today()
# 今天的星期数(周一:1,周日:7)
today_week_num = datetime.date.isoweekday(today)
return cur_time, week_num, today_week_num
@staticmethod
def get_week_list(start_idx, days):
"""
星期循环迭代生成器
根据起始下标生成目标天数个星期
:param start_idx: 起始下标
:param days: 目标天数
:return:
"""
week_list = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日']
for i in range(days):
idx = (start_idx + i) % 7 # (起始下标+第几个)%星期列表长度,取余得下标
yield week_list[idx] # 迭代生成器:记住上次返回的位置,下次迭代就从这个位置之后开始
@staticmethod
def save_and_update_record(head, body, people_str, each_group_num_str, group_num_str):
"""
保存排班记录并更新输入记录
"""
# 保存排班记录
yaml_write('scheduling_record.yaml', {head: body})
# 更新输入记录
if os.path.exists('last_input_record.yaml'): # 若已经存在则先清空
yaml_clear('last_input_record.yaml')
data = {'people': people_str, 'each_group_num': each_group_num_str, 'group_num': group_num_str}
yaml_write('last_input_record.yaml', data)
@staticmethod
def show_text_style(show_test):
"""
设置展示框的样式
"""
bold_font = Font(weight="bold") # 定义加粗字体
show_test.tag_add('bold_center', 1.0, '1.end') # 添加名称为align_center的标签
show_test.tag_config('bold_center', font=bold_font, justify=CENTER) # 将bold_center标签设置为加粗并居中对齐
show_test.tag_add('center', 2.0, END) # 添加名称为center的标签
show_test.tag_config('center', justify=CENTER) # 将center标签设置为居中对齐
def show_scheduling(self, head, body):
"""
展示生成的排班表
"""
self.show_text.config(state=NORMAL) # 设置文本框可编辑
self.show_text.delete(1.0, END) # 清空文本框
self.show_text.insert(END, head, 'bold_center') # 将时间和本年第几周插入bold_center标签
self.show_text.insert(END, '\n', 'center') # 插入换行符
self.show_text.insert(END, body, 'center') # 将排班表插入center标签
self.show_text.config(state=DISABLED) #
self.generate_button.config(text='已生成排班', state=DISABLED) # 修改“生成排班”按钮的文字并设置为不可编辑
self.root.focus() # 移除焦点
def show_last_input_record(self):
"""
输入框显示之前的输入记录
:return:
"""
# 打开程序时显示上一次的输入记录,若无则输入框为空
if os.path.exists("last_input_record.yaml"):
input_record = yaml_read("last_input_record.yaml") # 读取输入记录Yaml文件
# 显示输入记录
self.name_input.insert(END, input_record['people'])
self.each_group_num.set(input_record['each_group_num'])
self.group_num.set(input_record['group_num'])
def bind_notebook_event(self):
"""
绑定tab标签页切换事件
"""
self.tabcontrol.bind("<<NotebookTabChanged>>",
lambda event: self.show_scheduling_record(self.tabcontrol, self.tab_idx,
self.show_record_text))
@staticmethod
def show_scheduling_record(tabcontrol, tab_idx, textbox):
"""
展示排班记录
"""
idx = tabcontrol.index(tabcontrol.select()) # 获取当前tab标签页的下标
tab_idx.set(idx) # 将当前tab标签页的下标赋值给tab_idx
# 切换到“查看记录”标签页时读取排班记录文件
if tab_idx.get() == 1:
# 排班记录文件存在则展示
if os.path.exists('scheduling_record.yaml'):
scheduling_record = yaml_read('scheduling_record.yaml')
textbox.config(state=NORMAL) # 设置文本框可编辑
textbox.delete(1.0, END) # 清空文本框
for i, k in enumerate(scheduling_record):
textbox.insert(END, k, 'bold_center') # 将时间和本年第几周插入bold_center标签
textbox.insert(END, '\n', 'center') # 插入换行符
textbox.insert(END, scheduling_record[k], 'center') # 将排班表插入center标签
textbox.insert(END, '\n', 'center') # 插入换行符
textbox.see(END) # 文本框内容超出显示范围时自动滚动到底部
textbox.config(state=DISABLED) # 设置文本框不可编辑
# 排班记录文件不存在时,展示提示
else:
textbox.config(state=NORMAL) # 设置文本框可编辑
textbox.delete(1.0, END) # 清空文本框
textbox.insert(END, "暂无排班记录", 'bold_center') # 将默认提示语插入bold_center标签
textbox.config(state=DISABLED) # 设置文本框不可编辑
if __name__ == "__main__":
root = ttk.Window(
title="排排班", # 窗口标题
themename="solar" # 主题
)
root.place_window_center() # # 设置窗口居中(左上角坐标在屏幕中间?)
root.iconbitmap('icon.ico') # 更改GUI图标
root.resizable(width=False, height=False) # 设置窗口大小不可变
GUI(root) # 展示GUI界面
root.mainloop() # 进入消息循环
运行结果:
三、结语
注意:
1.根据当前的星期为起点生成指定个值班组,应用场景为每周第一个工作日生成一次
2.需要导入在Python制作排班小工具【一】中编写好的Scheduling.py文件
3.对输入框为空、未按要求等异常输入情况添加了提示弹窗
4.使用添加标签tag_add以及tag_config的方式,优化了文本框Text的文本内容展示;同时将文本框设置为不可编辑的状态,避免误修改文本框内容
5.切换到【查看记录】标签页,绑定事件时不能返回方法的结果,故使用到了lambda函数
问题:
1.重新打开软件再次点击【生成排班】按钮会直接生成排班记录,从而产生脏数据
2.输入的“参与排班人员姓名”只能使用“、”分隔,方式比较局限
3.实现主要功能正常使用,UI展示不美观
4.未做不同操作系统、屏幕尺寸等的兼容
5.其他未知的问题,需要根据使用情况解决和优化