C++ Qt Widget绘图画布缩放与平移:实现CAD级交互体验

在图形应用程序开发中,实现流畅的缩放和平移功能是创建专业级绘图工具的基础。本文将深入探讨如何在Qt Widget中实现CAD级别的交互体验,包括视图变换、坐标系统管理以及交互功能实现。

核心概念:视图变换与坐标系统

在图形应用中,我们需要区分两种坐标系统:

  1. 逻辑坐标:图形的实际坐标,构成场景的数学模型
  2. 屏幕坐标:在窗口上实际绘制的像素位置

视图变换由两个参数控制:

QPointF panOffset;  // 平移偏移量
double currentScale; // 当前缩放比例

坐标转换通过以下函数实现:

QPointF DrawingWidget::screenToLogical(const QPoint& screenPos) const
{QPoint center = rect().center();return QPointF((screenPos.x() - center.x() - panOffset.x()) / currentScale,(center.y() - screenPos.y() - panOffset.y()) / currentScale);
}QPointF DrawingWidget::logicalToScreen(const QPointF& logicalPos) const
{QPoint center = rect().center();return QPointF(center.x() + logicalPos.x() * currentScale + panOffset.x(),center.y() - logicalPos.y() * currentScale - panOffset.y());
}

解决方案与实现

1. 视图初始化与自动居中

首次显示时自动调整视图以适应场景:

void DrawingWidget::adjustViewToFit()
{// 计算场景包围盒QRectF boundingRect;for (const Circle& circle : circles) {QRectF circleRect(circle.center.x() - circle.radius, circle.center.y() - circle.radius,2 * circle.radius, 2 * circle.radius);boundingRect = boundingRect.united(circleRect);}// 添加边距double margin = 0.1 * qMax(boundingRect.width(), boundingRect.height());boundingRect.adjust(-margin, -margin, margin, margin);// 计算最佳缩放比例double widthRatio = width() / boundingRect.width();double heightRatio = height() / boundingRect.height();currentScale = qMax(qMin(widthRatio, heightRatio), minScale);// 计算居中偏移QPointF centerLogical = boundingRect.center();panOffset = QPointF(-centerLogical.x() * currentScale,-centerLogical.y() * currentScale);
}
2. 鼠标交互实现

平移功能(中键拖动):

void DrawingWidget::mousePressEvent(QMouseEvent* event)
{if (event->button() == Qt::MiddleButton) {isPanning = true;lastMousePos = event->pos();setCursor(Qt::ClosedHandCursor);}
}void DrawingWidget::mouseMoveEvent(QMouseEvent* event)
{if (isPanning) {QPoint delta = event->pos() - lastMousePos;panOffset += delta; // 仅修改视图参数lastMousePos = event->pos();update();}
}

缩放功能(鼠标滚轮):

void DrawingWidget::wheelEvent(QWheelEvent* event)
{double zoomFactor = 1.1;double oldScale = currentScale;if (event->angleDelta().y() > 0) {currentScale *= zoomFactor;} else {currentScale = qMax(currentScale / zoomFactor, minScale);}// 保持缩放中心不变QPointF mousePos = event->pos();QPointF logicalMousePos = screenToLogical(mousePos.toPoint());panOffset = (panOffset + mousePos - rect().center()) * (currentScale / oldScale)- mousePos + rect().center();update();
}
3. 坐标信息显示(右键功能)
void DrawingWidget::mousePressEvent(QMouseEvent* event)
{if (event->button() == Qt::RightButton) {QPointF logicalPos = screenToLogical(event->pos());showPosition(logicalPos);}
}void DrawingWidget::showPosition(const QPointF& logicalPos)
{QString message = QString::fromUtf8("实际坐标:\nX: %1\nY: %2").arg(logicalPos.x(), 0, 'f', 2).arg(logicalPos.y(), 0, 'f', 2);QMessageBox::information(this, QString::fromUtf8("坐标信息"), message, QMessageBox::Ok);
}

完整实现代码

DrawingWidget.h
#ifndef DRAWINGWIDGET_H
#define DRAWINGWIDGET_H#include <QWidget>
#include <QMouseEvent>
#include <QPainter>
#include <QVector>
#include <QPointF>
#include <QWheelEvent>
#include <QResizeEvent>
#include <QMessageBox>class DrawingWidget : public QWidget
{Q_OBJECTpublic:explicit DrawingWidget(QWidget* parent = nullptr);~DrawingWidget();protected:void paintEvent(QPaintEvent* event) override;void mousePressEvent(QMouseEvent* event) override;void mouseMoveEvent(QMouseEvent* event) override;void mouseReleaseEvent(QMouseEvent* event) override;void wheelEvent(QWheelEvent* event) override;void resizeEvent(QResizeEvent* event) override;private:// 绘图对象struct Circle {QPointF center;double radius;QColor color;};struct Line {QPointF start;QPointF end;QColor color;};struct Point {QPointF position;QColor color;double radius = 5.0;bool onCircle = false;    // 是否在圆上Circle* circle = nullptr; // 关联的圆bool onLine = false;      // 是否在直线上Line* line = nullptr;     // 关联的直线};// 视图控制QPointF panOffset;      // 平移偏移量double currentScale;    // 当前缩放比例double minScale;        // 最小缩放比例bool isPanning;         // 是否正在平移QPoint lastMousePos;    // 上次鼠标位置int draggingPointIndex; // 正在拖动的点索引bool initialized;       // 是否已初始化// 绘图数据QVector<Circle> circles;QVector<Line> lines;QVector<Point> points;// 坐标转换函数QPointF screenToLogical(const QPoint& screenPos) const;QPointF logicalToScreen(const QPointF& logicalPos) const;// 点拖动约束void movePointToCircle(Point& point, const QPointF& newPos);void movePointToLine(Point& point, const QPointF& newPos);// 初始化示例场景void initScene();// 调整视图以适应窗口大小void adjustViewToFit();// 显示坐标信息void showPosition(const QPointF& logicalPos);
};#endif // DRAWINGWIDGET_H
DrawingWidget.cpp
#include "DrawingWidget.h"
#include <cmath>
#include <QPainter>
#include <QWheelEvent>
#include <QDebug>
#include <QResizeEvent>
#include <QApplication>DrawingWidget::DrawingWidget(QWidget* parent): QWidget(parent), currentScale(1.0), minScale(0.1), isPanning(false), draggingPointIndex(-1), panOffset(0, 0), initialized(false)
{setMouseTracking(true);setMinimumSize(400, 400);setWindowTitle(QString::fromUtf8("CAD级绘图画布"));initScene();
}DrawingWidget::~DrawingWidget() {}void DrawingWidget::initScene()
{// 创建三个不同颜色的圆circles.append({{0, 0}, 100, Qt::blue});circles.append({{-150, 150}, 70, Qt::green});circles.append({{150, -150}, 80, Qt::red});// 创建三条不同方向的直线lines.append({{-200, -200}, {200, 200}, Qt::darkBlue});lines.append({{-200, 0}, {200, 0}, Qt::darkGreen});lines.append({{0, -200}, {0, 200}, Qt::darkRed});// 在圆上创建点for (int i = 0; i < circles.size(); i++) {Circle& c = circles[i];points.append({{c.center.x() + c.radius, c.center.y()}, Qt::red, 5.0, true, &c});points.append({{c.center.x(), c.center.y() + c.radius},Qt::blue, 5.0, true, &c});}// 在直线上创建点for (int i = 0; i < lines.size(); i++) {Line& l = lines[i];QPointF midPoint = (l.start + l.end) / 2;points.append({midPoint, Qt::magenta, 6.0, false, nullptr, true, &l});}initialized = true;adjustViewToFit();
}QPointF DrawingWidget::screenToLogical(const QPoint& screenPos) const
{QPoint center = rect().center();return QPointF((screenPos.x() - center.x() - panOffset.x()) / currentScale,(center.y() - screenPos.y() - panOffset.y()) / currentScale);
}QPointF DrawingWidget::logicalToScreen(const QPointF& logicalPos) const
{QPoint center = rect().center();return QPointF(center.x() + logicalPos.x() * currentScale + panOffset.x(),center.y() - logicalPos.y() * currentScale - panOffset.y());
}void DrawingWidget::adjustViewToFit()
{if (!initialized) return;QRectF boundingRect;for (const Circle& circle : circles) {QRectF circleRect(circle.center.x() - circle.radius, circle.center.y() - circle.radius,2 * circle.radius, 2 * circle.radius);boundingRect = boundingRect.united(circleRect);}for (const Line& line : lines) {boundingRect = boundingRect.united(QRectF(line.start, line.end));}if (boundingRect.isEmpty()) return;double margin = 0.1 * qMax(boundingRect.width(), boundingRect.height());boundingRect.adjust(-margin, -margin, margin, margin);double widthRatio = width() / boundingRect.width();double heightRatio = height() / boundingRect.height();currentScale = qMax(qMin(widthRatio, heightRatio), minScale);QPointF centerLogical = boundingRect.center();panOffset = QPointF(-centerLogical.x() * currentScale,-centerLogical.y() * currentScale);update();
}void DrawingWidget::paintEvent(QPaintEvent* event)
{Q_UNUSED(event);QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing);// 绘制背景和网格painter.fillRect(rect(), Qt::white);// 绘制坐标轴QPoint center = rect().center();painter.setPen(Qt::black);painter.drawLine(0, center.y() + panOffset.y(), width(), center.y() + panOffset.y());painter.drawText(width() - 20, center.y() + panOffset.y() + 15, QString::fromUtf8("X"));painter.drawLine(center.x() + panOffset.x(), 0, center.x() + panOffset.x(), height());painter.drawText(center.x() + panOffset.x() + 10, 15, QString::fromUtf8("Y"));// 绘制网格painter.setPen(QPen(Qt::lightGray, 0.5));int gridSize = 20;for (int x = static_cast<int>(panOffset.x()) % gridSize; x < width(); x += gridSize) {painter.drawLine(x, 0, x, height());}for (int y = static_cast<int>(panOffset.y()) % gridSize; y < height(); y += gridSize) {painter.drawLine(0, y, width(), y);}// 绘制直线for (const Line& line : lines) {QPointF start = logicalToScreen(line.start);QPointF end = logicalToScreen(line.end);painter.setPen(QPen(line.color, 2));painter.drawLine(start, end);}// 绘制圆for (const Circle& circle : circles) {QPointF centerScreen = logicalToScreen(circle.center);double radiusScreen = circle.radius * currentScale;painter.setPen(QPen(circle.color, 2));painter.setBrush(Qt::NoBrush);painter.drawEllipse(centerScreen, radiusScreen, radiusScreen);}// 绘制点for (const Point& point : points) {QPointF posScreen = logicalToScreen(point.position);double radiusScreen = point.radius * currentScale;painter.setPen(Qt::black);painter.setBrush(point.color);painter.drawEllipse(posScreen, radiusScreen, radiusScreen);}// 显示缩放比例painter.setPen(Qt::black);painter.drawText(10, 20, QString::fromUtf8("缩放: %1x").arg(currentScale, 0, 'f', 1));
}void DrawingWidget::mousePressEvent(QMouseEvent* event)
{if (event->button() == Qt::MiddleButton) {isPanning = true;lastMousePos = event->pos();setCursor(Qt::ClosedHandCursor);}else if (event->button() == Qt::LeftButton) {QPoint screenPos = event->pos();for (int i = 0; i < points.size(); i++) {const Point& point = points[i];QPointF pointScreen = logicalToScreen(point.position);double dx = pointScreen.x() - screenPos.x();double dy = pointScreen.y() - screenPos.y();double distance = std::sqrt(dx*dx + dy*dy);if (distance < 10.0 * currentScale) {draggingPointIndex = i;return;}}}else if (event->button() == Qt::RightButton) {QPointF logicalPos = screenToLogical(event->pos());showPosition(logicalPos);}
}void DrawingWidget::mouseMoveEvent(QMouseEvent* event)
{if (isPanning) {QPoint delta = event->pos() - lastMousePos;panOffset += delta;lastMousePos = event->pos();update();}else if (draggingPointIndex >= 0) {Point& point = points[draggingPointIndex];QPointF newLogicalPos = screenToLogical(event->pos());if (point.onCircle && point.circle) {movePointToCircle(point, newLogicalPos);} else if (point.onLine && point.line) {movePointToLine(point, newLogicalPos);} else {point.position = newLogicalPos;}update();}
}void DrawingWidget::mouseReleaseEvent(QMouseEvent* event)
{if (event->button() == Qt::MiddleButton) {isPanning = false;setCursor(Qt::ArrowCursor);}else if (event->button() == Qt::LeftButton) {draggingPointIndex = -1;}
}void DrawingWidget::wheelEvent(QWheelEvent* event)
{double zoomFactor = 1.1;double oldScale = currentScale;if (event->angleDelta().y() > 0) {currentScale *= zoomFactor;} else {currentScale = qMax(currentScale / zoomFactor, minScale);}QPointF mousePos = event->pos();panOffset = (panOffset + mousePos - rect().center()) * (currentScale / oldScale)- mousePos + rect().center();update();event->accept();
}void DrawingWidget::resizeEvent(QResizeEvent* event)
{Q_UNUSED(event);adjustViewToFit();
}void DrawingWidget::movePointToCircle(Point& point, const QPointF& newPos)
{if (!point.circle) return;Circle& circle = *point.circle;QPointF dir = newPos - circle.center;double distance = std::sqrt(dir.x()*dir.x() + dir.y()*dir.y());if (distance > 0) {point.position = circle.center + dir * (circle.radius / distance);}
}void DrawingWidget::movePointToLine(Point& point, const QPointF& newPos)
{if (!point.line) return;Line& line = *point.line;QPointF lineVec = line.end - line.start;double lineLengthSquared = lineVec.x()*lineVec.x() + lineVec.y()*lineVec.y();if (lineLengthSquared > 0) {QPointF relVec = newPos - line.start;double t = (relVec.x()*lineVec.x() + relVec.y()*lineVec.y()) / lineLengthSquared;t = qBound(0.0, t, 1.0);point.position = line.start + lineVec * t;}
}void DrawingWidget::showPosition(const QPointF& logicalPos)
{QString message = QString::fromUtf8("实际坐标:\nX: %1\nY: %2").arg(logicalPos.x(), 0, 'f', 2).arg(logicalPos.y(), 0, 'f', 2);QMessageBox::information(this, QString::fromUtf8("坐标信息"), message);
}

关键技术与最佳实践

  1. 坐标系统分离

    • 严格区分逻辑坐标(场景坐标)和屏幕坐标(显示坐标)
    • 所有图形对象使用逻辑坐标存储
    • 仅在绘制时转换为屏幕坐标
  2. 高效视图变换

    • 使用panOffsetcurrentScale控制视图
    • 避免修改原始图形数据
    • 矩阵运算保持高性能
  3. 智能视图初始化

    • 自动计算场景包围盒
    • 添加合理边距
    • 自适应窗口尺寸
  4. 交互体验优化

    • 中键平移自然流畅
    • 滚轮缩放以光标为中心
    • 右键坐标显示实用直观
  5. 约束点拖动

    • 圆上点沿圆周移动
    • 线上点沿线段移动
    • 保持几何关系不变

总结

本文详细介绍了在Qt Widget中实现CAD级绘图画布的核心技术,包括视图变换、坐标系统管理、交互功能实现等关键内容。通过分离逻辑坐标和屏幕坐标,我们实现了:

  1. 流畅的缩放和平移体验
  2. 稳定的坐标系统(图形实际坐标不随视图改变)
  3. 实用的右键坐标显示功能
  4. 智能的视图初始化与自适应
  5. 约束点拖动功能

这些技术不仅适用于CAD类应用,也可用于科学可视化、数据分析和任何需要复杂交互的图形应用程序。通过本文提供的完整实现,开发者可以快速构建出专业级的图形交互界面。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/913257.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/913257.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Paimon 位图索引解析:高效等值查询的秘密( Bit-Sliced Index)

BitmapFileIndexBitmapFileIndex 这个类 是 Paimon 中一个非常重要的索引类型&#xff0c;它使用位图&#xff08;Bitmap&#xff09;来精确定位数据&#xff0c;尤其擅长处理低基数&#xff08;low-cardinality&#xff09;列的等值查询。BitmapFileIndex 实现了 FileIndexer …

S7-1200 CPU 与 S7-200 CPU S7通信(S7-1200 作为服务器

7-1200 CPU 与 S7-200 CPU S7通信&#xff08;S7-1200 作为服务器&#xff09; S7-1200 CPU 与 S7-200 CPU 之间的通信只能通过 S7 通信来实现&#xff0c;因为 S7-200 的以太网模块只支持S7 通信。当S7-200作为客户端&#xff0c;S7-1200作为服务器&#xff0c;需在客户端单边…

pyspark大规模数据加解密优化实践

假如有1亿行数据 方法1 spark udf解密 from pyspark.sql import SparkSession import pyspark.sql.functions as F from pyDes import * import binasciisparkSparkSession.builder.getOrCreate()def dec_fun(text):key triple_des(b"HHHHHHHHHHHHHHHHHHHHHHHH", CB…

华为云Flexus+DeepSeek征文|华为云ECS与CCE:从介绍到架构部署·仅需要此文足矣

前引&#xff1a;当今的企业面临着前所未有的技术挑战&#xff1a;如何构建既安全又高效、既灵活又可靠的云服务架构&#xff1f;如何有效整合人工智能技术&#xff0c;打造智能化的运维和服务体系&#xff1f;这些问题的答案&#xff0c;正在悄然改变着企业级IT基础设施的生态…

DAY 50 预训练模型+CBAM模块

浙大疏锦行https://blog.csdn.net/weixin_45655710 知识点回顾&#xff1a; resnet结构解析CBAM放置位置的思考针对预训练模型的训练策略 差异化学习率三阶段微调 作业&#xff1a; 好好理解下resnet18的模型结构尝试对vgg16cbam进行微调策略 ResNet-18 结构核心思想 可以将R…

docker连接mysql

查看在运行的容器&#xff1a;docker ps -s 进入容器&#xff1a;docker exec -it 容器号或名 /bin/bash&#xff0c;如&#xff1a;docker exec -it c04c438ff177 /bin/bash 或docker exec -it mysql /bin/bash。 3. 登录mysql&#xff1a;mysql -uroot -p123456

javaweb第182节Linux概述~ 虚拟机连接不上FinalShell

问题描述 虚拟机无法连接到finalshell 报错 session.connect:java.net.socketexception:connection reset 或者 connection is closed by foreign host 解决 我经过一系列的排查&#xff0c;花费了一天的时间后&#xff0c;发现&#xff0c;只是因为&#xff0c;我将连接…

高压电缆护层安全的智能防线:TLKS-PLGD 监控设备深度解析

在现代电力系统庞大复杂的网络中&#xff0c;高压电缆护层是守护电力传输的 "隐形铠甲"&#xff0c;其安全直接影响电网稳定。传统监测手段响应慢、精度低&#xff0c;难以满足安全运维需求。TLKS-PLGD 高压电缆护层环流监控设备应运而生&#xff0c;提供智能化解决方…

Element-Plus Cascader 级联选择器获取节点名称和value值方法

html 部分 <template><el-cascaderref"selectAeraRef":options"areas":disabled"disabled":props"optionProps"v-model"selectedOptions"filterablechange"handleChange"><template #default"…

STM32中实现shell控制台(命令解析实现)

文章目录一、核心设计思想二、命令系统实现详解&#xff08;含完整注释&#xff09;1. 示例命令函数实现2. 初始化命令系统3. 命令注册函数4. 命令查找函数5. 命令执行函数三、命令结构体&#xff08;cmd\_t&#xff09;四、运行效果示例五、小结在嵌入式系统的命令行控制台&am…

基于matlab的二连杆机械臂PD控制的仿真

基于matlab的二连杆机械臂PD控制的仿真。。。 chap3_5input.m , 1206 d2plant1.m , 1364 hs_err_pid2808.log , 15398 hs_err_pid4008.log , 15494 lx_plot.m , 885 PD_Control.mdl , 35066 tiaojie.m , 737 chap2_1ctrl.asv , 988 chap2_1ctrl.m , 905

TCP、HTTP/1.1 和HTTP/2 协议

TCP、HTTP/1.1 和 HTTP/2 是互联网通信中的核心协议&#xff0c;它们在网络分层中处于不同层级&#xff0c;各有特点且逐步演进。以下是它们的详细对比和关键特性&#xff1a;1. TCP&#xff08;传输控制协议&#xff09; 层级&#xff1a;传输层&#xff08;OSI第4层&#xff…

Java+Vue开发的进销存ERP系统,集采购、销售、库存管理,助力企业数字化运营

前言&#xff1a;在当今竞争激烈的商业环境中&#xff0c;企业对于高效管理商品流通、采购、销售、库存以及财务结算等核心业务流程的需求日益迫切。进销存ERP系统作为一种集成化的企业管理解决方案&#xff0c;能够整合企业资源&#xff0c;实现信息的实时共享与协同运作&…

【趣谈】Android多用户导致的UserID、UID、shareUserId、UserHandle术语混乱讨论

【趣谈】Android多用户导致的UserID、UID、shareUserId、UserHandle术语混乱讨论 备注一、概述二、概念对比1.UID2.shareUserId3.UserHandle4.UserID 三、结论 备注 2025/07/02 星期三 在与Android打交道时总遇到UserID、UID、shareUserId、UserHandle这些术语&#xff0c;但是…

P1424 小鱼的航程(改进版)

题目描述有一只小鱼&#xff0c;它平日每天游泳 250 公里&#xff0c;周末休息&#xff08;实行双休日)&#xff0c;假设从周 x 开始算起&#xff0c;过了 n 天以后&#xff0c;小鱼一共累计游泳了多少公里呢&#xff1f;输入格式输入两个正整数 x,n&#xff0c;表示从周 x 算起…

<二>Sping-AI alibaba 入门-记忆聊天及持久化

请看文档&#xff0c;流程不再赘述&#xff1a;官网及其示例 简易聊天 环境变量 引入Spring AI Alibaba 记忆对话还需要我们有数据库进行存储&#xff0c;mysql&#xff1a;mysql-connector-java <?xml version"1.0" encoding"UTF-8"?> <pr…

【机器学习深度学习】模型参数量、微调效率和硬件资源的平衡点

目录 一、核心矛盾是什么&#xff1f; 二、微调本质&#xff1a;不是全调&#xff0c;是“挑着调” 三、如何平衡&#xff1f; 3.1 核心策略 3.2 参数量 vs 微调难度 四、主流轻量微调方案盘点 4.1 冻结部分参数 4.2 LoRA&#xff08;低秩微调&#xff09; 4.3 量化训…

【V13.0 - 战略篇】从“完播率”到“价值网络”:训练能预测商业潜力的AI矩阵

在上一篇 《超越“平均分”&#xff1a;用多目标预测捕捉观众的“心跳曲线”》 中&#xff0c;我们成功地让AI学会了预测观众留存曲线&#xff0c;它的诊断能力已经深入到了视频的“过程”层面&#xff0c;能精确地指出观众是在哪个瞬间失去耐心。 我的AI现在像一个顶级的‘心…

java微服务(Springboot篇)——————IDEA搭建第一个Springboot入门项目

在正文开始之前我们先来解决一些概念性的问题 &#x1f355;&#x1f355;&#x1f355; 问题1&#xff1a;Spring&#xff0c;Spring MVC&#xff0c;Spring Boot和Spring Cloud之间的区别与联系&#xff1f; &#x1f36c;&#x1f36c;&#x1f36c;&#xff08;1&#xff0…

服务器间接口安全问题的全面分析

一、服务器接口安全核心威胁 文章目录**一、服务器接口安全核心威胁**![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/6f54698b9a22439892f0c213bc0fd1f4.png)**二、六大安全方案深度对比****1. IP白名单机制****2. 双向TLS认证(mTLS)****3. JWT签名认证****4. OAuth…