基于QT实现的图元拖曳、定点滚轮旋转和缩放
基于QT實現的圖元拖曳、定點滾輪旋轉和縮放可視化錨點的演示
資源下載地址:https://download.csdn.net/download/sheziqiong/85745901
資源下載地址:https://download.csdn.net/download/sheziqiong/85745901
一、概述
在算法模塊方面,實現了直線和多邊形的 DDA、Bresenham 算法;實現了中點圓和中點橢圓算法;實現了圖元平移、縮放、旋轉和兩種裁剪算法;實現了 n 階貝塞爾曲線和三次均勻 B 樣條算法。
在文件輸入接口方面,實現了一個命令行程序,支持解析固定格式的字符串命令。在用戶交互接口方面,提供基于鼠標點擊的直線、多邊形、橢圓、曲線的繪制和實時渲染;實現了基于鏈表遍歷的圖元捕獲,提供基于鼠標拖曳的圖元移動操作;提供基于可視化錨點及鼠標滾輪的圖元旋轉、圖元縮放操作.
二、算法重述
2.1 DDA 算法
DDA 算法,即數值差分分析算法,直接利用直線 x 或 y 方向增量 △x 或 △y,在直線投影較長的坐標軸上,以單位增量對線段離散取樣,確定另一個坐標軸上最靠近線段路徑的對應整數值。實際實現時,采用增量法確定這個整數值,另一個坐標軸上的增量應滿足的要求是,符號使起始點具備向結束點移動的趨勢,模長等于當前坐標軸投影和較長坐標軸投影的比值。
2.2 Bresenham 算法
Bresenham 算法利用了光柵掃描時,線段離散取樣位置的有限性,只有兩個可能的位置符合采樣要求,于是設計整型參量來表示兩個侯選位置和理想位置的偏移量,通過檢測這個整型參量的符號,在侯選位置里二選一。
算法推導如下:
對斜率 0<m<1 的情況,yk+1=mxk+1+b,比較 yk+1 和 yk、yk+1 的偏移,d1 = y - yk = m(xk + 1) + b - yk, d2 = yk+1 - m(xk + 1) - b,有 Δx(d1 - d2) = 2m(xk + 1) - 2yk + 2b - 1,設置決策參數 pk=Δx(d1 - d2), pk 大于 0 意味著 yk+1 比 yk 更接近理想位置。
計算 pk+1 和 pk 的差,可知, pk 大于 0 取高像素 yk+1 時的增量為 2Δx-2Δy,pk 小于 0 取低像素 yk 時的增量為 2Δy。
代碼實現如下:
void drawLineByBresenham(int x1, int y1, int x2, int y2, QImage* _img, const QRgb _color) {int stepx(1), stepy(1);if (x1 > x2) stepx = -1;if (y1 > y2) stepy = -1;if (x1 == x2) { // 針對豎直線的優化…}else if (y1 == y2) { // 針對水平線的優化… }int x(x1), y(y1), dx(abs(x2 - x1)), dy(abs(y2 - y1));if (dx == dy) { // 針對對角線的優化while (x != x2) {setPix(QPoint(x, y), _color);x += stepx; y += stepy;}} // 正式開始Bresenham算法else if (dx > dy) { int p(2 * dy - dx), twody(2 * dy), twody_2dx(2 * (dy - dx)), i(dx);while ((i--) >= 0) {setPix(QPoint(x, y), _color);x += stepx;if (p < 0)p += twody;else {p += twody_2dx; y += stepy;}} }else { // 所有變量反演…} }從實現上看,Bresenham 算法不需要對浮點數取整,不存在 DDA 算法因取整造成的整體偏差。
在性能方面,因為現在的 CPU 性能挺好,很難看出 DDA 和 Bresenham 算法在用戶體驗方面的差異,在 Qt 應用的主線程中分別運行 DDA 和 Bresenham 算法來繪制直線和多邊形,并且調用 update 函數立即渲染,肉眼無法察覺鼠標快速拖曳時,窗體畫面的延時。
2.3 中點圓和中點橢圓算法
中點圓算法
決策參數和增量的推導類似 Bresenham 算法,推導如下:定義圓函數:
fcircle(x, y) = x^2 + y^2 – r^2
圓邊界上的點(x, y)滿足 fcircle(x, y) = 0
任意點(x, y)與圓周的相對位置關系可由對圓函數符號的檢測來決定:
- 若 fcircle(x, y) < 0,(x, y)位于圓邊界內;
- 若 fcircle(x, y) = 0,(x, y)位于圓邊界內;
- 若 fcircle(x, y) > 0,(x, y)位于圓邊界外。
第 k 個決策參數是圓函數在兩候選像素中點處求值,
pk = fcircle(xk+1, (yk+1 + yk) / 2) 其中 yk+1= yk-1 所以 pk = fcircle(xk+1, yk – 1/2)
pk < 0,中點在圓周邊界內,選擇像素位置(xk+1, yk);
pk > 0,中點位于圓周邊界外,選擇像素位置(xk+1, yk-1);
pk 符號決定兩候選像素中點位置(yk+2 + yk+1) / 2 的取值,
若 pk < 0,(yk+2 + yk+1) / 2 = yk - 0.5,即 pk+1 = fcircle(xk+2, yk - 0.5);
若 pk > 0,(yk+2 + yk+1) / 2 = yk - 1.5,即 pk+1 = fcircle(xk+2, yk - 1.5)。
只需要計算八分之一圓弧,另外七個圓弧通過對稱、對映操作得到坐標。
代碼實現:
if (rx == ry) { // 標準圓算法int x(0), y(rx), p(3 - 2 * rx); // 控制增量while (x <= y) {setPix(QPoint(x0 + x, y0 + y), _color);setPix(QPoint(x0 - x, y0 - y), _color);setPix(QPoint(x0 + x, y0 - y), _color);setPix(QPoint(x0 - x, y0 + y), _color);setPix(QPoint(x0 + y, y0 + x), _color);setPix(QPoint(x0 - y, y0 + x), _color);setPix(QPoint(x0 - y, y0 - x), _color);setPix(QPoint(x0 + y, y0 - x), _color);if (p >= 0) {p += 4 * (x - y) + 10; y--;}elsep += 4 * x + 6;x++;} }中點橢圓算法
橢圓的對稱性比圓要弱一些,決策參數和增量在圓周斜率在過 1 時要進行調整,采用計算四分之一圓周,對稱、對映出另外四分之三圓周的方案。另外,在每次步進之后,都要重新計算斜率,來判斷是否更換決策參數和增量。代碼實現:
if (rx > ry) { // 中點橢圓算法int x(0), y(ry);double pk(0);int ry2(ry * ry), rx2(rx * rx), rx2ry2(rx2 * ry2);setPix(QPoint(x0 + x, y0 + y), _color);setPix(QPoint(x0 - x, y0 - y), _color);setPix(QPoint(x0 + x, y0 - y), _color);setPix(QPoint(x0 - x, y0 + y), _color);pk = ry2 - rx2 * ry + rx2 / 4.0;while (ry2 * x <= rx2 * y) {x++;if (pk < 0) pk += (2 * ry2 * x + ry2);else {y--; pk += (2 * ry2 * x - 2 * rx2 * y + ry2);}setPix(QPoint(x0 + x, y0 + y), _color);…}pk = ry2 * (x + 0.5) * (x + 0.5) + rx2 * (y - 1.0) * (y - 1.0) - rx2ry2;while (y > 0) {y--;if (pk > 0) pk += (-2 * rx2 * y + rx2);else {x++; pk += (2 * ry2 * x - 2 * rx2 * y + rx2);}setPix(QPoint(x0 + x, y0 + y), _color);…}} else {swap(x0, y0); swap(rx, ry); // 先反演所有坐標int x(0), y(ry); // 再執行 rx > ry 的中點橢圓算法… }2.4 圖元編輯算法
圖元平移
二維平面上的圖元平移可通過二維向量的加減運算來描述,對于控制點(x0,y0),平移(x,y)即意味著平移到(x0+x,y0+y)。編程的時候需注意,橢圓的實軸和虛軸長度不是控制點,不能參與平移計算。
圖元旋轉
對于將控制點緩沖中的點逆時針繞(x,y)旋轉角度制 r 的變化,可以通過以下函數描述: const double pi = 3.1415926; double cosr(cos(r * pi / 180.0)), sinr(sin(r * pi / 180.0)); for (auto& i : ctrlbuffer) { int x0 = i.x(), y0 = i.y();
const double pi = 3.1415926; double cosr(cos(r * pi / 180.0)), sinr(sin(r * pi / 180.0));for (auto& i : ctrlbuffer) {int x0 = i.x(), y0 = i.y();i.setX(x + (x0 - x) * cosr - (y0 - y) * sinr);i.setY(y + (x0 - x) * sinr + (y0 - y) * cosr); }推導的方式是設出兩條射線和水平軸的夾角 r、r+x 和半徑 h,dx=h*cos(r+x),利用三角公式展開,利用原射線和水平軸夾角 x 的三角函數值,即坐標(x0,y0),替換掉 h 和關于 x 的三角函數,即得到上面的函數表達式。
圖元縮放
對于同一直線上的三個點 A(Xi,Yi)、B(X,Y)、C(a,b),對于水平方向,設放縮比例為 Sx,做的是 A 以 B 為中心向 C 的縮放,有比例關系(Xi-X)*Sx=(a-X),可以通過以下函數描述:
for (auto& i : ctrlbuffer) {i.setX(x + (i.x() - x) * sx);i.setY(y + (i.y() - y) * sy); }CohenSutherland 裁剪算法
對目標點做四個方向九個區域的編碼測試,用四個比特位表達目標點在九個區域中的哪一個,然后計算射線和目標點靠近的邊框的交點,替換目標點,直到兩端的目標點落在邊框內,或都不可能落在邊框內,結束算法。編碼的策略如下:
short code(0b0000);if (point.y() > y2) code |= 0b0001;if (point.y() < y1) code |= 0b0010;if (point.x() < x1) code |= 0b0100;if (point.x() > x2) code |= 0b1000;計算交點的策略如下:
if ((code & 0b0100)) {setX(round(xmin));setY(round(a.y() + (p.x() - a.x()) * m)); } else if ((code & (0b1000))) {p.setX(round(xmax));setY(round(a.y() + (p.x() - a.x()) * m)); } else if ((code & (0b0001))) {p.setY(round(ymax));setX(round(a.x() + (p.y() - a.y()) / m)); } else if ((code & (0b0010))) {p.setY(round(ymin));setX(round(a.x() + (p.y() - a.y()) / m)); }這里的 m 表示兩個端點構成的直線的斜率,這個斜率可能不存在。為了方便編寫代碼,我計算 m 的方法是:
double m = (q.y() - p.y()) / (q.x() - p.x() + 0.000000000001);
為了避免整形舍入的誤差,計算交點時使用 round 函數來避免完全的向下舍入。
設有 n 個控制點,對于[0,1]中的每一個參數 t,需要做(n-1)次線段的 t 比例分割,第 i 次分割會產生(n-i) 個中間型值點,第(n-1)次分割可以得到 1 個型值點,這個點就是需要的最終型值點。
算法舉例如下:對于 4 個控制點,迭代 3 次獲得一個最終型值點:
代碼實現如下:
vector<QPointF> p; p.assign(input.begin(), input.end());QPointF tmp = p[0]; // 為了避免誤差累積,全程使用浮點數計算 int div = sqrt(n); if (div < 1)div = 1;// 根據控制點個數調整步長for (double t = 0; t <= 1 + 0.000000001; t += 0.01 / div) {p.assign(input.begin(), input.end());for (int i = 1; i < n; i++) { // 外層循環n-1次,即做n-1次t比分for (int j = 0; j < n - i; j++) { //每層循環計算出n-1,n-2,...,1個切分點p[j] = (1.0 - t) * p[j] + t * p[j + 1];}}drawLineByBresenham( tmp.x(), tmp.y(), p[0].x(), p[0].y(), buffer, false);tmp = p[0]; }通過 div 參數來控制參數 t 的步長,避免曲線過長(控制點過多)時,步長太小導致的出現折線的問題。
編程的過程中需要注意,必須使用浮點數做中間運算,否則迭代的過程中,整型變量會發生連續舍入,使得部分曲線呈現階梯狀的特點。
三次均勻 B 樣條
使用 de Boor-Cox 算法,對于 k 次的 B 樣條基函數,構造一個遞推的公式,由 0 次多項式的遞推構造 1 次的, 1 次的遞推構造 2 次……遞推公式如下:
一階的多項式涉及一個區間兩個節點,K 階的 Bi,k 涉及 k 個區間 k+1 個節點。
代碼實現如下:
遞歸函數
double Proc::bspline(double* U, double u, int i, int k) {double result;if (k == 1) {if (U[i] < u && u < U[i + 1]) result = 1;else result = 0;}else {// 用條件語句體現約定: 0/0=0 result = 0;if (i + k - 1 != i)// 要求 U[i + k - 1] - U[i] != 0 result += (u - U[i]) / (U[i + k - 1] - U[i]) * bspline(U, u, i, k - 1); if (i + k != i + 1)// 要求 U[i + k] - U[i + 1] != 0 result += (U[i + k] - u) / (U[i + k] - U[i + 1]) * bspline(U, u, i + 1, k - 1);}return result; }參數的步長迭代
for (int i = 0; i < n + k + 1; i++)U[i] = i;…… for (double u = U[k - 1]; u < U[n + 1]; u += 0.01 / div) {QPointF curP(0, 0);for (int i = 0; i < n + 1; i++)curP += input[i] * bspline(U, u, i, k);if (fabs(curP.x()) > 0.0001 || fabs(curP.y()) > 0.0001)tmpBuf.push_back(curP); }對于公式中 U 的取值,只要保證基函數系數的分子分母數量級一致即可,所以這里直接用區間段的索引給 U 賦值。迭代中進行額外的判斷,避免兩端處,(0,0)被加入型值點序列。
三、應用設計
以 Qt 為編程框架,C++ 為編程語言,程序分為三個模塊:圖形學算法、命令行交互和手繪板交互。
圖形學算法方面,將所有圖元生成算法以靜態成員函數的形式封裝在 Proc 類中,在這些函數里實現上述算法,采用面向過程的風格,公共接口設計如下:
/*向buffer填充構成直線(x1,y1)-(x2,y2)的點,clear變量控制是否清空幀緩存*/ static void drawLineByDDA(int x1, int y1, int x2, int y2, std::vector<QPoint>& buffer, bool clear = true ); static void drawLineByBresenham(int x1, int y1, int x2, int y2, std::vector<QPoint>& buffer, bool clear = true );/*向buffer填充構成多邊形{xi,yi}的點*/ static void drawPolygonByDDA(const std::vector<int>& xs, const std::vector<int>& ys, std::vector<QPoint>& buffer ); static void drawPolygonByBresenham(const std::vector<int>& xs, const std::vector<int>& ys, std::vector<QPoint>& buffer );/*向buffer填充構成橢圓{xi,yi}的點*/ static void drawEllipse(int x0, int y0, int rx, int ry, std::vector<QPoint>& buffer);/*向buffer填充構成貝塞爾曲線{xi,yi}的點*/ static void drawCurveByBezier(const std::vector<int>& xs, const std::vector<int>& ys, std::vector<QPoint>& buffer );/*向buffer填充構成B樣條曲線{xi,yi}的點*/ static void drawCurveByBSpline(const std::vector<int>& xs, const std::vector<int>& ys, std::vector<QPoint>& buffer );/*修改ctrlp為包含在矩形(x1,y1)(x2,y2)中的線段端點*/ static void clipByCohenSutherland(int x1, int y1, int x2, int y2, std::vector<QPoint>& ctrlp); static void clipByLiangBarsky(int x1, int y1, int x2, int y2, std::vector<QPoint>& ctrlp);/*將ctrlbuffer中的點平移(x,y),這里的ctrlbuffer是控制點,例如直線的端點,橢圓的中心等*/ static void translate(int x, int y, std::vector<QPoint>& ctrlbuffer);/*將ctrlbuffer中的點以(x,y)為中心順時針旋轉角度r,這里的ctrlbuffer是控制點*/ static void rotate(int x, int y, int r, std::vector<QPoint>& ctrlbuffer);/*將ctrlbuffer中的點以(x,y)為中心放縮s,這里的ctrlbuffer是控制點,例如直線的端點,橢圓的中心等*/ static void scale(int x, int y, float sx, float sy, std::vector<QPoint>& ctrlbuffer);命令行交互方面,在 class Cli 中解析命令,調用 Proc 提供的算法,公共 API 設計如下:
bool handleCmd(std::string _cmd = std::string("resetcanvas 100 100")); bool handleScript(const char* filename = "");手繪板交互方面,通過在手繪面板 class ScribbleArea 中攔截四種鼠標事件,完成用戶輸入的獲取,調用 Proc 類提供的圖元生成算法,將結果實時渲染到窗體上。
GUI 的涂鴉功能的實現細節在此不再贅述,下面介紹圖元編輯的實現。
對于圖元編輯的 GUI 交互,采用捕捉被點擊圖元的方法,為當前所有可見圖元構造矩形框,存儲在一個鏈表中,在 mouseMoveEvent 中捕捉滿足 QRect::contains(QPoint)的鼠標點,對符合要求的圖元的矩形框做特殊標注,意味著鼠標捕獲了目標圖元。
考察 Qt 使用的圖形視圖框架,內部通過 BSP 樹實現鼠標和圖元的快速對應。事實上,當二維空間中的圖元數量達到一定數量級,像我目前這樣遍歷鏈表而用 Rect.contains(Point)的做法捕獲圖元是極為緩慢的。 GUI 框架大多通過樹形結構比如 BSP 樹、4 叉樹來從坐標索引圖元。對于畫板,考慮到可能的交互強度,使用鏈表遍歷來查找圖元,延時是完全可以接受的。
在擁有了從鼠標點擊索引圖元的實現以后,對于圖元平移,記錄圖元的原始位置和當前鼠標位置,每一次鼠標移動,先把圖元移回初始位置,再渲染到當前鼠標位置,從用戶界面觀察,相當于自己在拖曳圖元。
對于縮放和旋轉,大作業的要求和 word 文檔、ppt 等軟件提供的交互邏輯有所出路,要求圍繞固定點做縮放和旋轉,所以我設計了基于可視化錨點的交互邏輯,點擊功能按鈕后,會要求用戶放下一個圖釘形狀的錨點,接下來用戶點擊圖元,實現圖元指定,轉動鼠標滾輪,通過滾輪的前進和后退,映射到縮放比例(>1 或 <1)和旋轉角度(順時針或逆時針)。提供基于鼠標滾輪的縮放和旋轉,需要解決的問題有精度問題,因為圖元控制點是用整型數記錄的,連續對一個圖元做幾十上百次浮點精度的變形,控制點的相對位置會發生扭曲,累計誤差不可接受。為了解決這個缺陷(根據大作業要求,控制點坐標用整數表示),我采用先把圖元恢復到初始位置,再重新渲染的方式,來消除對同一個圖元連續操作時的誤差累計。
資源下載地址:https://download.csdn.net/download/sheziqiong/85745901
資源下載地址:https://download.csdn.net/download/sheziqiong/85745901
總結
以上是生活随笔為你收集整理的基于QT实现的图元拖曳、定点滚轮旋转和缩放的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: openstreetmap-tile-s
- 下一篇: JavaScript - for 循环结