PYQT5:基于QsciScintilla的代码编辑器分析5--多文档编辑区介绍

这里提供本编辑器可执行文件(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》。

写完工程视图,应该进入编辑区视图了。这个项目虽然大部分完成了,期间也查过大量关于QsciScintilla的资料,可是到现在我还是对它懵懵懂懂,一知半解。真是不知道如何说起。csdn有个博主theXX_SHI 的一篇文章介绍得很好,QScintilla for Python 中文文档请前往阅读。我这里直接贴源码,源码尽量增加注释,方便读者阅读。

1.c语法的高亮显示

MyLexerCPP继承自QsciLexerCPP,字体信息由QsciScintilla传入。在这里,不同语法内容的颜色是固定的,不能更改。也可以通过对话框设置各种颜色,在第10章10–语法高亮颜色选择详讲。

class MyLexerCPP(QsciLexerCPP):
    def __init__(self,parent):
        QsciLexerCPP.__init__(self,parent)
        self.setFont(self.parent().Font)
        self.setColor(QColor(0, 0, 0))          #设置默认的字体颜色
        self.setPaper(QColor(255, 255, 255))    #设置底色
        self.setColor(QColor("#B0171F"), QsciLexerCPP.Keyword)   
        
        self.setColor(QColor("#008000"), QsciLexerCPP.CommentDoc)#文档注释 /**开头的颜色
        self.setColor(QColor("#008000"), QsciLexerCPP.Comment)#块注释 的颜色
        self.setColor(QColor("#008000"), QsciLexerCPP.CommentLine)#行注释的颜色
        self.setColor(QColor("#007f7f"), QsciLexerCPP.Number)       #数字 的颜色
        self.setColor(QColor("#ff00ff"), QsciLexerCPP.DoubleQuotedString)#双引号字符串的颜色
        self.setColor(QColor("#ff00ff"), QsciLexerCPP.SingleQuotedString)#单引号字符的颜色
        self.setColor(QColor("#be07ff"), QsciLexerCPP.PreProcessor)#预编译语句的颜色
        self.setColor(QColor("#191970"), QsciLexerCPP.Operator)
        #self.setColor(QColor("#000000"), QsciLexerCPP.Identifier)  #可识别字符的颜色,这个范围很广,包含了关键词,函数名;所以要取消这句
        self.setColor(QColor("#0000FF"), QsciLexerCPP.UnclosedString)#未完成输入的字符串的颜色

        font = QFont(self.parent().Font)
        font.setBold(True)
        self.setFont(font,5)    #默认的字体加粗。
        
        font = QFont(self.parent().Font)
        font.setItalic(True)        
        self.setFont(font,QsciLexerCPP.Comment)   #注释的字体用斜体。

2.页边设置和自动补全设置

SciTextEdit继承自QsciScintilla,传入参数有文件名,主窗口。

class SciTextEdit(QsciScintilla):
    NextId = 1

    def __init__(self, filename='', wins=None, parent=None):
        global g_allFuncList
        super(QsciScintilla, self).__init__(parent)
        self.win=wins
        self.jumpName=''
        self.list_line=[]
        self.Font = self.win.EditFont   #采用主窗口传入的字体
        self.Font.setFixedPitch(True)
        self.setFont(self.Font)
        #1.设置文档的编码格式为 “utf8” ,换行符为 windows   【可选linux,Mac】
        self.setUtf8(True)        
        self.setEolMode(QsciScintilla.SC_EOL_CRLF)#文件中的每一行都以EOL字符结尾(换行符为 \r \n)
        #2.设置括号匹配模式
        self.setBraceMatching(QsciScintilla.StrictBraceMatch)# 
        #3.设置 Tab 键功能
        self.setIndentationsUseTabs(True)#行首缩进采用Tab键,反向缩进是Shift +Tab
        self.setIndentationWidth(4)     #行首缩进宽度为4个空格
        self.setIndentationGuides(True)#    显示虚线垂直线的方式来指示缩进
        self.setTabIndents(True)    #编辑器将行首第一个非空格字符推送到下一个缩进级别
        self.setAutoIndent(True)    #插入新行时,自动缩进将光标推送到与前一个相同的缩进级别
        self.setBackspaceUnindents(True)
        self.setTabWidth(4)         # Tab 等于 4 个空格
        #4.设置光标
        self.setCaretWidth(2)           #光标宽度(以像素为单位),0表示不显示光标
        self.setCaretForegroundColor(QColor("darkCyan"))    #光标颜色
        self.setCaretLineVisible(True)      #是否高亮显示光标所在行
        self.setCaretLineBackgroundColor(QColor('#FFCFCF'))     #光标所在行的底色
        #5.设置页边特性。        这里有3种Margin:[0]行号    [1]改动标识   [2]代码折叠
            #5.1 设置行号
        self.setMarginsFont(self.Font)      #行号字体
        self.setMarginLineNumbers(0,True)    #设置标号为0的页边显示行号    
        self.setMarginWidth(0,'00000')  #行号宽度
        self.setMarkerForegroundColor(QColor("#FFFFFF"),0)  
            #5.2 设置改动标记    
        self.setMarginType(1, QsciScintilla.SymbolMargin)   # 设置标号为1的页边用于显示改动标记 
        self.setMarginWidth(1, "0000")          #改动标记占用的宽度
        img = QPixmap(":/leftside.png")     #改动标记图标,大小是48 x 48
        sym_1 = img.scaled(QSize(16, 16))       #图标缩小为 16 x 16
        self.markerDefine(sym_1, 0)     
        self.setMarginMarkerMask(1, 0b1111)
        self.setMarkerForegroundColor(QColor("#ee1111"),1)  #00ff00    
            #5.3  设置代码自动折叠区域
        self.setFolding(QsciScintilla.PlainFoldStyle)
        self.setMarginWidth(2,12)
                #5.3.1 设置代码折叠和展开时的页边标记 - +
        self.markerDefine(QsciScintilla.Minus, QsciScintilla.SC_MARKNUM_FOLDEROPEN)
        self.markerDefine(QsciScintilla.Plus, QsciScintilla.SC_MARKNUM_FOLDER)
        self.markerDefine(QsciScintilla.Minus, QsciScintilla.SC_MARKNUM_FOLDEROPENMID)
        self.markerDefine(QsciScintilla.Plus, QsciScintilla.SC_MARKNUM_FOLDEREND)
                #5.3.2 设置代码折叠后,+ 的颜色FFFFFF
        self.setMarkerBackgroundColor(QColor("#FFBCBC"), QsciScintilla.SC_MARKNUM_FOLDEREND)
        self.setMarkerForegroundColor(QColor("red"), QsciScintilla.SC_MARKNUM_FOLDEREND)
    
        #6.语法高亮显示
            #6.1语法高亮的设置见 MyLexerCPP类 源码
        self.lexer=MyLexerCPP(self)
        self.setLexer(self.lexer)
            #6.2设置自动补全
        self.mod=False
        self.__api = QsciAPIs(self.lexer)
            # SDCC编译器的关键字 列表
        sdcc_kwlist=['__data','__idata','__pdata','__xdata','__code','__bit','__sbit',
                        '__sfr' , 'u8', 'u16' , 'WORD', 'BYTE','define' , 'include','__interrupt',  
                        'auto' , 'double' , 'int' , 'struct' , 'break' , 'else' , 'long' , 
                        'switch' , 'case','enum' , 'register' , 'typedef' , 'default' , 
                        'char' , 'extern' , 'return' , 'union' , 'const' , 'float',
                       'short' , 'unsigned' , 'continue' , 'for' , 'signed' , 'void' , 
                       'goto','sizeof' , 'volatile' , 'do' , 'while' , 'static' , 'if']
        autocompletions = keyword.kwlist+sdcc_kwlist
        for ac in autocompletions:
            self.__api.add(ac)
        self.__api.prepare()
        self.autoCompleteFromAll()
        self.setAutoCompletionSource(QsciScintilla.AcsAll) #自动补全所以地方出现的
        self.setAutoCompletionCaseSensitivity(True) #设置自动补全大小写敏感
        self.setAutoCompletionThreshold(1);     #输入1个字符,就出现自动补全 提示
        self.setAutoCompletionReplaceWord(False)
        self.setAutoCompletionUseSingle(QsciScintilla.AcusExplicit)
        self.setAttribute(Qt.WA_DeleteOnClose)
        #设置函数名为关键字2    KeyWord = sdcc_kwlistcc  ;KeywordSet2 = 函数名
        self.SendScintilla(QsciScintilla.SCI_SETKEYWORDS, 0," ".join(sdcc_kwlist).encode(encoding='utf-8'))
        self.SendScintilla(QsciScintilla.SCI_STYLESETFORE, QsciLexerCPP.KeywordSet2, 0x7f0000)
        self.SendScintilla(QsciScintilla.SCI_SETKEYWORDS, 1," ".join(g_allFuncList).encode(encoding='utf-8'))
                
        self.filename = filename
        if self.filename=='':
            self.filename = str("未命名-{0}".format(SciTextEdit.NextId))
            SciTextEdit.NextId += 1
        self.setModified(False)
        #设置文档窗口的标题
        self.setWindowTitle(QFileInfo(self.filename).fileName())
        #将槽函数链接到文本改动的信号
        self.textChanged.connect(self.textChangedAction)
        #给文档窗口添加右键菜单
        self.setContextMenuPolicy(Qt.CustomContextMenu)#
        self.customContextMenuRequested.connect(self.RightMenu)

3.文本改动标记–在页边显示改动标记

重写textChangedAction函数,该函数是关联了文档内容改变的信号的槽函数,当内容变化时就自动触发这个函数。 保存文件时消除改动标记。
改动标记
回看上一节class SciTextEdit().init()与改动标识相关的页边设置源码:

        self.setMarginType(1, QsciScintilla.SymbolMargin)   # 设置标号为1的页边用于显示改动标记 
        self.setMarginWidth(1, "0000")          #改动标记占用的宽度
        img = QPixmap(":/leftside.png")     #改动标记图标,大小是48 x 48
        sym_1 = img.scaled(QSize(16, 16))       #图标缩小为 16 x 16
        self.markerDefine(sym_1, 0)     
        self.setMarginMarkerMask(1, 0b1111)
        self.setMarkerForegroundColor(QColor("#ee1111"),1)  #00ff00              

编辑区文档内容变动信号触发的槽函数:


    def textChangedAction(self):
        line, index=self.getCursorPosition()    #获取当前光标所在行
        handle_01 = self.markerAdd(line, 0)     # 添加改动标记
        self.list_line.append(handle_01)        #保存改动标记所在的地方,给后面保存文档时消除标记提供信息

在save()函数中消除改动标记:

    def save(self):
        ......
        #将保存过的文件消除改动标识,清空记录改动的列表
        for line in self.list_line:
            self.markerDeleteHandle(line)
        self.list_line.clear()

4.跳转到函数体和打开包含文件

在编辑区右击文字时,将判断所在行是否有包含文件,或者光标所在的字符串是否是函数名,然后显示右键菜单,进行下一步操作。(这里有2个细节在下一章《获取c文件的函数名列表》中讲述:1.获取光标所在单词;2.如何知道函数体在哪个文件哪一行。)

    def RightMenu(self):
        line_num, index=self.getCursorPosition()
        text=self.text()
        #0.获取光标所在行的全部字符
        str1=text.splitlines(False)[line_num]
        if len(str1) != 0:
            #0.1 如果字符串有 ‘#in’ ,那么应该包含文件名
            if '#in' in str1:     
                self.jumpName = self.getIncludeFile(str1)
            #0.2 如果含有 字符 (  ,那么应该有函数名
            elif '(' in str1:  
                self.jumpName= self.getFuncNameInLine(str1)
        #1.Jump to
        self.popMenu = QMenu()
        Jump2Function=QAction('Jump to '+self.jumpName, self)
        self.popMenu.addAction(Jump2Function)
        Jump2Function.triggered.connect(self.do_Jump2Function)
        #setEnabled   isRedoAvailable  isUndoAvailable
        #2.undo    
        undoAction =QAction('Undo', self)
        undoAction.triggered.connect(self.undo)
        undoAction.setEnabled(self.isUndoAvailable())
        self.popMenu.addAction(undoAction)
        #3.redo 
        redoAction =QAction('Redo', self)
        redoAction.triggered.connect(self.redo)
        redoAction.setEnabled(self.isRedoAvailable())
        self.popMenu.addAction(redoAction)
        #4.copy      
        copyAction =QAction('Copy', self)
        copyAction.triggered.connect(self.copy)
        self.popMenu.addAction(copyAction)
        #5.cut 
        cutAction =QAction('Cut', self)
        cutAction.triggered.connect(self.cut)
        self.popMenu.addAction(cutAction)
        #6.paste
        pasteAction =QAction('Paste', self)
        pasteAction.triggered.connect(self.paste)
        self.popMenu.addAction(pasteAction)
         #7.在鼠标位置显示右菜单    
        self.popMenu.exec_(QCursor.pos())

因为跳转涉及到其它文档,必须向主窗口发送信号。先在主窗口类声明2个信号:

class MainWindow(QMainWindow):
    Jump2Func_Signal = pyqtSignal(str)
    Jump2IncludeFile_Signal = pyqtSignal(str)
    ......

将信号和槽函数关联起来:

         #给SciTextEdit 文档编辑窗口右键菜单 发过来的消息
        self.Jump2Func_Signal[str].connect(self.do_Jump2Function)#右键跳转到函数  
        self.Jump2IncludeFile_Signal[str].connect(self.do_Jump2IncludeFile)#右键跳转到包含文件

然后看看跳转函数是怎么实现的,代码中比较难理解的是字典self.fileFuncDict 的数据结构(也在下一章《获取c文件的函数名列表》详解):

    def do_Jump2Function(self, str1):   #传入参数str1 为函数名      
        for file in self.file_list:         #查找文件列表中的c文件
            if file[-2: ]=='.c' or  file[-2: ]=='.C' :  
                [dirname,filename]=os.path.split(file)
                _dict=self.fileFuncDict[filename]   #这个字典是存放文件名,函数名,函数所在行信息的。
                funcs=list(_dict.keys())
                for func in funcs:
                    if func == str1:    #1. 字典中的函数名和传参相等,则查找结束,跳转
                        for textEdit_MSW in self.mdi.subWindowList(): #1.1 遍历已打开文档,判断函数所在的文件是否已经打开
                            textEdit=textEdit_MSW.widget()
                            if textEdit.filename == file:
                                self.mdi.setActiveSubWindow(textEdit_MSW)
                                break
                        else:   #1.2   确认该文件还没有打开,需要打开
                            self.loadFile(file) 
                                    
                        textEdit=self.mdi.currentSubWindow().widget()
                        #2.跳转到该函数名所在的文件的行数
                        if textEdit != None:
                            # self.fileFuncDict[filename][str1] 就是行数,在字典中先查文件名,再查函数名
                            textEdit.setCursorPosition(self.fileFuncDict[filename][str1], 0 ) #设置光标的行数,列数
                            textEdit.setCaretLineBackgroundColor(Qt.lightGray)  #光标所在行显示灰色
                            textEdit.setFocus()                 #当前文档赋给焦点                            
                        return

看看打开包含文件的代码:

    def do_Jump2IncludeFile(self, str1):
        end = len(str1)-1
        start = 1
        if str1[0] == '"':  # "driver/STC8F.h"  可能1
            if str1[1]=='.': # "./driver/STC8F.h"   可能2
                start = 3
            elif str1[1]=="\\" or str1[1]=="/" :# "/driver/STC8F.h"     可能3
                start = 2
            else:
                pass            
            textEdit = self.mdi.activeSubWindow()
            textEdit=textEdit.widget()
            if textEdit is None :
                return
            _fdir,_fename=os.path.split(textEdit.filename)
            fileName = _fdir+'/'+str1[start:end]            
        elif  str1[0] == '<':   #系统的包含文字在编译器目录下的 '/include/mcs51/'
            _fdir,_fename=os.path.split(str1[1:end])
            fileName = self.compilePath+'/include/mcs51/'+_fename 
        else:
            return
        self.loadFile(fileName)
posted @ 2023-03-10 15:22  汉塘阿德  阅读(142)  评论(0编辑  收藏  举报  来源