【Qt筆記】座標系統

在經歷過實際操做,以及前面一節中咱們見到的那個translate()函數以後,咱們能夠詳細瞭解下 Qt 的座標系統了。泛泛而談座標系統,有時候會以爲枯燥無味,難以理解,好在如今咱們已經有了基礎。算法

座標系統是由QPainter控制的。咱們前面說過,QPaintDeviceQPaintEngineQPainter是 Qt 繪製系統的三個核心類。QPainter用於進行繪製的實際操做;QPaintDevice是那些可以讓QPainter進行繪製的「東西」(準確的術語叫作,二維空間)的抽象層(其子類有QWidgetQPixmapQPictureQImageQPrinter等);QPaintEngine提供供QPainter使用的用於在不一樣設備上繪製的統一的接口。函數

 

因爲QPaintDeice是進行繪製的對象,所以,所謂座標系統,也就是QPaintDevice上面的座標。默認座標系統位於設備的左上角,也就是座標原點 (0, 0)。x 軸方向向右;y 軸方向向下。在基於像素的設備上(好比顯示器),座標的默認單位是像素,在打印機上則是點(1/72 英寸)。this

QPainter的邏輯座標與QPaintDevice的物理座標進行映射的工做,是由QPainter的變換矩陣(transformation matrix)、視口(viewport)和窗口(window)完成的。若是你不理解這些術語,能夠簡單瞭解下有關圖形學的內容。實際上,對圖形的操做,底層的數學都是進行的矩陣變換、相乘等運算。rest

在 Qt 的座標系統中,每一個像素佔據 1×1 的空間。你能夠把它想象成一張方格紙,每一個小格都是1個像素。方格的焦點定義了座標,也就是說,像素 (x, y) 的中心位置實際上是在 (x + 0.5, y + 0.5) 的位置上。這個座標系統其實是一個「半像素座標系」。咱們能夠經過下面的示意圖來理解這種座標系:code

咱們使用一個像素的畫筆進行繪製,能夠看到,每個繪製像素都是以座標點爲中心的矩形。注意,這是座標的邏輯表示,實際繪製則與此不一樣。由於在實際設備上,像素是最小單位,咱們不能像上面同樣,在兩個像素之間進行繪製。因此在實際繪製時,Qt 的定義是,繪製點所在像素是邏輯定義點的右下方的像素。orm

咱們前面已經介紹過,Qt 的繪製分爲走樣和反走樣兩種。對此,咱們必須分別對待。對象

一個像素的繪製最簡單,咱們從這裏開始:接口

從上圖能夠看出,當咱們繪製矩形左上角 (1, 2) 時,實際繪製的像素是在右下方。get

當繪製大於1個像素時,狀況比較複雜:若是繪製像素是偶數,則實際繪製會包裹住邏輯座標值;若是是奇數,則是包裹住邏輯座標值,再加上右下角一個像素的偏移。具體請看下面的圖示:數學

從上圖能夠看出,若是實際繪製是偶數像素,則會將邏輯座標值夾在相等的兩部分像素之間;若是是奇數,則會在右下方多出一個像素。

Qt 的這種處理,帶來的一個問題是,咱們可能獲取不到真實的座標值。因爲歷史緣由,QRect::right()QRect::bottom()的返回值並非矩形右下角點的真實座標值:QRect::right()返回的是 left() + width() – 1;QRect::bottom()則返回 top() + height() – 1,上圖的綠色點指出了這兩個函數的返回點的座標。

爲避免這個問題,咱們建議是使用QRectFQRectF使用浮點值,而不是整數值,來描述座標。這個類的兩個函數QRectF::right()QRectF::bottom()是正確的。若是你不得不使用QRect,那麼能夠利用 x() + width() 和 y() + height() 來替代 right() 和 bottom() 函數。

對於反走樣,實際繪製會包裹住邏輯座標值:

這裏咱們不去解釋爲何在反走樣是,像素顏色不是一致的,這是因爲反走樣算法致使,已經超出本節的內容。

Qt 一樣提供了座標變換。前面說,圖形學大部分算法依賴於矩陣計算,座標變換即是其中的表明:每一種變換都對應着一個矩陣乘法(若是你想知道學的線性代數有什麼用處,這就是應用之一了 ;-P)。咱們會以一個實際的例子來了解座標變換。在此以前,咱們須要瞭解兩個函數:QPainter::save()QPainter::restore()

前面說過,QPainter是一個狀態機。那麼,有時我想保存下當前的狀態:當我臨時繪製某些圖像時,就可能想這麼作。固然,咱們有最原始的辦法:將可能改變的狀態,好比畫筆顏色、粗細等,在臨時繪製結束以後再所有恢復。對此,QPainter提供了內置的函數:save()restore()save()就是保存下當前狀態;restore()則恢復上一次保存的結果。這兩個函數必須成對出現:QPainter使用棧來保存數據,每一次save(),將當前狀態壓入棧頂,restore()則彈出棧頂進行恢復。

在瞭解了這兩個函數以後,咱們就能夠進行示例代碼了:

void PaintDemo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.fillRect(10, 10, 50, 100, Qt::red);
    painter.save();
    painter.translate(100, 0); // 向右平移 100px
    painter.fillRect(10, 10, 50, 100, Qt::yellow);
    painter.restore();
    painter.save();
    painter.translate(300, 0); // 向右平移 300px
    painter.rotate(30); // 順時針旋轉 30 度
    painter.fillRect(10, 10, 50, 100, Qt::green);
    painter.restore();
    painter.save();
    painter.translate(400, 0); // 向右平移 400px
    painter.scale(2, 3); // 橫座標單位放大 2 倍,縱座標放大 3 倍
    painter.fillRect(10, 10, 50, 100, Qt::blue);
    painter.restore();
    painter.save();
    painter.translate(600, 0); // 向右平移 600px
    painter.shear(0, 1); // 橫向不變,縱向扭曲 1 倍
    painter.fillRect(10, 10, 50, 100, Qt::cyan);
    painter.restore();
}

Qt 提供了四種座標變換:平移 translate,旋轉 rotate,縮放 scale 和扭曲 shear。在這段代碼中,咱們首先在 (10, 10) 點繪製一個紅色的 50×100 矩形。保存當前狀態,將座標系平移到 (100, 0),繪製一個黃色的矩形。注意,translate()操做平移的是座標系,不是矩形。所以,咱們仍是在 (10, 10) 點繪製一個 50×100 矩形,如今,它跑到了右側的位置。而後恢復先前狀態,也就是把座標系從新設爲默認座標系(至關於進行translate(-100, 0)),再進行下面的操做。以後也是相似的。因爲咱們只是保存了默認座標系的狀態,所以咱們以後的translate()橫座標值必須增長,不然就會覆蓋掉前面的圖形。全部這些操做都是針對座標系的,所以在繪製時,咱們提供的矩形的座標參數都是不變的。

運行結果以下:

Qt 的座標分爲邏輯座標和物理座標。在咱們繪製時,提供給QPainter的都是邏輯座標。以前咱們看到的座標變換,也是針對邏輯座標的。所謂物理座標,就是繪製底層QPaintDevice的座標。單單隻有邏輯座標,咱們是不能在設備上進行繪製的。要想在設備上繪製,必須提供設備認識的物理座標。Qt 使用 viewport-window 機制將咱們提供的邏輯座標轉換成繪製設備使用的物理座標,方法是,在邏輯座標和物理座標之間提供一層「窗口」座標。視口是由任意矩形指定的物理座標;窗口則是該矩形的邏輯座標表示。默認狀況下,物理座標和邏輯座標是一致的,都等於設備矩形。

視口座標(也就是物理座標)和窗口座標是一個簡單的線性變換。好比一個 400×400 的窗口,咱們添加以下代碼:

void PaintDemo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setWindow(0, 0, 200, 200);
    painter.fillRect(0, 0, 200, 200, Qt::red);
}

咱們將窗口矩形設置爲左上角座標爲 (0, 0),長和寬都是 200px。此時,座標原點不變,仍是左上角,可是,對於原來的 (400, 400) 點,新的窗口座標是 (200, 200)。咱們能夠理解成,邏輯座標被「從新分配」。這有點相似於translate(),可是,translate()函數只是簡單地將座標原點從新設置,而setWindow()則是將整個座標系進行了修改。這段代碼的運行結果是將整個窗口進行了填充。

試比較下面兩行代碼的區別(仍是 400×400 的窗口):

painter.translate(200, 200);
painter.setWindow(-160, -320, 320, 640);

第一行代碼,咱們將座標原點設置到 (200, 200) 處,橫座標範圍是 [-200, 200],縱座標範圍是 [-200, 200]。第二行代碼,座標原點也是在窗口正中心,可是,咱們將物理寬 400px 映射成窗口寬 320px,物理高 400px 映射成窗口高 640px,此時,橫座標範圍是 [-160, 160],縱座標範圍是 [-320, 320]。這種變換是簡單的線性變換。假設原來有個點座標是 (64, 60),那麼新的窗口座標下對應的座標應該是 ((-160 + 64 * 320 / 400), (-320 + 60 * 640 / 400)) = (-108.8, -224)。

下面咱們再來理解下視口的含義。仍是以一段代碼爲例:

void PaintDemo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setViewport(0, 0, 200, 200);
    painter.fillRect(0, 0, 200, 200, Qt::red);
}

這段代碼和前面同樣,只是把setWindow()換成了setViewport()。前面咱們說過,window 表明窗口座標,viewport 表明物理座標。也就是說,咱們將物理座標區域定義爲左上角位於 (0, 0),長高都是 200px 的矩形。而後仍是繪製和上面同樣的矩形。若是你認爲運行結果是 1/4 窗口被填充,那就錯了。實際是隻有 1/16 的窗口被填充。這是因爲,咱們修改了物理座標,可是沒有修改相應的窗口座標。默認的邏輯座標範圍是左上角座標爲 (0, 0),長寬都是 400px 的矩形。當咱們將物理座標修改成左上角位於 (0, 0),長高都是 200px 的矩形時,窗口座標範圍不變,也就是說,咱們將物理寬 200px 映射成窗口寬 400px,物理高 200px 映射成窗口高 400px,因此,原始點 (200, 200) 的座標變成了 ((0 + 200 * 200 / 400), (0 + 200 * 200 / 400)) = (100, 100)。

如今咱們能夠用一張圖示總結一下邏輯座標、窗口座標和物理座標之間的關係:

咱們傳給QPainter的是邏輯座標(也稱爲世界座標),邏輯座標能夠經過變換矩陣轉換成窗口座標,窗口座標經過 window-viewport 轉換成物理座標(也就是設備座標)。

相關文章
相關標籤/搜索