需要对label进行提升,新建MyLabel类,并将其提升到label控件上,详见上篇控件提升
mylabelmouse.h
#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_mylabelmouse.h"
#include <QMenu>
#include "MyLabel.h" // 引入 MyLabel 类
class mylabelmouse : public QMainWindow
{Q_OBJECT
public:mylabelmouse(QWidget *parent = nullptr);~mylabelmouse();void openImage();
protected:void contextMenuEvent(QContextMenuEvent* event) override;
private:Ui::mylabelmouseClass ui;QMenu* m_pMenu;double scaleX, scaleY;
};
mylabelmouse.cpp
#include "mylabelmouse.h"
#include <QMenu>
#include <QAction>
#include <QFileDialog>
#include "MyLabel.h"
mylabelmouse::mylabelmouse(QWidget* parent): QMainWindow(parent)
{ui.setupUi(this);// 创建 MyLabel 实例,并替换 ui.labelui.label = new MyLabel(this);ui.label->setGeometry(20, 40, 800, 800); // 初始设置label的大小和位置// 设置label的黑色边框ui.label->setStyleSheet("border: 2px solid black;");// 设置右键菜单m_pMenu = new QMenu(this);QAction* pAc1 = new QAction(QString::fromLocal8Bit("结束绘制"), this);QAction* pAc2 = new QAction(QString::fromLocal8Bit("清除"), this);QAction* pAc3 = new QAction(QString::fromLocal8Bit("删除上一路径"), this);QAction* pAc4 = new QAction(QString::fromLocal8Bit("退出菜单"), this);m_pMenu->addAction(pAc1);m_pMenu->addAction(pAc2);m_pMenu->addAction(pAc3);m_pMenu->addAction(pAc4);m_pMenu->setStyleSheet("QMenu{font:18px;}");// 连接动作connect(pAc1, &QAction::triggered, [=] {ui.label->endDraw();});connect(pAc2, &QAction::triggered, [=] {ui.label->clearPath();});connect(pAc3, &QAction::triggered, [=] {ui.label->deleteLastPath();});connect(ui.openButton, &QPushButton::clicked, this, &mylabelmouse::openImage);connect(ui.savedropButton, &QPushButton::clicked, ui.label, &MyLabel::savePath);connect(ui.saveimageButton, &QPushButton::clicked, ui.label, &MyLabel::saveImageWithPaths);
}
mylabelmouse::~mylabelmouse() {}
void mylabelmouse::openImage()
{// 在切换图像之前,先清除已有的路径ui.label->clearPath();QString fileName = QFileDialog::getOpenFileName(this, QStringLiteral("打开图片"), "", QStringLiteral("Images (*.png *.xpm *.jpg *.bmp)"));if (!fileName.isEmpty()) {QPixmap pixmap(fileName);if (!pixmap.isNull()) {int imgWidth = pixmap.width();int imgHeight = pixmap.height();// 设置目标宽度和高度int targetWidth = 1000;int targetHeight = 800;// 根据图片的宽高比来选择缩放方式if (imgWidth > imgHeight) {// 图片是横向的,优先缩放宽度double scaleX = targetWidth / static_cast<double>(imgWidth);double scaleY = scaleX; // 根据宽度缩放,保持比例// 设置图片缩放ui.label->setPixmap(pixmap.scaled(targetWidth, targetHeight, Qt::KeepAspectRatio));ui.label->resize(targetWidth, targetHeight); // 根据缩放后的尺寸设置标签大小// 计算缩放系数ui.label->setScaleFactors(scaleX, scaleY);}else {// 图片是纵向的,优先缩放高度double scaleY = targetHeight / static_cast<double>(imgHeight);double scaleX = scaleY; // 根据高度缩放,保持比例// 设置图片缩放ui.label->setPixmap(pixmap.scaled(targetWidth, targetHeight, Qt::KeepAspectRatio));ui.label->resize(targetWidth, targetHeight); // 根据缩放后的尺寸设置标签大小// 计算缩放系数ui.label->setScaleFactors(scaleX, scaleY);}// 设置label的对齐方式为左上角对齐ui.label->setAlignment(Qt::AlignLeft | Qt::AlignTop);ui.label->setScaledContents(false); // 保持图像原始比例}}
}
void mylabelmouse::contextMenuEvent(QContextMenuEvent* event)
{m_pMenu->move(cursor().pos());m_pMenu->show();
}
MyLabel.h
#ifndef MYLABEL_H
#define MYLABEL_H
#include <QLabel>
#include <QList>
#include <QPointF>
#include <QVector>
#include <QPen>
#include <QBrush>
class MyLabel : public QLabel
{Q_OBJECT
public:explicit MyLabel(QWidget* parent = nullptr);void endDraw(); // 结束绘制void clearPath(); // 清空所有路径void deleteLastPath(); // 删除上一路径void savePath();void saveImageWithPaths();void setScaleFactors(double scaleX, double scaleY);// 设置缩放系数
protected:void paintEvent(QPaintEvent* event) override;void mousePressEvent(QMouseEvent* e) override;void mouseMoveEvent(QMouseEvent* e) override;void mouseReleaseEvent(QMouseEvent* e) override;void mouseDoubleClickEvent(QMouseEvent* event) override;
private:double m_scaleX, m_scaleY;//记录缩放系数bool m_bStartDraw; // 是否开始绘制bool bMove; // 是否正在移动绘制QPoint movePoint; // 鼠标移动的临时点QPoint m_draggedPoint = QPoint(-1, -1); // (-1, -1)表示没有点在被拖动QList<QList<QPointF>> allContours; // 存储多个轮廓的点集QList<QPointF> currentContour; // 当前绘制的轮廓点集
};
#endif // MYLABEL_H
MyLabel.cpp
#include "MyLabel.h"
#include <QPainter>
#include <QMouseEvent>
#include <QLineF>
#include <QPainterPath>
#include <QVector2D>
#include <QMessageBox>
#include <QFileDialog>
#include <QTextStream>
#include <QDateTime>
#include <cmath> // 包含 std::round
using namespace Qt;
MyLabel::MyLabel(QWidget* parent) : QLabel(parent), m_bStartDraw(false), bMove(false)
{setMouseTracking(true);//鼠标自动触发,默认任务需要按下才能开始
}
void MyLabel::endDraw()
{if (!currentContour.isEmpty()) {currentContour.push_back(currentContour[0]); // 闭合路径allContours.push_back(currentContour); // 添加到轮廓集合}m_bStartDraw = false;this->update(); // 更新视图
}
void MyLabel::clearPath()
{allContours.clear(); // 清空所有轮廓currentContour.clear(); // 清空当前路径m_bStartDraw = false; // 重置绘制状态bMove = false; // 重置鼠标移动状态this->update(); // 触发重绘
}
void MyLabel::deleteLastPath()
{allContours.removeLast(); // 删除最后一条轮廓路径this->update(); // 更新视图
}
void MyLabel::paintEvent(QPaintEvent* event)
{QPainter painter(this);const QPixmap* currentPixmap = pixmap();if (currentPixmap && !currentPixmap->isNull()) {painter.drawPixmap(0, 0, *currentPixmap);}QPen pen(red);pen.setStyle(SolidLine);pen.setWidth(2);painter.setPen(pen);painter.setRenderHint(QPainter::Antialiasing);// 绘制所有路径for (const auto& contour : allContours) {if (!contour.isEmpty()) {QPainterPath path;path.moveTo(contour[0]);for (const auto& pt : contour)path.lineTo(pt);path.closeSubpath();QBrush brush(QColor(255, 165, 0, 100)); // 半透明填充色painter.setBrush(brush);painter.fillPath(path, brush);QVector<QLineF> lines;for (int i = 0; i < contour.size() - 1; i++) {lines.push_back(QLineF(contour[i], contour[i + 1]));}painter.drawLines(lines);}}// 绘制当前正在绘制的路径if (m_bStartDraw && !currentContour.isEmpty()) {QPainterPath currentPath;currentPath.moveTo(currentContour[0]);for (const auto& pt : currentContour)currentPath.lineTo(pt);QBrush brush(QColor(255, 165, 0, 100)); // 半透明填充色painter.setBrush(brush);painter.fillPath(currentPath, brush);QVector<QLineF> currentLines;for (int i = 0; i < currentContour.size() - 1; i++) {currentLines.push_back(QLineF(currentContour[i], currentContour[i + 1]));}painter.drawLines(currentLines);// 绘制连接最后一点与鼠标位置的线if (bMove) {painter.drawLine(currentContour.last(), movePoint);}}// 绘制首点为小红点if (!currentContour.isEmpty()) {painter.setBrush(red);painter.setPen(NoPen);painter.drawEllipse(currentContour[0], 5, 5); // 绘制一个半径为5的圆}
}
void MyLabel::mousePressEvent(QMouseEvent* e)
{const QPixmap* currentPixmap = pixmap();if (currentPixmap && !currentPixmap->isNull()) {QRect imageRect(0, 0, currentPixmap->width(), currentPixmap->height());if (e->button() == LeftButton) {if (!m_bStartDraw) {// 只在图片区域内开始绘制if (imageRect.contains(e->pos())) {currentContour.clear(); // 清空当前轮廓点// 将鼠标位置转换为图像真实像素坐标QPoint realPoint(e->pos().x(), e->pos().y());currentContour.push_back(realPoint); // 将转换后的坐标作为起点m_bStartDraw = true;}}}}
}
void MyLabel::mouseMoveEvent(QMouseEvent* e)
{// 处理鼠标左键按下时的绘制if (e->buttons() & Qt::LeftButton) // 左键按下且移动时触发{if (m_bStartDraw) {const QPixmap* currentPixmap = pixmap();if (currentPixmap && !currentPixmap->isNull()) {QRect imageRect(0, 0, currentPixmap->width(), currentPixmap->height());if (imageRect.contains(e->pos())) {movePoint = e->pos();this->update(); // 更新视图bMove = true; // 标记为正在移动}}}}else // 左键松开或没有按下时的情况{bMove = false; // 重置鼠标移动状态// 检查鼠标是否靠近已有的点,并改变鼠标形状bool isNearPoint = false;for (const auto& contour : allContours) {for (const auto& point : contour) {// 手动计算曼哈顿距离qreal dx = qAbs(e->pos().x() - point.x());qreal dy = qAbs(e->pos().y() - point.y());// 判断距离是否小于等于 4if (dx + dy <= 4) {isNearPoint = true; // 标记为靠近某个点break;}}if (isNearPoint) break; // 如果已经找到靠近的点,跳出外层循环}if (isNearPoint) {// 改变鼠标为手型setCursor(PointingHandCursor);}else {// 恢复为默认箭头setCursor(ArrowCursor);}}
}void MyLabel::mouseReleaseEvent(QMouseEvent* e)
{const QPixmap* currentPixmap = pixmap();if (currentPixmap && !currentPixmap->isNull()) {QRect imageRect(0, 0, currentPixmap->width(), currentPixmap->height());if (e->button() == LeftButton) {if (m_bStartDraw) {// 如果鼠标释放后添加最后的点,确保释放点在图片区域内if (imageRect.contains(e->pos())) {// 将释放点的坐标转换为图像真实像素坐标QPoint realPoint(e->pos().x(), e->pos().y());currentContour.push_back(realPoint);bMove = false;this->update();}}}}
}
void MyLabel::mouseDoubleClickEvent(QMouseEvent* event)
{endDraw(); // 双击时结束绘制
}
void MyLabel::savePath()
{QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");QString fileName = QFileDialog::getSaveFileName(this, "Save Path", timestamp + ".txt", "Text Files (.txt);;All Files ()");if (fileName.isEmpty()) {return;}QFile file(fileName);if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {QMessageBox::warning(this, "Save Error", "Failed to open the file for saving.");return;}QTextStream out(&file);for (const auto& contour : allContours) {for (const auto& point : contour) {// 计算真实像素坐标,并四舍五入取整int x = static_cast<int>(std::round(point.x() / m_scaleX));//如果需要保存图片真实像素位置就加 / m_scaleX获取真实坐标,如果保存改变后图像大小和像素就不需要加int y = static_cast<int>(std::round(point.y() / m_scaleY));// 保存四舍五入后的整数坐标out << x << " " << y << "\n";}out << "\n"; // 每个轮廓之间用空行分隔}file.close();QMessageBox::information(this, "Save Success", "Path saved successfully.");
}
void MyLabel::saveImageWithPaths()
{const QPixmap* currentPixmap = pixmap();if (currentPixmap && !currentPixmap->isNull()) {// 复制当前图片到新的 QPixmap 上QPixmap pixmapWithPaths = *currentPixmap;QPainter painter(&pixmapWithPaths); // 使用 QPainter 在新图像上绘制painter.setRenderHint(QPainter::Antialiasing);painter.setPen(QPen(red, 2)); // 设置红色笔刷,2px宽painter.setBrush(transparent); // 不填充路径// 绘制所有的路径for (const auto& contour : allContours) {if (!contour.isEmpty()) {QPainterPath path;path.moveTo(contour[0]);for (const auto& pt : contour)path.lineTo(pt);path.closeSubpath();painter.drawPath(path); // 绘制路径}}// 弹出保存对话框,选择保存路径和文件名QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");QString fileName = QFileDialog::getSaveFileName(this, "Save Image with Paths", timestamp + ".png", "Images (.png *.jpg *.bmp)");if (!fileName.isEmpty()) {// 保存图像为文件pixmapWithPaths.save(fileName);QMessageBox::information(this, "Save Success", "Image saved successfully.");}}
}
void MyLabel::setScaleFactors(double scaleX, double scaleY) {m_scaleX = scaleX;m_scaleY = scaleY;
}
详见代码内部解析
运行状况