【Python】实现一个类似于Thief的摸鱼软件

一 背景说明

        之前用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()

        实际使用效果如下:

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值