实现头像编辑框

前情回顾

前文我们实现了心跳,今天来实现头像框裁剪的功能,为以后头像上传和资源服务器做准备。

大体上头像上传框的效果如下

image-20250511075018888

添加设置页面

我们需要在聊天对话框左侧添加设置按钮

image-20250511075548367

左侧设置按钮是我们封装的类StateWidget

image-20250511075648519

右侧添加UserInfoPage界面

image-20250511075822544

UserInfoPage界面布局

image-20250511082150907

属性表

image-20250511082230811

头像裁剪逻辑

点击上传按钮

  1. //上传头像
  2. void UserInfoPage::on_up_btn_clicked()
  3. {
  4. // 1. 让对话框也能选 *.webp
  5. QString filename = QFileDialog::getOpenFileName(
  6. this,
  7. tr("选择图片"),
  8. QString(),
  9. tr("图片文件 (*.png *.jpg *.jpeg *.bmp *.webp)")
  10. );
  11. if (filename.isEmpty())
  12. return;
  13. // 2. 直接用 QPixmap::load() 加载,无需手动区分格式
  14. QPixmap inputImage;
  15. if (!inputImage.load(filename)) {
  16. QMessageBox::critical(
  17. this,
  18. tr("错误"),
  19. tr("加载图片失败!请确认已部署 WebP 插件。"),
  20. QMessageBox::Ok
  21. );
  22. return;
  23. }
  24. QPixmap image = ImageCropperDialog::getCroppedImage(filename, 600, 400, CropperShape::CIRCLE);
  25. if (image.isNull())
  26. return;
  27. QPixmap scaledPixmap = image.scaled( ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小
  28. ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上
  29. ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小
  30. QString storageDir = QStandardPaths::writableLocation(
  31. QStandardPaths::AppDataLocation);
  32. // 2. 在其下再建一个 avatars 子目录
  33. QDir dir(storageDir);
  34. if (!dir.exists("avatars")) {
  35. if (!dir.mkpath("avatars")) {
  36. qWarning() << "无法创建 avatars 目录:" << dir.filePath("avatars");
  37. QMessageBox::warning(
  38. this,
  39. tr("错误"),
  40. tr("无法创建存储目录,请检查权限或磁盘空间。")
  41. );
  42. return;
  43. }
  44. }
  45. // 3. 拼接最终的文件名 head.png
  46. QString filePath = dir.filePath("avatars/head.png");
  47. // 4. 保存 scaledPixmap 为 PNG(无损、最高质量)
  48. if (!scaledPixmap.save(filePath, "PNG")) {
  49. QMessageBox::warning(
  50. this,
  51. tr("保存失败"),
  52. tr("头像保存失败,请检查权限或磁盘空间。")
  53. );
  54. } else {
  55. qDebug() << "头像已保存到:" << filePath;
  56. // 以后读取直接用同一路径:storageDir/avatars/head.png
  57. }
  58. }

内部调用了我们的ImageCropperDialog,弹出对话框后会显示裁剪图片的界面。

接下来我们看看ImageCropperDialog实现

  1. #ifndef IMAGECROPPER_H
  2. #define IMAGECROPPER_H
  3. #include <QWidget>
  4. #include <QDialog>
  5. #include <QPainter>
  6. #include <QLabel>
  7. #include <QPixmap>
  8. #include <QString>
  9. #include <QMessageBox>
  10. #include <QHBoxLayout>
  11. #include <QVBoxLayout>
  12. #include <QPushButton>
  13. #include "imagecropperlabel.h"
  14. /*******************************************************
  15. * Loacl private class, which do image-cropping
  16. * Used in class ImageCropper
  17. *******************************************************/
  18. class ImageCropperDialogPrivate : public QDialog {
  19. Q_OBJECT
  20. public:
  21. ImageCropperDialogPrivate(const QPixmap& imageIn, QPixmap& outputImage,
  22. int windowWidth, int windowHeight,
  23. CropperShape shape, QSize cropperSize = QSize()) :
  24. QDialog(nullptr), outputImage(outputImage)
  25. {
  26. this->setAttribute(Qt::WA_DeleteOnClose, true);
  27. this->setWindowTitle("Image Cropper");
  28. this->setMouseTracking(true);
  29. this->setModal(true);
  30. imageLabel = new ImageCropperLabel(windowWidth, windowHeight, this);
  31. imageLabel->setCropper(shape, cropperSize);
  32. imageLabel->setOutputShape(OutputShape::RECT);
  33. imageLabel->setOriginalImage(imageIn);
  34. imageLabel->enableOpacity(true);
  35. QHBoxLayout* btnLayout = new QHBoxLayout();
  36. btnOk = new QPushButton("OK", this);
  37. btnCancel = new QPushButton("Cancel", this);
  38. btnLayout->addStretch();
  39. btnLayout->addWidget(btnOk);
  40. btnLayout->addWidget(btnCancel);
  41. QVBoxLayout* mainLayout = new QVBoxLayout(this);
  42. mainLayout->addWidget(imageLabel);
  43. mainLayout->addLayout(btnLayout);
  44. connect(btnOk, &QPushButton::clicked, this, [this](){
  45. this->outputImage = this->imageLabel->getCroppedImage();
  46. this->close();
  47. });
  48. connect(btnCancel, &QPushButton::clicked, this, [this](){
  49. this->outputImage = QPixmap();
  50. this->close();
  51. });
  52. }
  53. private:
  54. ImageCropperLabel* imageLabel;
  55. QPushButton* btnOk;
  56. QPushButton* btnCancel;
  57. QPixmap& outputImage;
  58. };
  59. /*******************************************************************
  60. * class ImageCropperDialog
  61. * create a instane of class ImageCropperDialogPrivate
  62. * and get cropped image from the instance(after closing)
  63. ********************************************************************/
  64. class ImageCropperDialog : QObject {
  65. public:
  66. static QPixmap getCroppedImage(const QString& filename,int windowWidth, int windowHeight,
  67. CropperShape cropperShape, QSize crooperSize = QSize())
  68. {
  69. QPixmap inputImage;
  70. QPixmap outputImage;
  71. if (!inputImage.load(filename)) {
  72. QMessageBox::critical(nullptr, "Error", "Load image failed!", QMessageBox::Ok);
  73. return outputImage;
  74. }
  75. ImageCropperDialogPrivate* imageCropperDo =
  76. new ImageCropperDialogPrivate(inputImage, outputImage,
  77. windowWidth, windowHeight,
  78. cropperShape, crooperSize);
  79. imageCropperDo->exec();
  80. return outputImage;
  81. }
  82. };
  83. #endif // IMAGECROPPER_H

私有对话框

  1. 继承自 QDialog
    • QDialog(nullptr):以无父窗口方式创建,独立弹出。
    • Qt::WA_DeleteOnClose:关闭时自动 delete 对象,防止内存泄漏。
    • setModal(true):对话框模式,阻塞主窗口输入。
  2. 成员变量
    • ImageCropperLabel* imageLabel:自定义裁剪视图。
    • QPushButton* btnOk, btnCancel:确认/取消按钮。
    • QPixmap& outputImage:引用外部提供的 QPixmap,用来保存裁剪结果。
  3. 布局管理
    • 水平布局 (QHBoxLayout) 放置按钮并居右。
    • 垂直布局 (QVBoxLayout) 先是大图,再是按钮区。
  4. Lambda 连接信号与槽
    • OK 时,将裁剪后的图像复制给外部引用,然后 close()
    • Cancel 时,将 outputImage 置空,表示用户放弃裁剪。

静态对话框

  • 统一接口:只要一行 ImageCropperDialog::getCroppedImage(…),就能弹出裁剪 UI 并获取结果。
  • 输入合法性检查:先用 QPixmap::load() 加载文件,失败则弹错并返回空图。
  • 阻塞执行exec() 会进入本地事件循环,直到用户点击 OK/Cancel 关闭对话框。
  • 返回结果:通过外部引用 outputImage 将裁剪结果“带出”函数作用域。

image-20250511112606921

头像裁剪控件

头文件声明

  1. /*************************************************************************
  2. * class: ImageCropperLabel
  3. * author: github@Leopard-C
  4. * email: leopard.c@outlook.com
  5. * last change: 2020-03-06
  6. *************************************************************************/
  7. #ifndef IMAGECROPPERLABEL_H
  8. #define IMAGECROPPERLABEL_H
  9. #include <QLabel>
  10. #include <QPixmap>
  11. #include <QPen>
  12. enum class CropperShape {
  13. UNDEFINED = 0,
  14. RECT = 1,
  15. SQUARE = 2,
  16. FIXED_RECT = 3,
  17. ELLIPSE = 4,
  18. CIRCLE = 5,
  19. FIXED_ELLIPSE = 6
  20. };
  21. enum class OutputShape {
  22. RECT = 0,
  23. ELLIPSE = 1
  24. };
  25. enum class SizeType {
  26. fixedSize = 0,
  27. fitToMaxWidth = 1,
  28. fitToMaxHeight = 2,
  29. fitToMaxWidthHeight = 3,
  30. };
  31. class ImageCropperLabel : public QLabel {
  32. Q_OBJECT
  33. public:
  34. ImageCropperLabel(int width, int height, QWidget* parent);
  35. void setOriginalImage(const QPixmap& pixmap);
  36. void setOutputShape(OutputShape shape) { outputShape = shape; }
  37. QPixmap getCroppedImage();
  38. QPixmap getCroppedImage(OutputShape shape);
  39. /*****************************************
  40. * Set cropper's shape
  41. *****************************************/
  42. void setRectCropper();
  43. void setSquareCropper();
  44. void setEllipseCropper();
  45. void setCircleCropper();
  46. void setFixedRectCropper(QSize size);
  47. void setFixedEllipseCropper(QSize size);
  48. void setCropper(CropperShape shape, QSize size); // not recommended
  49. /*****************************************************************************
  50. * Set cropper's fixed size
  51. *****************************************************************************/
  52. void setCropperFixedSize(int fixedWidth, int fixedHeight);
  53. void setCropperFixedWidth(int fixedWidht);
  54. void setCropperFixedHeight(int fixedHeight);
  55. /*****************************************************************************
  56. * Set cropper's minimum size
  57. * default: the twice of minimum of the edge lenght of drag square
  58. *****************************************************************************/
  59. void setCropperMinimumSize(int minWidth, int minHeight)
  60. { cropperMinimumWidth = minWidth; cropperMinimumHeight = minHeight; }
  61. void setCropperMinimumWidth(int minWidth) { cropperMinimumWidth = minWidth; }
  62. void setCropperMinimumHeight(int minHeight) { cropperMinimumHeight = minHeight; }
  63. /*************************************************
  64. * Set the size, color, visibility of rectangular border
  65. *************************************************/
  66. void setShowRectBorder(bool show) { isShowRectBorder = show; }
  67. QPen getBorderPen() { return borderPen; }
  68. void setBorderPen(const QPen& pen) { borderPen = pen; }
  69. /*************************************************
  70. * Set the size, color of drag square
  71. *************************************************/
  72. void setShowDragSquare(bool show) { isShowDragSquare = show; }
  73. void setDragSquareEdge(int edge) { dragSquareEdge = (edge >= 3 ? edge : 3); }
  74. void setDragSquareColor(const QColor& color) { dragSquareColor = color; }
  75. /*****************************************
  76. * Opacity Effect
  77. *****************************************/
  78. void enableOpacity(bool b = true) { isShowOpacityEffect = b; }
  79. void setOpacity(double newOpacity) { opacity = newOpacity; }
  80. signals:
  81. void croppedImageChanged();
  82. protected:
  83. /*****************************************
  84. * Event
  85. *****************************************/
  86. virtual void paintEvent(QPaintEvent *event) override;
  87. virtual void mousePressEvent(QMouseEvent *e) override;
  88. virtual void mouseMoveEvent(QMouseEvent *e) override;
  89. virtual void mouseReleaseEvent(QMouseEvent *e) override;
  90. private:
  91. /***************************************
  92. * Draw shapes
  93. ***************************************/
  94. void drawFillRect(QPoint centralPoint, int edge, QColor color);
  95. void drawRectOpacity();
  96. void drawEllipseOpacity();
  97. void drawOpacity(const QPainterPath& path); // shadow effect
  98. void drawSquareEdge(bool onlyFourCorners);
  99. /***************************************
  100. * Other utility methods
  101. ***************************************/
  102. int getPosInCropperRect(const QPoint& pt);
  103. bool isPosNearDragSquare(const QPoint& pt1, const QPoint& pt2);
  104. void resetCropperPos();
  105. void changeCursor();
  106. enum {
  107. RECT_OUTSIZD = 0,
  108. RECT_INSIDE = 1,
  109. RECT_TOP_LEFT, RECT_TOP, RECT_TOP_RIGHT, RECT_RIGHT,
  110. RECT_BOTTOM_RIGHT, RECT_BOTTOM, RECT_BOTTOM_LEFT, RECT_LEFT
  111. };
  112. const bool ONLY_FOUR_CORNERS = true;
  113. private:
  114. QPixmap originalImage;
  115. QPixmap tempImage;
  116. bool isShowRectBorder = true;
  117. QPen borderPen;
  118. CropperShape cropperShape = CropperShape::UNDEFINED;
  119. OutputShape outputShape = OutputShape::RECT;
  120. QRect imageRect; // the whole image area in the label (not real size)
  121. QRect cropperRect; // a rectangle frame to choose image area (not real size)
  122. QRect cropperRect_; // cropper rect (real size)
  123. double scaledRate = 1.0;
  124. bool isLButtonPressed = false;
  125. bool isCursorPosCalculated = false;
  126. int cursorPosInCropperRect = RECT_OUTSIZD;
  127. QPoint lastPos;
  128. QPoint currPos;
  129. bool isShowDragSquare = true;
  130. int dragSquareEdge = 8;
  131. QColor dragSquareColor = Qt::white;
  132. int cropperMinimumWidth = dragSquareEdge * 2;
  133. int cropperMinimumHeight = dragSquareEdge * 2;
  134. bool isShowOpacityEffect = false;
  135. double opacity = 0.6;
  136. };
  137. #endif // IMAGECROPPERLABEL_H

具体实现

  1. #include "imagecropperlabel.h"
  2. #include <QPainter>
  3. #include <QPainterPath>
  4. #include <QMouseEvent>
  5. #include <QDebug>
  6. #include <QBitmap>
  7. ImageCropperLabel::ImageCropperLabel(int width, int height, QWidget* parent) :
  8. QLabel(parent)
  9. {
  10. this->setFixedSize(width, height);
  11. this->setAlignment(Qt::AlignCenter);
  12. this->setMouseTracking(true);
  13. borderPen.setWidth(1);
  14. borderPen.setColor(Qt::white);
  15. borderPen.setDashPattern(QVector<qreal>() << 3 << 3 << 3 << 3);
  16. }
  17. void ImageCropperLabel::setOriginalImage(const QPixmap &pixmap) {
  18. originalImage = pixmap;
  19. int imgWidth = pixmap.width();
  20. int imgHeight = pixmap.height();
  21. int labelWidth = this->width();
  22. int labelHeight = this->height();
  23. int imgWidthInLabel;
  24. int imgHeightInLabel;
  25. if (imgWidth * labelHeight < imgHeight * labelWidth) {
  26. scaledRate = labelHeight / double(imgHeight);
  27. imgHeightInLabel = labelHeight;
  28. imgWidthInLabel = int(scaledRate * imgWidth);
  29. imageRect.setRect((labelWidth - imgWidthInLabel) / 2, 0,
  30. imgWidthInLabel, imgHeightInLabel);
  31. }
  32. else {
  33. scaledRate = labelWidth / double(imgWidth);
  34. imgWidthInLabel = labelWidth;
  35. imgHeightInLabel = int(scaledRate * imgHeight);
  36. imageRect.setRect(0, (labelHeight - imgHeightInLabel) / 2,
  37. imgWidthInLabel, imgHeightInLabel);
  38. }
  39. tempImage = originalImage.scaled(imgWidthInLabel, imgHeightInLabel,
  40. Qt::KeepAspectRatio, Qt::SmoothTransformation);
  41. this->setPixmap(tempImage);
  42. if (cropperShape >= CropperShape::FIXED_RECT) {
  43. cropperRect.setWidth(int(cropperRect_.width() * scaledRate));
  44. cropperRect.setHeight(int(cropperRect_.height() * scaledRate));
  45. }
  46. resetCropperPos();
  47. }
  48. /*****************************************
  49. * set cropper's shape (and size)
  50. *****************************************/
  51. void ImageCropperLabel::setRectCropper() {
  52. cropperShape = CropperShape::RECT;
  53. resetCropperPos();
  54. }
  55. void ImageCropperLabel::setSquareCropper() {
  56. cropperShape = CropperShape::SQUARE;
  57. resetCropperPos();
  58. }
  59. void ImageCropperLabel::setEllipseCropper() {
  60. cropperShape = CropperShape::ELLIPSE;
  61. resetCropperPos();
  62. }
  63. void ImageCropperLabel::setCircleCropper() {
  64. cropperShape = CropperShape::CIRCLE;
  65. resetCropperPos();
  66. }
  67. void ImageCropperLabel::setFixedRectCropper(QSize size) {
  68. cropperShape = CropperShape::FIXED_RECT;
  69. cropperRect_.setSize(size);
  70. resetCropperPos();
  71. }
  72. void ImageCropperLabel::setFixedEllipseCropper(QSize size) {
  73. cropperShape = CropperShape::FIXED_ELLIPSE;
  74. cropperRect_.setSize(size);
  75. resetCropperPos();
  76. }
  77. // not recommended
  78. void ImageCropperLabel::setCropper(CropperShape shape, QSize size) {
  79. cropperShape = shape;
  80. cropperRect_.setSize(size);
  81. resetCropperPos();
  82. }
  83. /*****************************************************************************
  84. * Set cropper's fixed size
  85. *****************************************************************************/
  86. void ImageCropperLabel::setCropperFixedSize(int fixedWidth, int fixedHeight) {
  87. cropperRect_.setSize(QSize(fixedWidth, fixedHeight));
  88. resetCropperPos();
  89. }
  90. void ImageCropperLabel::setCropperFixedWidth(int fixedWidth) {
  91. cropperRect_.setWidth(fixedWidth);
  92. resetCropperPos();
  93. }
  94. void ImageCropperLabel::setCropperFixedHeight(int fixedHeight) {
  95. cropperRect_.setHeight(fixedHeight);
  96. resetCropperPos();
  97. }
  98. /**********************************************
  99. * Move cropper to the center of the image
  100. * And resize to default
  101. **********************************************/
  102. void ImageCropperLabel::resetCropperPos() {
  103. int labelWidth = this->width();
  104. int labelHeight = this->height();
  105. if (cropperShape == CropperShape::FIXED_RECT || cropperShape == CropperShape::FIXED_ELLIPSE) {
  106. cropperRect.setWidth(int(cropperRect_.width() * scaledRate));
  107. cropperRect.setHeight(int(cropperRect_.height() * scaledRate));
  108. }
  109. switch (cropperShape) {
  110. case CropperShape::UNDEFINED:
  111. break;
  112. case CropperShape::FIXED_RECT:
  113. case CropperShape::FIXED_ELLIPSE: {
  114. cropperRect.setRect((labelWidth - cropperRect.width()) / 2,
  115. (labelHeight - cropperRect.height()) / 2,
  116. cropperRect.width(), cropperRect.height());
  117. break;
  118. }
  119. case CropperShape::RECT:
  120. case CropperShape::SQUARE:
  121. case CropperShape::ELLIPSE:
  122. case CropperShape::CIRCLE: {
  123. int imgWidth = tempImage.width();
  124. int imgHeight = tempImage.height();
  125. int edge = int((imgWidth > imgHeight ? imgHeight : imgWidth) * 3 / 4.0);
  126. cropperRect.setRect((labelWidth - edge) / 2, (labelHeight - edge) / 2, edge, edge);
  127. break;
  128. }
  129. }
  130. }
  131. QPixmap ImageCropperLabel::getCroppedImage() {
  132. return getCroppedImage(this->outputShape);
  133. }
  134. QPixmap ImageCropperLabel::getCroppedImage(OutputShape shape) {
  135. int startX = int((cropperRect.left() - imageRect.left()) / scaledRate);
  136. int startY = int((cropperRect.top() - imageRect.top()) / scaledRate);
  137. int croppedWidth = int(cropperRect.width() / scaledRate);
  138. int croppedHeight = int(cropperRect.height() / scaledRate);
  139. QPixmap resultImage(croppedWidth, croppedHeight);
  140. resultImage = originalImage.copy(startX, startY, croppedWidth, croppedHeight);
  141. // Set ellipse mask (cut to ellipse shape)
  142. if (shape == OutputShape::ELLIPSE) {
  143. QSize size(croppedWidth, croppedHeight);
  144. QBitmap mask(size);
  145. QPainter painter(&mask);
  146. painter.setRenderHint(QPainter::Antialiasing);
  147. painter.setRenderHint(QPainter::SmoothPixmapTransform);
  148. painter.fillRect(0, 0, size.width(), size.height(), Qt::white);
  149. painter.setBrush(QColor(0, 0, 0));
  150. painter.drawRoundRect(0, 0, size.width(), size.height(), 99, 99);
  151. resultImage.setMask(mask);
  152. }
  153. return resultImage;
  154. }
  155. void ImageCropperLabel::paintEvent(QPaintEvent *event) {
  156. // Draw original image
  157. QLabel::paintEvent(event);
  158. // Draw cropper and set some effects
  159. switch (cropperShape) {
  160. case CropperShape::UNDEFINED:
  161. break;
  162. case CropperShape::FIXED_RECT:
  163. drawRectOpacity();
  164. break;
  165. case CropperShape::FIXED_ELLIPSE:
  166. drawEllipseOpacity();
  167. break;
  168. case CropperShape::RECT:
  169. drawRectOpacity();
  170. drawSquareEdge(!ONLY_FOUR_CORNERS);
  171. break;
  172. case CropperShape::SQUARE:
  173. drawRectOpacity();
  174. drawSquareEdge(ONLY_FOUR_CORNERS);
  175. break;
  176. case CropperShape::ELLIPSE:
  177. drawEllipseOpacity();
  178. drawSquareEdge(!ONLY_FOUR_CORNERS);
  179. break;
  180. case CropperShape::CIRCLE:
  181. drawEllipseOpacity();
  182. drawSquareEdge(ONLY_FOUR_CORNERS);
  183. break;
  184. }
  185. // Draw cropper rect
  186. if (isShowRectBorder) {
  187. QPainter painter(this);
  188. painter.setPen(borderPen);
  189. painter.drawRect(cropperRect);
  190. }
  191. }
  192. void ImageCropperLabel::drawSquareEdge(bool onlyFourCorners) {
  193. if (!isShowDragSquare)
  194. return;
  195. // Four corners
  196. drawFillRect(cropperRect.topLeft(), dragSquareEdge, dragSquareColor);
  197. drawFillRect(cropperRect.topRight(), dragSquareEdge, dragSquareColor);
  198. drawFillRect(cropperRect.bottomLeft(), dragSquareEdge, dragSquareColor);
  199. drawFillRect(cropperRect.bottomRight(), dragSquareEdge, dragSquareColor);
  200. // Four edges
  201. if (!onlyFourCorners) {
  202. int centralX = cropperRect.left() + cropperRect.width() / 2;
  203. int centralY = cropperRect.top() + cropperRect.height() / 2;
  204. drawFillRect(QPoint(cropperRect.left(), centralY), dragSquareEdge, dragSquareColor);
  205. drawFillRect(QPoint(centralX, cropperRect.top()), dragSquareEdge, dragSquareColor);
  206. drawFillRect(QPoint(cropperRect.right(), centralY), dragSquareEdge, dragSquareColor);
  207. drawFillRect(QPoint(centralX, cropperRect.bottom()), dragSquareEdge, dragSquareColor);
  208. }
  209. }
  210. void ImageCropperLabel::drawFillRect(QPoint centralPoint, int edge, QColor color) {
  211. QRect rect(centralPoint.x() - edge / 2, centralPoint.y() - edge / 2, edge, edge);
  212. QPainter painter(this);
  213. painter.fillRect(rect, color);
  214. }
  215. // Opacity effect
  216. void ImageCropperLabel::drawOpacity(const QPainterPath& path) {
  217. QPainter painterOpac(this);
  218. painterOpac.setOpacity(opacity);
  219. painterOpac.fillPath(path, QBrush(Qt::black));
  220. }
  221. void ImageCropperLabel::drawRectOpacity() {
  222. if (isShowOpacityEffect) {
  223. QPainterPath p1, p2, p;
  224. p1.addRect(imageRect);
  225. p2.addRect(cropperRect);
  226. p = p1.subtracted(p2);
  227. drawOpacity(p);
  228. }
  229. }
  230. void ImageCropperLabel::drawEllipseOpacity() {
  231. if (isShowOpacityEffect) {
  232. QPainterPath p1, p2, p;
  233. p1.addRect(imageRect);
  234. p2.addEllipse(cropperRect);
  235. p = p1.subtracted(p2);
  236. drawOpacity(p);
  237. }
  238. }
  239. bool ImageCropperLabel::isPosNearDragSquare(const QPoint& pt1, const QPoint& pt2) {
  240. return abs(pt1.x() - pt2.x()) * 2 <= dragSquareEdge
  241. && abs(pt1.y() - pt2.y()) * 2 <= dragSquareEdge;
  242. }
  243. int ImageCropperLabel::getPosInCropperRect(const QPoint &pt) {
  244. if (isPosNearDragSquare(pt, QPoint(cropperRect.right(), cropperRect.center().y())))
  245. return RECT_RIGHT;
  246. if (isPosNearDragSquare(pt, cropperRect.bottomRight()))
  247. return RECT_BOTTOM_RIGHT;
  248. if (isPosNearDragSquare(pt, QPoint(cropperRect.center().x(), cropperRect.bottom())))
  249. return RECT_BOTTOM;
  250. if (isPosNearDragSquare(pt, cropperRect.bottomLeft()))
  251. return RECT_BOTTOM_LEFT;
  252. if (isPosNearDragSquare(pt, QPoint(cropperRect.left(), cropperRect.center().y())))
  253. return RECT_LEFT;
  254. if (isPosNearDragSquare(pt, cropperRect.topLeft()))
  255. return RECT_TOP_LEFT;
  256. if (isPosNearDragSquare(pt, QPoint(cropperRect.center().x(), cropperRect.top())))
  257. return RECT_TOP;
  258. if (isPosNearDragSquare(pt, cropperRect.topRight()))
  259. return RECT_TOP_RIGHT;
  260. if (cropperRect.contains(pt, true))
  261. return RECT_INSIDE;
  262. return RECT_OUTSIZD;
  263. }
  264. /*************************************************
  265. *
  266. * Change mouse cursor type
  267. * Arrow, SizeHor, SizeVer, etc...
  268. *
  269. *************************************************/
  270. void ImageCropperLabel::changeCursor() {
  271. switch (cursorPosInCropperRect) {
  272. case RECT_OUTSIZD:
  273. setCursor(Qt::ArrowCursor);
  274. break;
  275. case RECT_BOTTOM_RIGHT: {
  276. switch (cropperShape) {
  277. case CropperShape::SQUARE:
  278. case CropperShape::CIRCLE:
  279. case CropperShape::RECT:
  280. case CropperShape::ELLIPSE:
  281. setCursor(Qt::SizeFDiagCursor);
  282. break;
  283. default:
  284. break;
  285. }
  286. break;
  287. }
  288. case RECT_RIGHT: {
  289. switch (cropperShape) {
  290. case CropperShape::RECT:
  291. case CropperShape::ELLIPSE:
  292. setCursor(Qt::SizeHorCursor);
  293. break;
  294. default:
  295. break;
  296. }
  297. break;
  298. }
  299. case RECT_BOTTOM: {
  300. switch (cropperShape) {
  301. case CropperShape::RECT:
  302. case CropperShape::ELLIPSE:
  303. setCursor(Qt::SizeVerCursor);
  304. break;
  305. default:
  306. break;
  307. }
  308. break;
  309. }
  310. case RECT_BOTTOM_LEFT: {
  311. switch (cropperShape) {
  312. case CropperShape::RECT:
  313. case CropperShape::ELLIPSE:
  314. case CropperShape::SQUARE:
  315. case CropperShape::CIRCLE:
  316. setCursor(Qt::SizeBDiagCursor);
  317. break;
  318. default:
  319. break;
  320. }
  321. break;
  322. }
  323. case RECT_LEFT: {
  324. switch (cropperShape) {
  325. case CropperShape::RECT:
  326. case CropperShape::ELLIPSE:
  327. setCursor(Qt::SizeHorCursor);
  328. break;
  329. default:
  330. break;
  331. }
  332. break;
  333. }
  334. case RECT_TOP_LEFT: {
  335. switch (cropperShape) {
  336. case CropperShape::RECT:
  337. case CropperShape::ELLIPSE:
  338. case CropperShape::SQUARE:
  339. case CropperShape::CIRCLE:
  340. setCursor(Qt::SizeFDiagCursor);
  341. break;
  342. default:
  343. break;
  344. }
  345. break;
  346. }
  347. case RECT_TOP: {
  348. switch (cropperShape) {
  349. case CropperShape::RECT:
  350. case CropperShape::ELLIPSE:
  351. setCursor(Qt::SizeVerCursor);
  352. break;
  353. default:
  354. break;
  355. }
  356. break;
  357. }
  358. case RECT_TOP_RIGHT: {
  359. switch (cropperShape) {
  360. case CropperShape::SQUARE:
  361. case CropperShape::CIRCLE:
  362. case CropperShape::RECT:
  363. case CropperShape::ELLIPSE:
  364. setCursor(Qt::SizeBDiagCursor);
  365. break;
  366. default:
  367. break;
  368. }
  369. break;
  370. }
  371. case RECT_INSIDE: {
  372. setCursor(Qt::SizeAllCursor);
  373. break;
  374. }
  375. }
  376. }
  377. /*****************************************************
  378. *
  379. * Mouse Events
  380. *
  381. *****************************************************/
  382. void ImageCropperLabel::mousePressEvent(QMouseEvent *e) {
  383. currPos = lastPos = e->pos();
  384. isLButtonPressed = true;
  385. }
  386. void ImageCropperLabel::mouseMoveEvent(QMouseEvent *e) {
  387. currPos = e->pos();
  388. if (!isCursorPosCalculated) {
  389. cursorPosInCropperRect = getPosInCropperRect(currPos);
  390. changeCursor();
  391. }
  392. if (!isLButtonPressed)
  393. return;
  394. if (!imageRect.contains(currPos))
  395. return;
  396. isCursorPosCalculated = true;
  397. int xOffset = currPos.x() - lastPos.x();
  398. int yOffset = currPos.y() - lastPos.y();
  399. lastPos = currPos;
  400. int disX = 0;
  401. int disY = 0;
  402. // Move cropper
  403. switch (cursorPosInCropperRect) {
  404. case RECT_OUTSIZD:
  405. break;
  406. case RECT_BOTTOM_RIGHT: {
  407. disX = currPos.x() - cropperRect.left();
  408. disY = currPos.y() - cropperRect.top();
  409. switch (cropperShape) {
  410. case CropperShape::UNDEFINED:
  411. case CropperShape::FIXED_RECT:
  412. case CropperShape::FIXED_ELLIPSE:
  413. break;
  414. case CropperShape::SQUARE:
  415. case CropperShape::CIRCLE:
  416. setCursor(Qt::SizeFDiagCursor);
  417. if (disX >= cropperMinimumWidth && disY >= cropperMinimumHeight) {
  418. if (disX > disY && cropperRect.top() + disX <= imageRect.bottom()) {
  419. cropperRect.setRight(currPos.x());
  420. cropperRect.setBottom(cropperRect.top() + disX);
  421. emit croppedImageChanged();
  422. }
  423. else if (disX <= disY && cropperRect.left() + disY <= imageRect.right()) {
  424. cropperRect.setBottom(currPos.y());
  425. cropperRect.setRight(cropperRect.left() + disY);
  426. emit croppedImageChanged();
  427. }
  428. }
  429. break;
  430. case CropperShape::RECT:
  431. case CropperShape::ELLIPSE:
  432. setCursor(Qt::SizeFDiagCursor);
  433. if (disX >= cropperMinimumWidth) {
  434. cropperRect.setRight(currPos.x());
  435. emit croppedImageChanged();
  436. }
  437. if (disY >= cropperMinimumHeight) {
  438. cropperRect.setBottom(currPos.y());
  439. emit croppedImageChanged();
  440. }
  441. break;
  442. }
  443. break;
  444. }
  445. case RECT_RIGHT: {
  446. disX = currPos.x() - cropperRect.left();
  447. switch (cropperShape) {
  448. case CropperShape::UNDEFINED:
  449. case CropperShape::FIXED_RECT:
  450. case CropperShape::FIXED_ELLIPSE:
  451. case CropperShape::SQUARE:
  452. case CropperShape::CIRCLE:
  453. break;
  454. case CropperShape::RECT:
  455. case CropperShape::ELLIPSE:
  456. if (disX >= cropperMinimumWidth) {
  457. cropperRect.setRight(currPos.x());
  458. emit croppedImageChanged();
  459. }
  460. break;
  461. }
  462. break;
  463. }
  464. case RECT_BOTTOM: {
  465. disY = currPos.y() - cropperRect.top();
  466. switch (cropperShape) {
  467. case CropperShape::UNDEFINED:
  468. case CropperShape::FIXED_RECT:
  469. case CropperShape::FIXED_ELLIPSE:
  470. case CropperShape::SQUARE:
  471. case CropperShape::CIRCLE:
  472. break;
  473. case CropperShape::RECT:
  474. case CropperShape::ELLIPSE:
  475. if (disY >= cropperMinimumHeight) {
  476. cropperRect.setBottom(cropperRect.bottom() + yOffset);
  477. emit croppedImageChanged();
  478. }
  479. break;
  480. }
  481. break;
  482. }
  483. case RECT_BOTTOM_LEFT: {
  484. disX = cropperRect.right() - currPos.x();
  485. disY = currPos.y() - cropperRect.top();
  486. switch (cropperShape) {
  487. case CropperShape::UNDEFINED:
  488. break;
  489. case CropperShape::FIXED_RECT:
  490. case CropperShape::FIXED_ELLIPSE:
  491. case CropperShape::RECT:
  492. case CropperShape::ELLIPSE:
  493. if (disX >= cropperMinimumWidth) {
  494. cropperRect.setLeft(currPos.x());
  495. emit croppedImageChanged();
  496. }
  497. if (disY >= cropperMinimumHeight) {
  498. cropperRect.setBottom(currPos.y());
  499. emit croppedImageChanged();
  500. }
  501. break;
  502. case CropperShape::SQUARE:
  503. case CropperShape::CIRCLE:
  504. if (disX >= cropperMinimumWidth && disY >= cropperMinimumHeight) {
  505. if (disX > disY && cropperRect.top() + disX <= imageRect.bottom()) {
  506. cropperRect.setLeft(currPos.x());
  507. cropperRect.setBottom(cropperRect.top() + disX);
  508. emit croppedImageChanged();
  509. }
  510. else if (disX <= disY && cropperRect.right() - disY >= imageRect.left()) {
  511. cropperRect.setBottom(currPos.y());
  512. cropperRect.setLeft(cropperRect.right() - disY);
  513. emit croppedImageChanged();
  514. }
  515. }
  516. break;
  517. }
  518. break;
  519. }
  520. case RECT_LEFT: {
  521. disX = cropperRect.right() - currPos.x();
  522. switch (cropperShape) {
  523. case CropperShape::UNDEFINED:
  524. case CropperShape::FIXED_RECT:
  525. case CropperShape::FIXED_ELLIPSE:
  526. case CropperShape::SQUARE:
  527. case CropperShape::CIRCLE:
  528. break;
  529. case CropperShape::RECT:
  530. case CropperShape::ELLIPSE:
  531. if (disX >= cropperMinimumHeight) {
  532. cropperRect.setLeft(cropperRect.left() + xOffset);
  533. emit croppedImageChanged();
  534. }
  535. break;
  536. }
  537. break;
  538. }
  539. case RECT_TOP_LEFT: {
  540. disX = cropperRect.right() - currPos.x();
  541. disY = cropperRect.bottom() - currPos.y();
  542. switch (cropperShape) {
  543. case CropperShape::UNDEFINED:
  544. case CropperShape::FIXED_RECT:
  545. case CropperShape::FIXED_ELLIPSE:
  546. break;
  547. case CropperShape::RECT:
  548. case CropperShape::ELLIPSE:
  549. if (disX >= cropperMinimumWidth) {
  550. cropperRect.setLeft(currPos.x());
  551. emit croppedImageChanged();
  552. }
  553. if (disY >= cropperMinimumHeight) {
  554. cropperRect.setTop(currPos.y());
  555. emit croppedImageChanged();
  556. }
  557. break;
  558. case CropperShape::SQUARE:
  559. case CropperShape::CIRCLE:
  560. if (disX >= cropperMinimumWidth && disY >= cropperMinimumHeight) {
  561. if (disX > disY && cropperRect.bottom() - disX >= imageRect.top()) {
  562. cropperRect.setLeft(currPos.x());
  563. cropperRect.setTop(cropperRect.bottom() - disX);
  564. emit croppedImageChanged();
  565. }
  566. else if (disX <= disY && cropperRect.right() - disY >= imageRect.left()) {
  567. cropperRect.setTop(currPos.y());
  568. cropperRect.setLeft(cropperRect.right() - disY);
  569. emit croppedImageChanged();
  570. }
  571. }
  572. break;
  573. }
  574. break;
  575. }
  576. case RECT_TOP: {
  577. disY = cropperRect.bottom() - currPos.y();
  578. switch (cropperShape) {
  579. case CropperShape::UNDEFINED:
  580. case CropperShape::FIXED_RECT:
  581. case CropperShape::FIXED_ELLIPSE:
  582. case CropperShape::SQUARE:
  583. case CropperShape::CIRCLE:
  584. break;
  585. case CropperShape::RECT:
  586. case CropperShape::ELLIPSE:
  587. if (disY >= cropperMinimumHeight) {
  588. cropperRect.setTop(cropperRect.top() + yOffset);
  589. emit croppedImageChanged();
  590. }
  591. break;
  592. }
  593. break;
  594. }
  595. case RECT_TOP_RIGHT: {
  596. disX = currPos.x() - cropperRect.left();
  597. disY = cropperRect.bottom() - currPos.y();
  598. switch (cropperShape) {
  599. case CropperShape::UNDEFINED:
  600. case CropperShape::FIXED_RECT:
  601. case CropperShape::FIXED_ELLIPSE:
  602. break;
  603. case CropperShape::RECT:
  604. case CropperShape::ELLIPSE:
  605. if (disX >= cropperMinimumWidth) {
  606. cropperRect.setRight(currPos.x());
  607. emit croppedImageChanged();
  608. }
  609. if (disY >= cropperMinimumHeight) {
  610. cropperRect.setTop(currPos.y());
  611. emit croppedImageChanged();
  612. }
  613. break;
  614. case CropperShape::SQUARE:
  615. case CropperShape::CIRCLE:
  616. if (disX >= cropperMinimumWidth && disY >= cropperMinimumHeight) {
  617. if (disX < disY && cropperRect.left() + disY <= imageRect.right()) {
  618. cropperRect.setTop(currPos.y());
  619. cropperRect.setRight(cropperRect.left() + disY);
  620. emit croppedImageChanged();
  621. }
  622. else if (disX >= disY && cropperRect.bottom() - disX >= imageRect.top()) {
  623. cropperRect.setRight(currPos.x());
  624. cropperRect.setTop(cropperRect.bottom() - disX);
  625. emit croppedImageChanged();
  626. }
  627. }
  628. break;
  629. }
  630. break;
  631. }
  632. case RECT_INSIDE: {
  633. // Make sure the cropperRect is entirely inside the imageRecct
  634. if (xOffset > 0) {
  635. if (cropperRect.right() + xOffset > imageRect.right())
  636. xOffset = 0;
  637. }
  638. else if (xOffset < 0) {
  639. if (cropperRect.left() + xOffset < imageRect.left())
  640. xOffset = 0;
  641. }
  642. if (yOffset > 0) {
  643. if (cropperRect.bottom() + yOffset > imageRect.bottom())
  644. yOffset = 0;
  645. }
  646. else if (yOffset < 0) {
  647. if (cropperRect.top() + yOffset < imageRect.top())
  648. yOffset = 0;
  649. }
  650. cropperRect.moveTo(cropperRect.left() + xOffset, cropperRect.top() + yOffset);
  651. emit croppedImageChanged();
  652. }
  653. break;
  654. }
  655. repaint();
  656. }
  657. void ImageCropperLabel::mouseReleaseEvent(QMouseEvent *) {
  658. isLButtonPressed = false;
  659. isCursorPosCalculated = false;
  660. setCursor(Qt::ArrowCursor);
  661. }

下面逐步讲解代码实现

枚举类型定义

  1. enum class CropperShape { };
  2. enum class OutputShape { };
  3. enum class SizeType { };
  • CropperShape:裁剪框的形状(矩形、正方形、椭圆、圆、以及固定尺寸的变种)。
  • OutputShape:导出时输出的形状,仅矩形或椭圆两种。
  • SizeType:内部用来控制当图片过大/过小时如何缩放至 Label 尺寸。

这些枚举让 API 更语义化、调用更直观。

类声明与成员变量

  1. class ImageCropperLabel : public QLabel {
  2. Q_OBJECT
  3. public:
  4. ImageCropperLabel(int width, int height, QWidget* parent);
  5. // … 设置图片、设置裁剪形状、获取结果等方法 …
  6. signals:
  7. void croppedImageChanged();
  8. protected:
  9. // 重载绘制与鼠标事件函数
  10. private:
  11. // 绘制辅助:drawFillRect、drawOpacity、drawRectOpacity 等
  12. // 工具方法:getPosInCropperRect、resetCropperPos、changeCursor 等
  13. // 状态变量
  14. QPixmap originalImage; // 原始图片
  15. QPixmap tempImage; // 缩放至 Label 尺寸后的临时位图
  16. bool isShowRectBorder = true; // 是否画裁剪框边框
  17. QPen borderPen; // 边框样式
  18. CropperShape cropperShape = CropperShape::UNDEFINED;
  19. OutputShape outputShape = OutputShape::RECT;
  20. QRect imageRect; // 在 Label 中显示图片的区域(可能有留白)
  21. QRect cropperRect; // 裁剪框在 Label 坐标系下的位置与大小
  22. QRect cropperRect_; // “真实”像素尺寸下的参考矩形(仅固定尺寸时有效)
  23. double scaledRate = 1.0;
  24. // 拖拽、缩放交互相关
  25. bool isLButtonPressed = false;
  26. bool isCursorPosCalculated = false;
  27. int cursorPosInCropperRect = 0; // 用上述匿名 enum 表示鼠标在裁剪框哪个位置
  28. QPoint lastPos, currPos;
  29. // 拖拽控制点样式
  30. bool isShowDragSquare = true;
  31. int dragSquareEdge = 8;
  32. QColor dragSquareColor = Qt::white;
  33. int cropperMinimumWidth = dragSquareEdge * 2;
  34. int cropperMinimumHeight = dragSquareEdge * 2;
  35. // 半透明遮罩
  36. bool isShowOpacityEffect = false;
  37. double opacity = 0.6;
  38. };
  • 核心状态:存了原图、临时图、裁剪框位置、缩放比例等。
  • 交互状态:鼠标按下/移动、在哪个拖拽点、是否在拖拽中。
  • 可配置属性:边框、拖拽手柄、最小尺寸、遮罩效果等,通过 public 方法暴露给外部。

构造函数(Label 初始化)

  1. ImageCropperLabel::ImageCropperLabel(int width, int height, QWidget* parent)
  2. : QLabel(parent)
  3. {
  4. setFixedSize(width, height);
  5. setAlignment(Qt::AlignCenter);
  6. setMouseTracking(true); // 即使不按按钮也能收到 mouseMove 事件
  7. borderPen.setWidth(1);
  8. borderPen.setColor(Qt::white);
  9. borderPen.setDashPattern(QVector<qreal>() << 3 << 3); // 虚线
  10. }
  • 固定尺寸:确保裁剪界面大小一致,不随容器拉伸。
  • 居中显示:图片展示时居中。
  • 边框样式:白色虚线。

加载并缩放原图

  1. void ImageCropperLabel::setOriginalImage(const QPixmap &pixmap) {
  2. originalImage = pixmap;
  3. // 计算在 label 里显示时的缩放比例和目标尺寸
  4. if (imgWidth * labelHeight < imgHeight * labelWidth) {
  5. scaledRate = labelHeight / double(imgHeight);
  6. compute imgWidthInLabel, imageRect
  7. } else {
  8. 另一种缩放方式
  9. }
  10. tempImage = originalImage.scaled(imgWidthInLabel, imgHeightInLabel,
  11. Qt::KeepAspectRatio, Qt::SmoothTransformation);
  12. setPixmap(tempImage);
  13. // 如果是固定尺寸裁剪框,需要按同样比例缩放
  14. if (cropperShape >= CropperShape::FIXED_RECT) {
  15. cropperRect.setWidth(int(cropperRect_.width() * scaledRate));
  16. }
  17. resetCropperPos();
  18. }
  • 按保持长宽比的方式,把原图缩放到 Label 区域内(letterbox 模式)。
  • imageRect:记录图像在 Label 坐标系下的实际绘制区域。
  • tempImage:在 Label 上展示的图,用于用户交互。

image-20250511114718983


image-20250511115038528

裁剪形状设置与重置

  1. void ImageCropperLabel::setRectCropper() { cropperShape = RECT; resetCropperPos(); }
  2. // 各种 setXXXCropper()
  3. void ImageCropperLabel::resetCropperPos() {
  4. // 根据 cropperShape,计算初始的 cropperRect:
  5. // - 固定尺寸时居中铺满
  6. // - 可变尺寸时取图片较短边的 3/4,居中
  7. }
  • 统一调用:每次改变 shape 或大小,都调用 resetCropperPos() 让裁剪框回到可见区域中央。

image-20250511115825046


image-20250511120205707

获取裁剪结果

  1. QPixmap ImageCropperLabel::getCroppedImage(OutputShape shape) {
  2. // 1. 根据缩放比例,把 cropperRect 从 Label 坐标系映射到原图坐标系:
  3. int startX = (cropperRect.left() - imageRect.left()) / scaledRate;
  4. compute croppedWidth, croppedHeight
  5. // 2. 从 originalImage 上 copy 出子图
  6. QPixmap resultImage = originalImage.copy(startX, startY, cw, ch);
  7. // 3. 如果输出椭圆,则用 QBitmap+setMask 做裁切
  8. if (shape == OutputShape::ELLIPSE) {
  9. QBitmap mask(size);
  10. QPainter p(&mask);
  11. p.fillRect(…, Qt::white);
  12. p.setBrush(Qt::black);
  13. p.drawRoundRect(0,0,w,h,99,99);
  14. resultImage.setMask(mask);
  15. }
  16. return resultImage;
  17. }

image-20250511120838187

  • 核心思路:先把用户框映射回原图,再按需求做矩形或椭圆裁剪。

为什么要除以 scaledRate

  1. 背景:裁剪区域的坐标 (cropperRect) 和尺寸 (cropperRect.width(), cropperRect.height()) 都是相对于图像在显示中的位置和大小,而不是原始图像的大小。这意味着显示上的裁剪框可能已经被缩放过。因此,scaledRate 是一个缩放比例,用来将裁剪区域从显示坐标系统(可能已经缩放)转换回原始图像的坐标系统。

  2. 代码解释

    • cropperRect.left() - imageRect.left() 表示裁剪框左边缘与原始图像左边缘的偏移量(即裁剪框相对于图像的起始位置)。
    • scaledRate 是图像在显示时的缩放比例(例如,显示的图像比原图小或大,scaledRate 可以是 1、0.5、2 等)。
    • 除以 scaledRate 就是将显示的坐标转换为原始图像的坐标。这样得到的是裁剪框在原始图像中的位置和大小。

    例如:假设 scaledRate = 0.5(显示图像是原图的 50%),则 cropperRect 表示的区域实际在原图中要乘以 2 才能得到正确的大小和位置。

为什么椭圆要单独处理?

裁剪区域的形状是矩形的,而图像本身可能要根据需求切割成不同的形状。如果要求裁剪区域是椭圆形状,那么矩形的裁剪区域必须通过遮罩(mask)来实现。

  1. 遮罩的作用
    • 默认情况下,裁剪区域是矩形的。为了让裁剪后的图像呈现椭圆形状,我们需要用一个遮罩来过滤掉矩形区域之外的部分。
    • 通过绘制一个椭圆(在矩形区域内),并设置遮罩(mask),使得图像在该遮罩的范围内显示,超出范围的部分将变为透明。
  2. 椭圆处理的步骤
    • 通过 QBitmap mask(size) 创建一个与裁剪区域大小相同的二值遮罩(黑白图像)。
    • 然后使用 QPainter 绘制一个椭圆形状。 drawRoundRect 方法画的其实是一个圆角矩形,但由于宽度和高度一样,且角的弯曲度非常高(99, 99),所以它的效果看起来是一个椭圆。
    • 最后,通过 resultImage.setMask(mask) 将这个椭圆形状应用到裁剪后的图像上,从而实现椭圆形的裁剪效果。

painter.setBrush(QColor(0, 0, 0)); 在这里的唯一目的是往那个 QBitmap 遮罩(mask) 上「画」一个黑色的圆角矩形,用来告诉 Qt 哪一块区域要保留、哪一块区域要透明——它并不是在往你的 resultImage 上画黑色。

  • mask
    • 黑色 → 可见
    • 白色 → 透明

如果你不 setBrush(QColor(0, 0, 0)) 去把圆角矩形「涂黑」,那么整张 mask 就只有白色(或只有透明),结果就是 整张图片都被裁成透明了,你看不见任何内容。

所以,setBrush(QColor(0, 0, 0)) 的作用只是:

  1. mask 上,填充一个黑色的圆角矩形;
  2. 当你调用 resultImage.setMask(mask); 时,Qt 会把这部分“黑色”区域映射为 保留原图像素,而把剩下的(白色)区域变成透明。

image-20250511121434047


绘制与遮罩效果

  1. void ImageCropperLabel::paintEvent(QPaintEvent *event) {
  2. // 1. 先调用父类,实现原始图像的绘制
  3. QLabel::paintEvent(event);
  4. // 2. 根据当前裁剪形状,绘制不同的“半透明遮罩”或“高光边”
  5. switch (cropperShape) {
  6. case CropperShape::UNDEFINED:
  7. break;
  8. case CropperShape::FIXED_RECT:
  9. drawRectOpacity();
  10. break;
  11. case CropperShape::FIXED_ELLIPSE:
  12. drawEllipseOpacity();
  13. break;
  14. case CropperShape::RECT:
  15. drawRectOpacity();
  16. drawSquareEdge(!ONLY_FOUR_CORNERS);
  17. break;
  18. case CropperShape::SQUARE:
  19. drawRectOpacity();
  20. drawSquareEdge(ONLY_FOUR_CORNERS);
  21. break;
  22. case CropperShape::ELLIPSE:
  23. drawEllipseOpacity();
  24. drawSquareEdge(!ONLY_FOUR_CORNERS);
  25. break;
  26. case CropperShape::CIRCLE:
  27. drawEllipseOpacity();
  28. drawSquareEdge(ONLY_FOUR_CORNERS);
  29. break;
  30. }
  31. // 3. 如果需要,给裁剪框本身画一条边框
  32. if (isShowRectBorder) {
  33. QPainter painter(this);
  34. painter.setPen(borderPen);
  35. painter.drawRect(cropperRect);
  36. }
  37. }
  • 绘制原图
    QLabel::paintEvent(event) 会根据当前设置的 pixmap 或者绘图内容,把“完整的”图像画到控件上。我们不做任何改动,保留原始像素。

    叠加遮罩或高光边
    根据 cropperShape(枚举当前选中的裁剪形状),有两类主要操作:

    • drawRectOpacity() / drawEllipseOpacity():在裁剪框以外的区域绘制半透明黑色遮罩,突出裁剪区域本身。
    • drawSquareEdge(...):在裁剪框的四条边或者四个角上绘制高对比度的“小方块”或“手柄”,以便用户拖动调整大小。

    绘制裁剪框边线
    如果 isShowRectBorder==true,再用 borderPen(一般是明亮的颜色或宽度可见的线条)精确地把 cropperRect 描边一次,让裁剪范围更清晰。

半透明遮罩

  1. void ImageCropperLabel::drawOpacity(const QPainterPath& path) {
  2. QPainter painterOpac(this);
  3. painterOpac.setOpacity(opacity); // 设定当前 painter 的透明度
  4. painterOpac.fillPath(path, QBrush(Qt::black)); // 用黑色填充整个 path 区域
  5. }
  • opacity:这是一个 [0.0 … 1.0] 之间的浮点值,控制遮罩的“浓度”。越接近 1.0,黑得越不透明;越接近 0.0,则越接近“无色”。
  • fillPath(path, QBrush(Qt::black)):把传入的 QPainterPath 区域,用半透明的黑色一次性“盖”上去。

drawRectOpacity()

  1. void ImageCropperLabel::drawRectOpacity() {
  2. if (!isShowOpacityEffect) return;
  3. // 1. p1:整个图像区域
  4. QPainterPath p1;
  5. p1.addRect(imageRect);
  6. // 2. p2:裁剪框区域
  7. QPainterPath p2;
  8. p2.addRect(cropperRect);
  9. // 3. 求差集:p = p1 - p2
  10. QPainterPath p = p1.subtracted(p2);
  11. // 4. 对 p 区域绘制半透明黑色遮罩
  12. drawOpacity(p);
  13. }
  • imageRect:通常是整个图片在控件上的显示区域。
  • cropperRect:用户定义的“裁剪框”矩形。
  • p1.subtracted(p2):把裁剪框内部切掉,结果 p 就是“图片区域减去裁剪框”的外部部分。
  • 遮罩效果:只有外部部分被半透明黑色盖住,裁剪框内——也就是用户关心的区域——保持原样未被遮盖。

椭圆遮罩 —— drawEllipseOpacity()(原理同上)

虽然你没贴出函数体,但它与 drawRectOpacity() 唯一区别就是把 p2.addRect(cropperRect) 换成:

  1. QPainterPath p2;
  2. p2.addEllipse(cropperRect);

这样 p1.subtracted(p2) 就是“整张图片减去椭圆区域”,半透明遮罩会围着椭圆“环绕”绘制。


image-20250511122719029

“方块手柄”高光 —— drawSquareEdge(bool onlyCorners)

  1. void ImageCropperLabel::drawSquareEdge(bool onlyFourCorners) {
  2. if (!isShowDragSquare)
  3. return;
  4. // Four corners
  5. drawFillRect(cropperRect.topLeft(), dragSquareEdge, dragSquareColor);
  6. drawFillRect(cropperRect.topRight(), dragSquareEdge, dragSquareColor);
  7. drawFillRect(cropperRect.bottomLeft(), dragSquareEdge, dragSquareColor);
  8. drawFillRect(cropperRect.bottomRight(), dragSquareEdge, dragSquareColor);
  9. // Four edges
  10. if (!onlyFourCorners) {
  11. int centralX = cropperRect.left() + cropperRect.width() / 2;
  12. int centralY = cropperRect.top() + cropperRect.height() / 2;
  13. drawFillRect(QPoint(cropperRect.left(), centralY), dragSquareEdge, dragSquareColor);
  14. drawFillRect(QPoint(centralX, cropperRect.top()), dragSquareEdge, dragSquareColor);
  15. drawFillRect(QPoint(cropperRect.right(), centralY), dragSquareEdge, dragSquareColor);
  16. drawFillRect(QPoint(centralX, cropperRect.bottom()), dragSquareEdge, dragSquareColor);
  17. }
  18. }

image-20250511123344886

此函数通常会:

  1. cropperRect 的四条边(或四个角)各计算几个固定大小的小矩形位置。
  2. 用不透明画刷(如白色或蓝色)绘制这些 “拖拽手柄”,让用户知道可以从这些点出发拖动调整大小。

onlyCorners 参数决定是只在四个角显示手柄,还是在四条边中央也显示。

手柄检测

isPosNearDragSquare(pt1, pt2):手柄附近检测

  1. bool ImageCropperLabel::isPosNearDragSquare(const QPoint& pt1, const QPoint& pt2) {
  2. return abs(pt1.x() - pt2.x()) * 2 <= dragSquareEdge
  3. && abs(pt1.y() - pt2.y()) * 2 <= dragSquareEdge;
  4. }
  • pt1:当前鼠标点(或触点)坐标。
  • pt2:某个拖拽手柄中心点坐标。
  • dragSquareEdge:定义手柄大小(宽或高)的常量。

逻辑:如果鼠标点到手柄中心的水平距离和垂直距离都不超过 dragSquareEdge/2,就认为“在手柄区域内”。乘以 2 只是把“不超过半边”转成”两倍距离不超过边长“的判断。


getPosInCropperRect(pt):整体位置分类

  1. int ImageCropperLabel::getPosInCropperRect(const QPoint &pt) {
  2. if (isPosNearDragSquare(pt, QPoint(cropperRect.right(), cropperRect.center().y())))
  3. return RECT_RIGHT;
  4. if (isPosNearDragSquare(pt, cropperRect.bottomRight()))
  5. return RECT_BOTTOM_RIGHT;
  6. if (isPosNearDragSquare(pt, QPoint(cropperRect.center().x(), cropperRect.bottom())))
  7. return RECT_BOTTOM;
  8. if (isPosNearDragSquare(pt, cropperRect.bottomLeft()))
  9. return RECT_BOTTOM_LEFT;
  10. if (isPosNearDragSquare(pt, QPoint(cropperRect.left(), cropperRect.center().y())))
  11. return RECT_LEFT;
  12. if (isPosNearDragSquare(pt, cropperRect.topLeft()))
  13. return RECT_TOP_LEFT;
  14. if (isPosNearDragSquare(pt, QPoint(cropperRect.center().x(), cropperRect.top())))
  15. return RECT_TOP;
  16. if (isPosNearDragSquare(pt, cropperRect.topRight()))
  17. return RECT_TOP_RIGHT;
  18. if (cropperRect.contains(pt, true))
  19. return RECT_INSIDE;
  20. return RECT_OUTSIZD;
  21. }

按照顺序,它分别检测:

  1. 右边中点 RECT_RIGHT
    (cropperRect.right(), cropperRect.center().y()) 为中心,看鼠标是否落在右侧手柄区域。
  2. 右下角 RECT_BOTTOM_RIGHT
    cropperRect.bottomRight() 为中心,看鼠标是否落在这个角的手柄。
  3. 下边中点 RECT_BOTTOM
    中点为 (center.x(), bottom)
  4. 左下角 RECT_BOTTOM_LEFT
  5. 左边中点 RECT_LEFT
  6. 左上角 RECT_TOP_LEFT
  7. 上边中点 RECT_TOP
  8. 右上角 RECT_TOP_RIGHT

如果以上八个拖拽手柄区域都没有命中,接着:

  • RECT_INSIDE:如果点严格落在 cropperRect 内部(第二个参数 true 表示内边缘也算),就返回“内部”标志。
  • RECT_OUTSIZD:都不符合,则认为在裁剪框外。

综合效果

  • 鼠标按下移动 时,调用 getPosInCropperRect(pt),能够快速定位出当前点相对于裁剪框的位置类型。
  • 上层逻辑(如鼠标事件处理)根据这个返回值,决定要进行哪种操作:
    • 如果是某个角或边的手柄,就进入“调整大小”模式,且拖拽方向锁定;
    • 如果是 RECT_INSIDE,则进入“移动整个裁剪框”模式;
    • 如果是 RECT_OUTSIZD,则不做任何裁剪框相关的拖拽操作。

这样,就实现了一个用户友好的「拖拽四角/边来调整裁剪框大小,或者拖拽内部来移动框」的交互体验。


鼠标按下移动释放

mousePressEvent

  1. void ImageCropperLabel::mousePressEvent(QMouseEvent *e) {
  2. currPos = lastPos = e->pos();
  3. isLButtonPressed = true;
  4. }

功能:当鼠标左键按下时调用。

做了什么

  1. e->pos()(相对于控件左上角的坐标)初始化 currPoslastPos,为后续移动计算做准备。
  2. isLButtonPressed 置为 true,开启拖动或缩放模式。

mouseMoveEvent

这是核心函数,处理移动和缩放。

  1. void ImageCropperLabel::mouseMoveEvent(QMouseEvent *e) {
  2. currPos = e->pos();
  3. // 首次进入时,确定鼠标在哪个区域:边角、边缘、框内或框外
  4. if (!isCursorPosCalculated) {
  5. cursorPosInCropperRect = getPosInCropperRect(currPos);
  6. changeCursor(); // 根据区域切换不同形状的鼠标指针
  7. }
  8. // 如果左键没有按下或鼠标移出了图片范围,就不做任何处理
  9. if (!isLButtonPressed || !imageRect.contains(currPos))
  10. return;
  11. isCursorPosCalculated = true; // 保证只计算一次区域
  12. // 计算本次移动增量
  13. int xOffset = currPos.x() - lastPos.x();
  14. int yOffset = currPos.y() - lastPos.y();
  15. lastPos = currPos;
  16. int disX = 0, disY = 0; // 用于后续缩放计算
  17. // 根据鼠标所在区域,选择对应的移动/缩放逻辑
  18. switch (cursorPosInCropperRect) {
  19. case RECT_OUTSIZD:
  20. break; // 在框外:不处理
  21. // —— 右下角 缩放 ——
  22. case RECT_BOTTOM_RIGHT: {
  23. disX = currPos.x() - cropperRect.left();
  24. disY = currPos.y() - cropperRect.top();
  25. switch (cropperShape) {
  26. // 固定模式:不允许缩放
  27. case CropperShape::UNDEFINED:
  28. case CropperShape::FIXED_RECT:
  29. case CropperShape::FIXED_ELLIPSE:
  30. break;
  31. // 正方形/圆形:强制保持宽高一致
  32. case CropperShape::SQUARE:
  33. case CropperShape::CIRCLE:
  34. setCursor(Qt::SizeFDiagCursor);
  35. // 保证没有小于最小尺寸且不超出图片下/right 边
  36. if (disX >= cropperMinimumWidth && disY >= cropperMinimumHeight) {
  37. if (disX > disY && cropperRect.top() + disX <= imageRect.bottom()) {
  38. // 宽度主导,伸长底边
  39. cropperRect.setRight(currPos.x());
  40. cropperRect.setBottom(cropperRect.top() + disX);
  41. }
  42. else if (disY >= disX && cropperRect.left() + disY <= imageRect.right()) {
  43. // 高度主导,伸长右边
  44. cropperRect.setBottom(currPos.y());
  45. cropperRect.setRight(cropperRect.left() + disY);
  46. }
  47. emit croppedImageChanged();
  48. }
  49. break;
  50. // 普通矩形/椭圆:独立伸缩宽或高
  51. case CropperShape::RECT:
  52. case CropperShape::ELLIPSE:
  53. setCursor(Qt::SizeFDiagCursor);
  54. if (disX >= cropperMinimumWidth) {
  55. cropperRect.setRight(currPos.x());
  56. emit croppedImageChanged();
  57. }
  58. if (disY >= cropperMinimumHeight) {
  59. cropperRect.setBottom(currPos.y());
  60. emit croppedImageChanged();
  61. }
  62. break;
  63. }
  64. break;
  65. }
  66. // —— 右侧边 缩放 ——
  67. case RECT_RIGHT: {
  68. disX = currPos.x() - cropperRect.left();
  69. if (cropperShape==CropperShape::RECT||cropperShape==CropperShape::ELLIPSE) {
  70. if (disX >= cropperMinimumWidth) {
  71. cropperRect.setRight(currPos.x());
  72. emit croppedImageChanged();
  73. }
  74. }
  75. break;
  76. }
  77. // —— 底部边 缩放 ——
  78. case RECT_BOTTOM: {
  79. disY = currPos.y() - cropperRect.top();
  80. if (cropperShape==CropperShape::RECT||cropperShape==CropperShape::ELLIPSE) {
  81. if (disY >= cropperMinimumHeight) {
  82. cropperRect.setBottom(cropperRect.bottom() + yOffset);
  83. emit croppedImageChanged();
  84. }
  85. }
  86. break;
  87. }
  88. // —— 左下角、左侧、上边…… 各角/边 缩放逻辑同上 ——
  89. // (代码中分别处理了 RECT_BOTTOM_LEFT、RECT_LEFT、RECT_TOP_LEFT、
  90. // RECT_TOP、RECT_TOP_RIGHT,核心思想与右下相似:计算 disX/disY,
  91. // 判断形状、最小尺寸、边界,再更新对应边或角的坐标并 emit。)
  92. // —— 框内拖动 ——
  93. case RECT_INSIDE: {
  94. // 先检测移动后是否会超出图片范围,将偏移量 xOffset/yOffset 裁剪到合法区间
  95. if (cropperRect.left() + xOffset < imageRect.left()) xOffset = imageRect.left() - cropperRect.left();
  96. if (cropperRect.right()+ xOffset > imageRect.right()) xOffset = imageRect.right() - cropperRect.right();
  97. if (cropperRect.top() + yOffset < imageRect.top()) yOffset = imageRect.top() - cropperRect.top();
  98. if (cropperRect.bottom()+ yOffset > imageRect.bottom()) yOffset = imageRect.bottom() - cropperRect.bottom();
  99. // 移动整个裁剪框
  100. cropperRect.translate(xOffset, yOffset);
  101. emit croppedImageChanged();
  102. break;
  103. }
  104. }
  105. repaint(); // 触发重绘,及时在界面上更新新的裁剪框
  106. }

关键点总结

  1. 首次定位
    当鼠标首次进入 mouseMoveEvent,用 getPosInCropperRect(currPos) 判断鼠标在裁剪框的哪个“热区”——外部、框内、四边、四角中的哪一个,并调用 changeCursor() 切换对应的鼠标指针样式(如移动箭头、水平/垂直/对角调整形状等),以提示用户下一步操作。

  2. 左右、上下、四角缩放

    • 对于矩形/椭圆,宽高可独立调整;
    • 对于正方形/圆,则保证 width == height,并根据位移量较大的一边来驱动另一边;
    • 对于“固定”模式,则完全不允许用户改变大小。
  3. 边界与最小尺寸约束

    • 缩放时先判断新的宽度/高度是否 ≥ cropperMinimumWidth/Height
    • 再判断新坐标是否会跑出 imageRect(图片区域)之外;
    • 最后才更新 cropperRect 并发信号 croppedImageChanged() 以便上层 UI 或逻辑更新裁剪后的图像。
  4. 拖动整个裁剪框

    • 鼠标在框内部拖动(RECT_INSIDE),计算每次的偏移 xOffset,yOffset
    • 并先“裁剪”偏移量,使整个框保持在图片范围内,
    • 最后调用 translate() 平移 cropperRect

mouseReleaseEvent(QMouseEvent *)

  1. void ImageCropperLabel::mouseReleaseEvent(QMouseEvent *) {
  2. isLButtonPressed = false;
  3. isCursorPosCalculated = false;
  4. setCursor(Qt::ArrowCursor);
  5. }
  • 功能:当鼠标左键松开时调用。
  • 做了什么
    1. isLButtonPressed 置为 false,停止后续的拖动/缩放处理。
    2. 重置 isCursorPosCalculated = false,下次再移动时会重新计算在哪个区域。
    3. 恢复默认箭头指针。

保存逻辑

  1. //上传头像
  2. void UserInfoPage::on_up_btn_clicked()
  3. {
  4. // 1. 让对话框也能选 *.webp
  5. QString filename = QFileDialog::getOpenFileName(
  6. this,
  7. tr("选择图片"),
  8. QString(),
  9. tr("图片文件 (*.png *.jpg *.jpeg *.bmp *.webp)")
  10. );
  11. if (filename.isEmpty())
  12. return;
  13. // 2. 直接用 QPixmap::load() 加载,无需手动区分格式
  14. QPixmap inputImage;
  15. if (!inputImage.load(filename)) {
  16. QMessageBox::critical(
  17. this,
  18. tr("错误"),
  19. tr("加载图片失败!请确认已部署 WebP 插件。"),
  20. QMessageBox::Ok
  21. );
  22. return;
  23. }
  24. QPixmap image = ImageCropperDialog::getCroppedImage(filename, 600, 400, CropperShape::CIRCLE);
  25. if (image.isNull())
  26. return;
  27. QPixmap scaledPixmap = image.scaled( ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小
  28. ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上
  29. ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小
  30. QString storageDir = QStandardPaths::writableLocation(
  31. QStandardPaths::AppDataLocation);
  32. // 2. 在其下再建一个 avatars 子目录
  33. QDir dir(storageDir);
  34. if (!dir.exists("avatars")) {
  35. if (!dir.mkpath("avatars")) {
  36. qWarning() << "无法创建 avatars 目录:" << dir.filePath("avatars");
  37. QMessageBox::warning(
  38. this,
  39. tr("错误"),
  40. tr("无法创建存储目录,请检查权限或磁盘空间。")
  41. );
  42. return;
  43. }
  44. }
  45. // 3. 拼接最终的文件名 head.png
  46. QString filePath = dir.filePath("avatars/head.png");
  47. // 4. 保存 scaledPixmap 为 PNG(无损、最高质量)
  48. if (!scaledPixmap.save(filePath, "PNG")) {
  49. QMessageBox::warning(
  50. this,
  51. tr("保存失败"),
  52. tr("头像保存失败,请检查权限或磁盘空间。")
  53. );
  54. } else {
  55. qDebug() << "头像已保存到:" << filePath;
  56. // 以后读取直接用同一路径:storageDir/avatars/head.png
  57. }
  58. }
  1. 选择图片文件(支持多种格式)
  1. QString filename = QFileDialog::getOpenFileName(
  2. this,
  3. tr("选择图片"),
  4. QString(),
  5. tr("图片文件 (*.png *.jpg *.jpeg *.bmp *.webp)")
  6. );
  7. if (filename.isEmpty())
  8. return;
  • 功能:当用户点击上传头像按钮时,弹出文件选择对话框(QFileDialog),允许用户选择图片文件。此对话框支持的文件格式包括 .png.jpg.jpeg.bmp.webp。如果用户没有选择文件(即点击了取消),则返回并不执行后续操作。
  1. 加载图片文件
  1. QPixmap inputImage;
  2. if (!inputImage.load(filename)) {
  3. QMessageBox::critical(
  4. this,
  5. tr("错误"),
  6. tr("加载图片失败!请确认已部署 WebP 插件。"),
  7. QMessageBox::Ok
  8. );
  9. return;
  10. }
  • 功能:通过 QPixmap 类加载用户选定的图片文件。如果加载失败(如文件损坏、格式不支持等),则弹出错误对话框提示用户,并退出当前函数。
  1. 裁剪图片
  1. QPixmap image = ImageCropperDialog::getCroppedImage(filename, 600, 400, CropperShape::CIRCLE);
  2. if (image.isNull())
  3. return;
  • 功能:调用 ImageCropperDialog::getCroppedImage 函数裁剪图片。这个函数会根据传入的文件路径(filename)、目标大小(600x400)和裁剪形状(此处是圆形 CropperShape::CIRCLE)返回一个裁剪后的图片 QPixmap。如果裁剪过程失败(即返回空 QPixmap),则函数直接退出。
  1. 缩放图片到指定的 QLabel 大小
  1. QPixmap scaledPixmap = image.scaled( ui->head_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
  2. ui->head_lb->setPixmap(scaledPixmap);
  3. ui->head_lb->setScaledContents(true);
  • 功能:将裁剪后的图片缩放到与界面上显示头像的 QLabelhead_lb)大小相匹配。使用 scaled() 方法,保持图片的宽高比 (Qt::KeepAspectRatio),并且应用平滑的图像转换(Qt::SmoothTransformation),保证缩放后的图片质量尽可能高。最后,将缩放后的图片设置到 QLabel 上,并开启 setScaledContents(true),使得 QLabel 自动调整内容大小以适应其尺寸。
  1. 获取应用程序的存储目录
  1. QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
  • 功能:通过 QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) 获取应用程序的可写数据存储目录。这个目录是操作系统为应用程序提供的一个常规存储路径,通常用于存储配置文件、数据文件等。
  1. 创建头像存储目录
  1. QDir dir(storageDir);
  2. if (!dir.exists("avatars")) {
  3. if (!dir.mkpath("avatars")) {
  4. qWarning() << "无法创建 avatars 目录:" << dir.filePath("avatars");
  5. QMessageBox::warning(
  6. this,
  7. tr("错误"),
  8. tr("无法创建存储目录,请检查权限或磁盘空间。")
  9. );
  10. return;
  11. }
  12. }
  • 功能:检查存储目录下是否已经存在一个名为 avatars 的子目录。如果不存在,则通过 mkpath() 创建该子目录。若创建失败,弹出警告对话框提示用户检查权限或磁盘空间。
  1. 拼接最终的保存路径
  1. QString filePath = dir.filePath("avatars/head.png");
  • 功能:拼接最终的文件路径,存储头像的文件名为 head.png,并位于 avatars 目录下。filePath 即为头像图片的完整存储路径。
  1. 保存裁剪后的图片
  1. if (!scaledPixmap.save(filePath, "PNG")) {
  2. QMessageBox::warning(
  3. this,
  4. tr("保存失败"),
  5. tr("头像保存失败,请检查权限或磁盘空间。")
  6. );
  7. } else {
  8. qDebug() << "头像已保存到:" << filePath;
  9. }
  • 功能:使用 QPixmap::save() 方法将裁剪并缩放后的图片保存到指定路径 filePath。保存格式为 PNG。如果保存失败,则弹出警告对话框提示用户;否则,输出日志,显示头像已成功保存的路径。

源码连接

https://gitee.com/secondtonone1/llfcchat

热门评论

热门文章

  1. 使用hexo搭建个人博客

    喜欢(533) 浏览(11108)
  2. Linux环境搭建和编码

    喜欢(594) 浏览(12766)
  3. Qt环境搭建

    喜欢(517) 浏览(23097)
  4. vscode搭建windows C++开发环境

    喜欢(596) 浏览(78029)
  5. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(5635)

最新评论

  1. 类和对象 陈宇航:支持!!!!
  2. 构造函数 secondtonone1:构造函数是类的基础知识,要着重掌握
  3. 面试题汇总(一) secondtonone1:看到网络上经常提问的go的问题,做了一下汇总,结合自己的经验给出的答案,如有纰漏,望指正批评。
  4. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。
  5. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  6. 网络编程学习方法和图书推荐 Corleone:啥程度可以找工作
  7. Qt 对话框 Spade2077:QDialog w(); //这里是不是不需要带括号
  8. visual studio配置boost库 一giao里我离giaogiao:请问是修改成这样吗:.\b2.exe toolset=MinGW
  9. 聊天项目(7) visualstudio配置grpc diablorrr:cmake文件得改一下 find_package(Boost REQUIRED COMPONENTS system filesystem),要加上filesystem。在target_link_libraries中也同样加上
  10. 利用栅栏实现同步 Dzher:作者你好!我觉得 std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); 这个例子中的assert fail并不会发生,原子变量设定了非relaxed内存序后一个线程的原子变量被写入,那么之后的读取一定会被同步的,c和d线程中只可能同时发生一个z++未执行的情况,最终z不是1就是2了,我测试了很多次都没有assert,请问我这个观点有什么错误,谢谢!
  11. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  12. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  13. 创建项目和编译 secondtonone1:谢谢支持
  14. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  15. 聊天项目(15) 客户端实现TCP管理者 lkx:已经在&QTcpSocket::readyRead 回调函数中做了处理了的。
  16. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?
  17. string类 WangQi888888:确实错了,应该是!isspace(sind[index]). 否则不进入循环,还是原来的字符串“some string”
  18. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  19. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google
  20. protobuf配置和使用 熊二:你可以把dll放到系统目录,也可以配置环境变量,还能把dll丢到lib里
  21. boost::asio之socket的创建和连接 项空月:发现一些错别字 :每隔vector存储  是不是是每个. asio::mutable_buffers_1 o或者    是不是多打了个o
  22. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  23. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊

个人公众号

个人微信