2025年3月22日 星期六 甲辰(龙)年 月廿一 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Python

Python+PyQt5创建Windows窗口程序的基础案例(适合新手学习)

时间:09-17来源:作者:点击数:59

今天写个如何用Python+PyQt5模块,单个python代码文件运行,实现Windows窗口应用工具的例子。

代码不长,去除注释可能都不到200行,很适合新人入门。

本文代码最终实现结果如下,

窗口界面参考Windows端鼎鼎大名文本查找程序SearchAndReplace,

顺便一提如有Windows单机海量文本或文件查找需求的,可以试试AnyTXT Searcher和Everything,这种可以自建索引实现高速查找文本或文件的工具。

在这里插入图片描述

正文

直接上菜

复制代码保存到单个py文件,执行即可。

后续如果需要将py文件打包exe的,可以搜索nuitka,知乎上有精通这方面的人,专门发过帖子。

  • # -*- coding: UTF8 -*-
  • import sys
  • import re
  • import os
  • import time
  • import traceback
  • from PyQt5.QtGui import QIcon, QBrush, QColor
  • from PyQt5.QtWidgets import QDialog, QPushButton, QTreeWidget, QMessageBox, QApplication, QVBoxLayout, QHBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFileDialog
  • from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
  • # @Created by CSDN-watfe
  • class AvoidJam(QThread):
  • """
  • 耗时会导致QTUI无响应的内容,放到这里执行。
  • 引入信号槽函数,是为了不卡死的同时,保持QTUI控件的交互数据更新(如果直接刷新主界面控件时,比如表格控件,并不会立即刷新,需要点击一下才可以刷新,则需要使用引入槽函数)。
  • """
  • signal = pyqtSignal(tuple) # 创建信号
  • def __init__(self, *args, **kwargs):
  • super(AvoidJam, self).__init__() # 子类调用父类,继承了父类QThread的属性和方法,并用父类初始化方法来初始化继承的属性
  • self.main_win = kwargs.get('main_win') # 主界面
  • self.signal.connect(self.refresh) # 信号绑定到函数
  • self.folder = self.main_win.windowTitle().split(':', 1)[1]
  • self.filetype = '(py|ipynb|txt|md)' # 限定文档类型
  • self.repat = self.main_win.lineEdit.text()
  • def run(self):
  • """
  • IO开销耗时,导致界面卡死的部分,放到这里执行;
  • 有需要传递给界面控件的,通过signal.emit发送tuple类型的信号sigtuple:(信号id,值1,值2,...)
  • """
  • try:
  • # 搜索文件,限制文件类型,返回结果
  • filepathArr = fileSearch(path=self.folder, repat=r'.*\.'+self.filetype, ignorecase=True, fullpath=True, children=True)
  • for index,filepath in enumerate(filepathArr):
  • # 当前执行序号
  • sigtuple = (2, index+1,len(filepathArr))
  • self.signal.emit(sigtuple) # 发送信号执行函数
  • # 打开文件,正则查找
  • with open(filepath,'r',encoding='utf-8') as f:
  • text = f.read()
  • textlen = len(text)
  • # 找到结果输出到界面
  • reIndexArr = [m.span() for m in re.finditer(self.repat, text)] # 正则文本位于全文字符序号位置
  • if reIndexArr:
  • reRowArr = [text[:start].count('\n')+1 for start,end in reIndexArr] # 正则文本位于全文的第几行
  • text = text.replace('\r\n',' \n').replace('\n',' ')
  • reArr = [(text[start-30 if start-30>0 else 0:start].replace('<', '&lt;').replace('>', '&gt;'),text[start:end].replace('<', '&lt;').replace('>', '&gt;'),text[end:end+30 if end+30<=textlen+1 else textlen+1].replace('<', '&lt;').replace('>', '&gt;')) for start,end in reIndexArr]
  • for i in reArr:
  • if '\n' in i[2]:
  • print(i)
  • sigtuple = (1, filepath, reArr, reIndexArr, reRowArr)
  • self.signal.emit(sigtuple) # 发送信号执行函数
  • # 结束
  • sigtuple = (3, len(filepathArr))
  • self.signal.emit(sigtuple) # 发送信号执行函数
  • except Exception as e:
  • print(e)
  • print(traceback.format_exc())
  • def refresh(self, sigtuple):
  • """
  • 这里完成QTUI控件交互
  • """
  • # sigtuple信号拆成:(信号id,值1,值2...)
  • sigid, *values = sigtuple
  • # 执行界面变动
  • if sigid == 1:
  • # 根节点(文件名)
  • filepath = values[0]
  • filename = os.path.split(filepath)[1]
  • root = QTreeWidgetItem(self.main_win.tree) # 设置根节点
  • root.setText(0, filename) # 文件名
  • root.setText(1, filepath) # 路径
  • root.setForeground(0, QBrush(Qt.blue))
  • # 子节点(匹配值)
  • reArr = values[1]
  • reIndexArr = values[2]
  • reRowArr = values[3]
  • for matchstr,startend,rowcount in zip(reArr,reIndexArr,reRowArr):
  • # 多个子节点逐个插入
  • child = QTreeWidgetItem(root)
  • label = QLabel() # 引入QLabel用彩色标记关键字
  • label.setText(f"{matchstr[0]}<font color=red>{matchstr[1]}</font>{matchstr[2]}")
  • child.setText(1,f'{rowcount:>5} 行,{startend[0]}-{startend[1]}')
  • self.main_win.tree.setItemWidget(child,0,label)
  • # # 展开全部
  • # self.main_win.tree.expandAll()
  • elif sigid == 2:
  • # 输出当前进度
  • index = values[0] # 当前执行到第几个文件
  • total = values[1] # 总计文件数
  • self.main_win.label.setText(f'{index}/{total}')
  • elif sigid == 3:
  • # 结束,展开全部,按钮变回开始
  • total = values[0]
  • self.main_win.label.setText(f'{total}项查完')
  • self.main_win.pushButton.setText('开始')
  • self.main_win.tree.expandAll()
  • class MainWidgetUI(QDialog):
  • def __init__(self):
  • super().__init__()
  • # 窗口标题、图标、大小
  • # folder应用程序当前位置(代替folder = os.getcwd(),否则建立快捷方式,等其他情况执行的时候,folder会返回C:\Windows\system32,而非运行目录
  • if getattr(sys, 'frozen', False): # 判断当前是脚本,还是exe文件
  • folder = os.path.dirname(sys.executable) # 脚本目录
  • elif __file__:
  • folder = os.path.dirname(__file__) # exe文件
  • self.setWindowTitle('文本查找:'+folder)
  • self.setWindowIcon(QIcon("favicon.ico"))
  • self.resize(1000, 700) # 禁止调整窗口大小
  • self.setWindowFlags(Qt.WindowCloseButtonHint) # 隐藏右上角问号
  • '''以下控件也可以通过QTUI设计去画,然后从转化出来的ui_Dialog.py中拷贝出来'''
  • # 窗口控件
  • self.pushButton = QPushButton("开始")
  • self.pushButton_folder = QPushButton("选择文件夹")
  • self.pushButton_folder.setFocusPolicy(Qt.NoFocus)
  • self.tree = QTreeWidget()
  • self.label = QLabel()
  • self.lineEdit = QLineEdit()
  • # 初始化label
  • self.label.setMinimumSize(QSize(100, 0))
  • self.label.setText('0/0')
  • # 初始化tree
  • self.tree.setColumnCount(2) # 设置列数
  • self.tree.setHeaderLabels(['文件名', '路径']) # 设置头的标题
  • self.tree.setColumnWidth(0, 800) # 设置列宽
  • self.tree.setColumnWidth(1, 900) # 设置列宽
  • self.tree.itemDoubleClicked.connect(self.doubleClickOpenFile) # 关联双击动作,打开文件
  • # 布局
  • # 创建水平布局,并嵌入到垂直中
  • LayoutH = QHBoxLayout()
  • LayoutH.addWidget(self.pushButton_folder)
  • LayoutH.addWidget(self.label)
  • LayoutH.addWidget(self.lineEdit)
  • LayoutH.addWidget(self.pushButton)
  • # 按照方向、添加顺序,绘制布局
  • Layout = QVBoxLayout() # QVBoxLayout()垂直、QHBoxLayout()水平, 查看布局请移步CURL-api.py
  • Layout.addLayout(LayoutH)
  • Layout.addSpacing(8) # 添加一个8px的空间距离 且不带弹性
  • Layout.addWidget(self.tree)
  • # 应用布局
  • self.setLayout(Layout)
  • # 按钮连接到函数
  • self.pushButton_folder.clicked.connect(self.BTfolder) # 选择文件夹
  • self.pushButton.clicked.connect(self.BTclick) # 执行查找
  • def doubleClickOpenFile(self, item, column_no):
  • # 双击子节点,调用系统方法打开父节点文件
  • if item.parent(): # 父目录是None的跳过
  • # 打开文件
  • filepath = item.parent().text(1)
  • os.startfile(filepath)
  • def BTfolder(self):
  • # 选择文件夹
  • folder = QFileDialog.getExistingDirectory(self, "选择要查找的文件夹(搜索将包含子文件夹)", "./")
  • # 当窗口非继承QtWidgets.QDialog时,self可替换成 None
  • self.setWindowTitle('文本查找:'+folder)
  • def BTclick(self):
  • # 点击按钮,执行查找
  • # AvoidJam的写法,支持IO开销大,需避免界面卡,支持随时停止
  • if self.pushButton.text() == '开始':
  • patStr = self.lineEdit.text()
  • # 尚未输入任何要找文本
  • if patStr=='':
  • QMessageBox.information(self, '注意', '请先输入要找(支持正则)文本', QMessageBox.Yes)
  • return
  • patStrStrip = patStr.lstrip('(').rstrip(')')
  • # 要找文本长度小于2位
  • if len(patStrStrip)<2:
  • QMessageBox.information(self, '注意', '至少输入2位要找字符', QMessageBox.Yes)
  • return
  • # 要找内容首尾包含不定长度匹配
  • if patStrStrip[1] in ['*','+'] and patStrStrip[0]!='\\' or patStrStrip[-1] in ['*','+'] and patStrStrip[-1]!='\\':
  • QMessageBox.information(self, '注意', '请勿在正则首尾,使用*或+等不定长度查找,将严重影响查询速度', QMessageBox.Yes)
  • return
  • self.tree.clear()
  • self.pushButton.setText('停止')
  • self.thread = AvoidJam(main_win=self)
  • self.thread.start()
  • else:
  • self.pushButton.setText('开始')
  • self.thread.terminate()
  • self.thread.quit()
  • def fileSearch(path='.', repat= r'.*', ignorecase=True, fullpath=True, children=True):
  • """
  • 文件查找:
  • 文件夹及子文件夹下,所有匹配文件,返回list文件列表,绝对路径形式
  • Args:
  • path: 文件路径(默认当前路径)
  • repat: 文件名正则匹配,不区分大小写(默认匹配所有文件)比如:r'\d+\.jpg',匹配所有数字名称的.jpg文件
  • ignorecase: True忽略大小写,否则区分大小写
  • fullpath: True返回文件的绝对路径,否则只返回文件名
  • children: True获取子目录下的文件,否则忽略子目录下的文件
  • Returns:
  • files_match: 文件列表
  • """
  • if not os.path.exists(path):
  • raise '路径不存在'
  • # 获取文件夹,及子文件夹下所有文件,并转为绝对路径
  • files = []
  • repat = '^'+repat+'$'
  • oswalk = os.walk(path)
  • # walk结果形式 [(folder:文件夹,[childrenFolderList:该文件夹下的子文件夹],[fileList:该文件夹下的文件]),(子文件夹1,[子子文件夹],[]),(子文件夹2,[],[])...]
  • # 该遍历会走遍所有子文件夹,返回上述形式的结果信息。
  • oswalk = oswalk if children else [next(oswalk)] # 是否遍历子目录
  • ignorecase = re.IGNORECASE if ignorecase else 0 # 是否忽略大小写
  • # 找出符合条件的文件名,合并绝对路径后,加入到返回列表
  • for folder, childrenFolderList, fileList in oswalk:
  • for file in fileList:
  • # 文件名是否符合要求
  • if re.match(pattern=repat, string=file, flags=ignorecase):
  • # 返回绝对路径或只是文件名
  • filepath = os.path.abspath(os.path.join(folder, file)).replace('\\', '/') if fullpath else file
  • files.append(filepath)
  • # print('找到{0}个文件'.format(len(files)))
  • # 返回满足要求的
  • return files
  • if __name__ == "__main__":
  • app = QApplication(sys.argv)
  • main_widget = MainWidgetUI()
  • main_widget.show()
  • sys.exit(app.exec_())
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门