Qt突出显示选定的行会覆盖单个单词的突出显示

Posted

技术标签:

【中文标题】Qt突出显示选定的行会覆盖单个单词的突出显示【英文标题】:Qt highlighting selected line overwrites highlighting of individual words 【发布时间】:2021-10-22 09:36:11 【问题描述】:

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

我的问题是我如何确保在行高亮完成之后完成单词highlighing,以便它们可以同时处于活动状态?

截图说明:

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

最小可重现示例:

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

【问题讨论】:

对我来说是愚蠢的问题,但在选择变化时,游标位置始终会更改?因此,您可能只使用cursorPositionChanged 信号并让您的插槽处理检查是否在其中选择了某些内容。 呃,我没有意识到这一点。我会尝试这样做 如果它确实有效,你能发布一个自我回答吗? 不幸的是,它不起作用。我怀疑应用突出显示的两种不同方式是原因 那么它可能是框架的(硬编码或默认)顺序。也许你可以找到一种强制不同顺序的方法。 【参考方案1】:

首先避免继承是 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() 看起来是一种快速而简单的方法。它是...但是我发现当你想把它提升到一个新的水平时它就不够了,而且,正如你所发现的,它与任何其他突出显示效果都不好。

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

对于这种背景提示用例,内置选择或突出显示的另一个小问题是它们没有覆盖文本块的整个背景。它们会在文本区域边界处停止几个像素或更差,具体取决于边距等,这让我的眼睛看起来很笨重。

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

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

编辑

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

首先,荧光笔:

#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 派生类如何使用类似上述内容的方式:

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


这是我所知道的提供选择项功能的最简单方法,同时解决了背景提示在同一块上干扰选择项突出显示的问题。

【讨论】:

抱歉回复晚了。你说我要过滤当前块/行的更改,只更新更改的部分。我怎么做?我无法找到任何令人满意的解释来说明如何做到这一点。除此之外,我还在使用 QSyntaxHighlighter。我只是在它之外做这个突出显示,因为它不能处理选择和光标。 Re:过滤块更改。我的意思是,如果光标块发生更改,您应该只调用 update(),因为 cursorPositionChanged() 会针对所有块更改发出,即使是那些不更改您要绘制的当前行提示的更改。 回复:使用 QSyntaxHighlighter。好的!那挺好的。那么你就在正确的道路上。您应该能够轻松地扩展您的荧光笔类以提供来自 highlightblock() 的术语选择匹配。它应该紧挨着您的荧光笔通常处理的常用内容,例如语法、关键字、括号和括号、搜索/替换结果等。为了提高效率,您的文本编辑器类可以只过滤单个单词的选择,然后将它们传递给荧光笔以在每个块中进行匹配。这应该与paintEvent结合起来做你想做的事。 我刚刚修改了答案以包含术语突出显示。我确信仍然存在问题,但希望它向您展示了解决原始问题的实用途径。干杯! 感谢您通过示例扩展您的答案。我针对我的情况对其进行了一些修改,它完全符合我的需要!我没有意识到我可以为此目的使用 QSyntaxHighlighter。

以上是关于Qt突出显示选定的行会覆盖单个单词的突出显示的主要内容,如果未能解决你的问题,请参考以下文章

代码并不总是突出显示富文本框中的选定文本

突出显示textarea angular 8中的特定单词

基于正则表达式在闪亮的 DT 中突出显示单词

使用python在单个字符串中突出显示并保存多个单词

ListView 中突出显示的选定项 Xamarin.Forms

使用 Word 加载项时如何突出显示单词