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
英文地址,請注明出處:http://en.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…