C++
Qt

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

More than 1 year has passed since last update.

Qtで文字列の画面上の幅を求めるには、QFontMetricsクラスを使います。


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のオーバーライドを番号で区別します。また、混乱の無いときはクラス名を省略します。

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

sizeboundingRect4boundingRect3を呼ぶので、boundingRect3と同一視します。


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();
}


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です


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が無い関数の場合

widthboundingRect2が該当します。

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

これらの関数は最終的にQTextEngine::calculateTabWidth関数をコールします。

その段階でタブ幅が0以下の場合、タブ幅は80pxになります。


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がある関数の場合

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

boundingRect3qpainter.cppで定義されるqt_format_text関数をコールします。ここで、boundingRect3の引数flagsに応じて重要な前処理が行われます。


Qt::TextExpandTabsを指定しない場合

タブは半角スペースに置き換えられます。タブが無くなるので、tabStopsの値は結果に影響を与えません。


Qt::TextExpandTabsを指定する場合

ドキュメントには以下のように書いてあります。


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にはなりません)。


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の値