The Linux Page

Sorting any numeric column of a QTableWidget

Various types of noodles sorted by category in a box with columns.

Introduction

A QTableWidget uses QVariant as its value type. By default, though, it expects a string (in the construtor, you do not have support for QVariant).

Note that the QVariant uses the QTableWidthItem::setData() function and it keeps the variant as such.

This is great, only sometimes (always?!) you need a formatted number and that's not likely to work as expected because a formatted number requires you to use a string, not just a number and a format (which would be a better solution, i.e. a per column format such as defined by the QString::arg() functions.)

Setup a QTableWidget for Sorting

For a QTableWidget to be sortable by the end user, you need to setup a few things.

One parameter that can be set in designer is the showSortIndicator. This is the little arrow like triangle shown to the right of the column label.

The other parameter that can be set in designer is the sortingEnabled. However, the truth is that whenever you add items to your table, you most certainly want that flag turned off. So in most cases you're not going to find the designer flag that useful. There is an example of my code when I load my data in the QTableWidget:

// turn off the sort or the setItem() below won't work right
//
f_table->setSortingEnabled(false);
f_table->clear();
f_table->setRowCount(data.size());
int row(0);
for(auto d : data)
{
    QTableNumberItem * item(new QTableNumberItem(n, QString("%1%").arg(n * 100.0, 0, "f", 2)));
    f_table->setItem(row, 0, item);
    ++row;
}

// here the table gets sorted by Qt before getting displayed
//
f_table->setSortingEnabled(true);

In my case, I reset the whole table each time because (1) there isn't that many rows, and (2) the data changes as fast as the mouse moves.

It is important, though, to have the sorting enabled by the time you are done adding data to your table widget.

Sorting The Items

Now, the good thing about the QTableWidget implementation is that each item in the table is a QTableWidgetItem and you can overload that function. This is done to handle any kind of type in your columns and allow for any sort order for which you just have to define the operator < () function.

So all we have to do is create a new class of ours that supports our own type(s) and allow for sorting appropriately.

I have such a class defined below. It takes a number and a string as input and will sort the column according to the numbers. If you don't need any special formatting, you can just pass the number. If you don't have the number, you can just pass the string, although that will be slower (i.e. it has to conveert th string back to a number!)

The expected usage is something like this:

// get n, a number, from your data
//
double n = data[idx];

// create the item from the number and a corresponding
// formatted string, here dollars such as $123.45
//
QTableNumberItem * item(new QTableNumberItem(n,
                    QString("$%1").arg(n, 0, "f", 2, QChar('0'));

// Add this item to the table_widget at "row" and "column"
//
table_widget->setItem(row, column, item);

As we can see, the number (n) is added as one of the parameters of the constructor so that way it can get saved as is inthe class (see f_value below.) THis means the operator < () can directly compare the f_value of the left and right hand-side objects together without an additional convertion at that moment making the sort very fast in comparison to most of the solutions that you find out there on the Internet.

// the following is dead fast compared to first having to
// convert a string to a number
//
return f_value < rhs.f_value;

So, here are the two important parts of this one:

  • Save the value as is and avoid potential conversion problems
  • Compare the value as fast as possible

Plus a third rather improved part:

  • The display text can be formatted as you wish

The QTableNumberItem class

Tne following is my class implementation. As you can see, it includes multiple constructor so you can define your table item data the way you want. It supports integers (long), floating point (double), and only text (QString).

The integer and floating point can be given a formatted text (QString) as shown in my example above.

An important aspect of this class, the operator < () function makes sure that right hand-side is also a QTableNumberItem. If not, then it throws an std::runtime_error(). It won't work if some items are not a QTableNumberItem because the other type of item operator < () funtion won't compare the two values as expected. Note that if you derive from QTableNumberItem, you'll be fine.

The textToDouble() function converts a string to a double when you only have a QString as input. It is best to have a way to pass the original values although you may be getting the original in text anyway (i.e. reading from a CSV file, for example.)

Note that I made the function a virtual so you can override it, just in case you have a different way to extract the number from the string. Here I remove all non-digits from the start and the end of the string, then I remove any spaces and commas in the string. After that, I use the QString::toDouble() function to convert the string.

class QTableNumberItem
    : public QTableWidgetItem
{
public:
    // with possible decorations
    //
    QTableNumberItem(QString const & text)
        : QTableWidgetItem(text)
        , f_value(textToDouble(text))
    {
    }

    QTableNumberItem(long n, QString const & text)
        : QTableWidgetItem(text)
        , f_value(n)
    {
    }

    QTableNumberItem(double n, QString const & text)
        : QTableWidgetItem(text)
        , f_value(n)
    {
    }

    QTableNumberItem(long n)
        : QTableWidgetItem(QString("%1").arg(n))
        , f_value(n)
    {
    }

    QTableNumberItem(double n)
        : QTableWidgetItem(QString("%1").arg(n))
        , f_value(n)
    {
    }

    bool operator < (QTableWidgetItem const & rhs) const
    {
        QTableNumberItem const * r(dynamic_cast<QTableNumberItem const *>(&rhs));
        if(r == nullptr)
        {
            throw std::runtime_error("mixed QTableWidgetItem types is not supported by QTableNumberItem.");
        }
        return f_value < r->f_value;
    }

    virtual double textToDouble(QString t)
    {
        // clean the beginning of the string of whatever
        // introducers
        //
        int const sp(t.indexOf(QRegExp("[0-9.]"), 0));
        if(sp > 0)
        {
            t = t.mid(sp);
        }

        // clean the end of the string of whatever postfix
        //
        int const ep(t.indexOf(QRegExp("[^0-9. ,]"), 0));
        if(ep > 0)
        {
            t = t.mid(ep);
        }

        // now remove commas and spaces
        //
        // note: replace() happens in place so we don't have to
        //       save the result in the variable itself
        //
        t.replace(QRegExp("[, ]+"), "");

        return t.toDouble();
    }

private:
    double      f_value = 0.0;
};

Tweaking the Class

If the basic number sort is working for you, then the class can just be derived and the texToDouble() overriden to support a different type of formatted numbers.

The default function may be too strong for your data or too weak.

Remember that of you have two integer numbers and are trying to get a sort going by converting those two numbers in a floating pointer number, you are likely to get errors you could otherwise avoid by using your own class and two numbers (see Creating Your Own Class below.)

Creating Your Own Class

If your data has multiple values that are needed to sort the column, create your own class and do the sort the way you need to happen. It could also be that you need a very special sort.

For example, the US Code uses numbers such as chapters, sections, paragraphs. These are separated by periods, but there are three of them, so the string does not represet a decimal number. Also, the numbers are very precise and converting to a floating point may not sort values correctly.

In this case, I would create a new class QTableUSCItem and use three or four integers as the case may be. Here I show an example that one would use with a new like this:

QTableUSCItem * item = new QTableUSCItem("3.5.2 Rights and protections under the Occupational Safety and Health Act of 1970; procedures for remedy of violations");

The constructor will convert the code in three numbers later used by the bool operartor < () function.

class QTableUSCItem
{
    QTableUSCItem(QString text)
        : QTableWidgetItem(text)
    {
        QStringList code_title(text.split(" "));
        if(code_title.size() < 2)
        {
            throw std::runtime_error("invalid entry");
        }

        QStringList csp(code_title[0].split("."));
        if(csp.size() != 3)
        {
            throw std::runtime_error("invalid code");
        }

        f_chapter = csp[0].toInt();
        f_section = csp[1].toInt();
        f_paragraph = csp[2].toInt();
    }

    bool operator < (QTableWidgetItem const & rhs) const
    {
        return f_chapter < r->f_chapter
            || (f_chapter == r->f_chapter && f_section < r->f_section)
            || (f_chapter == r->f_chapter && f_section == r->f_section && f_paragraph < r->f_paragraph);
    }

private:
    long f_chapter;
    long f_section;
    long f_paragraph;
};

As we can see the operator < () makes use of the three numbers to sort the items.

Extend Your Knowledge

Qt5 C++ GUI Programming Cookbook, the picture on the front page represents a stream with a fall in the background and a small lake in the front.For more about Qt, I suggest you checkout the Qt5 C++ GUI Programming Cookbook book on Amazon. It will definitely help you at work, solving many problems in your development using Qt5. Oh and remember that employers like to get you tools that allow them to reduce their costs and that includes books!