For autocompletion follow the Custom Completer Example or the Completer Example.
The code below follows the first one, which I blatantly, unashamedly copied and integrated into the BackgroundHighlighter class and main.cpp.
This answer will contain five files within a project along with a Qt Resource File.
highlighter.h (Highlighter Class for Syntax) highlighter.cpp backgroundHighlighter.h (BackgroundHighlighter Class) backgroundHighlighter.cpp main.cpp res.qrc (optional, not needed, you can hardcode your text) res (directory) (optional) |- symbols.txt (optional, you can set your own default text) |- wordlist.txt (optional, copied from example but you could use your own line-delimited word list and set this in main.cpp with a QStringListModel)
Note that the implementation of the Highlighter class for (1) and (2) can be found in the Qt Syntax Highlighter Example. I will leave its implementation as an exercise for the reader.
In calling the BackgroundHighlighter class, one can pass it a file name to load text from a file. (This wasn't in the OP's specification, but was convenient to implement due to the large amount of text I wanted to test.)
Also note that I integrated the Custom Completer Example into the class.
Here's backgroundHighlighter.h (3) (~45 lines, ~60 lines with completer):
#ifndef BACKGROUNDHIGHLIGHTER_H #define BACKGROUNDHIGHLIGHTER_H #include <QtWidgets> #include <QtGui> // this is the file to your highlighter #include "myhighlighter.h" class BackgroundHighlighter : public QTextEdit { Q_OBJECT public: BackgroundHighlighter(const QString &fileName = QString(), QWidget *parent = nullptr); void loadFile(const QString &fileName); void setCompleter(QCompleter *completer); QCompleter *completer() const; protected: void keyPressEvent(QKeyEvent *e) override; void focusInEvent(QFocusEvent *e) override; public slots: void onCursorPositionChanged(); private slots: void insertCompletion(const QString &completion); private: // this is your syntax highlighter Highlighter *syntaxHighlighter; // stores the symbol being highlighted QString highlightSymbol; // stores the position (front of selection) where the cursor was originally placed int mainHighlightPosition; // stores character formats to be used QTextCharFormat mainFmt; // refers to format block directly under the cursor QTextCharFormat subsidiaryFmt; // refers to the formatting blocks on matching words QTextCharFormat defaultFmt; // refers to the default format of the **entire** document which will be used in resetting the format void setWordFormat(const int &position, const QTextCharFormat &format); void runHighlight(); void clearHighlights(); void highlightMatchingSymbols(const QString &symbol); // completer, copied from example QString textUnderCursor() const; QCompleter *c; }; #endif // BACKGROUNDHIGHLIGHTER_H
And here's backgroundHighlighter.cpp (4) (~160 lines, ~250 lines with completer):
#include "backgroundhighlighter.h" #include <QDebug> // constructor BackgroundHighlighter::BackgroundHighlighter(const QString &fileName, QWidget *parent) : QTextEdit(parent) { // I like Monaco setFont(QFont("Monaco")); setMinimumSize(QSize(500, 200)); // load initial text from a file OR from a hardcoded default if (!fileName.isEmpty()) loadFile(fileName); else { QString defaultText = "This is a default text implemented by " "a stackoverflow user. Please upvote the answer " "at https://stackoverflow.com/a/53351512/10239789."; setPlainText(defaultText); } // set the highlighter here QTextDocument *doc = document(); syntaxHighlighter = new Highlighter(doc); // TODO change brush/colours to match theme mainFmt.setBackground(Qt::yellow); subsidiaryFmt.setBackground(Qt::lightGray); defaultFmt.setBackground(Qt::white); // connect the signal to our handler connect(this, &QTextEdit::cursorPositionChanged, this, &BackgroundHighlighter::onCursorPositionChanged); } // convenience function for reading a file void BackgroundHighlighter::loadFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) return; // the file could be in Plain Text OR Html setText(file.readAll()); } void BackgroundHighlighter::setCompleter(QCompleter *completer) { if (c) QObject::disconnect(c, 0, this, 0); c = completer; if (!c) return; c->setWidget(this); c->setCompletionMode(QCompleter::PopupCompletion); c->setCaseSensitivity(Qt::CaseInsensitive); QObject::connect(c, SIGNAL(activated(QString)), this, SLOT(insertCompletion(QString))); } QCompleter *BackgroundHighlighter::completer() const { return c; } void BackgroundHighlighter::keyPressEvent(QKeyEvent *e) { if (c && c->popup()->isVisible()) { // The following keys are forwarded by the completer to the widget switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: case Qt::Key_Escape: case Qt::Key_Tab: case Qt::Key_Backtab: e->ignore(); return; // let the completer do default behavior default: break; } } bool isShortcut = ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_E); // CTRL+E if (!c || !isShortcut) // do not process the shortcut when we have a completer QTextEdit::keyPressEvent(e); const bool ctrlOrShift = e->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier); if (!c || (ctrlOrShift && e->text().isEmpty())) return; static QString eow("~!@#$%^&*()_+{}|:\"<>?,./;'[]\\-="); // end of word bool hasModifier = (e->modifiers() != Qt::NoModifier) && !ctrlOrShift; QString completionPrefix = textUnderCursor(); if (!isShortcut && (hasModifier || e->text().isEmpty()|| completionPrefix.length() < 3 || eow.contains(e->text().right(1)))) { c->popup()->hide(); return; } if (completionPrefix != c->completionPrefix()) { c->setCompletionPrefix(completionPrefix); c->popup()->setCurrentIndex(c->completionModel()->index(0, 0)); } QRect cr = cursorRect(); cr.setWidth(c->popup()->sizeHintForColumn(0) + c->popup()->verticalScrollBar()->sizeHint().width()); c->complete(cr); // pop it up! } void BackgroundHighlighter::focusInEvent(QFocusEvent *e) { if (c) c->setWidget(this); QTextEdit::focusInEvent(e); } // convenience function for setting a `charFmt` at a `position` void BackgroundHighlighter::setWordFormat(const int &position, const QTextCharFormat &charFmt) { QTextCursor cursor = textCursor(); cursor.setPosition(position); cursor.select(QTextCursor::WordUnderCursor); cursor.setCharFormat(charFmt); } // this will handle the `QTextEdit::cursorPositionChanged()` signal void BackgroundHighlighter::onCursorPositionChanged() { // if cursor landed on different format, the `currentCharFormat` will be changed // we need to change it back to white setCurrentCharFormat(defaultFmt); // this is the function you're looking for runHighlight(); } void BackgroundHighlighter::insertCompletion(const QString &completion) { if (c->widget() != this) return; QTextCursor tc = textCursor(); int extra = completion.length() - c->completionPrefix().length(); tc.movePosition(QTextCursor::Left); tc.movePosition(QTextCursor::EndOfWord); tc.insertText(completion.right(extra)); setTextCursor(tc); } QString BackgroundHighlighter::textUnderCursor() const { QTextCursor tc = textCursor(); tc.select(QTextCursor::WordUnderCursor); return tc.selectedText(); } /** * BRIEF * Check if new highlighting is needed * Clear previous highlights * Check if the word under the cursor is a symbol (i.e. matches ^[A-Za-z0-9_]+$) * Highlight all relevant symbols */ void BackgroundHighlighter::runHighlight() { // retrieve cursor QTextCursor cursor = textCursor(); // retrieve word under cursor cursor.select(QTextCursor::WordUnderCursor); QString wordUnder = cursor.selectedText(); qDebug() << "Word Under Cursor:" << wordUnder; // get front of cursor, used later for storing in `highlightPositions` or `mainHighlightPosition` int cursorFront = cursor.selectionStart(); // if the word under cursor is the same, then save time // by skipping the process if (wordUnder == highlightSymbol) { // switch formats setWordFormat(mainHighlightPosition, subsidiaryFmt); // change previous main to subsidiary setWordFormat(cursorFront, mainFmt); // change position under cursor to main // update main position mainHighlightPosition = cursorFront; // jump the gun return; } // clear previous highlights if (mainHighlightPosition != -1) clearHighlights(); // check if selected word is a symbol if (!wordUnder.contains(QRegularExpression("^[A-Za-z0-9_]+$"))) { qDebug() << wordUnder << "is not a symbol!"; return; } // set the highlight symbol highlightSymbol = wordUnder; // store the cursor position to check later mainHighlightPosition = cursorFront; // highlight all relevant symbols highlightMatchingSymbols(wordUnder); qDebug() << "Highlight done\n\n"; } // clear previously highlights void BackgroundHighlighter::clearHighlights() { QTextCursor cursor = textCursor(); // wipe the ENTIRE document with the default background, this should be REALLY fast // WARNING: this may have unintended consequences if you have other backgrounds you want to keep cursor.select(QTextCursor::Document); cursor.setCharFormat(defaultFmt); // reset variables mainHighlightPosition = -1; highlightSymbol.clear(); } // highlight all matching symbols void BackgroundHighlighter::highlightMatchingSymbols(const QString &symbol) { // highlight background of congruent symbols QString docText = toPlainText(); // use a regex with \\b to look for standalone symbols QRegularExpression regexp("\\b" + symbol + "\\b"); // loop through all matches in the text int matchPosition = docText.indexOf(regexp); while (matchPosition != -1) { // if the position setWordFormat(matchPosition, matchPosition == mainHighlightPosition ? mainFmt : subsidiaryFmt); // find next match matchPosition = docText.indexOf(regexp, matchPosition + 1); } }
Finally, here's main.cpp (5) (~10 lines, ~45 lines with completer)
#include <QApplication> #include <backgroundhighlighter.h> QAbstractItemModel *modelFromFile(const QString& fileName, QCompleter *completer) { QFile file(fileName); if (!file.open(QFile::ReadOnly)) return new QStringListModel(completer); #ifndef QT_NO_CURSOR QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); #endif QStringList words; while (!file.atEnd()) { QByteArray line = file.readLine(); if (!line.isEmpty()) words << line.trimmed(); } #ifndef QT_NO_CURSOR QApplication::restoreOverrideCursor(); #endif return new QStringListModel(words, completer); } int main(int argc, char *argv[]) { QApplication a(argc, argv); BackgroundHighlighter bh(":/res/symbols.txt"); QCompleter *completer = new QCompleter(); completer->setModel(modelFromFile(":/res/wordlist.txt", completer)); // use this and comment the above if you don't have or don't want to use wordlist.txt // QStringListModel *model = new QStringListModel(QStringList() << "aaaaaaa" << "aaaaab" << "aaaabb" << "aaacccc", completer); // completer->setModel(model); completer->setModelSorting(QCompleter::CaseInsensitivelySortedModel); completer->setCaseSensitivity(Qt::CaseInsensitive); completer->setWrapAround(false); bh.setCompleter(completer); bh.show(); return a.exec(); }
In res.qrc add a / prefix and add files (res/symbols.txt, res/wordlist.txt) from the res/ subdirectory.
I have tested with a symbols.txt file resembling
symbol1 symbol2 symbol3 symbol4 symbol5 symbol1 symbol2 symbol3 symbol4 symbol5 symbol1 symbol2 symbol3 symbol4 symbol5 // ... ditto 500 lines
It takes about 1 second, which probably isn't ideal (100ms is probably more ideal).
However, you might want to watch over for the line count as it grows. With the same text file at 1000 lines, the program will start to take approx. 3 seconds for highlighting.
Note that... I haven't optimised it entirely. There could possibly be a better implementation which formats only when the symbol scrolls into the user's view. This is just a suggestion. How to implement it I don't know.
Notes
- For reference, I've attached symbols.txt and wordlist.txt on github.
- If you want to change the background colour of formatting, go to lines 27 to 29 of
backgroundhighlighter.cpp. There, you can see that I centralised the formatting. BackgroundHighlighter::clearHighlights() might clear away any background highlights originally added as it sets the ENTIRE document's character background to the default format. This may be an unintended consequence of the result.