Search code examples
c++qtqwidget

Use tab "\t" on QTableWidget gives different spacing behavior than QLabel texts


Question

  • What is the rule for Qt tabbing?
  • Can I override the tab calculation somehow?

Example

Using tabs "\t" on QTableWidget gives different spacing behavior than QLabel texts.

With the following example code:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , m_qText1( nullptr )
    , m_qText2( nullptr )
    , m_qText3( nullptr )
    , m_qTable( nullptr )
{
    ui->setupUi(this);

    m_qText1 = new QLabel(this);
    m_qText1->setGeometry( 0, 100, 400, 100);
    m_qText1->setText( "dafdsfsdf\te" );

    m_qText2 = new QLabel(this);
    m_qText2->setGeometry( 0, 200, 400, 100);
    m_qText2->setText( "dafddsasddsafsdf\te" );

    m_qText3 = new QLabel(this);
    m_qText3->setGeometry( 0, 300, 400, 100);
    m_qText3->setText( "dafasdassadasdsasadfsdf\te" );

    m_qTable = new QTableWidget(this);
    m_qTable->horizontalHeader()->setVisible(false);
    m_qTable->verticalHeader()->setVisible(false);
    m_qTable->setGeometry( 0, 0, 200, 100 );

    for (int iRow = 0; iRow <= 2; ++iRow)
    {
        int rowCount = m_qTable->rowCount();
        m_qTable->insertRow( rowCount );
    }
    m_qTable->insertColumn(0);
    m_qTable->setColumnWidth( 0, 200);
    QTableWidgetItem* myItem = new QTableWidgetItem( "dafdsfsdf\te" );
    m_qTable->setItem( 0, 0, myItem);
    myItem = new QTableWidgetItem( "dafddsasddsafsdf\te" );
    m_qTable->setItem( 0, 1, myItem);
    myItem = new QTableWidgetItem( "dafasdassadasdsasadfsdf\te" );
    m_qTable->setItem( 0, 2, myItem);
}

And the header

private:
    QLabel* m_qText1;
    QLabel* m_qText2;
    QLabel* m_qText3;
    QTableWidget* m_qTable;

Result

The following result:

result

As we can see the QLabels have fixed spacing for tabs "\t".

However, the QTableWidget fix the tab to cell widths. My guess is that before half of the cell, the tab adds spaces untill half of the cell and after half of the cell, untill the end of the cell.

QTextEngine

FWIW: Qt has this class QTextEngine and the function QTextEngine::calculateTabWidth is given by:

QFixed QTextEngine::calculateTabWidth(int item, QFixed x) const
{
    const QScriptItem &si = layoutData->items.at(item);

    QFixed dpiScale = 1;
    if (QTextDocumentPrivate::get(block) != nullptr && QTextDocumentPrivate::get(block)->layout() != nullptr) {
        QPaintDevice *pdev = QTextDocumentPrivate::get(block)->layout()->paintDevice();
        if (pdev)
            dpiScale = QFixed::fromReal(pdev->logicalDpiY() / qreal(qt_defaultDpiY()));
    } else {
        dpiScale = QFixed::fromReal(fnt.d->dpi / qreal(qt_defaultDpiY()));
    }

    QList<QTextOption::Tab> tabArray = option.tabs();
    if (!tabArray.isEmpty()) {
        if (isRightToLeft()) { // rebase the tabArray positions.
            auto isLeftOrRightTab = [](const QTextOption::Tab &tab) {
                return tab.type == QTextOption::LeftTab || tab.type == QTextOption::RightTab;
            };
            const auto cbegin = tabArray.cbegin();
            const auto cend = tabArray.cend();
            const auto cit = std::find_if(cbegin, cend, isLeftOrRightTab);
            if (cit != cend) {
                const int index = std::distance(cbegin, cit);
                auto iter = tabArray.begin() + index;
                const auto end = tabArray.end();
                while (iter != end) {
                    QTextOption::Tab &tab = *iter;
                    if (tab.type == QTextOption::LeftTab)
                        tab.type = QTextOption::RightTab;
                    else if (tab.type == QTextOption::RightTab)
                        tab.type = QTextOption::LeftTab;
                    ++iter;
                }
            }
        }
        for (const QTextOption::Tab &tabSpec : std::as_const(tabArray)) {
            QFixed tab = QFixed::fromReal(tabSpec.position) * dpiScale;
            if (tab > x) {  // this is the tab we need.
                int tabSectionEnd = layoutData->string.size();
                if (tabSpec.type == QTextOption::RightTab || tabSpec.type == QTextOption::CenterTab) {
                    // find next tab to calculate the width required.
                    tab = QFixed::fromReal(tabSpec.position);
                    for (int i=item + 1; i < layoutData->items.size(); i++) {
                        const QScriptItem &item = layoutData->items.at(i);
                        if (item.analysis.flags == QScriptAnalysis::TabOrObject) { // found it.
                            tabSectionEnd = item.position;
                            break;
                        }
                    }
                }
                else if (tabSpec.type == QTextOption::DelimiterTab)
                    // find delimiter character to calculate the width required
                    tabSectionEnd = qMax(si.position, layoutData->string.indexOf(tabSpec.delimiter, si.position) + 1);

                if (tabSectionEnd > si.position) {
                    QFixed length;
                    // Calculate the length of text between this tab and the tabSectionEnd
                    for (int i=item; i < layoutData->items.size(); i++) {
                        const QScriptItem &item = layoutData->items.at(i);
                        if (item.position > tabSectionEnd || item.position <= si.position)
                            continue;
                        shape(i); // first, lets make sure relevant text is already shaped
                        if (item.analysis.flags == QScriptAnalysis::Object) {
                            length += item.width;
                            continue;
                        }
                        QGlyphLayout glyphs = this->shapedGlyphs(&item);
                        const int end = qMin(item.position + item.num_glyphs, tabSectionEnd) - item.position;
                        for (int i=0; i < end; i++)
                            length += glyphs.advances[i] * !glyphs.attributes[i].dontPrint;
                        if (end + item.position == tabSectionEnd && tabSpec.type == QTextOption::DelimiterTab) // remove half of matching char
                            length -= glyphs.advances[end] / 2 * !glyphs.attributes[end].dontPrint;
                    }

                    switch (tabSpec.type) {
                    case QTextOption::CenterTab:
                        length /= 2;
                        Q_FALLTHROUGH();
                    case QTextOption::DelimiterTab:
                    case QTextOption::RightTab:
                        tab = QFixed::fromReal(tabSpec.position) * dpiScale - length;
                        if (tab < x) // default to tab taking no space
                            return QFixed();
                        break;
                    case QTextOption::LeftTab:
                        break;
                    }
                }
                return tab - x;
            }
        }
    }
    QFixed tab = QFixed::fromReal(option.tabStopDistance());
    if (tab <= 0)
        tab = 80; // default
    tab *= dpiScale;
    QFixed nextTabPos = ((x / tab).truncate() + 1) * tab;
    QFixed tabWidth = nextTabPos - x;

    return tabWidth;
}

Maybe it is used to calculate tabs width? But I wonder why it only happends on Tables and not on Labels...

After some research I could not find the definitive answer, so What is the rule for Qt tab spacing "\t"? and Can I override the tab calculation somehow?


Solution

  • Your tests are unreliable, as you're using arbitrary text lengths that don't properly show when the tab distance is actually applied.

    Let's do some more accurate examples. We start with a list of QLabels, with each one of them having one more letter before the tab: o\to, oo\to, etc.

    Labels showing proper position of tabs

    Now let's do the same, with an item view:

    item view showing proper position of tabs

    As you can see, the tab spacing is always respected, it just uses a different width. For comparison, you can see the two examples side by side, and how the different tab distances change the result:

    comparison of different tab distances

    QLabel actually has two ways of laying out (and drawing) text.

    When it's using plain text and no text interaction flag is set, it renders the text directly using the label style's drawItemText() which, by default, calls the QPainter drawText() that uses the font metrics' horizontalAdvance() of the letter x multiplied by 8 times as the default tab width.

    When rich text (the "HTML" subset of Qt) is used, or a text interaction flag is set, it uses the QTextDocument interface, which defaults the tab distance to 80 pixels unless changed through the QTextOption setTabStopDistance() function. As soon as a text interaction flag is set, you'll probably see that the tab width changes, following the behavior of the item view, and that's because views use the same API to display text.

    There is no way to set a default "global" tab distance, and the inconsistency of QLabel/QPainter certainly doesn't help: QPainter is only based on the width of 8 x letters of the given font, QTextDocument always uses 80 pixels.

    There is also no direct way to change the behavior of item views, if not by using a custom delegate and draw the text on your own by overriding paint(), which could be achieved by creating a QTextDocument for the text, setting a different tabStopDistance() for its default QTextOption, and then call its drawContents() function with the active painter.

    Interestingly enough, this is a bit simpler with labels, even if it's a bit "hacky": when a text interaction flag is set on the label, it immediately creates an internal QWidgetTextControl that has its own QTextDocument, which can be accessed using findChild().

    The following example is written in Python for simplicity (and also because I'm not able to write proper C++ code), but clearly shows how to achieve the above:

    import sys
    from PyQt5.QtCore import *
    from PyQt5.QtWidgets import *
    
    app = QApplication(sys.argv)
    
    label1 = QLabel('def\tworld') # default behavior
    label2 = QLabel('cus\tworld') # custom tab distance
    
    label2.setTextInteractionFlags(Qt.TextSelectableByMouse)
    # we don't actually need selection, it's only to ensure
    # that the text control is created, therefore we disable
    # mouse interaction
    label2.setAttribute(Qt.WA_TransparentForMouseEvents)
    # and prevent focus stealing
    label2.setFocusPolicy(Qt.NoFocus)
    
    doc = label2.findChild(QTextDocument)
    opt = doc.defaultTextOption()
    opt.setTabStopDistance(50)
    doc.setDefaultTextOption(opt)
    
    test = QWidget()
    layout = QVBoxLayout(test)
    layout.addWidget(label1)
    # show the default tab distance for QLabel/QPainter
    layout.addWidget(QLabel('xxxxxxxx'))
    layout.addWidget(label2)
    
    test.show()
    sys.exit(app.exec())
    

    And here is the result:

    Custom tab distance for QLabel