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 );
}
这是我所知道的最简单的方法,可以提供您想要的选择词功能,同时解决背景提示干扰选择词突出显示的问题,当它们在同一块上时。
我有一个 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 );
}
这是我所知道的最简单的方法,可以提供您想要的选择词功能,同时解决背景提示干扰选择词突出显示的问题,当它们在同一块上时。