How to move the Gtk::Entry cursor? - c++

I'm trying to make a custom Gtk::Entry widget (gtkmm4) that accepts only numbers and shows text as currency. Decimal and thousand separators are automatically added to the text. So I derived from Gtk::Entry and connected the signal_changed() with a member function that formats the input:
class CurrencyEntry : public Gtk::Entry{
public:
CurrencyEntry() {
set_placeholder_text("0.00");
connectionChange = signal_changed().connect(
sigc::mem_fun(*this, &CurrencyEntry::filterInput)
);
}
protected:
sigc::connection connectionChange;
Glib::ustring oldText;
void filterInput(){
auto currentText = get_text();
/* format currentText */
connectionChange.block();
set_text(currentText);
connectionChange.unblock();
/* move the cursor */
}
};
The problem is: the user presses one key at time, but more than one symbol can be added to the text in specific cases. It seems that the default behavior of the cursor is to always move 1 place per key pressed, ignoring the extra symbols. This is the result (| is the cursor):
Current Text
Typed Key
Result
Desired Result
| (empty)
1
0|.01
0.01|
123.45|
6
1,234.5|6
1,234.56|
98|0,123.45|
7
9,8|70,123.45
9,87|0,123.45
I need to move the cursor to the correct position. At first seemed trivial, but so far I have tried:
Calling set_position(position) at the end of filterInput().
Calling gtk_editable_set_position( GTK_EDITABLE(this->gobj()), position) at the end of filterInput().
Overriding Entry::on_insert_text(const Glib::ustring& text, int* position) and change the value pointed by the position parameter.
Calling Editable::on_insert_text(const Glib::ustring& text, int* position) directly at the end of filterInput(), passing a new position value.
Nothing happens. All these commands seem to be ignored or ignore the position parameter. Am I doing something wrong or is this some kind of bug? How can I set the cursor position correctly in a Gtk::Entry widget?

The position seems not to be updated from the entry's handler. I tried other handlers (like insert_text) and the same issue arises. One way to solve this is to, from within you entry's handler, add a function to be executed in the idle loop. In that function, you can update the position. Here is the code:
#include <algorithm>
#include <iostream>
#include <string>
#include <gtkmm.h>
class CurrencyEntry : public Gtk::Entry
{
public:
CurrencyEntry()
{
m_connection = signal_changed().connect(
[this]()
{
// Get the current edit box content:
std::string str = get_text();
// Make it upper case:
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
// Set the updated text. The connection is blocked to avoid
// recursion:
m_connection.block();
set_text(str);
m_connection.unblock();
// Update the position in the idle loop:
Glib::signal_idle().connect(
[this]()
{
set_position(2);
return false;
});
});
}
private:
sigc::connection m_connection;
};
class MainWindow : public Gtk::ApplicationWindow
{
public:
MainWindow();
private:
CurrencyEntry m_entry;
};
MainWindow::MainWindow()
{
add(m_entry);
}
int main(int argc, char *argv[])
{
auto app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
MainWindow window;
window.show_all();
return app->run(window);
}
This is a simplified version of you case: all inserted text is transformed to upper case and, if possible, the cursor's position is set to 2. I think you can adapt to your use case from that.

Related

Parent function terminates whenever I try to call QTextCharFormat on QTextCursor selection

I recently ran into a weird issue where my QPlainTextEdit::selectionChanged handler function terminates prematurely whenever QTextCursor::mergeCharFormat/setCharFormat/setBlockCharFormat is called. Additionally, after terminating it gets called again and runs into the same issue, leading to an infinite loop.
I'm trying to replicate a feature present in many text editors (such as Notepad++) where upon selecting a word, all similar words in the entire document are highlighted. My TextEditor class is overloaded from QPlainTextEdit.
The minimal reproducible example is as follows:
main.cpp:
#include "mainWindow.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
mainWindow w;
w.show();
return a.exec();
}
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 dolor sit test");
ui.tabWidget->addTab(textEdit, "Editor"); //Or any other way of adding the widget to the window
}
private:
Ui::mainWindowClass ui;
};
TextEditor.h:
The regex highlighter part is based on this SO answer.
#pragma once
#include <QPlainTextEdit>
class TextEditor : public QPlainTextEdit
{
Q_OBJECT
public:
TextEditor(QWidget* parent) : QPlainTextEdit(parent)
{
connect(this, &QPlainTextEdit::selectionChanged, this, &TextEditor::selectChangeHandler);
}
private:
void selectChangeHandler()
{
//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;
//Prepare text format
QTextCharFormat format;
format.setBackground(Qt::green);
//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(qselection);
auto cur = textCursor();
auto index = reg.indexIn(plaintext, pos);
while (index >= 0)
{
//Select matched text and apply format
cur.setPosition(index);
cur.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor, 1);
cur.mergeCharFormat(format); //This causes the selectChangeHandler function to terminate and then execute again, causing an infinite loop leading to a stack overflow
//Move to next match
pos = index + (size_t)reg.matchedLength();
index = reg.indexIn(plaintext, pos);
}
}
};
I suspect the format fails to apply for some reason, possibly causing an exception that gets caught inside Qt and terminates the parent function. I tried adding my own try-catch handler around the problematic area, but it did nothing (as expected).
I'm not sure whether this is my fault or a bug inside Qt. Does anybody know what I'm doing wrong or how to work around this issue?
An infinite loop is being generated because it seems that getting the text changes also changes the selection. One possible solution is to block the signals using QSignalBlocker:
void selectChangeHandler()
{
const QSignalBlocker blocker(this); // <--- this line
//Ignore empty selections
if (textCursor().selectionStart() >= textCursor().selectionEnd())
return;
// ...

How to emit QTextTexid::textChanged signal only in certain cases selectively?

Can you trigger the textChanged signal only for certain cases? for example triggering it for inserted text, inserted space characters but not backspace?
I am running checks for typed characters inside a QTextEdit every time the text changes and based on the results highlighting text inside a read-only QTextEdit in the background used as an always-on placeholder text for the user to look at while typing. If the user makes a mistake the character gets highlighted red and reset to it's initial background color after the mistake is fixed. The problem arises when the backspace key is pressed, as it is registered as a mistake and as a result the previous character also gets highlighted red.
void Widget::onTextChanged()
{
QChar c;
QString txt_contents = txtedit_->toPlainText();
if(txt_contents.isEmpty()){
c = '\0';
//reset text display
txtdisplay_->clear();
txtdisplay_->append(*label_text_);
}
else
c = txtedit_->toPlainText().back();
if(!texteditq_->isEmpty()){
if(c == texteditq_->head()){
//refresh text display
correct_++;
txtdisplay_->clear();
txtdisplay_->append(*label_text_);
//remove character that was used for the successful check from the
//queue
texteditq_->dequeue();
}else{
//set backgroud color to red for errors
fmt_->setBackground(Qt::red);
if(!txtedit_->toPlainText().isEmpty()){
//move cursor to the end of the editor, where the error is and save
//the position the error occurs at
c_edit_->movePosition(QTextCursor::End);
quint32 error_pos = c_edit_->position();
//move the cursor in the display for the background text and
//use the KeepAnchor move mode to highlight the misspelled char
c_display_->setPosition(error_pos-1,QTextCursor::MoveAnchor);
c_display_->setPosition(error_pos,QTextCursor::KeepAnchor);
//apply formating to that character
c_display_->setCharFormat(*fmt_);
}
}
}
}
As per OP's request, I am posting a solution that is a class called CustomTextEdit which is subclassing QTextEdit. It is hooking to keyPressEvent() and checking the key that was pressed. If it was other than Backspace then the custom signal keyPressed() will get emitted.
class CustomTextEdit : public QTextEdit {
Q_OBJECT
public:
explicit CustomTextEdit(QWidget *parent = nullptr) : QTextEdit(parent) {}
signals:
void keyPressed();
protected:
void keyPressEvent(QKeyEvent *e) override {
if (e->key() != Qt::Key_Backspace) {
emit keyPressed();
}
QTextEdit::keyPressEvent(e);
}
};

How to access ui from main window in another qdialog?

I am having trouble accessing a QTextEdit from a main window in another form. Please help.
void properties::on_okWordPushButton_clicked()
{
if (ui->wordcombo->currentText() == "All Words") {
int wordCount = notepad->textEdit->toPlainText().split(QRegExp("(\\s|\\n|\\r)+"), QString::SkipEmptyParts).count();
ui->wordcountlabel->setText(QString::number(wordCount));
}
}
I am getting an error since I cannot read notepad->textEdit
You can use at least 2 possibilities:
Dirty way:
On form creation, pass pointer to your QTextEdit:
// mainwindow.cpp
auto myProperties = new properties(notepad->textEdit);
...
// properties.h
QTextEdit *outerEditor;
// properties.cpp
properties::properties(QTextEdit *editor) {
outerEditor = editor;
...
}
Then, on your slot you can use:
int wordCount = editor->toPlainText().split(QRegExp("(\\s|\\n|\\r)+"), QString::SkipEmptyParts).count();
Qt-way:
Remember - signals/slots are awesome.
Just after form creation, you can connect signal from MainWindow to properties passing text in your QTextEdit and store it locally:
// MainWindow.cpp
auto myProperties = new properties(notepad->textEdit);
connect(this->textEdit, QOverload<QString>::of(&QTextEdit::valueChanged), myProperties, GetNewValue);
// properties.h
void GetNewValue(QString val);
// properties.cpp
void properties::GetNewValue(QString val) {
ui->wordcountlabel->setText(QString::number(val.toPlainText().split(QRegExp("(\\s|\\n|\\r)+"), QString::SkipEmptyParts).count());
}
You can't do this, the ui is a private member of a widget, create a function that returns or sets what you need!

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

I would like to display a rectangle behind a word I selected like Qt Creator does here:
I am experimenting with the example of QSyntaxHighlighter. I am able to change styles based on keyword patterns. I would like to have graphics or widgets for custom autocompletion lists.
For autocompletion follow the Custom Completer Example or the Completer Example.
The code below follows the first one, which I blatantly, unashamedly copied and integrated into the BackgroundHighlighter class and main.cpp.
This answer will contain five files within a project along with a Qt Resource File.
highlighter.h (Highlighter Class for Syntax)
highlighter.cpp
backgroundHighlighter.h (BackgroundHighlighter Class)
backgroundHighlighter.cpp
main.cpp
res.qrc (optional, not needed, you can hardcode your text)
res (directory) (optional)
|- symbols.txt (optional, you can set your own default text)
|- wordlist.txt (optional, copied from example but you could use your own line-delimited word list and set this in main.cpp with a QStringListModel)
Note that the implementation of the Highlighter class for (1) and (2) can be found in the Qt Syntax Highlighter Example. I will leave its implementation as an exercise for the reader.
In calling the BackgroundHighlighter class, one can pass it a file name to load text from a file. (This wasn't in the OP's specification, but was convenient to implement due to the large amount of text I wanted to test.)
Also note that I integrated the Custom Completer Example into the class.
Here's backgroundHighlighter.h (3) (~45 lines, ~60 lines with completer):
#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
And here's backgroundHighlighter.cpp (4) (~160 lines, ~250 lines with completer):
#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);
}
}
Finally, here's main.cpp (5) (~10 lines, ~45 lines with completer)
#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();
}
In res.qrc add a / prefix and add files (res/symbols.txt, res/wordlist.txt) from the res/ subdirectory.
I have tested with a symbols.txt file resembling
symbol1 symbol2 symbol3 symbol4 symbol5
symbol1 symbol2 symbol3 symbol4 symbol5
symbol1 symbol2 symbol3 symbol4 symbol5
// ... ditto 500 lines
It takes about 1 second, which probably isn't ideal (100ms is probably more ideal).
However, you might want to watch over for the line count as it grows. With the same text file at 1000 lines, the program will start to take approx. 3 seconds for highlighting.
Note that... I haven't optimised it entirely. There could possibly be a better implementation which formats only when the symbol scrolls into the user's view. This is just a suggestion. How to implement it I don't know.
Notes
For reference, I've attached symbols.txt and wordlist.txt on github.
If you want to change the background colour of formatting, go to lines 27 to 29 of backgroundhighlighter.cpp. There, you can see that I centralised the formatting.
BackgroundHighlighter::clearHighlights() might clear away any background highlights originally added as it sets the ENTIRE document's character background to the default format. This may be an unintended consequence of the result.

Is there a method or a way to check whether a user has visited a certain tab page on a QTabWidget?

I have a QTabWidget on my application, so user can navigate through the tab pages by clicking on the title, I want to know when user open a tab page, whether he/she visited this page previously. In QWizard class there is a method hasVisitedPage() which does the exact same thing on a wizard, but I couldn't find a similar method in QTabWidget class. What I want to know is, whether there is a method to do this like in QWizard?
this is the similar method in QWizard class http://doc.qt.io/archives/qt-4.8/qwizard.html#hasVisitedPage
Currently I am using a QList to store the visited page indexes and each time when a user open a tabpage check whether QList contains the index of the opened page, I think it would be more easy if I had a method to check
What I want to know is, whether there is a method to do this like in QWizard?
Unfortunatelly, there is not.
Currently I am using a QList to store the visited page indexes and each time when a user open a tabpage check whether QList contains the index of the opened page
QWizard does the same, i.e. has a QList<int> history; attribute. So, in my opinion you are doing it the right way.
Take a look at the source code for more details. In particular, QWizardPrivate::switchToPage might be interesting to you, in order to give you an idea how it is done in QWizard, so you can check your own implementation against that and adapt it if necessary.
The history property is easy enough to add:
// https://github.com/KubaO/stackoverflown/tree/master/questions/tabwidget-history-52033092
#include <QtWidgets>
#include <array>
static const char kHistory[] = "history";
auto getHistory(const QTabWidget *w) {
return w->property(kHistory).value<QList<int>>();
}
void addHistory(QTabWidget *tabWidget) {
QObject::connect(tabWidget, &QTabWidget::currentChanged, [tabWidget](int index) {
if (index < 0) return;
auto history = getHistory(tabWidget);
history.removeAll(index);
history.append(index);
tabWidget->setProperty(kHistory, QVariant::fromValue(history));
});
if (tabWidget->currentIndex() >= 0)
tabWidget->setProperty(
kHistory, QVariant::fromValue(QList<int>() << tabWidget->currentIndex()));
}
bool hasVisitedPage(const QTabWidget *w, int index) {
return getHistory(w).contains(index);
}
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget ui;
QVBoxLayout layout{&ui};
QTabWidget tabWidget;
QLabel history;
layout.addWidget(&tabWidget);
layout.addWidget(&history);
std::array<QLabel, 5> tabs;
for (auto &l : tabs) {
auto const n = &l - &tabs[0] + 1;
l.setText(QStringLiteral("Label on Page #%1").arg(n));
tabWidget.addTab(&l, QStringLiteral("Page #%1").arg(n));
}
addHistory(&tabWidget);
auto showHistory = [&] {
auto text = QStringLiteral("History: ");
for (auto i : tabWidget.property("history").value<QList<int>>())
text.append(QString::number(i + 1));
history.setText(text);
};
showHistory();
QObject::connect(&tabWidget, &QTabWidget::currentChanged, showHistory);
tabWidget.currentChanged(tabWidget.currentIndex());
ui.show();
return app.exec();
}