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