一 背景说明
之前用Thief摸鱼(Thief官网),觉得挺好用。对于其最基本的TXT摸鱼,准备在Python中扩展一下功能,使其能够通过爬虫,支持爬取热门小说网站的内容。
软件已经开源到:MoFish软件开源地址
功能是,可以悄咪咪地看本地的txt电子书或者爬取有效电子书网站(例如:纵横中文网)的电子书资源,提供设置界面以便修改配置文件。
二 设计实现
工具包含以下几个模块类:
【1】配置参数类:支持对配置Json文件进行读写;
【2】界面操作类:支持绑定键盘/鼠标按键进行上下翻页、老板键等功能;
【3】最小化托盘类:支持程序最小化到系统托盘,实现快捷操作、配置以及退出;
三 配置参数类
配置参数类 cfgJsonRW 可以在初始化的时候读取配置文件信息,并提供单独修改当前页数的方法 cfgJsonWritePage 和修改所有参数的方法 cfgJsonWriteAll 。代码如下:
import json
class cfgJsonRW(object):
def __init__(self):
with open("cfg.json") as json_file:
cfg = json.load(json_file)
self.file_name = cfg["file_name"] # 文件名
self.coding_mode = cfg["coding_mode"] # 编码格式
self.char_num = cfg["char_num"] # 每页字数
self.is_prog = cfg["is_prog"] # 是否显示进度
self.web_num = cfg["web_num"] # 一次性读取网页自动加载的章节(不宜太多,会卡死)
self.key_boss = cfg["key_boss"] # 老板键
self.key_up = cfg["key_up"] # 上翻键
self.key_down = cfg["key_down"] # 下翻键
self.label_width = cfg["label_width"] # 文本框宽度
self.font_color = cfg["font_color"] # 文本颜色
self.font_size = cfg["font_size"] # 文本大小
self.page_now = cfg["page_now"] # 记录当前页并写json,方便下一次进程序时直接跳转
#打印配置信息
cfg_info1 = "【1 文件读取】\n文件名:%s\n编码方式:%s\n每页字数:%d\n是否显示进度:%d\n一次性读取章节数:%d\n" % (str(self.file_name), self.coding_mode, self.char_num, self.is_prog, self.web_num)
cfg_info2 = "【2 按键设置】\n老板键:%s\n上翻键:%s\n下翻键:%s\n" % (str(self.key_boss), str(self.key_up), str(self.key_down))
cfg_info3 = "【3 文本样式】\n文本框宽度:%d\n文本颜色:%s\n文本大小:%d\n" % (self.label_width, self.font_color, self.font_size)
cfg_info4 = "【4 其他配置】\n当前页:%d\n" % self.page_now
self.cfg_info = "----【配置信息】----\n" + cfg_info1 + cfg_info2 + cfg_info3 + cfg_info4
print(self.cfg_info)
def cfgJsonWritePage(self, now_page):
self.page_now = now_page
with open("cfg.json") as json_file:
cfg = json.load(json_file)
cfg["page_now"] = self.page_now
with open("cfg.json", "w") as json_file:
json.dump(cfg, json_file)
def cfgJsonWriteAll(self, para):
str = {}
str["file_name"] = para["file_name"].get()
str["coding_mode"] = para["coding_mode"].get()
str["char_num"] = para["char_num"].get()
str["is_prog"] = para["is_prog"].get()
str["web_num"] = para["web_num"].get()
str["key_boss"] = para["key_boss"].get()
str["key_up"] = para["key_up"].get()
str["key_down"] = para["key_down"].get()
str["label_width"] = para["label_width"].get()
str["font_color"] = para["font_color"].get()
str["font_size"] = para["font_size"].get()
str["page_now"] = para["page_now"].get()
with open("cfg.json") as json_file:
cfg = json.load(json_file)
if len(str["file_name"]) != 0:
cfg["file_name"] = str["file_name"]
if len(str["coding_mode"]) != 0:
cfg["coding_mode"] = str["coding_mode"]
if len(str["char_num"]) != 0:
cfg["char_num"] = int(str["char_num"])
if len(str["is_prog"]) != 0:
cfg["is_prog"] = int(str["is_prog"])
if len(str["web_num"]) != 0:
cfg["web_num"] = int(str["web_num"])
if len(str["key_boss"]) != 0:
cfg["key_boss"] = str["key_boss"]
if len(str["key_up"]) != 0:
cfg["key_up"] = str["key_up"]
if len(str["key_down"]) != 0:
cfg["key_down"] = str["key_down"]
if len(str["label_width"]) != 0:
cfg["label_width"] = int(str["label_width"])
if len(str["font_color"]) != 0:
cfg["font_color"] = str["font_color"]
if len(str["font_size"]) != 0:
cfg["font_size"] = int(str["font_size"])
if len(str["page_now"]) != 0:
cfg["page_now"] = int(str["page_now"])
with open("cfg.json", "w") as json_file:
json.dump(cfg, json_file)
配置文件 cfg.json 结构如下:
{"file_name": "test.txt", "coding_mode": "utf8", "char_num": 200, "is_prog": 1, "web_num": 20, "key_boss": "<F7>", "key_up": "<Up>", "key_down": "<Down>", "label_width": 900, "font_color": "black", "font_size": 9, "page_now": 63}
四 界面操作类
界面操作类 winExec 支持在初始化的时候绑定鼠标和键盘,鼠标拖动界面、鼠标双击退出、老板键、上下翻页键等操作均在此类中完成。代码如下:
class winExec(object):
def __init__(self, win_main, win_cfg, win_content, all_page):
self.root = win_main
self.cfg = win_cfg
self.con = win_content
self.root.bind("<Button-1>", self.mouseDown) # 按下鼠标左键绑定MouseDown函数
self.root.bind("<B1-Motion>", self.mouseMove) # 鼠标左键按住拖曳事件,3个函数都不要忘记函数写参数
self.root.bind("<Double-Button-1>", self.commExit) # 双击鼠标左键,关闭窗体
self.root.bind("<Escape>", self.commExit) # 退出
self.root.bind(self.cfg.key_boss, self.keyBoss) #按下键盘隐藏窗口
self.root.bind(self.cfg.key_up, self.keyUp) #向上翻页
self.root.bind(self.cfg.key_down, self.keyDown) #向下翻页
self.boss_flag = 0
self.now_page = self.cfg.page_now
self.all_page = all_page
def mouseDown(self, event): #鼠标按下
self.mousX, self.mousY = event.x, event.y # 获取鼠标相对于窗体左上角的X/Y坐标
def mouseMove(self, event): #鼠标移动
self.root.geometry(f'+{event.x_root - self.mousX}+{event.y_root - self.mousY}') # 窗体移动代码(event.x_root/event.y_root为窗体相对于屏幕左上角的X/Y)
def commExit(self, event): #通用退出(双击或者Esc)
self.root.destroy()
def keyBoss(self, event): #老板键
if self.boss_flag == 0:
self.root.attributes("-alpha", 0)
self.boss_flag = 1
else:
self.root.attributes("-alpha", 1)
self.boss_flag = 0
def keyUp(self, event): #向上翻页
self.now_page = self.now_page - 1
if self.cfg.is_prog == 0:
info.set(self.con[self.now_page * self.cfg.char_num : (self.now_page + 1) * self.cfg.char_num])
else:
info.set(self.con[self.now_page * self.cfg.char_num : (self.now_page + 1) * self.cfg.char_num] + " (%d/%d)" % (self.now_page, self.all_page))
self.cfg.cfgJsonWritePage(self.now_page) #当前页写json文件存储
def keyDown(self, event): #向下翻页
self.now_page = self.now_page + 1
if self.cfg.is_prog == 0:
info.set(self.con[self.now_page * self.cfg.char_num : (self.now_page + 1) * self.cfg.char_num])
else:
info.set(self.con[self.now_page * self.cfg.char_num : (self.now_page + 1) * self.cfg.char_num] + " (%d/%d)" % (self.now_page, self.all_page))
self.cfg.cfgJsonWritePage(self.now_page) #当前页写json文件存储
五 最小化托盘界面类
最小化托盘类 sysTray 支持程序最小化托盘,通过托盘图标,实现快捷操作(老板键、上下翻页)、配置(通过GUI界面与Json文件进行交互)以及退出。代码如下:
import threading
import pystray # 最小化到托盘
from PIL import Image # 导入 PIL 库中的 Image 模块
import tkinter # 绘制操作界面
from tkinter import messagebox
from tkinter.filedialog import askopenfilename
class sysTray(object):
def __init__(self, win_main, win_cfg, win_exec): #主界面/配置类/操作类
self.root = win_main
self.cfg = win_cfg
self.exec = win_exec
self.tray = {}
def createSysTray(self): # 使用 Pystray 创建系统托盘图标
menu = (
pystray.MenuItem('老板键', self.trayKeyBoss),
pystray.MenuItem('上一页', self.trayKeyUp),
pystray.MenuItem('下一页', self.trayKeyDown),
pystray.Menu.SEPARATOR, # 在系统托盘菜单中添加分隔线
pystray.MenuItem('配置', self.trayConfig),
pystray.Menu.SEPARATOR, # 在系统托盘菜单中添加分隔线
pystray.MenuItem('退出', self.trayExit)
)
image = Image.open("mofish.ico")
self.icon = pystray.Icon("name", image, "mofish", menu)
threading.Thread(target=self.icon.run, daemon=True).start()
def trayKeyBoss(self):
self.exec.keyBoss(self.cfg.key_boss)
def trayKeyUp(self):
self.exec.keyUp(self.cfg.key_up)
def trayKeyDown(self):
self.exec.keyDown(self.cfg.key_down)
def trayExit(self, icon: pystray.Icon):
icon.stop() # 停止 Pystray 的事件循环
self.root.destroy() # 销毁应用程序的主窗口和所有活动
def trayConfig(self):
self.winTray = tkinter.Toplevel(self.root)
self.winTray.title('配置信息')
frame1 = tkinter.Frame(self.winTray)
frame1.pack()
frame2 = tkinter.Frame(self.winTray)
frame2.pack()
frame3 = tkinter.Frame(self.winTray)
frame3.pack()
frame4 = tkinter.Frame(self.winTray)
frame4.pack()
l1 = tkinter.Label(frame1, text='文件名', fg='blue').grid(row=0, column=0)
self.tray["file_name"] = tkinter.StringVar()
e1 = tkinter.Entry(frame1, width=40, textvariable=self.tray["file_name"]).grid(row=0, column=1)
b1 = tkinter.Button(frame1, text="路径",fg='white', bg='dodgerblue', command=self.selectPath).grid(row=0, column=2)
l2 = tkinter.Label(frame1, text='编码格式', fg='blue').grid(row=0, column=3)
self.tray["coding_mode"] = tkinter.StringVar()
e2 = tkinter.Entry(frame1, width=5, textvariable=self.tray["coding_mode"]).grid(row=0, column=4)
l3 = tkinter.Label(frame2, text='当前页数', fg='blue').grid(row=0, column=0)
self.tray["page_now"] = tkinter.StringVar()
e3 = tkinter.Entry(frame2, textvariable=self.tray["page_now"]).grid(row=0, column=1)
l4 = tkinter.Label(frame2, text='每页字数', fg='blue').grid(row=0, column=2)
self.tray["char_num"] = tkinter.StringVar()
e4 = tkinter.Entry(frame2, textvariable=self.tray["char_num"]).grid(row=0, column=3)
l5 = tkinter.Label(frame2, text='显示进度', fg='blue').grid(row=1, column=0)
self.tray["is_prog"] = tkinter.StringVar()
e5 = tkinter.Entry(frame2, textvariable=self.tray["is_prog"]).grid(row=1, column=1)
l6 = tkinter.Label(frame2, text='加载网页', fg='blue').grid(row=1, column=2)
self.tray["web_num"] = tkinter.StringVar()
e6 = tkinter.Entry(frame2, textvariable=self.tray["web_num"]).grid(row=1, column=3)
l7 = tkinter.Label(frame3, text='老板键', fg='blue').grid(row=0, column=0)
self.tray["key_boss"] = tkinter.StringVar()
e7 = tkinter.Entry(frame3, textvariable=self.tray["key_boss"]).grid(row=0, column=1)
l8 = tkinter.Label(frame3, text='上翻键', fg='blue').grid(row=1, column=0)
self.tray["key_up"] = tkinter.StringVar()
e8 = tkinter.Entry(frame3, textvariable=self.tray["key_up"]).grid(row=1, column=1)
l9 = tkinter.Label(frame3, text='下翻键', fg='blue').grid(row=2, column=0)
self.tray["key_down"] = tkinter.StringVar()
e9 = tkinter.Entry(frame3, textvariable=self.tray["key_down"]).grid(row=2, column=1)
l10 = tkinter.Label(frame3, text='文本宽度', fg='blue').grid(row=0, column=2)
self.tray["label_width"] = tkinter.StringVar()
e10 = tkinter.Entry(frame3, textvariable=self.tray["label_width"]).grid(row=0, column=3)
l11 = tkinter.Label(frame3, text='字体颜色', fg='blue').grid(row=1, column=2)
self.tray["font_color"] = tkinter.StringVar()
e11 = tkinter.Entry(frame3, textvariable=self.tray["font_color"]).grid(row=1, column=3)
l12 = tkinter.Label(frame3, text='字体大小', fg='blue').grid(row=2, column=2)
self.tray["font_size"] = tkinter.StringVar()
e12 = tkinter.Entry(frame3, textvariable=self.tray["font_size"]).grid(row=2, column=3)
b2 = tkinter.Button(frame4, text="保存",fg='white', bg='dodgerblue', width=15, command=lambda:self.saveCfg(self.tray))
b2.pack()
self.loadCfg()
def selectPath(self): #路径选择
self.tray["file_name"].set(askopenfilename())
def loadCfg(self): #加载配置
self.tray["file_name"].set(self.cfg.file_name) # 文件名
self.tray["coding_mode"].set(self.cfg.coding_mode) # 编码格式
self.tray["page_now"].set(self.cfg.page_now) # 当前页数
self.tray["char_num"].set(self.cfg.char_num) # 每页字数
self.tray["is_prog"].set(self.cfg.is_prog) # 是否显示进度
self.tray["web_num"].set(self.cfg.web_num) # 自动加载网页数
self.tray["key_boss"].set(self.cfg.key_boss) # 老板键
self.tray["key_up"].set(self.cfg.key_up) # 上翻键
self.tray["key_down"].set(self.cfg.key_down) # 下翻键
self.tray["label_width"].set(self.cfg.label_width) # 文本宽度
self.tray["font_color"].set(self.cfg.font_color) # 字体颜色
self.tray["font_size"].set(self.cfg.font_size) # 字体大小
def saveCfg(self, para): #保存配置
self.cfg.cfgJsonWriteAll(para)
if tkinter.messagebox.askokcancel('提示', '配置保存成功,重启才能生效!\n是否需要重启?'):
self.trayExit(self.icon)
最小化托盘以及配置界面效果如下:
六 主程序
主程序里创建主界面,并配合上面的几个模块,实现隐蔽看本地TXT功能(如果路径中包含“http”关键字,则认为是网页,自动爬取网页内容并显示)。代码如下:
import tkinter # 绘制操作界面
import requests
from bs4 import BeautifulSoup
from config import cfgJsonRW
from tray import sysTray
win = tkinter.Tk()
win_cfg = cfgJsonRW()
if "http" in win_cfg.file_name:
win_content, all_page = readWeb(win_cfg.file_name, win_cfg.char_num, win_cfg.web_num) # 读取网页(输入参数:文件名/每页字数/一次性加载网页章节数)
else:
win_content, all_page = readFile(win_cfg.file_name, win_cfg.coding_mode, win_cfg.char_num) # 读取文件(输入参数:文件名/编码方式/每页字数)
win_exec = winExec(win, win_cfg, win_content, all_page)
win.overrideredirect(True) # 实现隐藏了整个标题栏的窗口
win.attributes("-transparentcolor", 'snow') #将snow颜色设置为透明色,可以替换不同的颜色
win.attributes("-topmost", True) # 将窗口保持最前
info = tkinter.StringVar()
info.set("Welcome to MoFish !\n" + win_cfg.cfg_info)
text = tkinter.Label(win, anchor="nw", justify='left', bg='snow', wraplength=win_cfg.label_width, fg=win_cfg.font_color, font=("", win_cfg.font_size), textvariable=info).pack() #把已经变成透明色的snow色设置为背景
sysTray(win, win_cfg, win_exec).createSysTray() # 创建最小化托盘对象
win.mainloop()
实际使用效果如下: