├── .gitignore ├── CustomTextEditor.qml ├── README.md ├── highlighter.cpp ├── highlighter.h ├── main.cpp ├── main.qml ├── qml-editor.pro ├── qml.qrc └── screenshots └── qmlTextEditor.png /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | qml-editor.pro.user -------------------------------------------------------------------------------- /CustomTextEditor.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.5 3 | import QtQuick.Controls.Material 2.3 4 | 5 | Item { 6 | id: root 7 | property string fontFamily: "Consolas" 8 | property var fontPointSize: 15 9 | property alias readOnly: edit.readOnly 10 | property alias text: edit.text 11 | property alias selectionColor: edit.selectionColor 12 | property alias textDocument: edit.textDocument 13 | function append(txt) { 14 | edit.append(txt) 15 | } 16 | Flickable { 17 | id: flick 18 | focus: false 19 | anchors { 20 | fill: parent 21 | } 22 | contentWidth: edit.paintedWidth 23 | contentHeight: edit.paintedHeight + 5 24 | clip: true 25 | boundsBehavior:Flickable.StopAtBounds 26 | function ensureVisible(r) { 27 | if (contentX >= r.x) 28 | contentX = r.x; 29 | else if (contentX+width <= r.x+r.width) 30 | contentX = r.x+r.width-width; 31 | if (contentY >= r.y) 32 | contentY = r.y; 33 | else if (contentY+height <= r.y+r.height) 34 | contentY = r.y+r.height-height + 10; 35 | } 36 | Rectangle { 37 | id: textBg 38 | z: 0 39 | anchors.left: edit.left 40 | anchors.leftMargin: 5 41 | color: "#333" 42 | opacity: 0.2 43 | radius: 4 44 | Behavior on width { 45 | NumberAnimation { duration: 1000; easing.type: Easing.OutElastic } 46 | } 47 | Behavior on height { 48 | NumberAnimation { duration: 1000; easing.type: Easing.OutElastic } 49 | } 50 | } 51 | Column{ 52 | id:lineNumberLabel 53 | anchors { 54 | left: parent.left 55 | } 56 | Repeater { 57 | model: edit.lineCount; 58 | Rectangle { 59 | width: lineNumberWidth(edit.lineCount) 60 | height: panding.contentHeight 61 | color: "#333" 62 | Text { 63 | id:showLineNumber 64 | anchors{ 65 | bottom:parent.bottom 66 | bottomMargin: 1 67 | horizontalCenter: parent.horizontalCenter 68 | } 69 | text:index + 1 70 | color: "gray" 71 | font.pointSize: fontPointSize 72 | font.family: fontFamily 73 | } 74 | } 75 | } 76 | } 77 | TextEdit{ 78 | id: panding 79 | font.pointSize: fontPointSize 80 | visible: false 81 | font.family: fontFamily 82 | text: " " 83 | } 84 | TextEdit { 85 | property bool ctrlPressed: false 86 | anchors { 87 | left:lineNumberLabel.right 88 | leftMargin: -4 89 | } 90 | id: edit 91 | readOnly: root.readOnly 92 | selectByMouse: true 93 | tabStopDistance: 20 94 | activeFocusOnPress: true 95 | focus: true 96 | clip: true 97 | selectionColor: Material.accent 98 | wrapMode: TextEdit.WordWrap 99 | leftPadding: 5 100 | topPadding: 0.5 101 | font.pointSize: fontPointSize 102 | font.family: fontFamily 103 | width: flick.width - 10 104 | height: edit.contentHeight > flick.height ? 105 | edit.contentHeight : flick.height 106 | anchors.margins: 10 107 | cursorVisible: true 108 | cursorDelegate: cursorDelegate 109 | onPaintedWidthChanged: { 110 | textBg.width = edit.paintedWidth + 10 111 | } 112 | onPaintedHeightChanged: { 113 | textBg.height = edit.paintedHeight + 1 114 | } 115 | onCursorRectangleChanged: flick.ensureVisible(cursorRectangle) 116 | MouseArea { 117 | anchors { 118 | fill: parent 119 | } 120 | propagateComposedEvents: true 121 | onClicked: mouse.accepted = false; 122 | onPressed: mouse.accepted = false; 123 | onReleased: mouse.accepted = false; 124 | onDoubleClicked: mouse.accepted = false; 125 | onPositionChanged: mouse.accepted = false; 126 | onPressAndHold: mouse.accepted = false; 127 | cursorShape: Qt.IBeamCursor 128 | onWheel: { 129 | var datl = wheel.angleDelta.y / 120 130 | if (datl>0 && edit.ctrlPressed) { 131 | fontPointSize += 1 132 | } else if (datl<0 && edit.ctrlPressed) { 133 | fontPointSize -= 1 134 | } 135 | wheel.accepted = false 136 | } 137 | } 138 | Keys.onPressed: { 139 | if(event.modifiers === Qt.ControlModifier) { 140 | ctrlPressed = true 141 | } 142 | event.accepted = false 143 | } 144 | Keys.onReleased: { 145 | if(!(event.modifiers&Qt.ControlModifier)) { 146 | ctrlPressed = false 147 | } 148 | event.accepted = false 149 | } 150 | } 151 | ScrollIndicator.horizontal: ScrollIndicator { } 152 | ScrollIndicator.vertical: ScrollIndicator { } 153 | } 154 | Component { 155 | id: cursorDelegate 156 | Rectangle { 157 | id: cursor 158 | color: Material.accent 159 | width: 2; 160 | height: 5 161 | SequentialAnimation { 162 | running: true; 163 | loops: ColorAnimation.Infinite 164 | NumberAnimation { 165 | easing { 166 | type: Easing.InQuint 167 | } 168 | property: "opacity" 169 | target: cursor; from: 1.0; to: 0.0; duration: 800; 170 | } 171 | NumberAnimation { 172 | easing { 173 | type: Easing.InQuint 174 | } 175 | property: "opacity" 176 | target: cursor; 177 | from: 0.0; 178 | to: 1.0; 179 | duration: 800; 180 | } 181 | } 182 | Behavior on x { 183 | SpringAnimation { spring: 3; damping: 0.2 } 184 | } 185 | Behavior on y { 186 | SpringAnimation { spring: 3; damping: 0.2 } 187 | } 188 | } 189 | } 190 | function lineNumberWidth(lineCount) { 191 | var width = 1; 192 | var space = 0; 193 | while(lineCount >= 10) { 194 | lineCount /= 10; 195 | ++width; 196 | } 197 | return space = width * fontPointSize 198 | } 199 | } 200 | 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qmlTextEditor 2 | QtQuick的TextEdit控件的使用方法 3 | 4 | 实现功能如下: 5 | - 支持行号显示 6 | - 支持字体缩放 7 | - 支持语法高亮 8 | 9 | 效果图: 10 | ![image](https://github.com/MrL-Dev/qmlTextEditor/raw/master/screenshots/qmlTextEditor.png) 11 | -------------------------------------------------------------------------------- /highlighter.cpp: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2016 The Qt Company Ltd. 4 | ** Contact: https://www.qt.io/licensing/ 5 | ** 6 | ** This file is part of the examples of the Qt Toolkit. 7 | ** 8 | ** $QT_BEGIN_LICENSE:BSD$ 9 | ** Commercial License Usage 10 | ** Licensees holding valid commercial Qt licenses may use this file in 11 | ** accordance with the commercial license agreement provided with the 12 | ** Software or, alternatively, in accordance with the terms contained in 13 | ** a written agreement between you and The Qt Company. For licensing terms 14 | ** and conditions see https://www.qt.io/terms-conditions. For further 15 | ** information use the contact form at https://www.qt.io/contact-us. 16 | ** 17 | ** BSD License Usage 18 | ** Alternatively, you may use this file under the terms of the BSD license 19 | ** as follows: 20 | ** 21 | ** "Redistribution and use in source and binary forms, with or without 22 | ** modification, are permitted provided that the following conditions are 23 | ** met: 24 | ** * Redistributions of source code must retain the above copyright 25 | ** notice, this list of conditions and the following disclaimer. 26 | ** * Redistributions in binary form must reproduce the above copyright 27 | ** notice, this list of conditions and the following disclaimer in 28 | ** the documentation and/or other materials provided with the 29 | ** distribution. 30 | ** * Neither the name of The Qt Company Ltd nor the names of its 31 | ** contributors may be used to endorse or promote products derived 32 | ** from this software without specific prior written permission. 33 | ** 34 | ** 35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 46 | ** 47 | ** $QT_END_LICENSE$ 48 | ** 49 | ****************************************************************************/ 50 | 51 | #include "highlighter.h" 52 | 53 | //! [0] 54 | Highlighter::Highlighter(QTextDocument *parent) 55 | : QSyntaxHighlighter(parent) 56 | { 57 | HighlightingRule rule; 58 | 59 | keywordFormat.setForeground(Qt::darkBlue); 60 | keywordFormat.setFontWeight(QFont::Bold); 61 | QStringList keywordPatterns; 62 | keywordPatterns << "\\bchar\\b" << "\\bclass\\b" << "\\bconst\\b" 63 | << "\\bdouble\\b" << "\\benum\\b" << "\\bexplicit\\b" 64 | << "\\bfriend\\b" << "\\binline\\b" << "\\bint\\b" 65 | << "\\blong\\b" << "\\bnamespace\\b" << "\\boperator\\b" 66 | << "\\bprivate\\b" << "\\bprotected\\b" << "\\bpublic\\b" 67 | << "\\bshort\\b" << "\\bsignals\\b" << "\\bsigned\\b" 68 | << "\\bslots\\b" << "\\bstatic\\b" << "\\bstruct\\b" 69 | << "\\btemplate\\b" << "\\btypedef\\b" << "\\btypename\\b" 70 | << "\\bunion\\b" << "\\bunsigned\\b" << "\\bvirtual\\b" 71 | << "\\bvoid\\b" << "\\bvolatile\\b" << "\\bbool\\b"; 72 | foreach (const QString &pattern, keywordPatterns) { 73 | rule.pattern = QRegularExpression(pattern); 74 | rule.format = keywordFormat; 75 | highlightingRules.append(rule); 76 | //! [0] //! [1] 77 | } 78 | //! [1] 79 | 80 | //! [2] 81 | classFormat.setFontWeight(QFont::Bold); 82 | classFormat.setForeground(Qt::darkMagenta); 83 | rule.pattern = QRegularExpression("\\bQ[A-Za-z]+\\b"); 84 | rule.format = classFormat; 85 | highlightingRules.append(rule); 86 | //! [2] 87 | 88 | //! [3] 89 | singleLineCommentFormat.setForeground(Qt::red); 90 | rule.pattern = QRegularExpression("//[^\n]*"); 91 | rule.format = singleLineCommentFormat; 92 | highlightingRules.append(rule); 93 | 94 | multiLineCommentFormat.setForeground(Qt::red); 95 | //! [3] 96 | 97 | //! [4] 98 | quotationFormat.setForeground(Qt::darkGreen); 99 | rule.pattern = QRegularExpression("\".*\""); 100 | rule.format = quotationFormat; 101 | highlightingRules.append(rule); 102 | //! [4] 103 | 104 | //! [5] 105 | functionFormat.setFontItalic(true); 106 | functionFormat.setForeground(Qt::blue); 107 | rule.pattern = QRegularExpression("\\b[A-Za-z0-9_]+(?=\\()"); 108 | rule.format = functionFormat; 109 | highlightingRules.append(rule); 110 | //! [5] 111 | 112 | //! [6] 113 | commentStartExpression = QRegularExpression("/\\*"); 114 | commentEndExpression = QRegularExpression("\\*/"); 115 | } 116 | //! [6] 117 | 118 | //! [7] 119 | void Highlighter::highlightBlock(const QString &text) 120 | { 121 | foreach (const HighlightingRule &rule, highlightingRules) { 122 | QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text); 123 | while (matchIterator.hasNext()) { 124 | QRegularExpressionMatch match = matchIterator.next(); 125 | setFormat(match.capturedStart(), match.capturedLength(), rule.format); 126 | } 127 | } 128 | //! [7] //! [8] 129 | setCurrentBlockState(0); 130 | //! [8] 131 | 132 | //! [9] 133 | int startIndex = 0; 134 | if (previousBlockState() != 1) 135 | startIndex = text.indexOf(commentStartExpression); 136 | 137 | //! [9] //! [10] 138 | while (startIndex >= 0) { 139 | //! [10] //! [11] 140 | QRegularExpressionMatch match = commentEndExpression.match(text, startIndex); 141 | int endIndex = match.capturedStart(); 142 | int commentLength = 0; 143 | if (endIndex == -1) { 144 | setCurrentBlockState(1); 145 | commentLength = text.length() - startIndex; 146 | } else { 147 | commentLength = endIndex - startIndex 148 | + match.capturedLength(); 149 | } 150 | setFormat(startIndex, commentLength, multiLineCommentFormat); 151 | startIndex = text.indexOf(commentStartExpression, startIndex + commentLength); 152 | } 153 | } 154 | //! [11] 155 | -------------------------------------------------------------------------------- /highlighter.h: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2016 The Qt Company Ltd. 4 | ** Contact: https://www.qt.io/licensing/ 5 | ** 6 | ** This file is part of the examples of the Qt Toolkit. 7 | ** 8 | ** $QT_BEGIN_LICENSE:BSD$ 9 | ** Commercial License Usage 10 | ** Licensees holding valid commercial Qt licenses may use this file in 11 | ** accordance with the commercial license agreement provided with the 12 | ** Software or, alternatively, in accordance with the terms contained in 13 | ** a written agreement between you and The Qt Company. For licensing terms 14 | ** and conditions see https://www.qt.io/terms-conditions. For further 15 | ** information use the contact form at https://www.qt.io/contact-us. 16 | ** 17 | ** BSD License Usage 18 | ** Alternatively, you may use this file under the terms of the BSD license 19 | ** as follows: 20 | ** 21 | ** "Redistribution and use in source and binary forms, with or without 22 | ** modification, are permitted provided that the following conditions are 23 | ** met: 24 | ** * Redistributions of source code must retain the above copyright 25 | ** notice, this list of conditions and the following disclaimer. 26 | ** * Redistributions in binary form must reproduce the above copyright 27 | ** notice, this list of conditions and the following disclaimer in 28 | ** the documentation and/or other materials provided with the 29 | ** distribution. 30 | ** * Neither the name of The Qt Company Ltd nor the names of its 31 | ** contributors may be used to endorse or promote products derived 32 | ** from this software without specific prior written permission. 33 | ** 34 | ** 35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 46 | ** 47 | ** $QT_END_LICENSE$ 48 | ** 49 | ****************************************************************************/ 50 | 51 | #ifndef HIGHLIGHTER_H 52 | #define HIGHLIGHTER_H 53 | 54 | #include 55 | #include 56 | #include 57 | 58 | QT_BEGIN_NAMESPACE 59 | class QTextDocument; 60 | QT_END_NAMESPACE 61 | 62 | //! [0] 63 | class Highlighter : public QSyntaxHighlighter 64 | { 65 | Q_OBJECT 66 | 67 | public: 68 | Highlighter(QTextDocument *parent = 0); 69 | 70 | protected: 71 | void highlightBlock(const QString &text) override; 72 | 73 | private: 74 | struct HighlightingRule 75 | { 76 | QRegularExpression pattern; 77 | QTextCharFormat format; 78 | }; 79 | QVector highlightingRules; 80 | 81 | QRegularExpression commentStartExpression; 82 | QRegularExpression commentEndExpression; 83 | 84 | QTextCharFormat keywordFormat; 85 | QTextCharFormat classFormat; 86 | QTextCharFormat singleLineCommentFormat; 87 | QTextCharFormat multiLineCommentFormat; 88 | QTextCharFormat quotationFormat; 89 | QTextCharFormat functionFormat; 90 | }; 91 | //! [0] 92 | 93 | #endif // HIGHLIGHTER_H 94 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "highlighter.h" 5 | 6 | template T childObject(QQmlApplicationEngine& engine, 7 | const QString& objectName, 8 | const QString& propertyName) { 9 | QList rootObjects = engine.rootObjects(); 10 | foreach (QObject* object, rootObjects) 11 | { 12 | QObject* child = object->findChild(objectName); 13 | if (child != nullptr) 14 | { 15 | std::string s = propertyName.toStdString(); 16 | QObject* object = child->property(s.c_str()).value(); 17 | Q_ASSERT(object != nullptr); 18 | T prop = dynamic_cast(object); 19 | Q_ASSERT(prop != 0); 20 | return prop; 21 | } 22 | } 23 | return nullptr; 24 | } 25 | 26 | int main(int argc, char *argv[]) 27 | { 28 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 29 | 30 | QGuiApplication app(argc, argv); 31 | 32 | QQmlApplicationEngine engine; 33 | const QUrl url(QStringLiteral("qrc:/main.qml")); 34 | QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 35 | &app, [url](QObject *obj, const QUrl &objUrl) { 36 | if (!obj && url == objUrl) 37 | QCoreApplication::exit(-1); 38 | }, Qt::QueuedConnection); 39 | engine.load(url); 40 | 41 | QQuickTextDocument* doc = 42 | childObject(engine, "editor", "textDocument"); 43 | Q_ASSERT(doc != nullptr); 44 | Highlighter* parser = new Highlighter(doc->textDocument()); 45 | // highlighter = new Highlighter(editor->document()); 46 | 47 | return app.exec(); 48 | } 49 | -------------------------------------------------------------------------------- /main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.12 2 | import QtQuick.Controls 2.5 3 | import QtQuick.Controls.Material 2.3 4 | 5 | ApplicationWindow { 6 | id: root 7 | objectName: "root" 8 | visible: true 9 | width: 640 10 | height: 480 11 | title: qsTr("QML Text Editor") 12 | Material.accent: Material.Blue 13 | CustomTextEditor { 14 | id: editor 15 | objectName: "editor" 16 | anchors.fill: parent 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /qml-editor.pro: -------------------------------------------------------------------------------- 1 | QT += quick 2 | CONFIG += c++11 3 | 4 | # The following define makes your compiler emit warnings if you use 5 | # any Qt feature that has been marked deprecated (the exact warnings 6 | # depend on your compiler). Refer to the documentation for the 7 | # deprecated API to know how to port your code away from it. 8 | DEFINES += QT_DEPRECATED_WARNINGS 9 | 10 | # You can also make your code fail to compile if it uses deprecated APIs. 11 | # In order to do so, uncomment the following line. 12 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 13 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 14 | 15 | SOURCES += \ 16 | highlighter.cpp \ 17 | main.cpp 18 | 19 | RESOURCES += qml.qrc 20 | 21 | # Additional import path used to resolve QML modules in Qt Creator's code model 22 | QML_IMPORT_PATH = 23 | 24 | # Additional import path used to resolve QML modules just for Qt Quick Designer 25 | QML_DESIGNER_IMPORT_PATH = 26 | 27 | # Default rules for deployment. 28 | qnx: target.path = /tmp/$${TARGET}/bin 29 | else: unix:!android: target.path = /opt/$${TARGET}/bin 30 | !isEmpty(target.path): INSTALLS += target 31 | 32 | HEADERS += \ 33 | highlighter.h 34 | -------------------------------------------------------------------------------- /qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | CustomTextEditor.qml 5 | 6 | 7 | -------------------------------------------------------------------------------- /screenshots/qmlTextEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrL-Dev/qmlTextEditor/de04044dff8565041ccbd5f495377b60bcb33631/screenshots/qmlTextEditor.png --------------------------------------------------------------------------------