如何在 Qt 中的 QTextEdit 中显示文本后面或前景的图形对象?

How to display graphics objects behind or foreground of text inside QTextEdit in Qt?

我想在我选择的单词后面显示一个矩形,就像 Qt Creator 在这里所做的那样:

我正在试验 QSyntaxHighlighter 的例子。我能够根据关键字模式更改样式。我想要自定义自动完成列表的图形或小部件。

对于自动完成,请遵循 Custom Completer Example or the Completer Example

下面的代码是第一个代码,我毫不掩饰地复制并集成到 BackgroundHighlighter class 和 main.cpp.


这个答案将包含一个项目中的五个文件以及一个 Qt 资源文件。

  1. highlighter.h(语法的荧光笔 Class)
  2. highlighter.cpp
  3. backgroundHighlighter.h(背景高光Class)
  4. backgroundHighlighter.cpp
  5. main.cpp
  6. res.qrc(可选,不需要,您可以对文本进行硬编码)
  7. res(目录)(可选)
  8. |- symbols.txt(可选,您可以设置自己的默认文本)
  9. |- wordlist.txt(可选,从example but you could use your own line-delimited word list and set this in main.cpp with a QStringListModel复制)

请注意,(1) 和 (2) 的 Highlighter class 的实现可以在 Qt Syntax Highlighter Example 中找到。我将把它的实现留作 reader.

的练习

在调用 BackgroundHighlighter class 时,可以将文件名传递给它以从文件中加载文本。 (这不在 OP 的规范中,但由于我要测试的文本量很大,因此实现起来很方便。)

另请注意,我将 Custom Completer Example 集成到 class 中。

这里是 backgroundHighlighter.h (3)(~45 行,~60 行加上补全):

#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

这里是 backgroundHighlighter.cpp (4)(~160 行,~250 行加补全):

#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 Whosebug user. Please upvote the answer "
                              "at ";

        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);
    }
}

最后,这里是 main.cpp (5)(~10 行,~45 行加补全)

#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();
}

res.qrc 中添加 / 前缀并从 res/ 子目录添加文件(res/symbols.txtres/wordlist.txt)。

我用一个 symbols.txt 文件测试过

symbol1 symbol2 symbol3 symbol4 symbol5
symbol1 symbol2 symbol3 symbol4 symbol5
symbol1 symbol2 symbol3 symbol4 symbol5
// ... ditto 500 lines

大约需要 1 秒,这可能不太理想(100 毫秒可能更理想)。

但是,您可能希望在行数增长时对其进行观察。对于 1000 行的相同文本文件,程序将开始花费大约。 3 秒突出显示。

请注意...我还没有完全优化它。可能有更好的实现,当符号滚动到用户视图时,格式化。这只是一个建议。不知道怎么实现。


备注

  • 作为参考,我在 github 上附加了 symbols.txt 和 wordlist.txt。
  • 如果要更改格式的背景颜色,请转至 backgroundhighlighter.cpp 的第 27 至 29 行。在那里,你可以看到我集中了格式。
  • BackgroundHighlighter::clearHighlights() 可能会清除最初添加的任何背景突出显示,因为它将整个文档的字符背景设置为默认格式。这可能是结果的意外后果。