PYQT5:基于QsciScintilla的代码编辑器分析7--编译器命令与信息输出视图
这里提供本编辑器可执行文件(exe)的下载链接:
1.Code51.Code51_STC
2.百度盘链接::https://pan.baidu.com/s/1Ihxb7WX0ozUuRs3KFyzApQ
提取码:i6my
3.源代码:百度盘链接:https://pan.baidu.com/s/1jlRvWgN2LFHTtnKogeUZZw
提取码:w437
4.源代码的码云链接:https://gitee.com/huangweide001/code4STC51
读者在调试代码时,可以直接运行主文件《texteditor2.py》。
1.编译器命令介绍
本代码编辑器是针对MCS51的C语言代码的,采用SDCC编译器。SDCC的全称是Small Device C Compiler,所以不仅仅是51单片机的编译器;目前SDCC支持Intel 8051, Maxim 80DS390, Zilog Z80与Motorola 68HC08 等系列CPU的代码编译。特点是免费、开源、跨平台。这里只用到关于Intel 8051部分,主要就用到3个功能:1.编译,将c文件编译成rel文件;2.链接,将rel链接成ihx文件;3.生成hex,把ihx文件转化为hex文件。
代码中直接调用subprocess模块执行命令,下面抄一段说明文字:
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False,
startup_info=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())
参数说明:
- args: 要执行的shell命令,可以是字符串,也可以是命令各个参数组成的序列。当该参数的值是一个字符串时,该命令的解释过程是与平台相关的,因此通常建议将args参数作为一个序列传递。
- bufsize: 指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。
- stdin, stdout, stderr: 分别表示程序标准输入、输出、错误句柄。
- preexec_fn: 用于指定一个将在子进程运行之前被调用的可执行对象,只在Unix平台下有效。
- close_fds: 如果该参数的值为True,则除了0,1和2之外的所有文件描述符都将会在子进程执行之前被关闭
- shell: 该参数用于标识是否使用shell作为要执行的程序,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。
- cwd: 如果该参数值不是None,则该函数将会在执行这个子进程之前改变当前工作目录。
- env: 用于指定子进程的环境变量,如果env=None,那么子进程的环境变量将从父进程中继承。如果env!=None,它的值必须是一个映射对象。
- universal_newlines: 如果该参数值为True,则该文件对象的stdin,stdout和stderr将会作为文本流被打开,否则他们将会被作为二进制流被打开。
- startupinfo和creationflags: 这两个参数只在Windows下有效,它们将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如主窗口的外观,进程优先级等。
1.1执行命令的源代码
在介绍源代码之前,说说我遇到的2个坑。第一个:调试时使用"subprocess.Popen"函数调用编译器程序完全没有问题,但是生成exe文件,已进入"subprocess.Popen"函数就退出。后来经过多次尝试,才知道问题所在。就是"stdin=None, stdout=None, stderr=None,"这3个参数都必须明确指定。
例如下面的源码中,就没有指定stdin,生成exe文件就有问题。至于是什么原因,我也不清楚。
p = subprocess.Popen(cmdlist, stdout=subprocess.PIPE,stderr=subprocess.PIPE,
cwd=self.cbpDir, bufsize=1)
第二个坑是参数“shell=False”。调用程序“packihx”将ihx文件转换成hex时,要设定“shell=True”,否则成功执行后,并没有产生hex文件。同样“知其然,不知其所以然”。
下面看complierProject类的源代码:
import os, shutil
import subprocess
from PyQt5.QtWidgets import QApplication
class compileProject:
def __init__(self, win):
if win == None:
return
self.win = win #主界面作为参数传入,可以直接调用主界面的参数
#1. 编译器路径选择
if self.win.isComplierPathDefault == '1':
self.compliePath = self.win.Code51_dir+'/sdcc'
else:
self.compliePath = self.win.compilePath
#2. 编译参数选择
if self.win.isXdataDefault == '1':
self.XdataLen = '8192'
else:
self.XdataLen=self.win.XdataLen
#3. 工程文件列表
self.fileList = win.file_list
#4. 信息输出视图
self.browser=win.text_browser
self.browser.setText('')
#5. 工程名称
self.cbpDir, cbpName = os.path.split(win.fullfile)
self.outputName, extension = os.path.splitext(cbpName)
self.objFolder =self.cbpDir +'/obj'
def projectClean(self): # 删除工程目录下子文件夹《obj》里面的全部文件
if os.path.exists(self.objFolder):
try:
shutil.rmtree(self.objFolder) #强制移除空文件夹
os.mkdir(self.objFolder) #新建空文件夹
except OSError:
self.browser.append("<font color=hotpink>%s </font> " %
"文件夹 {0} 被占用。".format(self.objFolder))
def CreatObjFolder(self): # 传入工程目录,在工程目录下新建obj文件夹
if not os.path.exists(self.objFolder):
os.mkdir(self.objFolder) #创建 空文件夹
def complie_fileBySDCC(self, abs_file_name): # 编译C文件
status = True
#文件列表中,c、h文件并存,跳过h文件
if '.h' in abs_file_name or '.H' in abs_file_name:
return status
(_, all_fileName)=os.path.split(abs_file_name)
(filename,extension) = os.path.splitext(all_fileName)
#1. 生成编译命令列表
sdcc_path=self.compliePath+'/bin/sdcc'
mcu_model='-mmcs51'
options1 = '--model-large'
options2 = '--opt-code-size'
options3 = '--out-fmt-ihx'
options4 = '--model-large'
options5 = '-mmcs51'
include_path='-I"'+self.compliePath+'/include"'
src_file =abs_file_name
dest_file= 'obj/' +filename+'.rel'
cmdlist=[sdcc_path, mcu_model , options1 ,options2 ,options3 ,options4 ,options5 ,
include_path , '-c', src_file ,'-o', dest_file ]
#2. 在信息输出视图显示 即将执行的命令
self.browser.append( "<font color=black>%s </font>" %" ".join(cmdlist))
QApplication.processEvents()
#3. 执行命令
p = subprocess.Popen(cmdlist, stdout=subprocess.PIPE,stderr=subprocess.PIPE,
cwd=self.cbpDir, bufsize=1)
#4. 打印命令执行结果--异常部分 (注意异常部分比正常部分高优先级,否则会陷入死循环)
for line in iter(p.stderr.readline, b''):
#print(b'stderr:'+line)
if b'warning' in line:
status = True
self.browser.append("<font color=hotpink>%s </font> " % line.decode("utf8","ignore"))
else:
status = False
self.browser.append("<font color=red>%s </font> " % line.decode("utf8","ignore"))
p.stdout.close()
p.wait()
return status
QApplication.processEvents()
#5. 打印命令执行结果--正常部分
for line in iter(p.stdout.readline, b''):
status = False
self.browser.append("<font color=red>%s </font> " % line.decode())
QApplication.processEvents()
p.stdout.close()
p.wait()
return status
def complieAllFile(self):
for file in self.fileList:
if self.complie_fileBySDCC(file) == False:
return False
return True
def LinkAllFile(self):
#1. 生成链接命令列表
sdcc_path=self.compliePath+'/bin/sdcc'
lib_path='-L"'+self.compliePath+'/lib"'
ihx_name='obj/'+self.outputName+'.ihx'
mcu_model='-mmcs51'
options1 = '--model-large'
options2 = '--xram-size'
options2_para = self.XdataLen
options3 = '--iram-size'
options3_para = str(256)
options4 = '--code-size'
options4_para = str(65536)
options5 = '--out-fmt-ihx'
rel_str =''
cmdlist=[sdcc_path,lib_path, '-o',ihx_name, mcu_model , options1 ,options2 ,options2_para,
options3 ,options3_para ,options4 ,options4_para ,options5]
#2. 链接命令中包含多个rel文件
for file in self.fileList: #提取c文件
if '.c' in file or '.C' in file:
(_, all_fileName)=os.path.split(file)
(filename,extension) = os.path.splitext(all_fileName)
rel_str = 'obj/'+filename+'.rel'
cmdlist.append(rel_str)
if len(rel_str) == 0:
return False
#3. 在信息输出视图显示 即将执行的命令
self.browser.append("<font color=black>%s </font>" %" ".join(cmdlist))
QApplication.processEvents()
#4. 执行命令
p = subprocess.Popen(cmdlist, stdout=subprocess.PIPE,stderr=subprocess.PIPE,
cwd=self.cbpDir, bufsize=1)
#5. 输出异常部分
for line in iter(p.stderr.readline, b''):
self.browser.append("<font color=red>%s </font>" % line.decode())
QApplication.processEvents()
p.stdout.close()
return False
#6. 输出正常部分 -- 实际上这里不会有信息输出,如果有,也是异常信息
for line in iter(p.stdout.readline, b''):
self.browser.append("<font color=black>%s </font>" % line.decode())
QApplication.processEvents()
p.stdout.close()
return False
p.stdout.close()
p.wait()
return True
def ihx2hex(self):
ihxFile = self.cbpDir+'/obj/'+self.outputName+'.ihx '
hexFile = self.cbpDir+'/obj/'+self.outputName+'.hex '
if os.path.exists(ihxFile):
packihx_path=self.compliePath+ '/bin/packihx'
cmdlist=[packihx_path, ihxFile, '>',hexFile ]
#_cmd = 'packihx ihxFile > hexFile'
self.browser.append(" ".join(cmdlist))
QApplication.processEvents()
p = subprocess.Popen(cmdlist, stdout=subprocess.PIPE,stderr=subprocess.PIPE,
shell=True, cwd=self.cbpDir, bufsize=1) #此处一定要加 shell参数,否则没有hex文件
#这个命令比较奇怪,正常的输出是在 p.stderr,所以如果输出结果包含 OK 就认为成功。
for line in iter(p.stderr.readline, b''):
if 'OK' in line.decode():
self.browser.append("<font color=black>%s </font>" % line.decode())
ihxFile=hexFile
break
else:
self.browser.append("<font color=red>%s </font>" % line.decode())
ihxFile = ''
QApplication.processEvents()
p.stdout.close()
p.wait()
return ihxFile
else:
return None
关于上面的代码,有3处要注意:
- 编译是有警告(warning)是允许的,比如变量声明了没有用到,不同变量类型之间赋值没有强制(显式)转换。
- 执行packihx命令时,一定要使subprocess.Popen的参数shell = True,否则显示成功了也没有hex文件产生。
- 执行packihx命令后的正常结果输出到stderr上。
1.2执行命令类complierProject的使用
在菜单–>project–>built的动作中调用了该类:
def ProjectBuilt(self):
self.fileSaveAll()
if len(self.fullfile) == 0 or os.path.exists(self.fullfile) == False:
self.text_browser.setText('工程无效!')
return False
self.compilePro = compileProject(self)
self.compilePro.projectClean()
self.compilePro.CreatObjFolder()
if self.compilePro.complieAllFile() == True:
if self.compilePro.LinkAllFile() == True:
self.Hexfile=self.compilePro.ihx2hex()
if self.Hexfile == None:
return False
return True
2.信息输出视图
在第3章中我们已经知道,信息输出视图以浮动窗口类QDockWidget作为容器,把多行文本框类QTextEdit实例化后存放到里面,只要往QTextEdit添加文字就可以实现信息输出。但是我们这个信息输出还有另外的功能:1.显示编译错误时,双击错误信息实现跳转到错误行;2.显示查找结果时,双击结果实现跳转到目标行。为了实现这个功能,我写了一个新类QtextEditClick,继承了QTextEdit。重载了2个槽函数:
1.textChangedAction()
2.mouseDoubleClickEvent(QEvent)
class QtextEditClick(QTextEdit):
TextEdit2Click_Signal = pyqtSignal()
def __init__(self, parent=None):
super(QtextEditClick, self).__init__(parent)
self.textChanged.connect(self.textChangedAction)#将槽函数链接到文本改动的信号
def mouseDoubleClickEvent(self, e):
self.TextEdit2Click_Signal.emit() #产生一个文本双击信号
def textChangedAction(self):
cursor=self.textCursor()
cursor.movePosition(QTextCursor.End)#移动鼠标至文档最后
self.setTextCursor(cursor)
QApplication.processEvents()
主界面程序把文本双击信号链接到对应的槽函数:
class MainWindow(QMainWindow):
......
self.text_browser = QtextEditClick(self) #QtextEditClick继承自QTextEdit,内置于信息输出视图
......
#给builtOutput窗口添加双击响应
self.text_browser.TextEdit2Click_Signal.connect(self.Jump2Error)
......
def Jump2Error(self):
tc =self.text_browser.textCursor()
str1 = tc.block().text()
for filename in self.file_list:
if filename in str1:#找到错误行中的文件名,判断是否已经打开
for textEdit_MSW in self.mdi.subWindowList():
textEdit=textEdit_MSW.widget()
if textEdit.filename == filename:
self.mdi.setActiveSubWindow(textEdit_MSW)
break
else:
self.loadFile(filename)
str2 = str1[len(filename)+1:] # 错误提示格式:文件名+‘:'+行数+':'+错误信息,所有文件名长度要加1
split_list=str2.split(':', 1)
textEdit=self.mdi.currentSubWindow().widget()
if textEdit != None:
textEdit.setCursorPosition(int(split_list[0])-1, 0 )
textEdit.setCaretLineBackgroundColor(Qt.lightGray)
#textEdit.show()
textEdit.setFocus()
break
查找结果的输出格式和编译错误的格式相同,是 【文件名 :行数 :该行内容】,编译错误的信息格式是【文件名 :行数 :错误信息】,可以共用上面的代码。