2020年9月 第一个小项目

实现django浏览器页面和日志嵌入gui,并打包成exe文件

用了两周多的时间,做出一个这样的需求。主要涉及django服务器打包、gui设计、浏览器嵌入、Windows窗口嵌入、python文件打包、ini配置文件传入参数。

需求

设计一个程序,使用GUI库(tkinter/PyQt)做出一个用户界面;用cefpython3或WebEngineView实现浏览器嵌入,在gui里可以直接点击按钮查看django页面,不需要额外下载或打开浏览器;把django服务器日志窗口嵌入gui中,点击按钮可以查看django服务器的日志。最后把整个python文件用pyinstaller打包,只需要点击.exe文件就能运行程序,不需要再配置相应的python环境。

完善

最后又把运行参数放到配置文件里改善了一下项目,只要把第一个打包好的服务器项目(或任何.exe运行文件)放到打包文件的同一目录里,修改配置文件的参数后,不需要重新打包python代码也可以被嵌入到gui窗口里。

首先,我从pyinstaller打包入手,练习如何打包一个python文件。

打包方法1. 可以使用一行命令实现python代码打包:

pyinstaller *-option* xxx.py

options可以参考官方文档:pyinstaller官方文档
-option我接触到的有三种:

  • -d:–onedir,生成一个文件目录包含可执行文件和相关动态链接库和资源文件等
  • -f:–onefile,仅生成一个可执行文件
  • -w:–windowed, --noconsole,不出现console窗口

打包方法2. python项目用spec方式打包:

第一步先生成spec文件,可以在生成的spec文件里进行一些打包配置,但我没有涉及到:

pyi-makespec -w xxx.py

然后使用spec执行打包命令:

pyinstaller -D xxx.spec

在打包我的总项目后,在虚拟机运行project.exe文件的时候,除了gui页面还会出来一个终端窗口,只要在执行打包命令的时候在最后加上-w就能不出现console窗口。

关于打包产生的问题,我遇到的有三种

  1. 版本冲突:我刚开始下载的是django和pyinstaller的最新版,但打包成功后运行也有问题,解决办法是把python3.7换成python3.5,django使用2.0版本。
  2. 运行manage.exe runserver出现了Error21,设备未就绪,后面跟了一串打包时候本机的地址。是django打包不规范导致,因为当时解决了版本冲突后打包的有点仓促,没注意哪里不规范,就会出现环境相关的问题,和代码无关。
  3. 把项目放到虚拟机里运行出现了Fatal error detected:Fail to excute script xxxxx。这也是打包过程不规范,如果没加-D,打包的exe文件在本地可以正常运行,但在其他电脑可能会出现问题,只要打包时把参数加上就行。

然后学习Tkinter库和PyQt库,练习用两个库的组件开发gui页面。

两个GUI库的效果差不多。我的项目里做出最简单的按钮跳转和布局就可以。因为查PyQt的相关资料比较多,所以我用的是PyQt。基本只用到了布局、放入组件、按钮跳转事件。

重写了主窗口和日志窗口的关闭事件

主窗口关闭时候要保证整个系统退出:

    def closeEvent(self, event):
        killProcess("manage.exe")
        sys.exit()

这里的killProcess()方法是我写的一个用于杀死进程及其子孙进程的方法,子进程需要手动关闭的问题查了很久才解决。在后面启动服务器命令里会讲。
日志窗口关闭时,要重新打开还能看到日志,因为使用的是获得窗口句柄的方法,如果关掉就需要再手动打开,否则就是一次性的查看日志。

    def closeEvent(self,event):
        """
        日志窗口假关闭事件

        点击右上角关闭日志窗口时,忽略关闭窗口事件,把日志窗口隐藏,用于保持日志不间断更新。
        """
        event.ignore()
        log.hide()

我定义了日志窗口假关闭事件,每次点击关闭日志窗口时,忽略关闭事件,把该窗口隐藏,再点击查看日志按钮时,会重新展示。

学习两种浏览器嵌入框架

在浏览器嵌入方面,cefpython3嵌入chromium的v66版本,WebEngineView嵌入版本没查到,不过可能较低。后续使用效果感觉cefpython3的浏览器加载速度快一点。两种方式的浏览器页面嵌入都很简单。
我学习两种框架后,先用的cefpython3,中间有个东西cefpython3不好实现,我后面就用WebEngineView了,两种方式实现的嵌入浏览器都很简单。

cefpython3的浏览器实现

    cef.Initialize()
    cef.CreateBrowserSync(url="http://www.baidu.com" )
    cef.MessageLoop()
    cef.Shutdown()

WebEngineView的浏览器嵌入实现

    view = QWebEngineView()  # 启动WebEngine浏览器
    
	class LogWindow(QWidget):
	# 这是在主窗口里定义的
	    def clickedEvent():
	        view.setWindowTitle("django页面")
	        hwnd2 = win32gui.FindWindowEx(0, 0, "Qt5150QWindowIcon",
	                                      "django页面")
	        if hwnd2 == 0:
	            try:
	                view.load(QUrl(webURL))
	                view.activateWindow()  # 用于把浏览器窗口弹出到最顶层
	                view.showNormal()
	            except Exception as e:
	                print(e)
	        else:
	            view.activateWindow()
	            view.showNormal()

这是我定义的按钮点击事件来显示django页面。
win32gui.FindWindowEx是win32gui包里的方法,使用窗口类名和窗口名称找到这个窗口的句柄,找到就返回窗口句柄,没找到就返回0。hwnd2句柄的用途是:我在打开django窗口后,再点击查看页面按钮,不会重新加载页面,只会把窗口弹出到桌面最顶层,在日志里也不会出现访问请求。

启动django服务器

可以用os.system(cmd)和subprocess.Popen(cmd)启动django服务器

os.system('cd D:\customProgram\python\sunyongle\PyQt\djangoprojectTest\mysite && python manage.py runserver')
subprocess.run('./manage.py runserver')

p = subprocess.Popen("python manage.py runserver", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

只要python代码和django的manage.py在同一目录下,就不用考虑路径问题,直接启动django服务器。

日志内容输出到GUI里可以使用输出重定向

这部分涉及一点进程间通信、文件IO,是操作系统的知识,目前有点缺乏,需要把操作系统学习一下。

失败的方法

subprocess启动的子进程可以用subprocess.output()直接得到其输出,但是对于django服务器来说,他只要在运行,就没有结束输出,所以这个方法会卡死进程,不可行。readlines()性质也相同,一样会卡死。
用readline()方法可以不卡死地输出django日志固定数目的几行,但是readline在读到django当前日志最后一行时也会卡死。对于不完整的日志,可以把readline()读到的内容写到gui的label或者textEdit里,但也不能得到全部日志。

对于输出固定内容的终端重定向:

import subprocess

p = subprocess.Popen("python stdTest.py", shell=True, stdout=subprocess.PIPE)
sout = p.stdout.readlines()
print (sout)

stdTest.py文件是向管道里写入一些内容。

sys.stdout.write('hello in \n')

成功的方法

后来使用写到本地txt文件的方法可以成功把django日志重定向输出到gui里,这样思路的实现方法比较简单、稳定,而且具体实现的方法有很多。

p = subprocess.Popen("python manage.py runserver > test2.log 2>&1 &", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

再把写入的txt读取,点击时重新输出,就能实现在GUI里实时更新django日志

f1 = open(r"D:\customProgram\python\sunyongle\PyQt\浏览器嵌入项目\djangoProject\test2.log", 'r')
s1 = f1.readlines()
line1 = str(s1)
line1 = ''.join(line1.split())
self.c = QTextEdit(self)
self.c.setText(line1)

但是这个方法实现有点简单而且比较传统,需要更独特一点的方法实现,于是就要把django运行的窗口嵌入gui里。

windows页面嵌入gui

后面采用的嵌入窗口思路是:把django打包成exe先独立运行,就能获得一个console窗口,再把运行时的console窗口嵌入gui里,实现实时查看服务器日志输出。

方法1:只能固定路径的方法

我首先查到的方法是使用spy++查看manage.exe窗口的类名和窗口名称,使用windows api的方法win32gui.FindWindowEx()获得窗口句柄,然后用QWindow的fromWinId( )方法,创建一个win32窗口的代理QWindow,最后使用QWidget的createWindowContainer()方法把代理窗口封装成一个QWidget控件,然后嵌入GUI。win32gui.FindWindowEx()的后两个参数是窗口的类名和窗口的名称,方法返回值为该窗口的句柄。

hwnd1 = win32gui.FindWindowEx(0, 0, "ConsoleWindowClass", "C:\Windows\System32\cmd.exe - manage.exe  runserver")  # 获得窗口句柄
window = QWindow.fromWinId(hwnd1)  # 创建一个win32窗口的代理QWindow
logwindow = self.createWindowContainer(window, self)  # 把窗口封装为一个QWidget控件

使用这样的windows窗口嵌入方法在本地运行没有问题,但是因为窗口名会随所在路径的改变而改变,所以在更换文件夹位置后,窗口名和代码里的窗口名不一样,不能用窗口名找句柄的方法嵌入。

方法2、3:失败的方法

我尝试用subprocess运行子进程的pid找到句柄,再用句柄实现嵌入。

myProcess = subprocess.Popen("./manage.exe runserver")  # 用subprocess方法启动django服务器
    SubPid = myProcess.pid  # 得到子进程的pid

查到一个获得句柄的方法看不懂,也不起作用:

def get_hwnds_for_pid(pid):
    def callback(hwnd, hwnds):
        if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd):
            _, found_pid = win32process.GetWindowThreadProcessId(hwnd)
            if found_pid == pid:
                hwnds.append(hwnd)
            return True

    hwnds = []
    win32gui.EnumWindows(callback, hwnds)
    hwndy = 0
    if hwnds:
        hwndy = hwnds[0]

    return hwndy

这个方法返回的句柄值为0,应该是没有根据进程皮带找到窗口句柄。

我查到win32里OpenProcess是根据进程pid获得窗口句柄的:

handel = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, SubPid)  # 使用win32的方法获得句柄

但是这个方法获得的句柄类型是PyHANDLE,需要的句柄类型是HWND,不管用。

在打包第二个项目前,我运行django的命令都采用了os的方法,而没有用subprocess的方法。因为os.system方法可以弹出窗口,subprocess的方法弹出窗口会被pycharm截获,日志就在pycharm的console里显示,不能实现窗口嵌入,但是只要python代码不在pycharm里运行就可以解决,比如打包后再运行,但是这样的操作太繁琐,所以一直用os.system的方法启动django服务器。但是其实两种运行方法在打包后的实现效果应该是一样的。

方法4:成功解决窗口名和句柄的方法

我看到有方法可以遍历出所有窗口的句柄和窗口名,这样就肯定能获得所需要的窗口句柄。虽然日志窗口名会随所在路径而改变,但是窗口名的最后一项是固定的,所以只要判断所有窗口名字符串的末尾符合所需要窗口的末尾字符串就能找到它。

获得所有存活窗口的句柄和窗口名称的方法:

hwnd_title = {}

def get_all_hwnd(hwnd, mouse):
    if (win32gui.IsWindow(hwnd)
            and win32gui.IsWindowEnabled(hwnd)
            and win32gui.IsWindowVisible(hwnd)):
        hwnd_title.update({hwnd: win32gui.GetWindowText(hwnd)})

调用该方法:

win32gui.EnumWindows(get_all_hwnd, 0)
for h, t in hwnd_title.items():
    if t:
        if t.endswith("manage.exe"):
            handle = h

获得了窗口名称末尾为"manage.exe"的句柄handle=h,用于窗口嵌入,而且也不会因为路径改变出问题。

杀死子进程的方法:

刚开始每次运行django服务器后,关闭页面和退出进程后还是能访问django页面,原因是manage.exe没有关闭,必须要手动关闭。
我使用subprocess的kill()、terminate()和os.killpg()方法都不能完全杀死子进程,原因是子进程又产生了子进程。
我查到了一个方法,使用psutil模块的process_iter()方法,迭代当前正在运行的所有进程,返回每个进程的Process对象。 根据方法输入参数进程名称判断,杀死该进程的所有子孙进程。killProcess()方法的输入参数为需要杀死的进程名称。

def killProcess(self):
    for p in psutil.process_iter():
        if p.name() == self:
            for child in p.children():
                os.kill(child.pid, -1)
            os.kill(p.pid, -1)

在GUI窗口的关闭事件里添加killProcess()就能实现每次退出程序时都把django服务器退出。

打包到虚拟机中运行

打包问题在上面说过,只有符合规范,打包后的exe程序在虚拟机或其他电脑上也可以正常运行。如果打包前正常,换到其他路径也跑的通,但打包后出现问题,就尝试改一下打包参数再打包。打包完后要把第二个打包过的总项目和第一个打包的django项目(或者其他exe文件)放在同一文件夹里。

使用ini文件进行参数配置

使用ini文件配置后,这些参数从ini文件读取,就可以适配其他打包的服务器或exe文件。

file = "config2.ini"
configFile = configparser.ConfigParser()
configFile.read(file)

cmd = configFile.get("runCommand", "cmd")
filename = configFile.get("fileName", "exeName")
viewLength = configFile.get("Sizes", "viewLength")
viewWidth = configFile.get("Sizes", "viewWidth")
GUILength = configFile.get("Sizes", "GUILength")
GUIWidth = configFile.get("Sizes", "GUIWidth")
logLength = configFile.get("Sizes", "logLength")
logWidth = configFile.get("Sizes", "logWidth")

我的第一篇帖子。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值