Qt 2D繪圖之二:抗鋸齒渲染和坐標系統


一、抗鋸齒渲染

1.1 邏輯繪圖

圖形基元的大小(寬度和高度)始終與其數學模型相對應,下圖示意了忽略其渲染時使用的畫筆的寬度的樣子。


1.2 物理繪圖(默認情況)

在默認的情況下,繪制會產生鋸齒,並且使用這樣的規則進行繪制: 當使用寬度為一個像素的畫筆進行渲染時,像素會在數學定義的點的右邊和下邊進行渲染,如下圖1所示。當使用一個擁有偶數像素的畫筆進行渲染時,像素會在數學定義的點的周圍對稱渲染;而當使用一個擁有奇數像素的面筆進行渲染時,首先按照偶數對稱繪制,最后一個像素會被渲染到數學定義的點的右邊和下邊,如下圖2所示。

所以看起來圖像不是很平滑,像是有鋸齒,所以為了消鋸齒,就要用到抗鋸齒繪圖。


1.3 抗鋸齒繪圖

抗鋸齒( Anti-aliased)又稱為反鋸齒或者反走樣,就是對圖像的邊緣進行平滑處理,使其看起來更加柔和流暢的一種技術。QPaint er 進行繪制時可以使用QPainter ::RenderHint 渲染提示來指定是否要使用抗鋸齒功能,RenderHint 取值分為以下三種。


如果在繪制時使用了抗鋸齒渲染提示,即使用 QPainter:: setRenderHint(RenderHint hint, bool on = true) 函數,將參數 hint 設置為了 QPainter:: Antialiasing。那么像素就會在數學定義的點的兩側對稱的進行渲染,如下圖所示。


示例程序為:

QPainter painter(this);
painter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); //抗鋸齒和使用平滑轉換算法

二、坐標系統

2.1 坐標系統簡介

Qt的坐標系統是由QPainter類控制的,而QPainter是在繪圖設備上繪制的。一個繪圖設備的默認坐標系統中原點(0, 0)在其左上角,x坐標向右增長,y坐標向下增長。在基於像素的設備上,默認的單位是一個像素,而在打印機上默認的單位是一個點(1/72英寸)。

下面仍然在上一節的程序中進行代碼演示,更改paintEvent()的內容如下:

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setBrush(Qt::red);
    painter.drawRect(0, 0, 100, 100);
    painter.setBrush(Qt::yellow);
    painter.drawRect(-50, -50, 100, 100);
}

我們先在原點(0,0)繪制了一個長寬都是100像素的紅色矩形,又在(-50,-50)點繪制了一個同樣大小的黃色矩形。可以看到,我們只能看到黃色矩形的四分之一部分。運行程序,效果如下圖所示。


2.2 坐標系統變換

默認情況下,QPainter在指定設備的坐標系統上進行繪制,在進行繪圖時,使用QPainter::translate()函數平移坐標系統;可以使用QPainter::scale()函數縮放坐標系統;使用QPainter::rotate()函數順時針旋轉坐標系統;還可以使用QPainter::shear()圍繞原點來扭曲坐標系統。如下圖所示。

我們可以使用前面提到的那些便捷函數進行坐標系統變換,也可以通過QTransform類實現。


(1)平移變換

將paintEvent()函數內容更改如下:

void Widget::paintEvent(QPaintEvent *)
{
    //平移坐標系統
    QPainter painter(this);
    painter.setBrush(Qt::yellow);
    painter.drawRect(0, 0, 50, 50);
    //將坐標系原點向右、向下平移100像素點,即使原點坐標變為(100,100)
    painter.translate(100, 100);
    painter.setBrush(Qt::red);
    painter.drawRect(0, 0, 50, 50);
    //將坐標系原點向左、向上平移100像素點,即重新使原點坐標變為(0,0)
    painter.translate(-100, -100);
    painter.drawLine(0, 0, 20, 20);
}

這里先在原點(0, 0)繪制了一個寬、高均為50的正方形,然后使用translate()函數將坐標系統進行了平移,使(100, 100)點成為了新原點,所以我們再次進行繪制的時候,雖然drawRect()中的邏輯坐標還是(0, 0)點,但實際顯示出來的卻是在(100, 100)點的紅色正方形。可以再次使用translate()函數進行反向平移,使原點重新回到窗口左上角。運行程序,效果如下圖所示。


(2)縮放變換

將paintEvent()函數中的內容更改如下:

void Widget::paintEvent(QPaintEvent *)
{
    //縮放坐標系統
    QPainter painter(this);
    painter.setBrush(Qt::yellow);
    painter.drawRect(0, 0, 100, 100);
    //將坐標系統的橫、縱坐標都放大兩倍
    painter.scale(2, 2);
    painter.setBrush(Qt::red);
    painter.drawRect(50, 50, 50, 50);
}

可以看到,當我們使用scale()函數將坐標系統的橫、縱坐標都放大兩倍以后,邏輯上的(50,50)點變成了窗口上的(100, 100)點,而邏輯上的長度50,繪制到窗口上的長度卻是100。運行程序,效果如下圖所示。


(3)旋轉變換

將paintEvent()函數更改如下:

void Widget::paintEvent(QPaintEvent *)
{
    //旋轉坐標系統
    QPainter painter(this);
    painter.drawLine(0, 0, 100, 0);
    //以原點為中心,順時針旋轉30度
    painter.rotate(30);
    painter.drawLine(0, 0, 100, 0);
    painter.translate(100, 100);
    painter.rotate(30);
    painter.drawLine(0, 0, 100, 0);
}

這里先繪制了一條水平的直線,然后將坐標系統旋轉了30度,又繪制了一條直線。可以看到,默認是以原點(0, 0)為中心旋轉的。如果想改變旋轉中心,可以使用translate()函數,比如這里將中心移動到了(100, 100)點,然后旋轉了30度,又繪制了一條直線。運行程序,效果如下圖所示。


(4)扭曲變換

將paintEvent()函數更改如下:

void Widget::paintEvent(QPaintEvent *)
{
    //扭曲坐標系統
    QPainter painter(this);
    painter.setBrush(Qt::yellow);
    painter.drawRect(0, 0, 50, 50);
    //縱向扭曲變形
    painter.shear(0, 1);
    painter.setBrush(Qt::red);
    painter.drawRect(50, 0, 50, 50);
}

shear()有兩個參數,第一個是對橫向進行扭曲,第二個是對縱向進行扭曲,而取值就是扭曲的程度。比如程序中對縱向扭曲值為1,那么就是紅色正方形左邊的邊下移一個單位,右邊的邊下移兩個單位,值為1就表明右邊的邊比左邊的邊多下移一個單位。大家可以更改取值,測試效果。運行程序,效果如下圖所示。


2.3 ”窗口-視口”轉換

在使用QPainter進行繪制時,會使用邏輯坐標進行繪制,然后再轉換為繪圖設備的物理坐標。邏輯坐標到物理坐標的映射由QPainter的worldTransform()函數和QPainter的viewport()以及window()函數進行處理。其中視口(viewport)表示物理坐標下指定的一個任意矩形,而窗口(window,與以前講的窗口部件的概念不同)表示邏輯坐標下的相同的矩形。默認的,邏輯坐標和物理坐標是重合的,它們都相當於繪圖設備上的矩形。

使用”窗口-視口”轉換可以使邏輯坐標系統適合應用的要求,這個機制也可以用來讓繪圖代碼獨立於繪圖設備。 下面來看一個例子。

創建一個Widget窗口應用(其默認寬400像素,高300像素,左上角為原點)。首先正常繪制一個正方形:

void Widget::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    p.setPen(Qt::blue);
    p.drawRect(0, 0, 100, 100);
}

這個是沒有問題的,因為現在的繪圖設備就是Widget,其左上角就是原點(0, 0)點。效果如下圖所示。


現在我們使用setWindow來設置邏輯坐標矩形:

void Widget::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    p.setPen(Qt::blue);
    p.drawRect(0, 0, 100, 100);

    p.setWindow(-50, -50, 100, 100);
    p.setPen(Qt::red);
    p.drawRect(0, 0, 100, 100);
}

這時,效果如下圖所示。


現在來說p.setWindow(-50, -50, 100, 100)的作用,它將邏輯坐標矩形(后面提到的術語window窗口)與我們現在的設備物理坐標矩形(后面提到的術語viewport視口)進行了線性映射,這里所說的設備物理坐標矩形就是我們可見的Widget的坐標,就是左上角為(0, 0)點,寬400,高300這樣的矩形,線性映射的示意圖如下:

也就是說,調用p.setWindow(-50, -50, 100, 100)之后,再次使用p進行繪制,那么坐標原點就不再是Widget的左上角了,而是到了其中心,以前繪制的寬100、高100的正方形,現在也會按比例變為寬400, 高300,也就是我們看到的這個紅色矩形。


再來修改代碼:

void Widget::paintEvent(QPaintEvent *)

{
    QPainter p(this);
    p.setPen(Qt::blue);
    p.drawRect(0, 0, 100, 100);

    p.setWindow(-50, -50, 100, 100);
    p.setPen(Qt::red);
    p.drawRect(0, 0, 20, 20);
}

運行效果如下圖所示:


我們將繪制的紅色矩形變小,可以明顯看到,本應該是個正方形,現在卻變成了長方形。就是因為上面說的比例變換造成的,那么怎么才能讓它顯示應有的形狀呢,我們來設置視口:

void Widget::paintEvent(QPaintEvent *)
{
    QPainter p(this);
    p.setPen(Qt::blue);
    p.drawRect(0, 0, 100, 100);

    int side = qMin(width(), height());
    p.setViewport((width() - side)/2, (height() - side) /2, side, side);
    p.setWindow(-50, -50, 100, 100);

    p.setPen(Qt::red);
    p.drawRect(0, 0, 20, 20);
}

現在使用setViewport設置視口為一個正方形,就是Widget可是區域上最大的正方形,這樣邏輯坐標和物理坐標進行比例變換的時候,紅色矩形的寬和高就不會因為縮放的比例不同而發生變形了,如下圖所示。


問題提出:那么為什么要修改這個邏輯坐標矩形?

這是為了便於我們繪圖,因為我們一般繪圖時只是想在標准的坐標系中應該繪制成什么樣子,不會考慮不同繪圖設備的具體坐標系(比如有的設備坐標原點在其左上角,有的在中心等等),也不會考慮窗口的大小不同而使用不同的代碼(比如我們只想在一個寬100、高100的繪圖區域的中心繪制一個高20、寬20的正方形,到底實際繪圖設備的單位是像素、還是英寸、還是厘米,我們不用考慮)。


引申術語:窗口、視口

這里說的我們想象中的寬100、高100、原點在中心的繪圖區域,就是邏輯坐標下的矩形,也就是使用setWindow設置的所謂的窗口;而實際的繪圖設備,比如這里的Widget部件,其可視化的區域上設置的一個矩形被稱為視口(英文為viewport),默認就是可視化區域的大小,但是可以通過setViewport來設置。窗口與視口相對應,可以進行線性變換,這樣,我們就可以通過先設置視口,再設置對應的窗口的方法,來確保我們的代碼在標准的想象中的坐標系中繪制的圖形,可以准確地顯示在不同的繪圖設備界面上。


參考:

67 2D繪圖(反走樣繪圖 / 抗鋸齒渲染)

Qt 2D繪圖部分窗口、視口的研究



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM