Bootstrap

鼠标绘制轮廓

需要对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.label
    ui.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());
                // 判断距离是否小于等于 4
                if (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;
}

 详见代码内部解析

运行状况

;