欢迎使用Markdown编辑器
产品说明
python写一个windows环境的工资条发放程序
程序界面设计如下:
1、通过路径浏览的方式,选择工资条xlsx所在路径
通过路径浏览的方式,指定名单txt所在路径
2、通过两个文本框指定搜索框的焦点坐标x,y
3、通过两个文本框指定对话输入框的焦点坐标a,b
4、通过两个文本框分别指定操点击间隔t1,单位为毫秒,搜索间隔t2,单位毫秒
5、一个校对按钮和一个发放按钮
6、label文字说明"表格第一列必须是姓名"
程序运行过程如下,运行开始时,先尝试加载工作路径中的配置文件,将之前用过的参数填入相应文本框
点击校对按钮
读取xlsx和txt文件,如果格式不对报错提示正确格式要求
对txt文件按行读取,每行用strip()处理之后不等于"姓名"的字段添加到name列表中
按照工资条表的sheet页循环,对每个sheet页做如下事情
{按行遍历,如果第一个单元格的文本通过strip()函数处理后不为"姓名",则在name列表中查询,如果没有记录在other列表中},注意如果第一个单元格为空值或者不为文本则跳过该行,sheet页便历结束后,弹窗提示陌生姓名,并保存在陌生姓名.txt中
点击发放按钮,要关闭后重新打开读取工资条xlsx,
对sheet页便历,对每个sheet页做如下事情
{用时间和函数获取当前年度和月份,记录在period变量中,格式为"xxxx年xx月"
设置titlerow标题行所在行号,namerow姓名行行号
按行便历,读取第一个单元格文本框的时候用strip()函数处理,如果第一个单元格为空值或者不为文本则跳过该行,如果为"姓名"则将这一行的行号,作为新的titlerow,
如果是name列表中的姓名,则要做这样一件事情{
将该姓名所在行行号,作为新的namerow
同时将titlerow和namerow对应的两行单元格保留其格式间距设置不变,用类似打印的方式,转换成一张图片保存在剪贴板中,然后点击x,y(搜索框的位置),输入namerow第一个的姓名值,等待t2ms后,输入回车,等待t1ms后,点击a,b(对话输入框位置),粘贴该图片,等待20ms,回车,点击a,b(对话输入框位置),输入period+sheet页名字+“请查收”,回车,等待t1ms
}
}
sheet页便历结束,将所有配置文件中的值保存在配置文件.txt中以便于下次调用,我需要知道名单里哪些名字没有发放过,弹窗提示并保存在 未发放列表.txt中
源代码
import os
import re
import sys
import tkinter as tk
from tkinter import filedialog, messagebox
import openpyxl
import pyautogui
import time
import pyperclip
from PIL import ImageGrab, Image
import configparser
from PIL import Image, ImageDraw, ImageFont
from openpyxl.utils import get_column_letter
from openpyxl.utils.cell import range_boundaries
from openpyxl.cell.cell import MergedCell
import win32clipboard
from io import BytesIO
def get_merged_value(sheet, cell):
"""获取合并单元格的值"""
for merged_range in sheet.merged_cells.ranges:
if cell.coordinate in merged_range:
min_col, min_row, max_col, max_row = range_boundaries(merged_range.coord)
if min_col!=None:
cell_value = sheet.cell(row=min_row, column=min_col).value
return cell_value if cell_value is not None else " "
return cell.value if cell.value is not None else " "
def get_text_size(font, text):
"""通用字体尺寸计算方法(兼容Pillow>=10.0.0)"""
if hasattr(font, 'getsize'):
return font.getsize(text)
else:
bbox = font.getbbox(text)
return (bbox[2] - bbox[0], bbox[3] - bbox[1])
def draw_centered_text(draw, font, text, x, y, box_width, box_height, fill_color):
"""自动居中的文字绘制方法"""
text_width, text_height = get_text_size(font, text)
tx = x + (box_width - text_width) // 2
ty = y + (box_height - text_height) // 2
draw.text((tx, ty), text, font=font, fill=fill_color)
def copy_table_to_clipboard(sheet, title_row, data_row):
"""将指定行生成表格图片并复制到剪贴板"""
FONT_SIZE = 14
FONT_NAME = "simhei.ttf"
ROW_HEIGHT = 40
CELL_PADDING = 10
COLUMN_WIDTH_RATIO = 9
MIN_COLUMN_WIDTH = 80
LINE_COLOR = (0, 0, 0)
HEADER_BG_COLOR = (240, 240, 240)
TEXT_COLOR = (0, 0, 0)
try:
font = ImageFont.truetype(FONT_NAME, FONT_SIZE)
except IOError:
messagebox.showerror("错误", f"找不到字体文件{FONT_NAME}")
return
columns = []
for col in range(1, sheet.max_column + 1):
title = str(get_merged_value(sheet, sheet.cell(title_row, col)))
data = str(get_merged_value(sheet, sheet.cell(data_row, col)))
title_width, _ = get_text_size(font, title)
data_width, _ = get_text_size(font, data)
excel_width = sheet.column_dimensions[get_column_letter(col)].width
converted_width = max(
int(excel_width * COLUMN_WIDTH_RATIO) if excel_width else 0,
title_width + 2 * CELL_PADDING,
data_width + 2 * CELL_PADDING,
MIN_COLUMN_WIDTH
)
columns.append({
"title": title,
"data": data,
"width": converted_width
})
merged_ranges = [range_boundaries(r.coord) for r in sheet.merged_cells.ranges]
final_columns = []
skip_cols = 0
for idx in range(len(columns)):
if skip_cols > 0:
skip_cols -= 1
continue
merged = False
for min_c, min_r, max_c, max_r in merged_ranges:
if min_r <= title_row <= max_r and min_c == idx + 1:
span = max_c - min_c + 1
if span > 1:
merged_width = sum(c["width"] for c in columns[idx:idx + span])
merged_title = columns[idx]["title"]
merged_data = columns[idx]["data"]
final_columns.append({
"title": merged_title,
"data": merged_data,
"width": merged_width
})
skip_cols = span - 1
merged = True
break
if not merged:
final_columns.append(columns[idx])
total_width = sum(c["width"] for c in final_columns) + 2 * CELL_PADDING
total_height = 2 * ROW_HEIGHT + 2 * CELL_PADDING
img = Image.new('RGB', (total_width, total_height), (255, 255, 255))
draw = ImageDraw.Draw(img)
x = CELL_PADDING
for col in final_columns:
draw.rectangle(
[x, CELL_PADDING, x + col["width"], CELL_PADDING + ROW_HEIGHT],
fill=HEADER_BG_COLOR,
outline=LINE_COLOR
)
draw_centered_text(
draw, font, col["title"],
x, CELL_PADDING,
col["width"], ROW_HEIGHT,
TEXT_COLOR
)
draw.rectangle(
[x, CELL_PADDING + ROW_HEIGHT, x + col["width"], CELL_PADDING + 2 * ROW_HEIGHT],
outline=LINE_COLOR
)
draw_centered_text(
draw, font, col["data"],
x, CELL_PADDING + ROW_HEIGHT,
col["width"], ROW_HEIGHT,
TEXT_COLOR
)
x += col["width"]
output = BytesIO()
img.save(output, format="BMP")
data = output.getvalue()[14:]
output.close()
win32clipboard.OpenClipboard()
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
win32clipboard.CloseClipboard()
class SalarySenderApp:
def __init__(self, master):
self.master = master
master.title("工资条发放程序")
master.geometry("600x600")
self.config_file = "config.ini"
self.config = configparser.ConfigParser()
self.names = []
self.sent_names = []
self.create_widgets()
self.load_config()
def create_widgets(self):
tk.Label(self.master, text="工资表路径:").grid(row=0, column=0)
self.excel_path = tk.Entry(self.master, width=40)
self.excel_path.grid(row=0, column=1)
tk.Button(self.master, text="浏览", command=self.select_excel).grid(row=0, column=2)
tk.Label(self.master, text="名单路径:").grid(row=1, column=0)
self.txt_path = tk.Entry(self.master, width=40)
self.txt_path.grid(row=1, column=1)
tk.Button(self.master, text="浏览", command=self.select_txt).grid(row=1, column=2)
tk.Label(self.master, text="搜索框坐标 (x,y):").grid(row=2, column=0)
self.x_entry = tk.Entry(self.master, width=8)
self.x_entry.grid(row=2, column=1)
self.y_entry = tk.Entry(self.master, width=8)
self.y_entry.grid(row=2, column=2)
tk.Label(self.master, text="输入框坐标 (a,b):").grid(row=3, column=0)
self.a_entry = tk.Entry(self.master, width=8)
self.a_entry.grid(row=3, column=1)
self.b_entry = tk.Entry(self.master, width=8)
self.b_entry.grid(row=3, column=2)
tk.Label(self.master, text="点击间隔 t1(ms):").grid(row=4, column=0)
self.t1_entry = tk.Entry(self.master)
self.t1_entry.grid(row=4, column=1)
tk.Label(self.master, text="搜索间隔 t2(ms):").grid(row=4, column=2)
self.t2_entry = tk.Entry(self.master)
self.t2_entry.grid(row=4, column=3)
tk.Button(self.master, text="校对", command=self.verify).grid(row=5, column=0)
tk.Button(self.master, text="发放", command=self.send).grid(row=5, column=1)
tk.Label(self.master, text="表格第一列必须是姓名", fg="red").grid(row=6, column=0, columnspan=3)
tk.Label(self.master, text="群发消息").grid(row=7, column=0)
self.message_entry = tk.Entry(self.master)
self.message_entry.grid(row=8, column=0, rowspan=6, columnspan=6)
def select_excel(self):
path = filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx")])
self.excel_path.delete(0, tk.END)
self.excel_path.insert(0, path)
def select_txt(self):
path = filedialog.askopenfilename(filetypes=[("文本文件", "*.txt")])
self.txt_path.delete(0, tk.END)
self.txt_path.insert(0, path)
def load_config(self):
if os.path.exists(self.config_file):
self.config.read(self.config_file)
self.excel_path.insert(0, self.config.get('DEFAULT', 'excel_path', fallback=''))
self.txt_path.insert(0, self.config.get('DEFAULT', 'txt_path', fallback=''))
self.x_entry.insert(0, self.config.get('DEFAULT', 'x', fallback=''))
self.y_entry.insert(0, self.config.get('DEFAULT', 'y', fallback=''))
self.a_entry.insert(0, self.config.get('DEFAULT', 'a', fallback=''))
self.b_entry.insert(0, self.config.get('DEFAULT', 'b', fallback=''))
self.t1_entry.insert(0, self.config.get('DEFAULT', 't1', fallback='500'))
self.t2_entry.insert(0, self.config.get('DEFAULT', 't2', fallback='1000'))
self.message_entry.insert(0, self.config.get('DEFAULT', 'm', fallback='1000'))
def save_config(self):
self.config['DEFAULT'] = {
'excel_path': self.excel_path.get(),
'txt_path': self.txt_path.get(),
'x': self.x_entry.get(),
'y': self.y_entry.get(),
'a': self.a_entry.get(),
'b': self.b_entry.get(),
't1': self.t1_entry.get(),
't2': self.t2_entry.get(),
'm':self.message_entry.get()
}
with open(self.config_file, 'w') as f:
self.config.write(f)
def verify(self):
try:
if not os.path.exists(self.txt_path.get()):
messagebox.showerror("错误", "名单文件不存在")
return
with open(self.txt_path.get(), 'r', encoding='utf-8') as f:
self.names = [re.sub(r"\s+", "",line) for line in f if re.sub(r"\s+", "",line) != "姓名"]
if not self.names:
messagebox.showerror("错误", "名单文件内容为空或格式错误")
return
wb = openpyxl.load_workbook(self.excel_path.get())
unknown = []
for sheet in wb:
for row in sheet.iter_rows(min_row=1):
first_cell = row[0].value
if not first_cell or not isinstance(first_cell, str):
continue
cleaned_name = re.sub(r"\s+", "",first_cell)
if cleaned_name == "姓名":
continue
if cleaned_name not in self.names:
unknown.append(cleaned_name)
if unknown:
with open("陌生姓名.txt", 'w', encoding='utf-8') as f:
f.write('\n'.join(unknown))
messagebox.showwarning("发现陌生姓名", f"发现{len(unknown)}个陌生姓名,已保存到文件")
else:
messagebox.showinfo("校验通过", "所有姓名均符合名单")
except Exception as e:
messagebox.showerror("错误", f"校验过程中出错: {str(e)}")
def send(self):
try:
if not all([self.x_entry.get(), self.y_entry.get(),
self.a_entry.get(), self.b_entry.get()]):
messagebox.showerror("错误", "请填写所有坐标参数")
return
x, y = int(self.x_entry.get()), int(self.y_entry.get())
a, b = int(self.a_entry.get()), int(self.b_entry.get())
t1 = int(self.t1_entry.get()) / 1000
t2 = int(self.t2_entry.get()) / 1000
m = self.message_entry.get()
if not self.names:
with open(self.txt_path.get(), 'r', encoding='utf-8') as f:
self.names = [re.sub(r"\s+", "",line) for line in f if re.sub(r"\s+", "",line) != "姓名"]
if not self.names:
messagebox.showerror("错误", "无法读取有效名单")
return
wb = openpyxl.load_workbook(self.excel_path.get())
period = time.strftime("%Y年%m月")
self.sent_names = []
for sheet in wb:
titlerow = None
namerow = None
for idx, row in enumerate(sheet.iter_rows(), 1):
first_cell = row[0].value
if not first_cell or not isinstance(first_cell, str):
continue
cleaned_name = re.sub(r"\s+", "",first_cell)
if cleaned_name == "姓名":
titlerow = idx
elif cleaned_name in self.names:
namerow = idx
try:
pyautogui.click(x, y)
pyperclip.copy(cleaned_name)
pyautogui.hotkey('ctrl', 'v')
time.sleep(0.1)
time.sleep(t2)
pyautogui.press('enter')
time.sleep(t1)
copy_table_to_clipboard(sheet, titlerow, namerow)
pyautogui.click(a, b)
time.sleep(t1)
pyautogui.hotkey('ctrl', 'v')
time.sleep(0.1)
time.sleep(0.02)
pyautogui.press('enter')
time.sleep(t2 / 2)
message = f"{period}{sheet.title}请查收.{m}"
pyperclip.copy(message)
pyautogui.click(a, b)
pyautogui.hotkey('ctrl', 'v')
time.sleep(0.1)
pyautogui.press('enter')
time.sleep(t2/2)
self.sent_names.append(cleaned_name)
except Exception as e:
messagebox.showwarning("操作中断", f"发送给 {cleaned_name} 时出错: {str(e)}")
continue
self.save_config()
unsent = list(set(self.names) - set(self.sent_names))
if unsent:
with open("未发放列表.txt", 'w', encoding='utf-8') as f:
f.write('\n'.join(unsent))
messagebox.showinfo("完成", f"发放完成,{len(unsent)}人未发放")
else:
messagebox.showinfo("完成", "所有人员工资条已成功发放")
except Exception as e:
messagebox.showerror("错误", f"发放过程中出错: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = SalarySenderApp(root)
root.mainloop()
END