建立一個不被 View 影響線條寬度 Graphics Item


之前在《建立自己的 QGraphicsItem》一篇文章裡,已經有大概提過要怎麼建立一個屬於自己、可以在 Qt 的 Graphics View 裡面使用的 QGraphicsItem 了。而這一篇,則算是一種延伸的特殊需要,主要是希望可以透過在 paint() 這個函式裡面做額外的控制,避免使用 QPainter 進行繪製的時候,會有畫筆(QPen)和筆刷(QBrush)一起受到 Graphics view 的 transformation(主要是放大和縮小)影響的問題。

下面是一個受到影響、簡單的例子:

這裡 Heresy 所設計的 item,基本上是一個四邊形,在 paint() 裡面,會先用指定的筆刷(m_qBrush)來填滿整個四邊形,然後再用指定的畫筆(m_qBorder),畫上他的邊界;程式碼基本上就是:

void paint( QPainter* pPainter,
            const QStyleOptionGraphicsItem* pOption,
            QWidget *pWidget )
{
  // fill selection area
  pPainter->fillRect( m_qSelectRect, m_qBrush );
 
  // draw selection border
  pPainter->setPen( m_qBorder );
  pPainter->drawRect( m_qSelectRect );
}

其中,m_qSelectRect 是一個 QRect 的物件,用來記錄四邊形的區域;而 m_qBrushm_qBorder 的基本設定,則是:

m_qBorder  = QPen( QBrush( QColor::fromRgb( 192, 255, 192, 255 ) ),
                   2, Qt::DashLine );
m_qBrush  = QBrush( QColor::fromRgb( 255, 255, 192, 96 ), Qt::Dense5Pattern );

而這樣的寫法,在 graphics view 是以 100% 的比例來顯示的時候,的確是沒有問題的、是預期的效果的(最上方左圖);但是如果是放大到一定的倍數的話,就會很明顯地發現到,不但邊框的線的寬度變得很寬,同時整個填滿的區域,也變很粗,和本來預期的效果不一樣了(最上方右圖)~

同時,如果是縮小到一定的程度的話,也會發現邊框的線也會因為縮小的關係,變得很不清楚(右圖)…

當然,這其實不是 Qt 的問題。因為實際上,這樣的縮放,在一般的狀況下,應該是合理的!

但是實際上,有的時候可能會不希望有這樣的效果,尤其是自己在實作 Graphics view 裡面的一些操作圖形介面的時候,這樣把線條也放大、縮小效果,其實在 Heresy 來看,或多或少是會造成問題的…

而如果不希望有這樣的效果,要怎麼解決呢?Heresy 這邊是用比較單純,針對現在碰到的方法來找解法的,所以主要只有針對兩個方向,一個是「QPen 的寬度」,另一個則是「QBrushDense5Pattern 這類的 BrushStyle」。


在《建立自己的 QGraphicsItem》一文中,其實有大概提到過,透過 paint() 的第二個參數、也就是型別為 QStyleOptionGraphicsItem 的變數,可以取得 LOD(Level-Of-Detail、多層次精細度)的相關參數;而這邊,要動態調整化的寬度,就是要用到這個 LOD 的資訊。

不過,實際上這邊要用的,是 QStyleOptionGraphicsItem 的一個 static member function:levelOfDetailFromTransform()。他可以去分析一個 QPainterworldTransform 資料,來取得目前在 view 裡面的縮放比例,基本用法大致上就是:

qreal qScale = pOption->levelOfDetailFromTransform( pPainter->worldTransform() );

其中,取得的 qScale 就是代表目前這個 item 的縮放比例。如果是放大兩倍的話,qScale 就會是 2;如果是縮小一半的話,他的值則會是 0.5。而有了這個值後,在 paint() 被呼叫到的時候,就可以知道自己呈現出來的比例、然後再做對應的調整了~

如果以這邊畫筆的寬度的問題來說,在知道這個縮放比例後,就很好動態去調整畫筆的寬了~程式的寫法大致如下:

QPen pen = m_qBorder;
qreal qScale = pOption->levelOfDetailFromTransform( pPainter->worldTransform() );
pen.setWidthF( m_qBorder.widthF() / qScale );
pPainter->setPen( pen );
pPainter->drawRect( m_qSelectRect );

上面的程式碼,基本上就是變成先建立一個臨時的畫筆(pen),讓他等於我們指定的畫筆(m_qBorder),接下來就透過 setWidthF() 調整他的畫筆寬度(除已縮放比例 qScale)後,就可以拿來固定最後呈現出來的畫筆寬度、而不會被放大了!

而實際上,如果希望根據縮放比例來控制自己要繪製的細緻程度的話,基本上也就是要根據這邊這個 qScale 的值了~如果物件本身的繪製過程非常複雜的話,在這邊是可以透過「根據 qScale 來調整要繪製的東西」這個概念,還做到 LOD 的效果(一般來說是為了效能,避免在明明已經看不清楚的情況下,還要畫出過度細節的東西)。


畫筆的問題基本上這樣就解決了。不過很遺憾的是,Qt 筆刷的 BrushStyle 似乎不能這樣靠簡單的計算來做掉…不過後來稍微研究了一下、結果發現 QBrush 有提供一個 setTransform() 的函示,可以用來控制筆刷本身的 transformation;而且由於他的 transformation 會和 QPainter 的 transformation 做合併,所以這邊只要透過設定筆刷的 transformation 來抵銷掉 QPainter 的 transformation 效果,就可以消除放大對於 BrushStyle 的影響了~

而這邊的程式碼就寫成下面的樣子:

QBrush brush = m_qBrush;
brush.setTransform( pPainter->worldTransform().inverted() );
pPainter->fillRect( m_qSelectRect, brush );

基本上,和畫筆時差不多,就是先使用一個臨時的筆刷 brush 來複製指定的筆刷參數(m_qBrush),然後再去設定他的 transformation,最後再用這個筆刷來話我們要的四邊形就可以了~

而由於這邊的 transformation 是要讓 QPainterworldTransform 無效化,所以這裡就是很單純地、把 brush 的 transformation 設定成他的反向轉換就可以了~Qt 在這裡,也很貼心地直接提供了 QTransform::inverted() 這個函式,可以直接用來取得一個 transformation 的反向轉換。


如果把本來 paint() 裡面繪圖用的部分,都改成這樣、有經過調整的畫法的話,就可以做出一個邊框粗細、內部筆刷的風格不會受到 graphics view 的 transformation 影響的 graphics item 了!

像右圖就是經過修改後的 item、在放大五倍後的呈現效果。這邊應該可以很明顯地看的出來,他的線條寬度看內部的筆刷,都沒有因為放大檢視、而也跟著放大;所以,這也就達到 Heresy 最初的目標了~

不過說實話,Heresy 不確定這個方法到底好不好?也不知道 Qt 在 Graphics view 的這個架構下,是否有提供其他功能可以更快地做到這件事?但是,再找到更好的方法前,這個應該算是一個可用的方案吧~

不過,這邊也要注意一下,Heresy 在這裡只是針對 Heresy 有用到的功能做處理。像是在筆刷的部分,Hereswy 就沒有考慮到它本身本來的 transform;所以如果本來有使用 setTransform() 做設定的話,他的值是會被洗掉的;另外像畫筆裡的筆刷,Heresy 是根本沒去處理,如果有設定的話,也有可能會有問題。

而其他如果在使用 QPainter 時,有用到其他會受 view 的 transformation 影響的功能的話,應該也是需要另外處理的。不然,就有可能造成局部的功能還是會受到影響。總之,基本上,就是看用到那些、就處理那些吧~


Nokia Qt 相關文章目錄

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.