Qt 4.6 - C++ - QTreeWidgetItem Iterator - c++

I have a QTreeWidget with items in it. The first column contains a unique number. This is set via item->setData(0, 0, unique_number);. The second column contains a checkbox, set via item->setCheckState(1, Qt::Unchecked);. The user can select the items (s)he would like to work with and presses a button. The slot for this button will run a loop on the checked items. In the Qt documentation an example is given. You use the QTreeWidgetItemIterator.
QTreeWidgetItemIterator it(treeWidget);
while (*it) {
if ((*it)->text(0) == itemText)
(*it)->setSelected(true);
++it;
}
It also says that you can specify an argument in the constructor to only iterate over the checked items. The flag is : QTreeWidgetItemIterator::Checked. My slightly customized loop is as follows:
QTreeWidgetItemIterator it(treeWidget, QTreeWidgetItemIterator::Checked);
while (*it)
{
QVariant w;
w = (*it)->data(0, 0);
std::cout << "Selected item # " << w.toInt() << "\n";
it++;
}
This code will compile fine but won't work when you actually run the program. It doesn't print any values.
Any tips? Thanks!

The one caveat missing from the Qt documentation is that the QTreeWidgetItemIterator::Checked flag only tests the check state for column 0 in each of your QTreeWidgetItems. Using column 0 for your checkbox and column 1 for your unique number should cause the loop to print values.

Thanks Roneel!
Another way to do this is like this (I just figured it out).
QTreeWidgetItemIterator it(ui->treeWidget);
while (*it)
{
if ((*it)->checkState(1) == 2)
{
QVariant w;
w = (*it)->data(4, 0);
std::cout << "Selected item # " << w.toString().toStdString() << "\n";
}
++it;
}
checkState takes a int column argument and the == 2 only allows checked items to be processed.

Just one thing... I think it's not a good habit to compare enum to int, as enum "int value" may change... Compare to Qt::Checked instead... It's just "for sure" and much more clean for reading

Related

Behavior of selectionChanged() on removing first row

Please run the following code (I am using Qt 5.9):
QTableWidget* tableWidget = new QTableWidget(2, 2, nullptr);
tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
connect(tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged,
[&](const QItemSelection& selected, const QItemSelection& deselected)
{ qDebug() << "selected =" << selected << endl << "deselected =" << deselected; });
tableWidget->show();
QTimer::singleShot(10000, [=](){ tableWidget->removeRow(0); });
Within 10 seconds, select the first of two rows. You will see debug ouput. It will show you that row 0 was selected by your click.
Then, after 10s, row 0 is removed automatically. Debug output now shows that row 1 is selected and row 0 is deselected.
The latter doesn't make any sense to me. When removing row 0 I would expect the "new" row 0 being selected afterwards. Also the visually selected row still is row 0 and row 1 simply doesn't exist anymore.
This also happens with a custom model and generic view and makes my application crash by pointing to a row that does not exist.
Is this desired behavior? Where is my misunderstanding?
It makes perfectly sense to change the selected row before deleting it. Doing the opposite may lead to reading dangling data, for example, if the UI is refreshed when the model has changed but the view holds outdated indices.
Think about removing row 0 twice: the second time is very obvious that the selection must be changed (deselected in this case) before removing the last row in the table to avoid having an invalid index as the selected row.
You can use the following modified example to see when the model is actually updated.
auto tableWidget = new QTableWidget(2, 2, nullptr);
tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
connect(tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged,
[&](const QItemSelection& selected, const QItemSelection& deselected)
{ qDebug() << "selected =" << selected << endl << "deselected =" << deselected; });
connect(tableWidget->model(), &QAbstractItemModel::rowsRemoved, [&](const QModelIndex &, int first, int last)
{ qDebug() << "first row removed =" << first << endl << "last row removed =" << last; });
tableWidget->show();
QTimer::singleShot(10000, [=](){ tableWidget->removeRow(0); });
QTimer::singleShot(15000, [=](){ tableWidget->removeRow(0); }); // remove twice
Workaround
The main problem is that, as you pointed out in the comments, you cannot rely on the signal information: those QModelIndex may or not be valid if a removal was performed. You can keep track of all changes but that would be exhausting.
Instead, you can try deferring the selection signal, so when it is handled the model has been updated and you can trust the information from the selection model. The trick is to use a timer: the function handling the timeout event will be executed in the next iteration of the events loop (even if the timeout time is 0), while the model and the widget are updated in the current iteration:
connect(tableWidget->selectionModel(), &QItemSelectionModel::selectionChanged,
[&](const QItemSelection&, const QItemSelection&) {
QTimer::singleShot(0, [&]() {
qDebug() << "selected =" << tableWidget->selectionModel()->selectedIndexes() << endl;
});
});

Understanding what QHash does when key not found

Note: You can find a minimal working example at the end of this post.
I'm using Qt 5.7. Let's say I have the following QHash:
QHash<HashKey, HashValue> hm;
with
enum HashKey {
K1,
K2,
K3,
K4,
K5
}
and
class HashValue {
public:
int x;
HashValue(int x) {
this->x = x;
}
}
I have initialized the hash map like this:
hm.insert(K1, HashValue((int)K1));
hm.insert(K2, HashValue((int)K2));
hm.insert(K3, HashValue((int)K3));
hm.insert(K4, HashValue((int)K4));
hm.insert(K5, HashValue((int)K5));
I have tested it by calling
cout << hm.value(K4).x << endl;
cout << hm.find(K4).value().x << endl;
Both return the same result that is 3. Now I tried doing the same with a key that is not part of the hash map by casting an integer to HashKey and calling the above two methods on it:
cout << hm.value(static_cast<HashKey>(100)).x << endl;
cout << hm.find(static_cast<HashKey>(100)).value().x << endl;
What I got is 8 (for the first call with value().x) and 5 (for the second call with find(...).value().x)
The docs states that
If there is no item with the specified key in the hash, these
functions return a default-constructed value.
I followed the link for default-constructed value and got the following:
[...] for example, QVector automatically initializes its items with
default-constructed values, and QMap::value() returns a
default-constructed value if the specified key isn't in the map. For
most value types, this simply means that a value is created using the
default constructor (e.g. an empty string for QString). But for
primitive types like int and double, as well as for pointer types, the
C++ language doesn't specify any initialization; in those cases, Qt's
containers automatically initialize the value to 0.
In my case this would mean a HashValue() call. However the fact that I get different results is baffling to say the least. I would expect to get the same result though the docs don't mention what find(...) does when an invalid key is passed as argument. All it says it finds the first occurrence of that key and returns an iterator (obviously since I call value() on it in the call above).
The quoted doc snippet from above is followed (again back to the document for QHash) by
If you want to check whether the hash contains a particular key, use
contains()
I can deal with having to call contains() every time I query my hash map though this means making two function calls - first to check if key is present and then to call value(...) to get the actual value if a valid entry is found. The call below returns "Key 100 not found":
cout << (hm.contains(static_cast<HashKey>(100)) ? "Key 100 found" : "Key 100 not found") << endl;
I would expect this check to be done internally but obviously this doesn't happen (my guess would be to prevent some performance impact on the querying functionality of this container).
The question here is why is all this happening and what is actually happening underneath all that?
Here is the project and the code for it:
HashTest.pro
QT += core
QT += gui
CONFIG += c++11
TARGET = HashTest
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += main.cpp
main.cpp
#include <QCoreApplication>
#include <QHash>
#include <iostream>
using namespace std;
enum HashKey {
K1 = 0,
K2 = 1,
K3 = 2,
K4 = 3,
K5 = 4
};
class HashValue {
public:
int x;
HashValue(int x) { this->x = x; }
HashValue() {}
};
int main(int argc, char *argv[])
{
QHash<HashKey, HashValue> hm;
hm.insert(K1, HashValue((int)K1));
hm.insert(K2, HashValue((int)K2));
hm.insert(K3, HashValue((int)K3));
hm.insert(K4, HashValue((int)K4));
hm.insert(K5, HashValue((int)K5));
cout << hm.value(K4).x << endl;
cout << hm.value(static_cast<HashKey>(100)).x << endl;
cout << hm.find(K4).value().x << endl;
cout << hm.find(static_cast<HashKey>(100)).value().x << endl;
cout << (hm.contains(static_cast<HashKey>(100)) ? "Key 100 found" : "Key 100 not found") << endl;
return a.exec();
}
The value() function is basically just for accessing values not checking if you have a one.
It returns a value and there is no way to indicate whether the value is "invalid" or not. So the choice if the design was to construct one. Qt could as an alternative throw an exception but this is not done here for several reasons (same as the containers of the c++ standard library btw.).
Secondly:
You are kind of using find() in a wrong way.
With find you can check whether the key is in the list and if not it point to the end() iterator of the hash.
QHash< Key,Value >::const_iterator valueIt = hash.find(<something>)
if(valueIt == hash.end())
{ // not found. error handling etc.
}
Value value = valueIt.value();
This is usually the "standard" way to check if a key exists and access it in a Map/Hash/Set/....
So when you use
find(...).value();
you could possibly access the end() iterator which causes undefined behavior.

How to chain delete pairs from a vector in C++?

I have this text file where I am reading each line into a std::vector<std::pair>,
handgun bullets
bullets ore
bombs ore
turret bullets
The first item depends on the second item. And I am writing a delete function where, when the user inputs an item name, it deletes the pair containing the item as second item. Since there is a dependency relationship, the item depending on the deleted item should also be deleted since it is no longer usable. For example, if I delete ore, bullets and bombs can no longer be usable because ore is unavailable. Consequently, handgun and turret should also be removed since those pairs are dependent on bullets which is dependent on ore i.e. indirect dependency on ore. This chain should continue until all dependent pairs are deleted.
I tried to do this for the current example and came with the following pseudo code,
for vector_iterator_1 = vector.begin to vector.end
{
if user_input == vector_iterator_1->second
{
for vector_iterator_2 = vector.begin to vector.end
{
if vector_iterator_1->first == vector_iterator_2->second
{
delete pair_of_vector_iterator_2
}
}
delete pair_of_vector_iterator_1
}
}
Not a very good algorithm, but it explains what I intend to do. In the example, if I delete ore, then bullets and bombs gets deleted too. Subsequently, pairs depending on ore and bullets will also be deleted (bombs have no dependency). Since, there is only one single length chain (ore-->bullets), there is only one nested for loop to check for it. However, there may be zero or large number of dependencies in a single chain resulting in many or no nested for loops. So, this is not a very practical solution. How would I do this with a chain of dependencies of variable length? Please tell me. Thank you for your patience.
P. S. : If you didn't understand my question, please let me know.
One (naive) solution:
Create a queue of items-to-delete
Add in your first item (user-entered)
While(!empty(items-to-delete)) loop through your vector
Every time you find your current item as the second-item in your list, add the first-item to your queue and then delete that pair
Easy optimizations:
Ensure you never add an item to the queue twice (hash table/etc)
personally, I would just use the standard library for removal:
vector.erase(remove_if(vector.begin(), vector.end(), [](pair<string,string> pair){ return pair.second == "ore"; }));
remove_if() give you an iterator to the elements matching the criteria, so you could have a function that takes in a .second value to erase, and erases matching pairs while saving the .first values in those being erased. From there, you could loop until nothing is removed.
For your solution, it might be simpler to use find_if inside a loop, but either way, the standard library has some useful things you could use here.
I couldn't help myself to not write a solution using standard algorithms and data structures from the C++ standard library. I'm using a std::set to remember which objects we delete (I prefer it since it has log-access and does not contain duplicates). The algorithm is basically the same as the one proposed by #Beth Crane.
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
#include <string>
#include <set>
int main()
{
std::vector<std::pair<std::string, std::string>> v
{ {"handgun", "bullets"},
{"bullets", "ore"},
{"bombs", "ore"},
{"turret", "bullets"}};
std::cout << "Initially: " << std::endl << std::endl;
for (auto && elem : v)
std::cout << elem.first << " " << elem.second << std::endl;
// let's remove "ore", this is our "queue"
std::set<std::string> to_remove{"bullets"}; // unique elements
while (!to_remove.empty()) // loop as long we still have elements to remove
{
// "pop" an element, then remove it via erase-remove idiom
// and a bit of lambdas
std::string obj = *to_remove.begin();
v.erase(
std::remove_if(v.begin(), v.end(),
[&to_remove](const std::pair<const std::string,
const std::string>& elem)->bool
{
// is it on the first position?
if (to_remove.find(elem.first) != to_remove.end())
{
return true;
}
// is it in the queue?
if (to_remove.find(elem.second) != to_remove.end())
{
// add the first element in the queue
to_remove.insert(elem.first);
return true;
}
return false;
}
),
v.end()
);
to_remove.erase(obj); // delete it from the queue once we're done with it
}
std::cout << std::endl << "Finally: " << std::endl << std::endl;
for (auto && elem : v)
std::cout << elem.first << " " << elem.second << std::endl;
}
#vsoftco I looked at Beth's answer and went off to try the solution. I did not see your code until I came back. On closer examination of your code, I see that we have done pretty much the same thing. Here's what I did,
std::string Node;
std::cout << "Enter Node to delete: ";
std::cin >> Node;
std::queue<std::string> Deleted_Nodes;
Deleted_Nodes.push(Node);
while(!Deleted_Nodes.empty())
{
std::vector<std::pair<std::string, std::string>>::iterator Current_Iterator = Pair_Vector.begin(), Temporary_Iterator;
while(Current_Iterator != Pair_Vector.end())
{
Temporary_Iterator = Current_Iterator;
Temporary_Iterator++;
if(Deleted_Nodes.front() == Current_Iterator->second)
{
Deleted_Nodes.push(Current_Iterator->first);
Pair_Vector.erase(Current_Iterator);
}
else if(Deleted_Nodes.front() == Current_Iterator->first)
{
Pair_Vector.erase(Current_Iterator);
}
Current_Iterator = Temporary_Iterator;
}
Deleted_Nodes.pop();
}
To answer your question in the comment of my question, that's what the else if statement is for. It's supposed to be a directed graph so it removes only next level elements in the chain. Higher level elements are not touched.
1 --> 2 --> 3 --> 4 --> 5
Remove 5: 1 --> 2 --> 3 --> 4
Remove 3: 1 --> 2 4 5
Remove 1: 2 3 4 5
Although my code is similar to yours, I am no expert in C++ (yet). Tell me if I made any mistakes or overlooked anything. Thanks. :-)

QComboBox::findData() always returns -1

I am trying to get the id of the record in the model from a QCombobox with findData(index), but when select a item, it retunrs -1. It has been working in another project, but this is the second one that doesn't work. Here's my code:
modAnfi = new QSqlTableModel(this);
modAnfi->setQuery("SELECT id, (nombres || ' ' || apellidos) as Nombre, nombres, apellidos FROM tbPersonas WHERE activo=1");
comboAnfitrion->setModel(modAnfi);
comboAnfitrion->setModelColumn(1);
comboAnfitrion->setEditable(true);
comboAnfitrion->completer()->setCompletionMode(QCompleter::PopupCompletion);
connect(comboAnfitrion, SIGNAL(currentIndexChanged(int)), this, SLOT(currentIndexChangeAnfitrion(int)));
and:
void controlReg::currentIndexChangeAnfitrion(int index)
{
qDebug() << comboAnfitrion->findData(index); // -1
qDebug()<< comboAnfitrion->itemData(1); // QVariant(Invalid)
}
Thanks for your time, any help will be appreciated.
You have to use the model you assign to the comboBox, use the index to look for it:
modAnfi->data(modAnfi->index( index, 0));
Check the QComboBox documentation; from the findData description, quoting:
Returns the index of the item containing the given data
Where you are passing index as the "given data". However, index is already an index in the combobox. But you're obviously not looking for an index (since you already have one).
I suspect you actually want to call the itemData method instead? That would retrieve the data associated with an element for a given index.

QCombobox doesn't select when changing currentIndex

In my constructor I connect to a Sqlite Database and read "Categories" (QStrings) from it.
I store them in a QList. I checked via debugger if they're empty, but everything is fine.
The int currCategoryIndex is set to 0 in my initializer list. And since QComboboxes start indexing from 0 it should be the first item.
An extract from my constructor:
dm.readDB_C(c_list); //reads into c_list
if(c_list.count() > 0) //has 1 or more items
updateCategories();
This is the part where I read the database, check if it's empty and if not call a function which adds those categories to a QComboBox.
updateCategories() function :
void MainWindow::updateCategories()
{
for(int i = 0; i < c_list.count(); i++){
if(ui->cmbCategory->findText(c_list[i]) != -1){ //-1 means "not found"
continue;
} else {
ui->cmbCategory->addItem(c_list[i]); //Add to QCombobox
}
}
ui->cmbCategory->setCurrentIndex(currCategoryIndex); //Should be the first item
}
I have all items in my QCombobox but none is selected. I have to click the box and select one myself. That's not supposed to happen.
What is wrong? Why doesn't it select one itself?
Edit:
currentIndexChanged signal:
void MainWindow::on_cmbCategory_currentIndexChanged(int index){
currCategoryIndex = index;
}
Perhaps the first item is empty?
Since you only add items to QComboBox, it is possible that index 0 is (from the beginning) an empty string.
try putting
ui->cmbCategory->removeItem(0);
in the beginning of updateCategories to check if it is the case
also, if currCategoryIndex is an index that does not exist (for example -1) QComboBox will also be empty (even if there is no empty string to choose) - in this case you can try to hardcode 0 in the function (if you want the item to always be the first one), or add additional check, for example:
if (0 > currentCategoryIndex || currentCategoryIndex > ui->cmbCategory->count())
currentCategoryIndex = 0