Как отобразить графические объекты позади или на переднем плане текста внутри QTextEdit в Qt? - PullRequest
0 голосов
/ 15 ноября 2018

Я хотел бы отобразить прямоугольник за словом, которое я выбрал, как это делает Qt Creator: QtCreator does this when I select a word.

Я экспериментирую с примером QSyntaxHighlighter. Я могу изменить стили на основе шаблонов ключевых слов. Я хотел бы иметь графику или виджеты для пользовательских списков автозаполнения.

1 Ответ

0 голосов
/ 17 ноября 2018

Для автозаполнения следуйте Custom Completer Example или Completer Example.

Код, приведенный ниже, следует за первым, который я нагло, без стеснения скопировал и интегрировал в класс BackgroundHighlighter и main.cpp.


Этот ответ будет содержать пять файлов в проекте вместе с файлом ресурсов Qt.

  1. highlighter.h (Класс подсветки для синтаксиса)
  2. highlighter.cpp
  3. backgroundHighlighter.h (BackgroundHighlighter Class)
  4. backgroundHighlighter.cpp
  5. main.cpp
  6. res.qrc (необязательно, не нужно, вы можете жестко закодировать текст)
  7. res (каталог) (необязательно)
  8. |- symbols.txt (необязательно, вы можете установить свой собственный текст по умолчанию)
  9. |- wordlist.txt (необязательно, скопировано из пример , но вы можете использовать собственный список слов с разделителями строк и установить его в main.cpp с QStringListModel)

Обратите внимание, что реализацию класса Highlighter для (1) и (2) можно найти в примере Qt Synx Highlighter. Я оставлю его реализацию в качестве упражнения для читателя.

При вызове класса BackgroundHighlighter можно передать ему имя файла для загрузки текста из файла. (Этого не было в спецификации ОП, но его было удобно реализовать из-за большого количества текста, который я хотел проверить.)

Также обратите внимание, что я интегрировал Custom Completer Example в класс.

Вот 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 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);
    }
}

Наконец, вот 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/symbols.txt, res/wordlist.txt) из подкаталога res/.

Я тестировал файл symbols.txt, напоминающий

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

Это займет около 1 секунды, что, вероятно, не идеально (100 мс, вероятно, более идеально).

Однако вы можете следить за количеством строк по мере их роста. При том же текстовом файле в 1000 строк программа начнет занимать ок. 3 секунды для выделения.

Обратите внимание, что ... я не оптимизировал полностью . Возможно, может быть лучшая реализация, которая форматирует только , когда символ прокручивается в поле зрения пользователя . Это всего лишь предложение. Как это реализовать, я не знаю.


Примечания

  • Для справки, я прикрепил symbols.txt и wordlist.txt на github .
  • Если вы хотите изменить цвет фона форматирования, перейдите к строкам с 27 по 29 backgroundhighlighter.cpp. Там видно, что я централизовал форматирование.
  • BackgroundHighlighter::clearHighlights() может убрать любые изначально выделенные фоновые блики, поскольку он устанавливает фоновый символ ВСЕГО документа в формат по умолчанию. Это может быть непреднамеренным следствием результата.
...