基于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实现的图元拖曳、定点滚轮旋转和缩放相关推荐

  1. 基于QT开发的开源局域网联机UNO卡牌游戏报告(附github仓库地址)

    源代码: https://github.com/yunwei37/UNO-game-oop 目录 1. 需求分析 1.1. UNO卡牌游戏的基本功能 1.2. UNO卡牌游戏的规则 2. 总体设计 3 ...

  2. Linux 平台下基于Qt 的电子地图的绘制

    Linux 平台下基于Qt 的电子地图的绘制 摘要-------------------------------------------1   关键词------------------------- ...

  3. 基于Qt大恒工业相机二次开发demo-C++

    目录 1.新建工程 2.文件及属性配置 2.1文件拷贝 2.2VS项目属性配置 2.2.1包含目录和库目录添加 2.2.2附加依赖项添加 3.添加基于官方mfc代码改写的CGXBitmap类 3.1添 ...

  4. 基于qt中QCalendarWidget的双日历时间范围选择控件(自定义)

    控件预览: 控件基于QT设计,单击日历设置时间范围起点,再次单击日历设置时间范围终点: 当起止时间为同一天时,所选日期右上角显示"单"字样: 控件设计说明: 控件基于QT中QDia ...

  5. QT学习笔记(三)——vs2019+Qt实现打开影像并以鼠标为中心用滚轮控制图片缩放

    vs2019+Qt实现打开影像并以鼠标为中心用滚轮控制图片缩放 之前写了一个博客讲怎么显示一张影像,那个是基于Qpainter的 今天使用QLabel来显示影像,并且用鼠标滚轮控制缩放. 关于图像的打 ...

  6. 《基于Qt的VR编辑器开发》(Yanlz+Unity+SteamVR+5G+AI+VR云游戏+Qt+编辑器+跨平台+人机交互+触发事件+立钻哥哥+==)

    <基于Qt的VR编辑器开发> <基于Qt的VR编辑器开发> 版本 作者 参与者 完成日期 备注 YanlzFramework_Qt_V01_1.0 严立钻 2019.09.04 ...

  7. QT5/C++项目:基于QT的跨平台网络对战象棋(三)(推荐★★★★)

    QT5/C++项目:基于QT的跨平台网络对战象棋(三)(推荐★★★★) 文章目录 QT5/C++项目:基于QT的跨平台网络对战象棋(三)(推荐★★★★) 本篇副标题: 本篇博客讲了什么or解决了什么问 ...

  8. 基于Qt的组态监控软件实现以及分析(转)

    转自:http://yleesun.blog.163.com/blog/static/2941340220094695359894/ 组态软件部分作为自动化网络平台客户端的实现部分,仅仅是其中的一小部 ...

  9. 基于QT的多场景机动车防碰撞算法仿真测试平台

    基于QT的多场景机动车防碰撞算法仿真测试平台 大创项目日志,仅供参考 1.道路模块的搭建与拼接 主界面如图所示 头文件 源文件 UI界面文件 mainwindow.h/.cpp/.ui 主界面 map ...

最新文章

  1. matlab心电图诊断系统,ECG-diag MATLAB心电图自动诊断程序 联合开发网 - pudn.com
  2. 数组方法大全ES5+ES6
  3. Java加密与解密的艺术~MD算法实现
  4. sqlyog设置简体中文_SQLyog中文版使用教程
  5. PostgreSQL的执行计划分析
  6. js bom dom
  7. 实体词典 情感词典_基于情感词典的情感分析
  8. 华为BIOS系统升级
  9. Sublime Text 2 注册码/破解方法【蕃薯耀】
  10. Protobuf编码规则详解
  11. 学习 C++,关键是要理解概念,而不应过于深究语言的技术细节
  12. “国六”新要求——基于OBD系统的量产车评估测试 (PVE)
  13. 【pandas】变形(长宽表变换)
  14. linux硬盘损坏无法启动,CentOS 硬盘损坏导致无法启动
  15. python基础19-36题
  16. assoc在php中,在PHP中使用array_diff_assoc函数
  17. request.getContextPath详解
  18. Android实现垂直型的SeekBar
  19. 今天去了海淀基督教堂
  20. 【原创】Ubuntu 下使用 NCverilog 仿真 Verilog 工程

热门文章

  1. 小米AX1800开SSH权限
  2. 明明已部署EDR,服务器为什么还是被入侵了?
  3. centos7安装配置yum软件仓库
  4. Linux shell脚本,Linux下的西红柿时间管理法 I
  5. c++ nth_element()函数
  6. 乐理基础知识-1.节奏
  7. NetSpot Pro for Mac(最好用的wifi检测软件)
  8. GTX960M搭建《深度学习图像识别技术》所需的环境
  9. 游戏党注意了,超80款Steam游戏可在优麒麟上畅玩
  10. 牧场上的草泥马(游荡的奶牛)