C++桌面应用—鼠标事件实现图片移动与缩放
本文为天软实训项目总结
文章目录
1.环境搭建
2.项目结构
3.CMakeLists配置说明
4.窗体头文件说明
①主窗体类
②子窗体类
5.源文件Cpp以及Ui文件
①MainWindow.cpp
```c++
/**
* @Description 主窗体类
* @Version 1.0.0
* @Date 2024/1/13 18:32
* @Author Kenton
*/
// 预处理器宏,用于防止多次包含同一个头文件。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPushButton>
#include <QPixmap>
#include <QDrag>
#include <QMouseEvent>
#include <QWheelEvent>
#include "sub_Widget.h"
// 包围在Qt库中定义的类型,这是Qt为了防止命名冲突而推荐的做法。
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
// 定义了一个名为MainWindow的类,它继承自QMainWindow,
// 并使用了Q_OBJECT宏,这是因为该类需要使用信号和槽机制。
Q_OBJECT
public:
// 空参构造函数 explicit关键字用于限制构造函数的隐式类型转换
// 提供了一个explicit空参数构造函数,用于创建MainWindow对象,
// 并接受可选的父窗口指针,默认为nullptr。
explicit MainWindow(QWidget* parent = nullptr);
// 重载了析构函数~MainWindow(),当MainWindow对象销毁时调用。
~MainWindow() override;
// 私有槽函数
private slots:
/**
* @Description 根据从子窗口接收到图片信号 重新绘制图片
* @param pixmap 表示已选中的图像
* @param initialPos QPoint在Qt中代表一个二维点,它的两个成员变量分别表示x轴和y轴坐标值
* @param initialSize 表示所选图像初始的尺寸大小。
*/
void repaintEvent(const QPixmap& pixmap, const QPoint& initialPos, const QSize& initialSize);
protected:
// 鼠标点击事件
void mousePressEvent(QMouseEvent* event) override;
// 鼠标移动事件
void mouseMoveEvent(QMouseEvent* event) override;
// 鼠标滚轮事件
void wheelEvent(QWheelEvent* event) override;
// 更新图片尺寸
void updateDisplaySize(qreal scaleFactor);
// 子窗体作为成员
sub_Widget *sub_widget;
// 创建并初始化子窗体
void initSubWidget(sub_Widget *sub_widget) const;
/**
*声明paintEvent函数 并表记为虚函数h和重载基类函数
* @Description 声明paintEvent函数 并表记为虚函数和重载基类函数 此函数会在窗口需要重绘时被调用,可以在此函数内实现窗口内容的绘制。
* @param event 参数QPaintEvent* event是一个指向QPaintEvent对象的指针,该事件对象包含了关于即将要绘制区域的信息,例如矩形区域、是否是全窗口重绘等。
* override关键字是C++11引入的,用于明确表明该函数是在覆盖基类中的同名虚函数。
* 通过使用override,编译器可以进行额外的检查以确保确实存在可覆盖的虚函数,
* 从而有助于防止错误和提升代码可读性。
*/
virtual void paintEvent(QPaintEvent* event) override;
private:
Ui::MainWindow* ui;
// 是否创建子窗体
mutable bool _iscreateSubWidget = false;
// 保存当前显示的图片
QPixmap _pixmap;
// 记录鼠标按下时的位置
QPoint _dragStartPosition;
// 保存当前显示图片的偏移量
QPoint _offset;
// 保存当前显示图片的原始尺寸大小。
QSize _originalSize;
};
#endif //MAINWINDOW_H
②主窗体Ui文件
③sub_Widget.cpp
```cpp
```c++
/**
* @Description 子窗体类
* @Version 1.0.0
* @Date 2024/1/14 11:03
* @Author Kenton
*/
#ifndef SUB_WIDGET_H
#define SUB_WIDGET_H
#include <QWidget>
#include <QPushButton>
#include <QFileDialog>
#include <QSignalMapper>
#include <QPixmap>
QT_BEGIN_NAMESPACE
namespace Ui {
class sub_Widget;
}
QT_END_NAMESPACE
class sub_Widget : public QWidget {
Q_OBJECT
signals:
/**
* @Description 发送选中的图片及其初始位置和大小信号给主窗体
* @param pixmap 已选中的图像数据
* @param initialPos 表示所选图像在某个坐标系下的初始位置。
* @param initialSize 表示所选图像原始的尺寸大小。
*/
void imageSelected(const QPixmap& pixmap, const QPoint& initialPos, const QSize& initialSize);
public:
explicit sub_Widget(QWidget* parent = nullptr);
~sub_Widget() override;
private slots:
// 定义槽函数
// 选择图片按钮
void selectImageButtonClicked();
// 点击确认按钮
void confirmButtonClicked();
private:
Ui::sub_Widget* ui;
// 已选图片数据
QPixmap _selectedPixmap;
// 初始位置
QPoint _initialPos;
// 初始尺寸
QSize _initialSize;
// 子窗体是否已经选择图片资源
bool isSelectPic = false;
// 子窗体信息是否已经完全加载
bool isLoad = false;
};
#endif //SUB_WIDGET_H
④子窗体ui文件
⑤程序入口和统一资源管理文件QRC
6.程序设计说明
由局部到整体讲解
子窗体sub_Widget.cpp
①点击选择图片按钮,调用selectImageButtonClicked函数
```c++
connect(ui->selectImageButton,&QPushButton::clicked,this,&sub_Widget::selectImageButtonClicked);
selectImageButtonClicked函数
```c++
// 选择图片按钮
void sub_Widget::selectImageButtonClicked() {
/* @Param parent 这是一个指向调用此函数的QWidget对象的指针
* @Param caption 通过tr()函数进行翻译处理,它定义了打开文件对话框的标题
* @Param dir 表示对话框打开时默认展示的目录路径 此处默认路径为桌面路径
* @Param filter 通过tr()函数进行翻译处理,它定义了打开文件对话框的过滤器 这里只能选择.png、.jpg、.bmp格式的文件
*/
// filePath将保存用户从对话框中选择的图片文件的完整路径名。如果没有选择任何文件或者用户取消了对话框,则filePath可能为空字符串。
QString filePath = QFileDialog::getOpenFileName(this,tr(""),"E:\\Desktop",tr("Image Files (*.png *.jpg *.bmp)"));
if (ui->xPosInput->text().isEmpty() || ui->yPosInput->text().isEmpty() || ui->widthInput->text().isEmpty() || ui->heightInput->text().isEmpty()) {
// 提示警告窗口 输入框不能为空
QMessageBox::warning(this,tr("警告"),tr("请输入坐标和尺寸"),QMessageBox::Ok);
}
if (!filePath.isEmpty()) {
qDebug() << "文件路径为:" << filePath.toUtf8().data();
// 加载图片
_selectedPixmap = QPixmap(filePath);
// 输入框是ui->xPosInput, ui->yPosInput, ui->widthInput, ui->heightInput
// 获取输入的坐标和尺寸并转换为实际值
_initialPos.setX(ui->xPosInput->text().toInt());
_initialPos.setY(ui->xPosInput->text().toInt());
_initialSize.setWidth(ui->widthInput->text().toInt());
_initialSize.setHeight(ui->heightInput->text().toInt());
isSelectPic = true;
}else {
// 提示警告窗口 没有选择图片
QMessageBox::warning(this,tr("警告"),tr("请选择图片"),QMessageBox::Ok);
}
}
②点击确定按钮,调用confirmButtonClicked函数
```c++
// 点击确定按钮后,调用此函数,将图片信息发送给父窗口
void sub_Widget::confirmButtonClicked() {
if (ui->xPosInput->text().isEmpty() || ui->yPosInput->text().isEmpty() || ui->widthInput->text().isEmpty() || ui->heightInput->text().isEmpty()) {
// 提示警告窗口 输入框不能为空
QMessageBox::warning(this,tr("警告"),tr("请输入坐标和尺寸"),QMessageBox::Ok);
if (!isSelectPic) {
// 提示警告窗口 没有选择图片
QMessageBox::warning(this,tr("警告"),tr("请选择图片"),QMessageBox::Ok);
}
}else {
// 发送信号,通知父窗口,父窗口将根据信号中的数据进行处理
emit imageSelected(_selectedPixmap, _initialPos, _initialSize);
// 子窗体信息加载成功
isLoad = true;
}
}
③发射imageSelected调用onImageSelected
```c++
// TODO 代码冗余 应该在主窗体类申明子窗体为属性 在主窗体类定义创建子窗体的槽函数 同时定义一个bool型属性isFlag记录是否创建子窗体
connect(newAction, QAction::triggered, this, [=] {
sub_Widget* sub_widget = new ::sub_Widget();
sub_widget->setWindowTitle("图片选择器");
sub_widget->setWindowIcon((QIcon(":/main/image/sub_Widget.png")));
// 连接子窗体的信号和槽
sub_widget->show();
connect(sub_widget, &sub_Widget::imageSelected, this, &MainWindow::onImageSelected);
});
主窗体MainWindow.cpp
①点击生成新窗体
```c++
void MainWindow::initSubWidget(sub_Widget *sub_widget) const {
if (_iscreateSubWidget) {
sub_widget->show();
return;
}else {
sub_widget->setWindowTitle("图片选择器");
sub_widget->setWindowIcon((QIcon(":/main/image/sub_Widget.png")));
sub_widget->show();
_iscreateSubWidget = true;
// 连接子窗体的信号和槽
connect(sub_widget, &sub_Widget::imageSelected, this, &MainWindow::repaintEvent);
}
}
②主窗体重写主窗体的绘制方法 用于初始化图片
```c++
// 重写父窗口的paintEvent函数,用于绘制图片
void MainWindow::paintEvent(QPaintEvent *event){
// 创建一个QPainter对象,用于在窗口上进行绘图操作。这里的this指代当前MainWindow实例,即绘图的目标区域。
QPainter painter(this);
// 判断图片是否为空,如果为空则不绘制
if (!_pixmap.isNull()){
// 参数 QRect{_offset.x(), _offset.y(), _originalSize.width(), _originalSize.height()} 定义了一个矩形区域,用来指定图片在窗口中的位置和大小。
// 其中 _offset 是图片左上角相对于窗口左上角的偏移量,而 _originalSize 表示图片原始的宽度和高度。
painter.drawPixmap(QRect{_offset.x(), _offset.y(), _originalSize.width(), _originalSize.height()}, _pixmap);
}
}
③窗体重新绘制图片
```c++
/**
* @Descraption 根据从子窗口接收到图片信号 重新绘制图片
* @param pixmap 表示已选中的图像
* @param initialPos QPoint在Qt中代表一个二维点,它的两个成员变量分别表示x轴和y轴坐标值
* @param initialSize 表示所选图像初始的尺寸大小。
*/
void MainWindow::repaintEvent(const QPixmap& pixmap, const QPoint& initialPos, const QSize& initialSize) {
_pixmap = pixmap;
_offset = initialPos;
_originalSize = initialSize;
// 重绘主窗口以显示图片
update();
}
④TODO 鼠标点击事件生成虚线
```c++
// 重写父窗口的mousePressEvent函数 点击图片虚线包围
void MainWindow::mousePressEvent(QMouseEvent* event) {
// 如果图片为空,直接返回
if (_pixmap.isNull()) {
return;
}
// 记录鼠标按下时的位置
_dragStartPosition = event->pos();
// TODO 给鼠标选中的图片加虚线
if (event->buttons().testFlag(Qt::LeftButton)) {
// 给鼠标选中的图片加虚线
QPainter painter(this);
painter.setPen(QPen(Qt::black, 1, Qt::DashLine));
painter.drawRect(rect().adjusted(0, 0, -1, -1));
}
}
⑤鼠标移动事件
```c++
// 用于处理鼠标移动事件的部分,主要用于实现图片的拖放功能
void MainWindow::mouseMoveEvent(QMouseEvent* event) {
// 如果图片为空或鼠标当前没有按下左键,直接返回
if (!_pixmap.isNull() && !event->buttons().testFlag(Qt::LeftButton)) {
return;
}
// 更新图片相对于窗口左上角的新偏移量
_offset = _offset + (event->pos() - _dragStartPosition);
// 更新拖拽起始位置为当前鼠标位置
_dragStartPosition = event->pos();
// 重绘窗口,以实现图片的拖动功能。
update();
}
⑥鼠标滚轮事件缩放图片
```c++
// 新增一个用于更新显示尺寸的函数
void MainWindow::updateDisplaySize(qreal scaleFactor) {
const qreal minScale = 0.5;
const qreal maxScale = 4.0;
/* 使用 qBound 函数简化范围限制操作
* qBound()是Qt库中的一个函数,用于对给定的数值进行范围约束。它接受三个参数,分别为最小值、原始值和最大值,并返回一个新的值,
* 这个新值确保在最小值和最大值之间(包括这两个边界值)。如果原始值小于最小值,则返回最小值;如果原始值大于最大值,则返回最大值;否则返回原始值。
*/
qreal newWidth = qBound(minScale * _pixmap.width(), _originalSize.width() * scaleFactor, maxScale * _pixmap.width());
qreal newHeight = qBound(minScale * _pixmap.height(), _originalSize.height() * scaleFactor, maxScale * _pixmap.height());
// 更新实际显示尺寸
_originalSize.setWidth(newWidth);
_originalSize.setHeight(newHeight);
}
//处理鼠标滚轮事件的部分,主要用于实现图片的缩放功能
void MainWindow::wheelEvent(QWheelEvent* event) {
// 如果图片为空,直接返回
if (_pixmap.isNull()) {
return;
}
// 计算缩放因子。根据滚轮滚动的角度增量(event->angleDelta().y()),
// 将其除以120并以2为底数计算指数,得到一个放大或缩小的比例系数。
qreal scaleFactor = pow((qreal) 2, event->angleDelta().y() / 120.0);
// 更新 _originalSize 变量,它存储了图片原始尺寸。
// 将当前的尺寸乘以缩放因子,从而实现图片的缩放。
// _originalSize *= scaleFactor;
//
// // 定义了最小和最大缩放比例常量 限制缩放到一定范围
// const qreal minScale = 0.1;
// const qreal maxScale = 4.0;
//
// // 使用 qMin 和 qMax 函数来确保缩放后的宽度和高度在指定范围内。
// // 对于宽度:先计算出按最大缩放比例缩放后的宽度,并与按最小缩放比例缩放后的宽度及当前宽度取最大值的结果比较,选择其中较小的一个作为新的宽度。
// _originalSize.setWidth(qMin(maxScale * _pixmap.width(),
// qMax(minScale * _pixmap.width(), static_cast<qreal>(_originalSize.width()))));
// // 对于高度:同理,先计算按最大和最小缩放比例缩放后的高度,然后与当前高度取最大值的结果比较,选取较小的一个作为新的高度。
// _originalSize.setHeight(qMin(maxScale * _pixmap.height(),
// qMax(minScale * _pixmap.height(), static_cast<qreal>(_originalSize.height()))));
// 更新实际显示尺寸(这里假设有一个成员变量 _displaySize 存储)
updateDisplaySize(scaleFactor);
// 重绘窗口以反映新的图片尺寸
update();
}
7.程序设计不合理以及待完善的功能
不合理
子窗体sub_Widget
- 输入验证: 在selectImageButtonClicked()函数中,如果坐标或尺寸输入框为空,则弹出警告窗口提示用户。然而,在点击“选择图片”按钮时,这些输入并不是必要的,只有在点击“确定”按钮时才需要验证它们。因此,可以把这部分输入验证移到confirmButtonClicked()函数中,并确保只在确认操作时进行。
- 错误处理: 当加载图片失败时(如文件不存在、格式不正确等),代码没有处理这种情况。建议在加载图片后添加错误检查,例如使用QPixmap::isNull()判断图片是否加载成功,若加载失败则给出相应提示。
- 信号槽连接: 确认按钮关闭子窗体的条件是isLoad变量为真。但这个变量是在confirmButtonClicked()函数内部设置的,而且它表示的是子窗体信息是否已加载成功。此处可能更希望改为:只要点击了确认按钮,无论是否加载成功都关闭子窗体,或者根据实际需求来调整关闭策略。
- 资源路径: 图片资源的路径可能需要动态获取或设置成相对路径,避免对特定目录的依赖,以提高代码的可移植性。
- 内存管理: 如果 _selectedPixmap 不再需要,应在合适的时候调用 deleteLater() 或者显式释放,防止内存泄漏。但在Qt中,对于短期使用的 QPixmap 对象,通常不需要手动释放,因为 Qt 的智能指针机制会在适当时候自动清理。
认识自己的不足,才能涅槃重生
主窗体MainWidget
- 子窗口对象初始化: 当前代码在构造函数中每次都创建一个新的sub_Widget实例,这可能导致内存泄漏。可以考虑在类成员变量中存储一个sub_Widget指针,并仅在需要时创建它。
sub_Widget* sub_widget = nullptr;
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
// ...
sub_widget = new sub_Widget(this);
// ...
}
MainWindow::~MainWindow() {
delete ui;
// 在析构函数中删除子窗口实例
delete sub_widget;
}
- 资源管理: 图片资源的路径设置是合理的,但需要注意的是,如果资源文件没有正确编译到项目中,可能会出现找不到资源的问题。确保在Cmake文件中添加了资源文件并进行了构建。
- paintEvent与repaintEvent: repaintEvent槽函数命名不当,与Qt内置的paintEvent方法容易混淆。实际应该将repaintEvent重命名为其他如onImageSelectedSlot,以避免误解。。
待完善的功能
// TODO 点击编辑按钮 把主窗体显示的图片在新独立窗口打开 可以实现用户自主裁剪和缩放
// TODO 点击设置按钮生成新窗体 此窗体上有四个输入框分别存放选中图片的横纵坐标和宽高 可以重新设置图片尺寸 点击确定按钮实现
// TODO 定义槽函数检查关闭之前是否做修改 如果修改,需要弹出对话框,用户确认是否保存修改,保存后退出;如果不修改,直接退出。
connect(exitAction, &QAction::triggered, this, [=] {
QMessageBox::question(this,"询问","是否保存修改?",QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
this->close();
});
``
// TODO 为了更好的交互 状态栏信息应该动态显示用户操作状态
auto* labelStatusInfo = new QLabel("用户正在使用本软件");
stBar->addWidget(labelStatusInfo);
// TODO 给鼠标选中的图片加虚线
if (event->buttons().testFlag(Qt::LeftButton)) {
// 给鼠标选中的图片加虚线
QPainter painter(this);
painter.setPen(QPen(Qt::black, 1, Qt::DashLine));
painter.drawRect(rect().adjusted(0, 0, -1, -1));
}
8.开发过程中遇到的问题和解决办法
①QRC文件的Cmake文件配置
Qt_CLion中配置资源文件 - DaoDao777999 - 博客园 (cnblogs.com)
②在CLion中开发QT
JetBrains官方手册
Qt projects | CLion Documentation (jetbrains.com)
9.额外的收获与思考
①Jetbrains的IDE自带的版本管理Local History
一开始我的想法是根据反编译原理,逆向工程生成源文件。后面查阅资料,实际是运用到快照技术。
它不依赖于外部版本控制系统(如Git、SVN),而是通过自动保存项目中文件的不同版本到用户的本地存储空间来实现历史记录追踪。
其工作原理主要包括以下几点:
**定期自动保存:**当用户编辑并保存文件时,Local History会自动捕捉这一时刻的文件内容,并将其存储起来。此外,IDE还可能根据设定的时间间隔定时保存文件状态,即使用户未主动保存也能捕获到文件的变化。
**差异存储:**为了节省存储空间,Local History通常不会存储整个文件的新副本,而是只存储与前一个版本之间的差异。这样可以高效地管理大量历史版本信息。
**索引和检索:**对存储的历史版本进行索引,以便用户能够快速查找和恢复特定时间点或更改前后的文件版本。
**查看和恢复:**用户可以在IDE内方便地浏览文件的历史版本列表,选择任意版本查看内容或恢复到指定版本。
②双键空格查看IDE缩略图
③配置有道翻译
④BookMarks管理端点
⑤Ui组件可以复制到新的Ui同时别名不会改变 仅限CLion
Ui文件本质是类似xml和yml这种标签
⑥代码对比 Ctrl+D
⑦构建管理
⑧CLion Debug查看内存地址
10.心得总结
不可,以一时之得意,而自夸其能;亦不可,以一时之失意,而自坠其志
一个人可以走得很快,但一群人才能走得更远
你最引以为傲的东西,最能束缚你
学历学位 不只是一纸文凭 更是对独立学习能力、抗压能力的认证
在《令人心动的offer》医生系列中有一集是这样的。一位是本硕博都是顶尖985名校浙江大学的刘畅,一位是二本本科学历的高尚。两人为一组搭档。刘畅虽然学历高临床经验不如高尚丰富,在面对患者时刘畅很快地从病人口述中了解病因,而刘畅实战经验不足无法从病人哪里得到关键信息。在后面的导师考核环节,初试第一的刘畅面对老师的提问一问三不知,而高尚回答地游刃有余。结束后高尚沾沾自喜很享受被别人夸赞的感觉,下班后高尚早早回去,而刘畅一直在医院复盘今天的经历,甚至再到病房询问患者。第二天导师考察昨天留下的几个问题,高尚凭借着自己的经验依然回答得游刃有余,到了第二个问题就支支吾吾不知道怎么回答。很显然高尚在前一天并没有把这个问题放在心上,而一旁的刘畅则轻松回答。而接下来的考核环节,高尚内心的骄傲被985刘畅打得支离破碎,导师安排两人一起对之前的病人做回访,让两人团队合作。而高尚却有着自己的骄傲,私下拒绝刘畅的合作,检查要分开汇报。到了第二天考试,老师看到高尚的报告质量奇差无比,毫不留言面地提出批评。导师以为是他们的没有合作好,询问他们从对方身上学到什么,高尚直言从刘畅身上什么也没学到。开会结束,高尚怒斥刘畅内卷做了三份报告,让自己在老师面对丢颜面,刘畅回怼"你作为29岁的人,自己不好好努力 怪别人。" 两个人因此闹不和不再展开合作,但第二天还有一个项目需要两人协力完成。高学历的刘畅这次选择独立完成,一人通宵查阅资料完成PPT制作,并且全英文翻译,最终带着高尚通过考核。
平衡感情与工作
安全感是自己给的,做自己的靠山,没有人会一直为你的行为买单。
在学校,学习是个人职责,进社会,工作是个人职责,问题摆在这里永远是问题,不会因为你今天心情不好,问题会自己消失。
可以心情不好,可以失恋大哭一场,但不能以此作为逃避责任的借口。
持续性的充电,不断提升个人竞争力
平衡感情与工作
安全感是自己给的,做自己的靠山,没有人会一直为你的行为买单。
在学校,学习是个人职责,进社会,工作是个人职责,问题摆在这里永远是问题,不会因为你今天心情不好,问题会自己消失。
可以心情不好,可以失恋大哭一场,但不能以此作为逃避责任的借口。
持续性的充电,不断提升个人竞争力
用时间冷却浮躁的心,用热爱坐住科研的"冷板凳"
天软实训答辩PPT百度云链接如下
感谢网友们对本文提出的批评与指正,任何问题欢迎评论区提问
作者邮箱:1441640907@qq.com
联系微信