PyQt 项目基本结构

PyQt 项目结构

  • 项目简介
  • 程序目录结构
  • config包
    • setting.py
    • 其他的配置文件
  • deorator包
    • lazy_porperty.py
  • utils 包
    • logger.py
    • thread.py
    • resourceLoad.py
    • fileutils.py(项目示例)
  • frame包
    • dialog.py
    • bar.py
  • activity 包
    • baseActivity.py
  • main.py
  • requirement.txt
  • 结语

项目简介

tags:[pyqt, 文件管理器, 文件拖拽, 窗口拉伸]
类文件管理器

程序目录结构

--
  |--- activity  # 活动窗口
  |--- frame     # 独立控件
  |--- config    # 配置信息
  |--- utils     # 工具包
  |--- deorator  # 装饰器
  |--- resource  # 静态资源
-main.py
-requirement.txt

以下秩序按照包相干程度排序。

config包

config包中保存在部署中可能发生改变的基本配置,如数据库、保存路径、常用字符串及其他服务的命令参数等。

setting.py

以下仅是示例文件,并不代表实际生产文件

import os

dir_path = os.path.abspath(os.path.dirname(__file__))
ROOT_PATH = os.path.abspath(os.path.join(dir_path, os.path.pardir))

DOC_PATH = os.path.abspath(os.path.join(ROOT_PATH, "doc"))

# 存放解压后的chm文件
temp_path = "temp"
# 存放chm文件
chm_path = "document"
# 存放高亮的临时文件
html_height_line = "html"


# crate dir

def check(path):
    if not os.path.exists(path):
        os.makedirs(path)
    return path


# document path

def get_chm_path():
    return check(os.path.join(ROOT_PATH, "chm"))

此文件保存了项目用到的基本路径。,可以让我在代码中多次使用,如在后期修改时,可以让我在其他代码完整无需改动的基础下快速完成需求。

其他的配置文件

如果项目中,存在大量的配置文件,可新建多个不同类别的配置文件。

deorator包

这个包保存常用的装饰器,如lazy,也可对数据库进行log封装

lazy_porperty.py

lazy 加载,有关lazy可参考其他博客说明,下例文件使用的是Django2.3中的lazy源码

from functools import total_ordering, wraps


# author by Django

class Promise:
    """
    在惰性函数的闭包中创建的代理类的基类。
    它用于识别代码中的承诺。
    """
    pass

    def __cast(self):
        pass


def _lazy_proxy_unpickle(func, args, kwargs, *result_classes):
    return lazy(func, *result_classes)(*args, **kwargs)


def lazy(func, *result_classes):
    """
    将任何可调用项转换为延迟加载的可调用项。结果类或类型
    是必需的——至少需要一个,以便自动强制
    延迟计算代码被触发。如果结果不存在则初始化
    """

    @total_ordering
    class Proxy(Promise):
        """
        封装函数调用并充当方法的代理
        调用该函数的结果。函数没有求值
        直到结果上的一个方法被调用。
        """
        __prepared = False

        def __init__(self, args, kw):
            self.__args = args
            self.__kw = kw
            if not self.__prepared:
                self.__prepare_class__()
            self.__prepared = True

        def __reduce__(self):
            return (
                _lazy_proxy_unpickle,
                (func, self.__args, self.__kw) + result_classes
            )

        def __repr__(self):
            return repr(self.__cast())

        @classmethod
        def __prepare_class__(cls):
            for resultclass in result_classes:
                for type_ in resultclass.mro():
                    for method_name in type_.__dict__:
                        # 所有 __promise__ 返回相同的包装器方法
                        if hasattr(cls, method_name):
                            continue
                        math = cls.__promise__(method_name)
                        setattr(cls, method_name, math)
            cls._delegate_bytes = bytes in result_classes
            cls._delegate_text = str in result_classes
            assert not (cls._delegate_bytes and cls._delegate_text), (
                "不能同时使用字节和文本返回类型调用lazy()")
            if cls._delegate_text:
                cls.__str__ = cls.__text_cast
            elif cls._delegate_bytes:
                cls.__bytes__ = cls.__bytes_cast

        @classmethod
        def __promise__(cls, method_name):
            def __wrapper(self, *args, **kw):
                res = func(*self.__args, **self.__kw)
                return getattr(res, method_name)(*args, **kw)

            return __wrapper

        def __text_cast(self):
            return func(*self.__args, **self.__kw)

        def __bytes_cast(self):
            return bytes(func(*self.__args, **self.__kw))

        def __bytes_cast_encoded(self):
            return func(*self.__args, **self.__kw).encode()

        def __cast(self):
            if self._delegate_bytes:
                return self.__bytes_cast()
            elif self._delegate_text:
                return self.__text_cast()
            else:
                return func(*self.__args, **self.__kw)

        def __str__(self):
            # object defines __str__(), so __prepare_class__() won't overload
            # a __str__() method from the proxied class.
            return str(self.__cast())

        def __eq__(self, other):
            if isinstance(other, Promise):
                other = other.__cast()
            return self.__cast() == other

        def __lt__(self, other):
            if isinstance(other, Promise):
                other = other.__cast()
            return self.__cast() < other

        def __hash__(self):
            return hash(self.__cast())

        def __mod__(self, rhs):
            if self._delegate_text:
                return str(self) % rhs
            return self.__cast() % rhs

        def __deepcopy__(self, memo):
            # 该类的实例实际上是不可变的。它只是一个函数集合。
            memo[id(self)] = self
            return self

    @wraps(func)
    def __wrapper__(*args, **kw):
        # 创建代理对象
        return Proxy(args, kw)

    return __wrapper__


class LazyProperty(object):
    """
    lazy 加载装饰器
    此方法不允许被嵌套使用:
    @LazyProperty
    def test_function():
        do something
    """
    def __init__(self, fun):
        self.fun = fun

    def __get__(self, instance, owner):
        if instance is None:
            return self
        value = self.fun(instance)
        setattr(instance, self.fun.__name__, value)
        return value

utils 包

工具包中保存常用的工具类如:字符串处理,文件处理,线程操作及日志等。

logger.py

python logging 日志包并不能直接满足实际需求,所以需要对logger进行重构(示例文件中,包含setting中日志的保存路径setting.get_log_path(), 除此之外可独立运行)

# coding=utf-8
"""
    create by pymu on 2020/4/29
    package: .logger.py
    project: status_document_0.1
"""
import datetime
import logging
import os
import re

from config import setting

description = "日志信息"
LOG_PATH = setting.get_log_path()


class Logger(logging.Logger):
    """

    """

    def __init__(self, name='system', level=logging.INFO):
        super().__init__(name, level)
        self.level = level
        self.name = name
        self.__set_log_handler()

    def __set_log_handler(self):
        """
        日志输出格式及输出等级,默认为INFO
        :return:
        """
        main_handler = MyLoggerHandler(filename=self.name, when='D', backup_count=5,
                                       encoding="utf-8")
        warn_handler = MyLoggerHandler(filename='警告日志', when='D',
                                       backup_count=5, encoding="utf-8")
        error_handler = MyLoggerHandler(filename='异常日志', when='D',
                                        backup_count=35, encoding="utf-8")
        # 设置日志格式
        formatter = logging.Formatter("%(asctime)s - %(levelname)s: %(message)s")
        _formatter = logging.Formatter("\n%(asctime)s - %(levelname)s: %(message)s")

        bug_filter = logging.Filter()
        bug_filter.filter = lambda record: record.levelno == logging.ERROR  # 设置过滤等级
        error_handler.addFilter(bug_filter)
        error_handler.setFormatter(_formatter)
        self.addHandler(error_handler)

        bug_filter = logging.Filter()
        bug_filter.filter = lambda record: record.levelno == logging.WARNING  # 设置过滤等级
        warn_handler.addFilter(bug_filter)
        warn_handler.setFormatter(_formatter)
        self.addHandler(warn_handler)

        bug_filter = logging.Filter()
        bug_filter.filter = lambda record: record.levelno < logging.WARNING  # 设置过滤等级
        main_handler.addFilter(bug_filter)
        main_handler.addFilter(bug_filter)
        main_handler.setFormatter(formatter)
        self.main_handler = main_handler
        self.addHandler(main_handler)

    def reset_name(self, name):
        """
        重新设置日志文件名
        :param name:
        :return:
        """
        self.name = name
        self.removeHandler(self.main_handler)
        self.__set_log_handler()


try:
    import codecs
except ImportError:
    codecs = None


class MyLoggerHandler(logging.FileHandler):
    def __init__(self, filename, when='M', backup_count=15, encoding=None, delay=False):
        self.prefix = os.path.join(LOG_PATH, '{name}'.format(name=filename))
        self.filename = filename
        self.when = when.upper()
        # S - Every second a new file
        # M - Every minute a new file
        # H - Every hour a new file
        # D - Every day a new file
        if self.when == 'S':
            self.suffix = "%Y-%m-%d_%H-%M-%S"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$"
        elif self.when == 'M':
            self.suffix = "%Y-%m-%d_%H-%M"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}\.log$"
        elif self.when == 'H':
            self.suffix = "%Y-%m-%d_%H"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}\.log$"
        elif self.when == 'D':
            self.suffix = "%Y-%m-%d"
            self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
        else:
            raise ValueError("Invalid rollover interval specified: %s" % self.when)
        self.filePath = "%s%s.log" % (self.prefix, datetime.datetime.now().strftime(self.suffix))
        try:
            if os.path.exists(LOG_PATH) is False:
                os.makedirs(LOG_PATH)
        except Exception as e:
            print("can not make dirs")
            print("filepath is " + self.filePath)
            print(e)

        self.backupCount = backup_count
        if codecs is None:
            encoding = None
        logging.FileHandler.__init__(self, self.filePath, 'a', encoding, delay)

    def write_log(self):
        _filePath = "%s%s.log" % (self.prefix, datetime.datetime.now().strftime(self.suffix))
        if _filePath != self.filePath:
            self.filePath = _filePath
            return 1
        return 0

    def change_file(self):
        self.baseFilename = os.path.abspath(self.filePath)
        if self.stream is not None:
            self.stream.flush()
            self.stream.close()
        if not self.delay:
            self.stream = self._open()
        if self.backupCount > 0:
            for s in self.delete_old_log():
                os.remove(s)

    def delete_old_log(self):
        dir_name, base_name = os.path.split(self.baseFilename)
        file_names = os.listdir(dir_name)
        result = []
        p_len = len(self.filename)
        for fileName in file_names:
            if fileName[:p_len] == self.filename:
                suffix = fileName[p_len:]
                if re.compile(self.extMatch).match(suffix):
                    result.append(os.path.join(dir_name, fileName))
        result.sort()
        if len(result) < self.backupCount:
            result = []
        else:
            result = result[:len(result) - self.backupCount]
        return result

    def emit(self, record):
        """
        Emit a record.
        """
        # noinspection PyBroadException
        try:
            if self.write_log():
                self.change_file()
            logging.FileHandler.emit(self, record)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)


if __name__ == "__main__":
    # to do something
    log = Logger('info')
    import time

    for i in range(12):
        time.sleep(1)
        log.info("测试" + str(i))

值得一提:日志可以在窗口基类中声明,如此一来便可在子类中调用,至于具体的日志声明由提交日志方法标注,或者由日志方法中的提取方法名。

thread.py

在qt中常有使用线程来异步更新数据,或者执行操作。为保证不拥塞UI线程的基础下,需要Qthread来运行耗时方法,如果直接使用thread方法更新界面,会警告被没有信号的线程更新UI。

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.thread.py
        版本     :v1.0
        创建     :pymu on 2020/4/22
        
        详情 :

---------------------------------------"""
from PyQt5.QtCore import QThread, pyqtSignal

from utils.logger import Logger


class FuncThread(QThread):
    """
    执行返回线程
    """
    # 正常通信信号
    thread_signal = pyqtSignal(dict)
    # 异常通信信号
    thread_error_signal = pyqtSignal(str)

    def __init__(self):
        super().__init__()
        self.func = None
        self.func_args = []
        self.func_kwargs = {}
        self.logger = Logger()

    def set_func(self, func, *args, **kwargs):
        """
        设置执行线程, 新建以一个通信对象之后,需要调用此方法设置执行方法之后才能开启
        通信执行线程
        :param func: 设置执行方法
        :param args:  设置执行参数
        :param kwargs: 设置执行参数(键值对)
        :return:  None
        """
        self.func = func
        self.func_args = args
        self.func_kwargs = kwargs
        self.logger.info("设置线程 func={}  arg={}  kwargs={}".format(func.__name__, args, kwargs))

    def run(self):
        """
        执行线程:
        在 start() 后启动
        其中有返回的结果,及执行异常时的通信
        """
        if not self.func:
            return
        self.logger.info("执行 func={}  arg={}  kwargs={}".format(self.func.__name__, self.func_args, self.func_kwargs))
        try:
            result = self.func(*self.func_args, **self.func_kwargs)
            if not isinstance(result, dict):
                result = {}
            self.thread_signal.emit(result)
        except Exception as e:
            self.thread_error_signal.emit(str(e))
            self.logger.error("执行失败 func={}  arg={}  kwargs={}, error={}".format(self.func.__name__,
                                                                                 self.func_args,
                                                                                 self.func_kwargs,
                                                                                 e))

注:方法中可以添加:除执行成功信号、异常信号外,还可以添加多个信号,但是需要在执行方法中声明提交信号(观察者模式),具体使用方法后续会实例说明。
#更新线程
self.update_thread = FuncThread()
self.update_thread.thread_signal.connect(self.set_tree_data)
self.update_thread.set_func(self.get_dir_dict, self.activity_path, _)
self.update_thread.start()

resourceLoad.py

静态资源加载器,在qt中通常使用了大量的图标文件、字体声明。(因为不知道qt是不是会保存在内存中,就自己加了一个)结合lazy加载可以快速访问(对于复杂布局)。

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.resourceLoad.py
        版本     :v1.0
        创建     :pymu on 2020/4/21
        
        详情 :   静态资源加载器

---------------------------------------"""
import os
import threading

import qtawesome
from PyQt5 import QtGui
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QBrush, QColor

from config import setting
from utils.lazy_property import LazyProperty


class Resource:
    """
        图像
    """
    _instance_lock = threading.Lock()

    def __init__(self):
        self.img_path = setting.get_img_path()

    @classmethod
    def instance(cls):
        with Resource._instance_lock:
            if not hasattr(Resource, "_instance"):
                Resource._instance = Resource()
        return Resource._instance

    def render_icon(self, name):
        """
        渲染
        :return:
        """
        path = os.path.join(self.img_path, name)
        icon = QtGui.QIcon()
        icon.addPixmap(QtGui.QPixmap(path), QtGui.QIcon.Normal, QtGui.QIcon.On)
        return icon

    @LazyProperty
    def QT_img_document(self):
        """
        文档 图标
        :return:
        """
        return self.render_icon("icon_tree_document.png")

    @LazyProperty
    def QT_img_dir(self):
        """
        文件夹 图标
        :return:
        """
        return self.render_icon("icon_tree_file.png")

    @LazyProperty
    def QT_img_doc(self):
        """
        doc 图标
        :return:
        """
        return self.render_icon("icon-word.png")

    @LazyProperty
    def QT_img_excel(self):
        """
        Excel 图标
        :return:
        """
        return self.render_icon("icon-excel.png")

    @LazyProperty
    def QT_img_pdf(self):
        """
        PDF 图标
        :return:
        """
        return self.render_icon("icon-pdf.png")

    @LazyProperty
    def QT_img_template_logo(self):
        """
        文书模板logo 图标
        :return:
        """
        return self.render_icon("logo-document.png")

    @LazyProperty
    def QT_img_new_folder(self):
        """
        新建文件夹 图标
        :return:
        """
        return self.render_icon("icon-add.png")

    @LazyProperty
    def QT_img_delete(self):
        """
        删除 图标
        :return:
        """
        return self.render_icon("icon-delete.png")

    @LazyProperty
    def QT_img_upload(self):
        """
        上传 图标
        :return:
        """
        return self.render_icon("icon-up.png")

    @LazyProperty
    def QT_img_download(self):
        """
        下载 图标
        :return:
        """
        return self.render_icon("icon-down.png")

    @LazyProperty
    def QT_img_search(self):
        """
        搜索 图标
        :return:
        """
        return self.render_icon("icon_search.png")

    @LazyProperty
    def QT_img_home_logo(self):
        """

        目录 图标
        :return:
        """
        return self.render_icon("icon-catalogue.png")

    @LazyProperty
    def QT_img_clear(self):
        """
        清除 图标
        :return:
        """
        return self.render_icon("icon_input_clear.png")

    @LazyProperty
    def QT_img_unknown(self):
        """
        未知文件 图标
        :return:
        """
        return self.render_icon("unknown.png")

    @LazyProperty
    def QT_img_none(self):
        """
        无图片
        :return:
        """
        return self.render_icon("??")

    @LazyProperty
    def QT_img_input_user(self):
        """
        输入账号
        :return:
        """
        # return self.render_icon("icon_login_user.png")
        return qtawesome.icon('fa.user', color="#666")

    @LazyProperty
    def QT_img_input_pass(self):
        """
        输入密码
        :return:
        """
        # return self.render_icon("icon_login_key.png")
        return qtawesome.icon('fa.unlock-alt', color="#666")

    @LazyProperty
    def QT_img_user_more(self):
        """
        下拉
        :return:
        """
        return qtawesome.icon('fa.angle-down', color="white")

    @LazyProperty
    def QT_font_16px_song(self):
        """
        14 px
        宋体
        :return:
        """
        font = QFont()
        font.setPixelSize(16)
        font.setFamily("宋体")
        return font

    @LazyProperty
    def QT_brush_font_color_333(self):
        """
        字体颜色样式 1
        :return:
        """
        brush = QBrush(QColor(51, 51, 51, 255))
        brush.setStyle(Qt.NoBrush)
        return brush

fileutils.py(项目示例)

这个保存了我自己项目中最烦躁的部分,冗余又拖沓的部分,堪堪可用。

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.fileutils.py
        版本     :v1.0
        创建     :pymu on 2020/4/13

        详情 : 文件处理工具

---------------------------------------"""
import base64
import json
import os
import shutil
import time
from lxml import etree

from config import setting


def get_file_name(path):
    file_name = os.path.basename(path)
    if not file_name:
        return
    else:
        return file_name.split(".")[0]


def unchm(path):
    """
    解压chm文件到指定的文件目录
    :param path:
    :return:
    """
    file_name = get_file_name(path)
    if not file_name:
        return
    temp_path = os.path.join(setting.get_temp_path(), file_name)
    if not os.path.exists(temp_path):
        os.makedirs(temp_path)
    command = "HH.EXE -decompile {export_path} {source_path}".format(export_path=temp_path, source_path=path)
    os.system(command)


def formatTime(temp_time):
    """
    '''格式化时间的函数'''
    :param temp_time:
    :return:
    """
    return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(temp_time))


def formatByte(number):
    """
    格式化内存大小
    :param number:
    :return:
    """
    if number < 1024:
        return "小于1字节"
    elif number <= 1024 * 1024:
        return "%.2f KB" % (number / 1024.0)
    elif number <= (1024 * 1024 * 1024):
        return "%.2f MB" % (number / 1024.0 / 1024.0)
    elif number <= (1024 * 1024 * 1024 * 1024):
        return "%.2f GB" % (number / 1024.0 / 1024.0 / 1024.0)
    else:
        return "未知文件大小"


def get_doc_dict(path, match_html=True):
    """
    输入一个路径,返回一个目录结构
    :return:
    """
    result = dict()
    if not os.path.exists(path):
        return result

    for i in os.listdir(path):
        file = os.path.join(path, i)
        if os.path.isdir(file):
            s = get_doc_dict(file)
            if s:
                result.update({i: s})
        else:
            f_type = i.split(".")
            if match_html:
                if len(f_type) > 0 and "html" in f_type[-1] or "htm" in f_type[-1]:
                    result.update({i: file})
            else:
                result.update({i: file})
    return None if len(result.keys()) == 0 else result


def get_doc_dict_new(path):
    """
    获取一个路径的目录结构
    :param path:
    :return:
    """
    result = dict()
    if not os.path.exists(path):
        return result

    for i in os.listdir(path):
        file = os.path.join(path, i)
        if os.path.isdir(file):
            s = get_doc_dict_new(file)
            result.update({i: s})
        else:
            result.update({i: file})
    return result


def get_dir_dict(path):
    """
    新启方法,仅返回目录不返回文件
    :param path:
    :return:
    """
    result = dict()
    if not os.path.exists(path):
        return result

    for i in os.listdir(path):
        file = os.path.join(path, i)
        if os.path.isdir(file):
            s = get_dir_dict(file)
            result.update({i: s})
    return result


def get_path_list(path, root_path):
    """
    输入一个路径获取路径下的文件夹及文件的列表
    :param root_path: 最顶级的目录,不允许跨目录操作
    :param path: 获取path下的结构
    :return:
    """
    result_dict = dict()
    result_dict.update({"root": path})
    result = list()
    for root, dirs, files in os.walk(path):
        for dir_t in dirs:
            item = dict()
            path = os.path.join(root, dir_t)
            item.update({
                "last_change_time": formatTime(os.path.getctime(path)),
                "path": path,
                "size": "",
                "name": dir_t,
                "type": "dir"
            })
            result.append(item)

        for file in files:
            item = dict()
            path = os.path.join(root, file)
            file_info = os.stat(path)
            item.update({"path": os.path.abspath(path),
                         "size": formatByte(os.path.getsize(path)),
                         "last_change_time": formatTime(file_info.st_mtime),
                         "name": file,
                         "type": os.path.splitext(path)[1]})
            result.append(item)
        if not result:
            result.insert(0, {"path": os.path.abspath(os.path.join(root, os.path.pardir)),
                              "size": "",
                              "last_change_time": "",
                              "name": "空空如也",
                              "type": "none"})
        if not root == root_path:
            result.insert(0, {"path": os.path.abspath(os.path.join(root, os.path.pardir)),
                              "size": "",
                              "last_change_time": "",
                              "name": "<<返回上一级",
                              "type": "back"})
        break
    result_dict.update({"data": result})
    return result_dict


def clean_temp_file(paths: list):
    """
    删除临时的解压文件
    :param paths:
    :return:
    """
    for file in os.listdir(setting.get_temp_path()):
        file_path = os.path.join(setting.get_temp_path(), file)
        if file_path not in paths:
            print(file_path)
            # noinspection PyBroadException
            try:
                shutil.rmtree(file_path)
            except Exception as e:
                print(e)


def check_chm():
    """
    检查目录下的chm文件
    更新文档时可调用
    :return:
    """
    file_dir = []
    for file in os.listdir(setting.get_chm_path()):
        # 如果是文件
        file_path = os.path.join(setting.get_chm_path(), file)
        if os.path.isfile(file_path):
            s = file.split(".")
            if len(s) > 1 and str(s[-1]).lower() == "chm":
                file_name = get_file_name(file_path)
                if not file_name:
                    return
                temp_path = os.path.join(setting.get_temp_path(), file_name)
                file_dir.append(temp_path)
                if not os.path.exists(temp_path):
                    unchm(file_path)
    clean_temp_file(file_dir)


def match_key(path, key) -> bool:
    """
    根据文件路径查找内容
    :param path:
    :param key:
    :return:
    """
    # noinspection PyBroadException
    try:
        with open(path, "rb") as f:
            response = etree.HTML(text=f.read())
            if key in response.xpath('string(.)'):
                return True
            return False
    except:
        return False


def test():
    from lxml import etree
    with open("temp/css/index.htm", "rb") as f:
        response = etree.HTML(text=f.read())
        if "其它经验1" in response.xpath('string(.)'):
            return True
        return False


def make_dir(path, name):
    """
    创建文件夹
    :param name:
    :param path:
    :return:
    """
    path = os.path.join(path, name)
    os.makedirs(path)


def filter_search(result: dict, data, m):
    """
    对目录结构进行循环遍历查找文件名字中包含key 的文件list
    :param result:
    :param data:
    :param m:
    :return:
    """
    if isinstance(data, dict):
        # key 表示文件名, value 表示路径
        for key in data:
            value = data.get(key)
            # 如果是文件夹时递归
            if isinstance(value, dict):
                filter_search(result, value, m)
            else:
                if m.lower() in key.lower():
                    item = {"path": value,
                            "size": "",
                            "last_change_time": "",
                            "name": key,
                            "type": os.path.splitext(str(value))[1]}
                    result.get("data").append(item)


def delete_any(path):
    """
    删除目录及文件
    """
    if os.path.isdir(path):
        shutil.rmtree(path)
    else:
        os.remove(path)


def delete_path(path):
    """
    根据地址删除文件及问价夹, 异常不用捕获,直接移交给线程管理器
    :param path:
    :return:
    """
    if isinstance(path, list):
        for i in path:
            delete_any(i)
        return
    if isinstance(path, str):
        delete_any(path)
        return
    raise Exception("未知的的数据类型{}".format(path))


def check_exists(source, target):
    """
    判断文件是否存在
    :param source:
    :param target:
    :return:
    """
    result = []
    if source:
        for i in source:
            file_name = os.path.basename(i)
            if os.path.exists(os.path.join(target, file_name)):
                result.append(file_name)
    return result


def copy_file(source, target):
    """
    拷贝文件
    :param target:
    :param source:
    :return:
    """
    if source:
        target = os.path.abspath(target)
        for i in source:
            target_ = os.path.join(target, os.path.basename(i))
            if os.path.isfile(i):
                shutil.copyfile(i, target_)
            else:
                if os.path.exists(target_):
                    shutil.rmtree(target_)
                shutil.copytree(i, target_)


def get_load_user_info():
    """
    获取本地保存的信息
    :return:
    """
    # noinspection PyBroadException
    try:
        with open(setting.get_user_ini_path(), "rb") as f:
            content = f.read()
        return json.loads(base64.b64decode(content).decode())
    except Exception as e:
        print(e)
        return dict()


# noinspection PyBroadException
def replace_html(path, key):
    """
    替换成高亮之后的文本
    :param key:
    :param path:
    :return:
    """
    if not path:
        return
    path = os.path.join(setting.get_activity_path(), path)
    if key:
        resource_path = setting.get_resource_path()
        html_path = os.path.join(resource_path, "html")
        html_path = os.path.join(html_path, "temp.html")
        js_path = os.path.join(resource_path, "js")

        # jq文件的路径
        jq_path = os.path.join(js_path, "jquery-1.12.4.min.js")

        # js替换的路径
        js_path = os.path.join(js_path, "height_line._js")

        # 读取源文件, 不管编码问题
        with open(path, "r", errors='ignore') as file:
            html_content = file.read()
        # 读取 需要替换的js
        with open(js_path, "r", errors='ignore') as file:
            js_content = file.read()

        # 替换文本
        if html_content and js_content:
            js_content = js_content.replace("{{key}}", key)
            js_content = js_content.replace("{{jq_path}}", "file:///" + jq_path.replace("\\", "/"))
            new_content = html_content.replace("", js_content + "")
            # 新建一个替换的文本
            with open(html_path, "w") as file:
                file.write(new_content)
            path = html_path
    return "file:///" + path.replace("\\", "/")


def set_load_user_info(data):
    """
    保存用户信息到本地
    :param data:
    :return:
    """
    with open(setting.get_user_ini_path(), "wb") as f:
        f.write(base64.b64encode(json.dumps(data).encode()))


def get_index_path():
    """
    默认的主页
    :return:
    """
    path = setting.get_index_html_path()
    return "file:///" + path.replace("\\", "/")


if __name__ == '__main__':
    # t = time.time()
    # a = test()
    # print(time.time() - t)
    print(get_doc_dict_new(setting.get_resource_path()))
    # print(os.listdir("temp"))
    # print(os.path.abspath(os.path.dirname(__file__)))
    # unchm("./document/css.chm")
    # get_doc_list()
    # print(os.path.basename("resource"))
    # print(os.path.dirname("resource"))

frame包

这个通常保存了独立的控件集合,独立控件不单单指一个qwidget对象,也可以是组合控件。以下为不完全的示例说明(仅取部分具有特色性说明)。

dialog.py

保存了一些常用的弹出层对话框。(日志操作可在此处添加)

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.dialog.py
        版本     :v1.0
        创建     :pymu on 2020/4/22
        
        详情 :

---------------------------------------"""
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QMessageBox


class Dialog(QDialog):
    """
    对话框
    """

    def __init__(self, flags, *args, **kwargs):
        super().__init__(flags, *args, **kwargs)
        # 设置对话框的标题及大小
        self.setWindowTitle('新建文件夹')
        self.resize(200, 200)

        # 设置窗口为模态,用户只有关闭弹窗后,才能关闭主界面
        self.setWindowModality(Qt.ApplicationModal)


def waring_dialog(flag, info):
    """
    警告
    :return:
    """
    return_code = QMessageBox.warning(flag, "警告", info, QMessageBox.Yes | QMessageBox.No)
    return not return_code == 65536


def error_dialog(flag, info):
    """操作失败"""
    QMessageBox.critical(flag, "操作失败", info, QMessageBox.Yes)

bar.py

这是有一个标题栏组合控件,会在不同的窗口中使用,因为使用不同的标题图标,所以抽离为组合控件体(了解基本逻辑即可)

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.bar.py
        版本     :v1.0
        创建     :pymu on 2020/4/17
        
        详情 : 标题栏

---------------------------------------"""
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QHBoxLayout, QMenuBar, QAction
from qtpy import QtCore, QtWidgets

from frame.base import BaseWidget
from frame.button import QTitleButton
# noinspection PyBroadException
from frame.dialog import waring_dialog


# noinspection PyBroadException
class Bar(BaseWidget):
    """
    窗口标题栏
    """

    def __init__(self, master, **kwargs):
        super().__init__(master)

        """ 初始化变量 """
        # 最大化,恢复按钮
        self.max_btn = None
        self.master = master
        self.logo = None
        self.right_close = None

        self.widget_name = kwargs.get("object_name", "bar_top")
        self.show_logo = kwargs.get("show_logo", True)
        self.show_user_img = kwargs.get("show_user_img", True)
        self.show_user_name = kwargs.get("show_user_name", True)
        self.show_user_more = kwargs.get("show_user_more", True)
        self.show_max_btn = kwargs.get("show_max_btn", True)
        self.show_min_btn = kwargs.get("show_min_btn", True)

        """ 配置基本属性 """
        self.configure()

        """ 放置布局 """
        self.place()

    def configure(self, *args, **kwargs):
        """
        初始化配置
        :return:
        """
        self.setFixedHeight(56)
        self.setMouseTracking(True)

    def place(self, *args, **kwargs):
        """
        布局
        :param args:
        :param kwargs:
        :return:
        """
        # 对标题新建一个水平布局

        main_layout = QHBoxLayout()
        main_layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(main_layout)

        # 新建一个布局用以装载整个标题栏
        content = QtWidgets.QWidget()
        content.setMouseTracking(True)
        widget_name = self.widget_name or "bar_top"
        content.setObjectName(widget_name)
        bar_layout = QHBoxLayout()

        # 在顶层布局中装载标题栏容器
        content.setLayout(bar_layout)
        main_layout.addWidget(content)

        # 布局内放置两个容器
        bar_left = QtWidgets.QWidget()
        bar_left.setMouseTracking(True)
        bar_right = QtWidgets.QWidget()
        bar_right.setObjectName("layoutWidget")
        bar_right.setMouseTracking(True)

        # 把左右布局添加到标题栏中,一个靠左,一个靠右, 且垂直居中
        bar_layout.addWidget(bar_left, alignment=QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
        bar_layout.addWidget(bar_right, alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)

        # 对右侧的信息组件新建一个水平布局
        bar_layout_right = QtWidgets.QHBoxLayout(bar_right)
        bar_layout_right.setContentsMargins(0, 0, 10, 0)
        bar_layout_right.setObjectName("bar_layout_right")
        bar_right.setLayout(bar_layout_right)

        # 左边也是如此
        bar_layout_left = QtWidgets.QHBoxLayout(bar_left)
        bar_layout_left.setContentsMargins(0, 0, 0, 0)
        bar_layout_left.setObjectName("bar_layout_left")
        bar_left.setLayout(bar_layout_left)

        """ 左边的控件组内容设定"""
        # logo
        left_app_icon = QtWidgets.QPushButton(bar_left)
        self.logo = left_app_icon
        logo_name = widget_name + "left_app_icon"
        left_app_icon.setObjectName(logo_name)
        left_app_icon.setFixedSize(200, 50)
        bar_layout_left.addWidget(left_app_icon)
        left_app_icon.setVisible(self.show_logo)

        """ 右边的控件组内容设定"""
        # 最小化按钮
        right_mini = QTitleButton(b'\xef\x80\xb0'.decode("utf-8"))
        right_mini.setToolTip("最小化")
        right_mini.setObjectName("MinMaxButton")
        if not self.show_min_btn:
            right_mini.setVisible(self.show_min_btn)

        # 最大化按钮
        right_max = QTitleButton(b'\xef\x80\xb1'.decode("utf-8"))
        right_max.setObjectName("MinMaxButton")
        right_max.setToolTip("最大化")
        if not self.show_max_btn:
            right_max.setVisible(self.show_max_btn)
        # 设置为全局变量
        self.max_btn = right_max

        # 关闭按钮
        self.right_close = QTitleButton(b'\xef\x81\xb2'.decode("utf-8"))
        self.right_close.setObjectName("CloseButton")
        self.right_close.setToolTip("关闭窗口")

        # 下拉按钮
        right_more = QMenuBar()
        right_more.setFont(QFont("Webdings"))
        if not self.show_user_more:
            right_more.setVisible(self.show_user_more)
        exit_menu = right_more.addMenu(self.resource.QT_img_user_more, "")

        exit_action = QAction('退出登录', self)
        exit_action.triggered.connect(self.exit)
        #
        exit_menu.addAction(exit_action)
        right_more.setObjectName("user_more")

        # 用户名
        right_user_name = QtWidgets.QLabel(bar_right)
        right_user_name.setObjectName("user_name")
        if not self.show_user_name:
            right_user_name.setVisible(self.show_user_name)

        # 用户头像
        right_user_image = QtWidgets.QPushButton()
        if not self.show_user_img:
            right_user_image.setVisible(False)
        right_user_image.setObjectName("user_image")
        right_user_image.setFixedSize(30, 30)

        # 信号槽
        if self.show_user_name:
            self.right_close.clicked.connect(self.master.close)  # 按钮信号连接到关闭窗口的槽函数
        else:
            self.right_close.clicked.connect(self.exit)
        right_mini.clicked.connect(self.master.showMinimized)  # 按钮信号连接到最小化窗口的槽函数
        right_max.clicked.connect(self._changeNormalButton)  # 按钮信号连接切换到恢复窗口大小按钮函数

        # 样式设置
        right_user_name.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
        right_user_name.setText("admin")

        self.right_close.setFixedSize(25, 25)  # 设置关闭按钮的大小
        right_max.setFixedSize(25, 25)  # 设置按钮大小
        right_mini.setFixedSize(25, 25)  # 设置最小化按钮大小

        # 装载至容器
        bar_layout_right.addWidget(right_user_image)
        bar_layout_right.addWidget(right_user_name)
        bar_layout_right.addWidget(right_more, alignment=QtCore.Qt.AlignVCenter)
        bar_layout_right.addWidget(right_mini)
        bar_layout_right.addWidget(right_max)
        bar_layout_right.addWidget(self.right_close)

    def _changeNormalButton(self):
        """
        切换到恢复窗口大小按钮
        :return:
        """
        try:
            self.master.showMaximized()  # 先实现窗口最大化
            self.max_btn.setText(b'\xef\x80\xb2'.decode("utf-8"))
            self.max_btn.setToolTip("恢复")  # 更改按钮提示
            self.max_btn.disconnect()  # 断开原本的信号槽连接
            self.max_btn.clicked.connect(self._changeMaxButton)  # 重新连接信号和槽
        except:
            pass

    def _changeMaxButton(self):
        """
        切换到最大化按钮
        :return:
        """
        try:
            self.master.showNormal()
            self.max_btn.setText(b'\xef\x80\xb1'.decode("utf-8"))
            self.max_btn.setToolTip("最大化")
            self.max_btn.disconnect()
            self.max_btn.clicked.connect(self._changeNormalButton)
        except:
            pass

    def mouseDoubleClickEvent(self, e):
        if self.show_max_btn:
            self.max_btn.click()

    def exit(self):
        """
        退出程序
        :return:
        """
        info = "确认退出吗?"
        if waring_dialog(self, info):
            QCoreApplication.instance().quit()

activity 包

此处保存所有窗口的activity,有类似Android的概念。

baseActivity.py

活动窗口的基类,在基类中声明窗口的无边框绘制阴影,窗口拉伸,窗口拖拽及文件拖拽等。

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.baseActivity.py
        版本     :v1.0
        创建     :pymu on 2020/4/23
        
        详情 :

---------------------------------------"""
from PyQt5.QtCore import Qt, QPoint, QRect
from PyQt5.QtGui import QPainter, QPixmap
from PyQt5.QtWidgets import QDialog

from config import setting
from utils.logger import Logger
from utils.resourceLoad import Resource


class BaseActivity(QDialog):

    def __init__(self, flags, *args, **kwargs):
        super().__init__(flags, *args, **kwargs)
        self.logger = Logger()

        self.bar = None
        self.resource = Resource()

        # # 窗口拖动位置列表
        self._right_rect = []
        self._bottom_rect = []
        self._corner_rect = []
        self.move_DragPosition = None
        self._padding = 5

        self.SHADOW_WIDTH = 10
        self.setAttribute(Qt.WA_TranslucentBackground)

        # # 扳机默认值
        self._move_drag = False
        self._corner_drag = False
        self._bottom_drag = False
        self._right_drag = False

    def configure(self):
        """
        配置, 包括标题头及内容的一些参数问题
        :return:
        """

    def place(self):
        """
        放置布局
        :return:
        """

    def resizeEvent(self, _event):
        """
        自定义窗口调整大小事件
        采用三个列表生成式生成三个列表, 用以保存一个鼠标可以拖动的范围
        :param _event:
        :return:
        """
        if not self.bar:
            return
        # 获取右侧边界
        self._right_rect = [QPoint(x, y) for x in range(self.width() - self._padding, self.width() + 1)
                            for y in range(self.bar.height(), self.height() + self._padding)]

        self._bottom_rect = [QPoint(x, y) for x in range(1, self.width() + self._padding)
                             for y in range(self.height() - self._padding, self.height() + 1)]

        self._corner_rect = [QPoint(x, y) for x in range(self.width() - self._padding, self.width() + 1)
                             for y in range(self.height() - self._padding, self.height() + 1)]

    def mousePressEvent(self, event):
        """
        重构鼠标点击事件
        :param event:
        :return:
        """
        if not self.bar:
            return
        if (event.button() == Qt.LeftButton) and (event.pos() in self._corner_rect):
            self._corner_drag = True
            event.accept()
        elif (event.button() == Qt.LeftButton) and (event.pos() in self._right_rect):
            self._right_drag = True
            event.accept()
        elif (event.button() == Qt.LeftButton) and (event.pos() in self._bottom_rect):
            self._bottom_drag = True
            event.accept()
        elif (event.button() == Qt.LeftButton) and (event.y() < self.bar.height()):
            self._move_drag = True
            self.move_DragPosition = event.globalPos() - self.pos()
            event.accept()

    def mouseMoveEvent(self, _):
        """
        判断鼠标位置 是否移动到了 指定的范围内
        以便更换鼠标样式
        :param _:
        :return:
        """
        if _.pos() in self._corner_rect:
            self.setCursor(Qt.SizeFDiagCursor)
        elif _.pos() in self._bottom_rect:
            self.setCursor(Qt.SizeVerCursor)
        elif _.pos() in self._right_rect:
            self.setCursor(Qt.SizeHorCursor)
        else:
            self.setCursor(Qt.ArrowCursor)

        if Qt.LeftButton and self._right_drag:
            self.resize(_.pos().x(), self.height())
            _.accept()
        elif Qt.LeftButton and self._bottom_drag:
            self.resize(self.width(), _.pos().y())
            _.accept()
        elif Qt.LeftButton and self._corner_drag:
            self.resize(_.pos().x(), _.pos().y())
            _.accept()
        elif Qt.LeftButton and self._move_drag:
            self.move(_.globalPos() - self.move_DragPosition)
            _.accept()

    def mouseReleaseEvent(self, _):
        """
        鼠标释放后,各扳机复位
        :param _:
        :return:
        """
        self._move_drag = False
        self._corner_drag = False
        self._bottom_drag = False
        self._right_drag = False

    def dragEnterEvent(self, event):
        """
        判断拖拽物体是否有路径,有时拖拽生效
        :param event:
        :return:
        """
        if event.mimeData().hasUrls:
            event.accept()
        else:
            event.ignore()

    def dragMoveEvent(self, event):
        """
        拖拽移动
        :param event:
        :return:
        """
        if event.mimeData().hasUrls:
            try:
                event.setDropAction(Qt.CopyAction)
            except Exception as e:
                print(e)
            event.accept()
        else:
            event.ignore()

    def dropEvent(self, event):
        """
        获取拖拽文件
        :param event:
        :return:
        """
        try:
            if event.mimeData().hasUrls:
                event.setDropAction(Qt.CopyAction)
                event.accept()
                links = []
                for url in event.mimeData().urls():
                    links.append(str(url.toLocalFile()))
                print(links)
            else:
                event.ignore()
        except Exception as e:
            print(e)

    def draw_shadow(self, painter):
        """
        绘制边框阴影
        :param painter:
        :return:
        """
        # 绘制左上角、左下角、右上角、右下角、上、下、左、右边框
        pix_maps = list()
        pix_maps.append(str(setting.get_img_file_path("left_top.png")))
        pix_maps.append(str(setting.get_img_file_path("left_bottom.png")))
        pix_maps.append(str(setting.get_img_file_path("right_top.png")))
        pix_maps.append(str(setting.get_img_file_path("right_bottom.png")))
        pix_maps.append(str(setting.get_img_file_path("top_mid.png")))
        pix_maps.append(str(setting.get_img_file_path("bottom_mid.png")))
        pix_maps.append(str(setting.get_img_file_path("left_mid.png")))
        pix_maps.append(str(setting.get_img_file_path("right_mid.png")))
        # 左上角
        painter.drawPixmap(0, 0, self.SHADOW_WIDTH, self.SHADOW_WIDTH, QPixmap(pix_maps[0]))
        # 右上角
        painter.drawPixmap(self.width() - self.SHADOW_WIDTH, 0, self.SHADOW_WIDTH, self.SHADOW_WIDTH,
                           QPixmap(pix_maps[2]))
        # 左下角
        painter.drawPixmap(0, self.height() - self.SHADOW_WIDTH, self.SHADOW_WIDTH, self.SHADOW_WIDTH,
                           QPixmap(pix_maps[1]))
        # 右下角
        painter.drawPixmap(self.width() - self.SHADOW_WIDTH, self.height() - self.SHADOW_WIDTH, self.SHADOW_WIDTH,
                           self.SHADOW_WIDTH, QPixmap(pix_maps[3]))
        # 左
        painter.drawPixmap(0, self.SHADOW_WIDTH, self.SHADOW_WIDTH, self.height() - 2 * self.SHADOW_WIDTH,
                           QPixmap(pix_maps[6]).scaled(self.SHADOW_WIDTH,
                                                       self.height() - 2 * self.SHADOW_WIDTH))
        # 右
        painter.drawPixmap(self.width() - self.SHADOW_WIDTH, self.SHADOW_WIDTH, self.SHADOW_WIDTH,
                           self.height() - 2 * self.SHADOW_WIDTH, QPixmap(pix_maps[7]).scaled(self.SHADOW_WIDTH,
                                                                                              self.height() - 2 * self.SHADOW_WIDTH))
        # 上
        painter.drawPixmap(self.SHADOW_WIDTH, 0, self.width() - 2 * self.SHADOW_WIDTH, self.SHADOW_WIDTH,
                           QPixmap(pix_maps[4]).scaled(self.width() - 2 * self.SHADOW_WIDTH,
                                                       self.SHADOW_WIDTH))
        # 下
        painter.drawPixmap(self.SHADOW_WIDTH, self.height() - self.SHADOW_WIDTH, self.width() - 2 * self.SHADOW_WIDTH,
                           self.SHADOW_WIDTH, QPixmap(pix_maps[5]).scaled(self.width() - 2 * self.SHADOW_WIDTH,
                                                                          self.SHADOW_WIDTH))

    def paintEvent(self, event):
        painter = QPainter(self)
        self.draw_shadow(painter)
        painter.setPen(Qt.NoPen)
        painter.setBrush(Qt.white)
        painter.drawRect(QRect(self.SHADOW_WIDTH, self.SHADOW_WIDTH, self.width() - 2 * self.SHADOW_WIDTH,
                               self.height() - 2 * self.SHADOW_WIDTH))

单独运行的话,是这样的效果:
avatar

main.py

程序的启动方法,利用socket唯一启动

#!/usr/bin/python3
# coding=utf-8


"""---------------------------------------

        project :实用法规
        包名/文件 :.main.py
        版本     :v1.0
        创建     :pymu on 2020/4/23
        
        详情 : 主程

---------------------------------------"""
import os
import sys

from PyQt5 import QtWidgets
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtNetwork import QLocalSocket, QLocalServer

from activity.index import MainActivity
from config import setting
from frame.dialog import error_dialog
from utils.logger import Logger
from utils.fileutils import check_chm

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    logger = Logger()
    check_chm()
    # noinspection PyBroadException
    try:
        # q_web_engine_view_path = QCoreApplication.applicationDirPath() + "/" + "CangegeWeb.exe"
        # print(q_web_engine_view_path)
        # os.putenv("QTWEBENGINEPROCESS_PATH", q_web_engine_view_path.toLocal8Bit())
        main_ass = os.path.join(setting.get_qss_path(), "main.qss")
        with open(main_ass) as f:
            app.setStyleSheet(f.read())
        serverName = 'chang_shen2.0'
        socket = QLocalSocket()
        socket.connectToServer(serverName)
        if socket.waitForConnected(500):
            logger.info("重复运行")
            error_dialog(None, "已有实例在运行")
            app.quit()
        else:
            localServer = QLocalServer()  # 没有实例运行,创建服务器
            localServer.listen(serverName)
            gui = MainActivity(None)
            sys.exit(app.exec_())
    except Exception as e:
        logger.error("启动失败{}".format(e))
        error_dialog(None, "启动失败: {}".format(e))

requirement.txt

PyQt5==5.14.2
lxml==4.5.0
PyQtWebEngine==5.14.0

结语

因为项目是保密项目,故不能全部公开所有的源码,如有相关的功能或者细节问题可评。

你可能感兴趣的:(pyqt)