6

I want to select an arbitrary amount of items from a list of arbitrary length. A dropdown (QComboBox) doesn't allow checkable items. A list of checkable items would become clumsy with a lot of items.

I found this question in the User Experience SE subsite and this answer seems to be the solution that best suits my needs. It has many names, as a comment in said answer remarks: Dual List, Accumulator, List builder, TwoListSelection ...

The version from OpenFaces.org shown in the answer linked above:

a

I couldn't find an implementation in Qt, though. Should I implement it myself or is there an implementation available in Qt? Is there a recommended approach?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Alechan
  • 817
  • 1
  • 10
  • 24
  • do a survey to Qt examples ... I have seen this in one example .. really can't recall which! – Mohammad Kanan Jan 18 '18 at 19:12
  • Done it before asking this question. Couldn't find anything similar. Do you remember something about the example? Maybe I missed it. – Alechan Jan 18 '18 at 19:17
  • you can take a Current item from a left side QListWidget and add Right Side using addItem(new QListWidgetItem()), you can create QListWidgetItem from other QListWidgetItem. If you want take all item from a left side, you can use while(ui->leftList->count() > 0) {and take a item from a index}, what is problem? – Tazo leladze Jan 18 '18 at 19:53
  • 1
    @Taz742, it seems like a common pattern and didn't want to reinvent the wheel. I'll implement it myself and add it as an answer for future reference from me and others. Thanks! – Alechan Jan 18 '18 at 20:08
  • If you have any problem, I'm here. I have already done the exact thing for myself. – Tazo leladze Jan 18 '18 at 20:22
  • @Taz742, I'm having problems when disabling the buttons if their respective lists are empty. Is there an easy way to add a signal to QListWidget that is emitted when it changes from empty to populated and viceversa? – Alechan Jan 18 '18 at 22:02
  • Yes, you can use eventFilter. https://stackoverflow.com/questions/36466415/qt-mouse-event-filter – Tazo leladze Jan 19 '18 at 05:48

1 Answers1

12

This widget does not come by default in Qt, but it is not complicated to build it, the first thing you should do is list the requirements:

  • The button with text >> is enabled if the list on the left is not empty, if pressed it must move all the items.

  • The button with text << similar to the previous one but with the list on the right

  • The button with text > is enabled if an item in the list on the left is selected, if pressed, the selected item should be moved to the right one.

  • The button with text < the same but for the right side.

  • The button with "up" text is enabled if an item is selected and this is not the first item in the list. when pressed you must move the current item to a higher position.

  • The button with "down" text is enabled if an item is selected and this is not the last item in the list. when pressed you must move the current item to a lower position.

As we see most of the logic depends on the selection of items, for this we connect the signal itemSelectionChanged with the slot that decides whether the buttons are enabled or not.

To move items we must use takeItem() and addItem(), the first removes the item and the second adds it.

Moving up or down is equivalent to removing the item and then inserting it, for this we use takeItem() again with insertItem()

All the above is implemented in the following widget:

#ifndef TWOLISTSELECTION_H
#define TWOLISTSELECTION_H

#include <QHBoxLayout>
#include <QListWidget>
#include <QPushButton>
#include <QWidget>

class TwoListSelection : public QWidget
{
    Q_OBJECT
public:
    explicit TwoListSelection(QWidget *parent = nullptr):QWidget{parent}{
        init();
        connections();
    }

    void addAvailableItems(const QStringList &items){
        mInput->addItems(items);
    }

    QStringList seletedItems(){
        QStringList selected;
        for(int i=0; i<mOuput->count(); i++)
            selected<< mOuput->item(i)->text();
        return selected;
    }
private:
    void init(){
        QHBoxLayout *layout = new QHBoxLayout(this);
        mInput = new QListWidget;
        mOuput = new QListWidget;

        mButtonToSelected = new QPushButton(">>");
        mBtnMoveToAvailable= new QPushButton(">");
        mBtnMoveToSelected= new QPushButton("<");
        mButtonToAvailable = new QPushButton("<<");

        layout->addWidget(mInput);

        QVBoxLayout *layoutm = new QVBoxLayout;
        layoutm->addItem(new QSpacerItem(10, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
        layoutm->addWidget(mButtonToSelected);
        layoutm->addWidget(mBtnMoveToAvailable);
        layoutm->addWidget(mBtnMoveToSelected);
        layoutm->addWidget(mButtonToAvailable);
        layoutm->addItem(new QSpacerItem(10, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));

        layout->addLayout(layoutm);
        layout->addWidget(mOuput);

        mBtnUp = new QPushButton("Up");
        mBtnDown = new QPushButton("Down");

        QVBoxLayout *layoutl =  new QVBoxLayout;
        layoutl->addItem(new QSpacerItem(10, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));
        layoutl->addWidget(mBtnUp);
        layoutl->addWidget(mBtnDown);
        layoutl->addItem(new QSpacerItem(10, 20, QSizePolicy::Minimum, QSizePolicy::Expanding));

        layout->addLayout(layoutl);
        setStatusButton();
    }

    void connections(){
        connect(mOuput, &QListWidget::itemSelectionChanged, this, &TwoListSelection::setStatusButton);
        connect(mInput, &QListWidget::itemSelectionChanged, this, &TwoListSelection::setStatusButton);
        connect(mBtnMoveToAvailable, &QPushButton::clicked, [=](){
           mOuput->addItem(mInput->takeItem(mInput->currentRow()));
        });

        connect(mBtnMoveToSelected, &QPushButton::clicked, [=](){
           mInput->addItem(mOuput->takeItem(mOuput->currentRow()));
        });

        connect(mButtonToAvailable, &QPushButton::clicked, [=](){
            while (mOuput->count()>0) {
                 mInput->addItem(mOuput->takeItem(0));
            }
        });

        connect(mButtonToSelected, &QPushButton::clicked, [=](){
            while (mInput->count()>0) {
                 mOuput->addItem(mInput->takeItem(0));
            }
        });

        connect(mBtnUp, &QPushButton::clicked, [=](){
            int row = mOuput->currentRow();
            QListWidgetItem *currentItem = mOuput->takeItem(row);
            mOuput->insertItem(row-1, currentItem);
            mOuput->setCurrentRow(row-1);
        });

        connect(mBtnDown, &QPushButton::clicked, [=](){
            int row = mOuput->currentRow();
            QListWidgetItem *currentItem = mOuput->takeItem(row);
            mOuput->insertItem(row+1, currentItem);
            mOuput->setCurrentRow(row+1);
        });
    }

    void setStatusButton(){
        mBtnUp->setDisabled(mOuput->selectedItems().isEmpty() || mOuput->currentRow() == 0);
        mBtnDown->setDisabled(mOuput->selectedItems().isEmpty() || mOuput->currentRow() == mOuput->count()-1);
        mBtnMoveToAvailable->setDisabled(mInput->selectedItems().isEmpty());
        mBtnMoveToSelected->setDisabled(mOuput->selectedItems().isEmpty());
    }

    QListWidget *mInput;
    QListWidget *mOuput;

    QPushButton *mButtonToAvailable;
    QPushButton *mButtonToSelected;

    QPushButton *mBtnMoveToAvailable;
    QPushButton *mBtnMoveToSelected;

    QPushButton *mBtnUp;
    QPushButton *mBtnDown;
};

#endif // TWOLISTSELECTION_H

enter image description here

In the following link you will find a complete example.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • + It is a good example but from the comments it seems that he himself wanted to do it. – Tazo leladze Jan 19 '18 at 05:51
  • I tried compiling it (both in Qt Creator and by qmake && make) and it raised too many errors to fix myself. But in theory it's very similar to what I ended up doing, so +1 and accepted . – Alechan Jan 19 '18 at 18:30
  • @Alechan What is your version of Qt? – eyllanesc Jan 19 '18 at 18:31
  • @eyllanesc, QMake version 3.0. Using Qt version 5.2.1 in /usr/lib/x86_64-linux-gnu – Alechan Jan 19 '18 at 18:46
  • 2
    its version of Qt is very old, from 5.6 or 5.7 Qt supports C ++ 11 where you can use the new connection style:https://wiki.qt.io/New_Signal_Slot_Syntax, in addition to other features. That is why it is not compatible with its version. I have tested it with Qt 5.10, but I think it is compatible with Qt 5.7 forwards. If I have time I give support for previous versions. – eyllanesc Jan 19 '18 at 18:50
  • I uploaded my version to https://github.com/Alechan/TwoListsExample. The main difference is that I check for empty list on operations instead of signals. – Alechan Jan 19 '18 at 19:58
  • Do not use `MainWindow::movexxxx`, you should simply use `movexxx` – eyllanesc Jan 19 '18 at 20:00