前へ 次へ
技術文章qvi

前稿でカーソルを自前描画するようにしたのだが、カーソル移動を行うと再描画が正しく行われず、 下図の様に画面にゴミが残るという問題があった。

多大な苦労の末にやっと対処できたのだが、読者の参考になるやもしれないので、その経緯を記述しておく。
(原因が分かってみれば、対処は簡単だったのだが、原因究明にすんげー時間を取られたので、苦労話をしたいだけでござるぞ)

何故ゴミが残るのか?

テスト的にプログラムを書いてみたり、デバッガで追いかけてみたり、プリント文をちりばめてみたところ、 カーソルの描画コードそのものには問題が無いようであった。

カーソルを左に移動させ画面を注意深く見ると、古い文字カーソルの右半分が消去されず、ゴミが残っていることがわかった。
下図は“う”の位置にテキストカーソルがあった時に、カーソルを左に移動したときに無効化される領域を赤枠で表したもの。 “う”の右側が再描画されないので、カーソルの右下部分がゴミとして残ってしまうというわけだ。

上図を曇りなき眼で見ると、無効化領域が左にずれているように思えた。 ビューポートの原点とかドキュメントマージンとかの問題の様に思えたのである。 それがこの問題の原因究明に時間を費やした元凶、犯人が周到に用意したトリックだったのだ。

しかし、いったいいつ誰がどこで無効化処理を行っているのか、QPlainTextEdit のソースを順に読んで行ってもなかなか判明しなかった。
そこで、領域を無効化している部分に目をつけることにした。 QWidget で領域を無効化するには update(const QRect&) を呼べばいいので、"src\gui\widgets\qplaintextedit.cpp" で update( を検索してみると、QPlainTextEditPrivate::_q_repaintContents() が怪しげだった。

 1: void QPlainTextEditPrivate::_q_repaintContents(const QRectF &contentsRect)
 2: {
 3:     .....
 4:     viewport->update(r);
 5:     emit q->updateRequest(r, 0);
 6: }

上記リストの4行目にブレークポイントを仕掛け、そこで止める。 呼び出し履歴を見れば誰がいつどこで領域を無効化しているのか分かるはずだ。 しかし、プログラムをアクティブにすると必ずここに飛んでくるので、カーソル移動時に領域を無効化している犯人を突き止めることができない。 犯人だけをひっかけようと罠を仕掛けたのに、誰も彼もが罠にかかってしまう。もっと選択的な罠でないといけない。
そこで、ステップ動作を行って処理を追っていくと、QWidgetBackingStore::markDirty(...) が無効化領域を記録しているとこだとわかった。

 1: void QWidgetBackingStore::markDirty(.....)
 2: {
 3:     .....
 4:     if (widget->d_func()->inDirtyList) {
 5:         if (!qt_region_strictContains(widget->d_func()->dirty, widgetRect))
 6:             widget->d_func()->dirty += widgetRect;
 7:     } else {
 8:     .....
 9: }

しかも上記リスト6行目の部分にブレークポイントを仕掛けると、ウィンドウアクティブ時にはひっかからず、 カーソル移動の時のみ停止する。やっと犯人のみをひっかける罠が見つかった。 そのときの呼び出し履歴をみれば、誰が無効化しているのかがすぐにわかる。

犯人は QTextControl::setTextCursor() がコールしている QTextControlPrivate::repaintOldAndNewSelection() だった。
その中で、QTextControlPrivate::cursorRectPlusUnicodeDirectionMarkers() をコールし、
さらに QRectF QTextControlPrivate::rectForPosition() をコールしてカーソル領域を取得している。
んで、ソースを見ると下記の様に cursorWidth プロパティの値を参照しているではないか。

 1: QRectF QTextControlPrivate::rectForPosition(int position) const
 2: {
 3:     .....
 4:     cursorWidth = docLayout->property("cursorWidth").toInt(&ok);
 5:     .....
 6: }

前稿では、カーソルを自前で描画するので、QPlainTextEdit のテキストカーソル描画を禁止するために setCursorWidth(0) をコールして、テキストカーソル幅をゼロにしていたのだ。

これではテキストカーソル領域が正しく無効化されず、ゴミがのこって当然である。orz

しかし、無効化領域を取得するのにカーソル幅を参照しているのであれば、幅ゼロに設定しているので無効化領域も空になるのではないか? 実際には元のテキストカーソル位置の左右数ドットが再描画されている。何故だ?
と思ったが、その謎はすぐ解けた。
なんと下記ソース4行目で、カーソル領域矩形を左右に4ピクセルずつ拡大していたのだ。orz

 1: QRectF QTextControlPrivate::cursorRectPlusUnicodeDirectionMarkers(const QTextCursor &cursor) const
 2: {
 3:     .....
 4:     return rectForPosition(cursor.position()).adjusted(-4, 0, 4, 0);
 5: }

問題の原因は無効化領域が左にずれていることだと推測していたのだが、 実は下図のように文字の左端に幅ゼロのテキストカーソル(青色の縦線)があり、その左右4ピクセルが無効化されている、というのが真相であった。 たまたま文字表示幅が8ピクセル程度だったために、犯人のトリックにまんまと騙され、左にずれているように見えてしまっていたのだ。

対処

原因が判明すれば対処は簡単だった。常にテキストカーソル幅をゼロにしてテキストカーソルを非表示にするのではなく、 QPlainTextEdit::paintEvent(event) を呼ぶ直前にゼロにし、画面描画終わったら直ぐに正しい値に戻してやればいいのだ。

ソースは下記のように修正した。

 1: void ViEditView::paintEvent(QPaintEvent * event)
 2: {
 3:     .....
 4:     setCursorWidth(0);
 5:     QPlainTextEdit::paintEvent(event);
 6:     setCursorWidth(m_cursorWidth);
 7:     .....
 8: }

これで、テキストカーソルのゴミが画面に残ることもなく、難事件が解決された後の様にすっきりした。

教訓

Tweet


前へ 次へ
技術文章qvi