Qtで文字列の画面上の幅を求める際のタブの扱い

Qtで文字列の画面上の幅を求めるには、[QFontMetrics](http://doc.qt.io/qt-5.7/qfontmetrics.html)クラスを使います。

```cpp:QFontMetrics
QRect boundingRect(QChar ch) const
QRect boundingRect(const QString &text) const
QRect boundingRect(const QRect &rect, int flags, const QString &text, int tabStops = 0, int *tabArray = Q_NULLPTR) const
QRect boundingRect(int x, int y, int width, int height, int flags, const QString &text, int tabStops = 0, int *tabArray = Q_NULLPTR) const
QSize size(int flags, const QString &text, int tabStops = 0, int *tabArray = Q_NULLPTR) const
int width(const QString&, int)
```

以降の議論では、`boundingRect`のオーバーライドを番号で区別します。また、混乱の無いときはクラス名を省略します。

```cpp
QRect boundingRect1(QChar ch) const
QRect boundingRect2(const QString &text) const
QRect boundingRect3(const QRect &rect, int flags, const QString &text, int tabStops = 0, int *tabArray = Q_NULLPTR) const
QRect boundingRect4(int x, int y, int width, int height, int flags, const QString &text, int tabStops = 0, int *tabArray = Q_NULLPTR) const
```

`size`と`boundingRect4`は`boundingRect3`を呼ぶので、`boundingRect3`と同一視します。

```cpp:qtbase/src/gui/text/qfontmetrics.cpp
QSize QFontMetrics::size(int flags, const QString &text, int tabStops, int *tabArray) const
{
    return boundingRect(QRect(0,0,0,0), flags | Qt::TextLongestVariant, text, tabStops, tabArray).size();
}
```

```cpp:qtbase/src/gui/text/qfontmetrics.h
inline QRect boundingRect(int x, int y, int w, int h, int flags, const QString &text,
                          int tabstops = 0, int *tabarray = Q_NULLPTR) const
    { return boundingRect(QRect(x, y, w, h), flags, text, tabstops, tabarray); }
```

`boundingRect1`は文字列ではなく文字を対象とするので、ここでは扱いません。

# タブの問題
文字列がタブを含む場合、関数(特に、boundingRectのどのバージョンを使うか)によって求まる幅が異なります。

その仕様は~~Qtの闇に包まれている~~ソースコードを読まないとわかりません。

ポイントは3つあります。

- 引数に`tabStops`があるか
- フラグ`Qt::TextExpandTabs`の有無
- `tabStops`の符号

タブ幅を含むテキストレイアウトのオプションは、QTextOptionクラスが保持しています。

[QTextOptionが保持するタブ幅のデフォルト値は-1です](http://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/text/qtextoption.cpp#n57)。

```cpp:qtbase/src/gui/text/qtextoption.cpp
QTextOption::QTextOption()
    : align(Qt::AlignLeft),
      wordWrap(QTextOption::WordWrap),
      design(false),
      unused(0),
      unused2(0),
      f(0),
      tab(-1),
      d(0)
{
   ⋮
}
```

## 引数にtabStopsが無い関数の場合

`width`、`boundingRect2`が該当します。

タブ幅はQTextOptionクラスで保持していて、その値はデフォルト値-1のままになります。

これらの関数は最終的に`QTextEngine::calculateTabWidth`関数をコールします。
その段階でタブ幅が0以下の場合、タブ幅は**80px**になります。

- [qtextengine.cpp](http://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/text/qtextengine.cpp)

```cpp:qtbase/src/gui/text/qtextengine.cpp
QFixed QTextEngine::calculateTabWidth(int item, QFixed x) const
{
    ⋮
    QFixed tab = QFixed::fromReal(option.tabStop());
    if (tab <= 0)
        tab = 80; // default
    tab *= dpiScale;
    QFixed nextTabPos = ((x / tab).truncate() + 1) * tab;
    QFixed tabWidth = nextTabPos - x;

    return tabWidth;
}
```

## 引数にtabStopsがある関数の場合

`boundingRect3`は`tabStops`を指定できます。デフォルト引数は0です。

`boundingRect3`は[qpainter.cpp](http://code.qt.io/cgit/qt/qtbase.git/tree/src/gui/painting/qpainter.cpp)で定義される`qt_format_text`関数をコールします。ここで、`boundingRect3`の引数`flags`に応じて重要な前処理が行われます。

### Qt::TextExpandTabsを指定しない場合
タブは半角スペースに置き換えられます。タブが無くなるので、`tabStops`の値は結果に影響を与えません。

### Qt::TextExpandTabsを指定する場合

[ドキュメント](http://doc.qt.io/qt-5.7/qfontmetrics.html#boundingRect-2)には以下のように書いてあります。

>  If `Qt::TextExpandTabs` is set in `flags`, then: if `tabArray` is non-null, it specifies a 0-terminated sequence of pixel-positions for tabs; otherwise if `tabStops` is non-zero, it is used as the tab spacing (in pixels).

しかし、実際は`boundingRect3`の引数`tabStops`の符号で3通りに分かれます。

`tabArray`は、`tabStops`の値がタブの深さで変わるというだけなので、以降は`tabStops`について考えます。

#### tabStopsが0の場合
`tabStops`を省略した場合も0になります。

このとき、タブ幅は「**文字`x`の幅の8倍**」になります (**0pxにはなりません**)。

```cpp:qpainter.cpp
void qt_format_text(const QFont &fnt, const QRectF &_r,
                    int tf, const QTextOption *option, const QString& str, QRectF *brect,
                    int tabstops, int *ta, int tabarraylen,
                    QPainter *painter)
{
    ⋮

    // compatible behaviour to the old implementation. Replace
    // tabs by spaces
    int old_offset = offset;
    for (; offset < text.length(); offset++) {
        QChar chr = text.at(offset);
        if (…) {
            ⋮
        } else if (chr == QLatin1Char('\t')) {
            if (!expandtabs) {
                text[offset] = QLatin1Char(' ');
            } else if (!tabarraylen && !tabstops) {
                tabstops = qRound(fm.width(QLatin1Char('x'))*8);
            }
        } else if (…) {
            ⋮
        }
    }
    ⋮
}
```

この後、最終的に`QTextEngine::calculateTabWidth`関数をコールします。

(タブ幅が0のままだと80pxになってしまいますが、この場合は先にxの8倍が代入されるのでそうはなりません)

#### tabStopsが負の場合

最終的に`QTextEngine::calculateTabWidth`関数をコールし、**80px**になります。

#### tabStopsが正の場合

ドキュメントの通り、`tabStops`の値が使われます。

# まとめ
デフォルト値として80pxと`x`の幅の8倍という2つの値があり、どちらが使われるかはドキュメントからはわかりません(そもそもデフォルト値自体が書かれていない)。

- 引数にtabStopsなし  
    80px
- 引数にtabStopsあり
    -  TextExpandTabs指定なし  
        半角スペースの幅
    -  TextExpandTabs指定あり
        - tabStops < 0  
            80px
        - tabStops = 0 (デフォルト)  
            `x`の幅の8倍
        - tabStops > 0  
            tabStopsの値