Qt高亮选中的行会覆盖单个单词的高亮

Qt highlighting selected line overwrites highlighting of individual words

我有一个 QPlainTextEdit,我想突出显示用户所在的当前行以及与用户选择的词相似的所有词。此单词突出显示在除当前选定行之外的所有行上都可以正常工作,因为“选定行”背景样式会覆盖应用于选定字词的“选定字词”样式。

我的问题是如何确保在行突出显示完成后完成单词突出显示,以便它们可以同时激活?

截图说明:

黄线是高亮显示的当前行。第一个 'test' 被选中,所以所有其他的都应该应用浅蓝色背景。除了突出显示的行上的 'test' 之外的所有内容都执行。

最小可重现示例:

MainWindow.h

#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_mainWindow.h"
#include "TextEditor.h"

class mainWindow : public QMainWindow
{
    Q_OBJECT

public:
    mainWindow(QWidget *parent = Q_NULLPTR) : QMainWindow(parent)
    {
        ui.setupUi(this);
        auto textEdit = new TextEditor(this);
        textEdit->setPlainText("test lorem ipsum test\n test dolor sit test\test amet test");
        ui.tabWidget->addTab(textEdit, "Editor");
    }

private:
    Ui::mainWindowClass ui;
};

TextEditor.h

#pragma once
#include <QPlainTextEdit>

class TextEditor : public QPlainTextEdit
{
    Q_OBJECT

public:
    TextEditor(QWidget* parent) : QPlainTextEdit(parent)
    {
        connect(this, &QPlainTextEdit::selectionChanged, this, &TextEditor::selectChangeHandler);
        connect(this, &QPlainTextEdit::cursorPositionChanged, this, &TextEditor::highlightCurrentLine);
    }

private:
    std::vector<std::pair<int, QTextCharFormat>> highlightedWords_;

    //Highlights the current line
    void highlightCurrentLine()
    {
        QList<QTextEdit::ExtraSelection> extraSelections;

        if (!isReadOnly())
        {
            QTextEdit::ExtraSelection selection;
            selection.format.setBackground(Qt::yellow);
            selection.format.setProperty(QTextFormat::FullWidthSelection, true);
            selection.cursor = textCursor();
            selection.cursor.clearSelection();
            extraSelections.append(selection);
        }

        setExtraSelections(extraSelections);
    }

    //Highlights all words similar to the currently selected word
    void selectChangeHandler()
    {
        //Unset previous selection
        resetHighlightedWords();

        //Ignore empty selections
        if (textCursor().selectionStart() >= textCursor().selectionEnd())
            return;

        //We only care about fully selected words (nonalphanumerical characters on either side of selection)
        auto plaintext = toPlainText();
        auto prevChar = plaintext.mid(textCursor().selectionStart() - 1, 1).toStdString()[0];
        auto nextChar = plaintext.mid(textCursor().selectionEnd(), 1).toStdString()[0];
        if (isalnum(prevChar) || isalnum(nextChar))
            return;

        auto qselection = textCursor().selectedText();
        auto selection = qselection.toStdString();

        //We also only care about selections that do not themselves contain nonalphanumerical characters
        if (std::find_if(selection.begin(), selection.end(), [](char c) { return !isalnum(c); }) != selection.end())
            return;

        //Highlight all matches of the given word in the editor
        blockSignals(true);
        highlightWord(qselection);
        blockSignals(false);
    }

    //Removes highlight from selected words
    void resetHighlightedWords()
    {
        if (highlightedWords_.empty())
            return;

        blockSignals(true);
        auto cur = textCursor();
        for (const auto& [index, oldFormat] : highlightedWords_)
        {
            cur.setPosition(index);
            cur.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor, 1);
            cur.setCharFormat(oldFormat);
        }
        blockSignals(false);

        highlightedWords_.clear();
    }

    //Applies the highlight style to all appearances of the given word
    void highlightWord(const QString& word)
    {
        auto plaintext = toPlainText();

        //Prepare text format
        QTextCharFormat format;
        format.setBackground(QColor::fromRgb(0x70, 0xED, 0xE0));

        //Find all words in our document that match the selected word and apply the background format to them
        size_t pos = 0;
        auto reg = QRegExp("\b" + word + "\b");
        auto cur = textCursor();
        auto index = reg.indexIn(plaintext, pos);
        while (index >= 0)
        {
            //Select matched text
            cur.setPosition(index);

            //Save old text style
            highlightedWords_.push_back(std::make_pair(index, cur.charFormat()));

            //Apply format
            cur.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor, 1);
            cur.mergeCharFormat(format);

            //Move to next match
            auto len = (size_t)reg.matchedLength();
            pos = index + (size_t)reg.matchedLength();
            index = reg.indexIn(plaintext, pos);
        }
    }
};

IMO 将避免继承作为第一手段是正确的做法,但在这种特殊情况下,这可能是最简单的方法。

#include <QPainter>

TextEditor::TextEditor( QWidget* parent ) : QPlainTextEdit( parent )
{
    connect( this, SIGNAL( cursorPositionChanged() ), viewport(), SLOT( update() ) );
    //Just for brevity. Instead of repainting the whole thing on every cursor change,
    //you'll want to filter for changes to the current block/line and only update the.
    //changed portions. And accommodate resize, etc.
}

void TextEditor::paintEvent( QPaintEvent* pEvent )
{
    QPainter painter( viewport() );
    QRect r = cursorRect();
    r.setLeft( 0 ); r.setRight( width() - 1 ); //Or more! 
    painter.setPen( Qt::NoPen );
    painter.setBrush( QColor( 228, 242, 244, 200 ) );
    painter.drawRect( r );
    QPlainTextEdit::paintEvent( pEvent );
}

光标块后面的背景提示是一个非常好的用户体验改进,因为它使光标位置在各种情况下一目了然。如果你有几个文本编辑器在一起,像这样的小细节就变得更加重要。

乍一看,setExtraSelections() 看起来是一种快速而简单的方法。它是...但我发现当你想将它提升到一个新的水平时它就不够用了,而且正如你发现的那样,它不能很好地与任何其他突出显示一起使用。

我怀疑 built-in ExtraSelection 方法是一种快速但肮脏的强力工具,用于快速显示错误或断点,即真正在视觉上为用户突出的东西。它基本上就像光标选择后面的辅助选择突出显示,因此像所有其他选择突出显示一样,它将呈现在文本后面但在其他所有内容之前。这意味着它还会使您使用 QTextFormat 或 QTextCharFormat 甚至 QSyntaxHighlighter 所做的任何自定义文本背景格式黯然失色。我个人认为不能接受。

对于这种背景提示用例,built-in 选择或突出显示的另一个小问题是它们不会覆盖文本块的整个背景。它们会在文本区域边界或更糟的地方停止几个像素,具体取决于边距等,使它在我眼中看起来很笨重。

就UI设计而言,电流线指示通常需要比大多数其他指示和所有其他高光更微妙,对比度更低,并且朝向非常远的背景。它需要看起来更像是小部件的一部分,而不是文本的一部分。这是一个提示,而不是一个选择,我发现在视觉上平衡它比 ExtraSelections 或常规文本格式能够提供的更多。

顺便说一句,如果你打算让它变得更复杂,例如作为代码编辑器,我还建议您考虑使用 QSyntaxHighlighter 来突出显示您选择的单词模式。 (它将删除大量的光标控制逻辑和所有信号中断。当(如果?)您添加关键字、变量、评论、搜索词等时,它也会更好地扩展。现在,您的突出显示涉及编辑您的document/data 直接建模,这适合您的 post 或简单的文本输入,但在其他情况下可能会出现问题。)

编辑

这里有更多代码显示如何使用扩展荧光笔和 paintEvent 覆盖。我将使用 headers 来希望更清楚地说明这种方法如何与您的实际项目的 classes.

集成

首先是荧光笔:

#include <QSyntaxHighlighter>
class QTextDocument;

class CQSyntaxHighlighterSelectionMatch : public QSyntaxHighlighter
{
    Q_OBJECT
public:
    explicit CQSyntaxHighlighterSelectionMatch( QTextDocument *parent = 0 );

public slots:
    void    SetSelectionTerm( QString term );

protected:
    virtual void highlightBlock( const QString &text );
    void ApplySelectionTermHighlight( const QString &text );

private:
    QString m_strSelectionTerm;
    struct HighlightingRule {
        QRegExp pattern;
        QTextCharFormat format;
    };
    HighlightingRule m_HighlightRuleSelectionTerm;
};

快速而肮脏的实现:

CQSyntaxHighlighterSelectionMatch::CQSyntaxHighlighterSelectionMatch( QTextDocument *parent )
    : QSyntaxHighlighter( parent )
{
    m_strSelectionTerm.clear();

    m_HighlightRuleSelectionTerm.format.setBackground( QColor(255, 210, 120 ) );
    //m_HighlightRuleSelectionTerm.format.setFontWeight( QFont::Bold ); //or italic, etc... 
}

void CQSyntaxHighlighterSelectionMatch::SetSelectionTerm( QString txtIn )
{
    if( txtIn == m_strSelectionTerm )
        return;

    if( !txtIn.isEmpty() )
    {
        txtIn = "\b" + txtIn + "\b";

        if( txtIn == m_strSelectionTerm )
            return;
    }

    m_strSelectionTerm = txtIn;

    Qt::CaseSensitivity cs = Qt::CaseSensitive;
    m_HighlightRuleSelectionTerm.pattern = QRegExp( m_strSelectionTerm, cs );
    rehighlight();
}

void CQSyntaxHighlighterSelectionMatch::highlightBlock( const QString &text )
{
    if( m_strSelectionTerm.length() > 1 )
        ApplySelectionTermHighlight( text );
}


void CQSyntaxHighlighterSelectionMatch::ApplySelectionTermHighlight( const QString &text )
{
    QRegExp expression( m_HighlightRuleSelectionTerm.pattern );
    int index, length;
    index = expression.indexIn( text );
    while ( index >= 0 )
    {
        length = expression.matchedLength();
        setFormat( index, length, m_HighlightRuleSelectionTerm.format );
        index = expression.indexIn( text, index + length );
    }
}

下面是 QPlainTextEdit 派生的 class 可能如何使用类似上面的内容:

#include <QPlainTextEdit>

class TextEditor : public QPlainTextEdit
{
    Q_OBJECT

public:
    TextEditor( QWidget *parent = 0 );

protected:
    virtual void paintEvent( QPaintEvent *event );

private slots:
    void CheckForCurrentBlockChange();
    void FilterSelectionForSingleWholeWord();

private:
    unsigned int m_uiCurrentBlock;
    CQSyntaxHighlighterSelectionMatch *m_pHighlighter;
};

#include <QPainter>

TextEditor::TextEditor(QWidget *parent)
    : QPlainTextEdit(parent)
{
    //Instead of repainting on every cursor change, we can filter for changes to the current block/line
    //connect( this, SIGNAL(cursorPositionChanged()), viewport(), SLOT(update()) );
    connect( this, SIGNAL(cursorPositionChanged()), this, SLOT(CheckForCurrentBlockChange()) );

    m_pHighlighter = new CQSyntaxHighlighterSelectionMatch( document() );
    connect( this, SIGNAL(selectionChanged()), this, SLOT(FilterSelectionForSingleWholeWord()) );
}


void TextEditor::paintEvent( QPaintEvent* pEvent )
{
    QPainter painter( viewport() );

    QRect r = cursorRect();
    r.setLeft( 0 );
    r.setRight( width()-1 );
    painter.setPen( Qt::NoPen );
    painter.setBrush( QColor( 228, 242, 244 ) );
    painter.drawRect( r );

    QPlainTextEdit::paintEvent( pEvent );
}


void TextEditor::CheckForCurrentBlockChange()
{
    QTextCursor tc = textCursor();
    unsigned int b = (unsigned int)tc.blockNumber();
    if( b == m_uiCurrentBlock )
        return;

    m_uiCurrentBlock = b;

    viewport()->update(); //I'll just brute force paint everything for this example. Your real code can be smarter with it's repainting it matters...

}


void TextEditor::FilterSelectionForSingleWholeWord()
{
    QTextCursor tc = textCursor();
    QString currentSelection = tc.selectedText();

    QStringList list = currentSelection.split(QRegExp("\s+"), QString::SkipEmptyParts);
    if( list.count() > 1 )
    {
        m_pHighlighter->SetSelectionTerm( "" );
        return;
    }

    tc.movePosition( QTextCursor::StartOfWord );
    tc.movePosition( QTextCursor::EndOfWord, QTextCursor::KeepAnchor );
    QString word = tc.selectedText();

    if( currentSelection != word )
    {
        m_pHighlighter->SetSelectionTerm( "" );
        return;
    }

    m_pHighlighter->SetSelectionTerm( currentSelection );
}

这是我所知道的最简单的方法,可以提供您想要的选择词功能,同时解决背景提示干扰选择词突出显示的问题,当它们在同一块上时。