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处要注意:

  1. 编译是有警告(warning)是允许的,比如变量声明了没有用到,不同变量类型之间赋值没有强制(显式)转换。
  2. 执行packihx命令时,一定要使subprocess.Popen的参数shell = True,否则显示成功了也没有hex文件产生。
  3. 执行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
        

查找结果的输出格式和编译错误的格式相同,是 【文件名 :行数 :该行内容】,编译错误的信息格式是【文件名 :行数 :错误信息】,可以共用上面的代码。

posted @ 2019-11-21 10:22  汉塘阿德  阅读(25)  评论(0编辑  收藏  举报  来源