Qt-图片裁切

在上传头像过程中,经常会需要将图片裁切成指定的大小,给定一个指定大小的裁切框,调整裁切框位置裁切出理想的图片,今天在这里实现一个简单的图片裁切的应用

已实现功能简介

为了方便演示,做了一个简单展示界面

临时展示界面

目前我实现了以下一些功能:

  • 加载图片,并在图片上添加裁切框,右下角显示了裁切框大小
  • 裁切框大小和位置均可调整,且在图片内部,不超出图片
  • 裁切框中央颜色不变,裁切框外部颜色变深
  • 裁切框内部的样式线条数可以配置
  • 裁切框可以固定尺寸,或者设置放缩规则,等比(1:1)缩放或者自由缩放
  • 按住键盘 ctrl 缩放为固定比例缩放, 按住键盘 alt 缩放为 1:1 长宽缩放

整体代码思路

现在先简单解释一下实现的 demo 逻辑

  • 使用 QLabel 显示图片,我这里创建一个 ImageShowLabel 类来显示图片
  • QLabel 上添加一个 QWidget 作为裁切框, 在创建一个继承 QWidgetCropBox 类来表示裁切框
  • 剩下的主要的就是裁切框 CropBox 大小位置,放缩等逻辑

ImageShowLabel 的实现

先简单看一下头文件 imageshowlabel.h 定义的一些函数

#include <QLabel>
#include "cropbox.h"

class QPixmap;
class ImageShowLabel : public QLabel
{
public:
    ImageShowLabel(QWidget *parent = 0);

    void setImage(const QPixmap &image);
    QPixmap getCroppedImage();

    void setCropBoxLine(const int & widthcount,const int& heightcount);
    void setCropBoxShape(CropBox::CropBoxShape shape  = CropBox::Rect);
    void setCropBoxZoomMode(CropBox::ZoomMode mode  = CropBox::Free);  
    void setEnableKeyPressEvent(bool enabled);
    void setfixCropBox(const int & width, const int& height, bool fixed = true);

protected:
    void paintEvent(QPaintEvent *event);

private:
    CropBox * m_pCropBox;
    QPixmap m_orginalImg;
};

ImageShowLabel 对象主要就是显示图片和返回裁切后的图片

  • void setImage(const QPixmap &image) 设置图片
  • QPixmap getCroppedImage(); 获取裁切框里的图片

作为裁切框 m_pCropBox 父对象的它,此外也需要提供设置 m_pCropBox 对象的接口

函数 描述
void setCropBoxLine(const int & widthcount,const int& heightcount); 设置 m_pCropBox 内部线条数
void setCropBoxShape(CropBox::CropBoxShape shape = CropBox::Rect); 设置 m_pCropBox 的形状,是方形还是圆形
void setCropBoxZoomMode(CropBox::ZoomMode mode = CropBox::Free); 设置 m_pCropBox 放缩的模式
void setEnableKeyPressEvent(bool enabled); 设置 m_pCropBox 是否监听键盘事件
void setfixCropBox(const int & width, const int& height, bool fixed = true); 设置 m_pCropBox 是否固定大小

这里只需要注意一点 void paintEvent(QPaintEvent *event); 函数的实现,这个函数需要实现裁切框内部高亮,外部变暗的功能

//enum CropBoxShape {
//    Rect,
//    Round
//}; 

void ImageShowLabel::paintEvent(QPaintEvent *event)
{
    // 调用 QLabel 的 paintEvent 函数是为了绘制图片
    QLabel::paintEvent(event);

    QPainterPath border, cropbox;
    // 获取 ImageShowLabel 整体区域
    border.setFillRule(Qt::WindingFill);
    border.addRect(0, 0, this->width(), this->height());

    // 获取裁切框 m_pCropBox 形状,根据形状,确定阴影的样式
    cropbox.setFillRule(Qt::WindingFill);
    if (m_pCropBox->getCropBoxShape() == CropBox::Rect)
        cropbox.addRect(m_pCropBox->pos().x()+2,m_pCropBox->pos().y()+2, m_pCropBox->width()-4, m_pCropBox->height()-4);
    else
        cropbox.addEllipse(m_pCropBox->pos().x()+2,m_pCropBox->pos().y()+2, m_pCropBox->width()-4, m_pCropBox->height()-4);

    // 2者相减,得到裁切框外部的区域
    QPainterPath end_path = border.subtracted(cropbox);

    // 使用画笔,对这个区域简单加一层有一定透明度的遮罩
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing, true);
    painter.fillPath(end_path, QColor(0, 0, 0, 100));
}

ImageShowLabel 这个类最主要就是这个 paintEvent 函数,我们可以得到如下的一个样式图,所以接下来主要就是绘制 CropBox 的样式

CropBox 的实现

CropBox 实际上裁切框实现的类,这个类需要实现

  1. 裁切框的样式
    • 形状
      • 圆形
      • 方形
    • 背景,内部的线条,边框的线条,边框的一些标志点,以及裁切框大小的显示
  2. 裁切框在图片内移动
  3. 裁切框在图片内放缩
    • 8方向均可放缩,鼠标样式的修改
    • 放缩的模式
      • 自由放缩
      • 固定比例放缩
      • 1:1 放缩

裁切框的样式

先看一下显示的样式图,我们结合样式图,来逐步解释代码的逻辑

void CropBox::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    QPainter painter(this);

    // 是否绘制内部的线条
    if (m_bDrawInternalLines)
        drawInternalLines(painter); 

    // 绘制边框
    drawBorder(painter);

    // 绘制一些边框上的点
    drawPoints(painter);

    // 显示裁切框大小
    drawSizeText(painter);
}

我又用网格图,绘制了一个大概的样式

样式demo

绘制边框的 drawBorder

#define LINEWIDTH 1
#define SPACING   2 

enum CropBoxShape {
    Rect,
    Round
};

void CropBox::drawBorder(QPainter &painter)
{
    // 绘制外边框线,外边框实线
    painter.setPen( QPen{QColor{3,125,203},SPACING});
    painter.drawRect( SPACING, SPACING, this->width()-SPACING*2,  this->height()-SPACING*2 );

    // 当形状是方形时,内边框线和外边框线是一样的,可以不用画
    // 当形状是圆形时,外边框不变,需要增加圆形的内边框线,内边框虚线
    if (m_shape == Round) {
        painter.setPen( QPen{QColor{255,255,255},LINEWIDTH,Qt::DashLine});
        painter.drawEllipse(SPACING, SPACING, this->width()-SPACING*2,  this->height()-SPACING*2 );
    }
}

绘制边框上点的 drawPoints

// 绘制外边框线上的几个标准点,我这边只画了8个,点缀一下
#define POINTSIZE 5
void CropBox::drawPoints(QPainter &painter)
{
    painter.setPen( QPen{QColor{3,125,203},POINTSIZE});
    painter.drawPoint(SPACING,SPACING);
    painter.drawPoint(this->width()/2, SPACING);
    painter.drawPoint(this->width()-SPACING, SPACING);
    painter.drawPoint(SPACING, this->height()/2);
    painter.drawPoint(SPACING, this->height()-SPACING);
    painter.drawPoint(this->width()-SPACING, this->height()/2);
    painter.drawPoint(this->width()-SPACING, this->height()-SPACING);
    painter.drawPoint(this->width()/2, this->height()-SPACING);
}

绘制内部线条的 drawInternalLines

结合最开始的样例,内部的线条是虚线

void CropBox::drawInternalLines(QPainter &painter)
{
    // 需要先计算出,内部线条的绘画区域,方形和圆形是有区分
    QPainterPath cropbox_path;
    if (m_shape == Round)
        cropbox_path.addEllipse(SPACING, SPACING, this->width()-SPACING*2,  this->height()-SPACING*2);
    else
        cropbox_path.addRect(SPACING, SPACING, this->width()-SPACING*2,  this->height()-SPACING*2);

    // 设置被限制的绘画区域
    painter.setClipPath(cropbox_path);
    painter.setClipping(true);

    // 绘画内部虚线线条
    painter.setPen( QPen{QColor{230,230,230},LINEWIDTH,Qt::DashLine});
    for (int i=1; i<m_widthCount; i++) {
        int width = this->width() / m_widthCount;
        painter.drawLine( i*width,  SPACING , i*width , this->height()-SPACING);
    }

    for (int i=1; i<m_heightCount; i++) {
        int heigth = this->height()/ m_heightCount;
        painter.drawLine( SPACING ,i*heigth,   this->width()- SPACING, i*heigth);
    }

    // 绘画完,取消被限制的区域
    painter.setClipping(false);
}

绘制裁切框大小 drawSizeText

void CropBox::drawSizeText(QPainter &painter)
{
    painter.setPen( QPen{QColor{255,0,0}});
    // 设置显示的内容,绘制Text 的区域, 字体呈现的对齐方式
    QString showText = QString("(") + QString::number(this->width()) + "," + QString::number(this->height()) + ")";
    QPointF topleft{(qreal)this->width()-(qreal)m_minWidth, (qreal)this->height()-(qreal)20};
    QSizeF size{(qreal)m_minWidth,20};
    QRectF position {topleft, size};
    QTextOption option{Qt::AlignVCenter | Qt::AlignRight };
    painter.drawText(position, showText, option);
}

裁切框的移动

移动这个功能的操作是:裁切框接收到鼠标左击事件,鼠标不松开的前提下移动鼠标,裁切框随鼠标移动,鼠标松开时,移动停止
为了确保鼠标移动事件的捕获,CropBox 初始化中需要添加 this->setMouseTracking(true);
所以窗口移动主要就涉及到以下3个函数

void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);

但是移动过程中有个细节需要注意,裁切框不能移动到图像外侧,需要对移动的位置进行判断,所以将判断移动到另外一个函数中

void handleMove(QPoint mouse_globalpos);

mousePressEvent

鼠标点击的时候,需要记录一下点击的状态 m_bMovingFlag=true ,移动过程中,需要判断是否点击了,鼠标松开时, 设置 m_bMovingFlag=false
于此同时,需要记录2个点坐标,裁切框初始的位置 this->pos() ,以及鼠标的全局坐标 event->globalPos()

那么在移动过程中裁切框的实时位置计算公式是 移动过程中鼠标实时全局坐标 - 鼠标点击时的全局坐标 + 鼠标点击时的初始位置, 为了方便, 所以点击的时候,记录了 鼠标点击时的全部坐标-鼠标点击时的初始位置 的值,也就是 m_dragPosition = event->globalPos() - this->pos(); 这样移动过程中,只需要用 移动过程中鼠标的实时全局坐标- m_dragPosition 即可

void CropBox::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_bMovingFlag = true;
        m_dragPosition = event->globalPos() - this->pos();
    }
    event->accept();
}

mouseMoveEvent

因为裁切框有移动和放缩的功能,所以鼠标在移动过程中,需要做一些额外的处理

  • 未点击状态下的鼠标移动时,当鼠标在裁切框边缘时,根据鼠标的位置,对鼠标样式进行对应的调整(8方向的鼠标设置)
    • 非边缘位置,鼠标样式为正常指针样式
    • 边缘位置,根据上下左右单独设置样式
  • 点击状态下,需要根据点击的位置,也就是鼠标的样式,区分是移动还是放缩

代码如下:

void CropBox::mouseMoveEvent(QMouseEvent *event)
{
    // event->pos() 获取的坐标是鼠标相对于裁切框的坐标
    QPoint point = event->pos();
    // 将这个坐标转换成相对于父对象的坐标,位置后放缩判断做准备
    QPoint parent_point = mapToParent(point);
    QPoint global_point = event->globalPos();

    if (!m_bMovingFlag) {
        setDirection(point);
    } else {
        if (m_curDirec == NONE)
            handleMove(global_point);
        else
            handleResize(parent_point);
    }

    event->accept();
}

mouseReleaseEvent

void CropBox::mouseReleaseEvent(QMouseEvent *event)
{
    this->setCursor(QCursor(Qt::ArrowCursor));
    if(event->button()==Qt::LeftButton)
        m_bMovingFlag = false;

    event->accept();
}

handleMove

因为需要裁切框在图片内部移动,所以需要获取图片的坐标, 由于是使用 QLabel 来展示图片, QLabel 的大小其实就是裁切框移动的范围

void move(const QPoint &); 参数的值是相对于父对象的坐标

void CropBox::handleMove(QPoint mouse_globalpos)
{
    QWidget* parent_widget = (QWidget *)this->parent();
    QPoint end_point = mouse_globalpos - m_dragPosition ;
    if (parent_widget) {
        // 保证最后移动到的位置是图片内部的位置,不超出图片
        int new_x = judgePosition(end_point.x(), 0, parent_widget->width()-this->width());
        end_point.setX(new_x);

        int new_y = judgePosition(end_point.y(), 0, parent_widget->height()-this->height());
        end_point.setY( new_y );
    }
    move( end_point );
}

inline int CropBox::judgePosition(int origin, int min, int max)
{
    if (origin < min)  return min;
    if (origin > max)  return max;
    return origin;
}

放缩

放缩主要分为2部分

  • 为了美观设置鼠标的样式, 8方向
  • 实现放缩功能,并且放缩模式分为自由放缩,固定比例放缩,1:1放缩 (首先需要明确一点1:1放缩 就是裁切框一直保持是一个正方形,也就是长宽比是 1, 而 固定长宽比 此时的长宽比是任意值)
  • 键盘控制放缩的模式

定义鼠标的位置, 8方向外加一个中央位置的 NONE

enum Direction { UP=0, DOWN, LEFT, RIGHT, LEFTTOP, LEFTBOTTOM, RIGHTBOTTOM, RIGHTTOP, NONE };

定义放缩模式的枚举

enum ZoomMode {
    Free,
    FixedRatio,
    Square,
};

放缩的鼠标样式

这一部分主要就是判断鼠标当前的位置距离裁切框的位置,然后设置成对应的鼠标样式

// 判断的阈值
#define PADDING 2 
void CropBox::setDirection(QPoint point)
{
    // 固定尺寸时,不存在放缩功能,不需要设置鼠标样式
    if (m_bFixSized) {
        m_curDirec = NONE;
        this->setCursor(QCursor(Qt::ArrowCursor));
        return;
    }

    int width = this->width();
    int heigth = this->height();

    if ( PADDING >= point.x() && 0 <= point.x() &&  PADDING  >= point.y() && 0 <= point.y())
    {
        m_curDirec = LEFTTOP;
        this->setCursor(QCursor(Qt::SizeFDiagCursor));
    }
    else if(width - PADDING <= point.x() && width >= point.x() && heigth - PADDING <= point.y() && heigth >= point.y())
    {
        m_curDirec = RIGHTBOTTOM;
        this->setCursor(QCursor(Qt::SizeFDiagCursor));
    }
    else if(PADDING >= point.x() && 0 <= point.x() && heigth - PADDING <= point.y() && heigth >= point.y())
    {
        m_curDirec = LEFTBOTTOM;
        this->setCursor(QCursor(Qt::SizeBDiagCursor));
    }
    else if(PADDING >= point.y() && 0 <= point.y() && width - PADDING <= point.x() && width >= point.x())
    {
        m_curDirec = RIGHTTOP;
        this->setCursor(QCursor(Qt::SizeBDiagCursor));
    }
    else if(PADDING >= point.x() && 0 <= point.x())
    {
        m_curDirec = LEFT;
        this->setCursor(QCursor(Qt::SizeHorCursor));
    }
    else if(PADDING >= point.y() && 0 <= point.y())
    {
        m_curDirec = UP;
        this->setCursor(QCursor(Qt::SizeVerCursor));
    }
    else if(width - PADDING <= point.x() && width >= point.x())
    {
        m_curDirec = RIGHT;
        this->setCursor(QCursor(Qt::SizeHorCursor));
    }
    else if(heigth - PADDING <= point.y() && heigth >= point.y())
    {
        m_curDirec = DOWN;
        this->setCursor(QCursor(Qt::SizeVerCursor));
    }
    else
    {
        m_curDirec = NONE;
        this->setCursor(QCursor(Qt::ArrowCursor));
    }
}

放缩逻辑

放缩分为8方向,对应每个方向有单独的放缩规则,所以封装成对应的处理函数,在函数里在根据放缩的模式进行放缩

通过 this->geometry(); 获取裁切框的几何形状 QRect rectMove, 而在放缩过程中:

  • 上,下,左, 右这4个方向放缩,对于裁切框而言只是需要修改 QRect rectMove 对应的上,下,左,右 的值
  • 左上, 右上,右下,左下 这4个方向放缩的时候,也是一样, 修改 QRect rectMove 对应方向的2个值
  • 最后将新的几何形状重新赋予裁切框 this->setGeometry(rectMove);即可实现放缩功能

大致的代码逻辑如下:

void CropBox::handleResize(QPoint mouse_parentpos)
{
    if (!m_bMovingFlag)
        return;

    // 记录当前的
    QRect rectMove = this->geometry();

    // 当鼠标移出图像外侧时,对放缩使用的坐标进行修正,这里只对最大值进行了修正,最小值,因为方向的问题,需要交给对应方向放缩的函数处理
    QPoint valid_point{mouse_parentpos} ;
    QWidget* parent_widget = (QWidget *)this->parent();
    valid_point.setX( judgePosition(valid_point.x(), 0, parent_widget->width()) );
    valid_point.setY( judgePosition(valid_point.y(), 0, parent_widget->height()) );

    switch(m_curDirec) {
    case UP:
        handleResizeUp(valid_point, rectMove, parent_widget);
        break;
    case DOWN:
        handleResizeDown(valid_point, rectMove, parent_widget);
        break;
    case LEFT:
        handleResizeLeft(valid_point, rectMove, parent_widget);
        break;
    case RIGHT:
        handleResizeRight(valid_point, rectMove, parent_widget);
        break;
    case RIGHTTOP:
        handleResizeRightTop(valid_point, rectMove, parent_widget);
        break;
    case RIGHTBOTTOM:
        handleResizeRightBottom(valid_point, rectMove, parent_widget);
        break;
    case LEFTTOP:
        handleResizeLeftTop(valid_point, rectMove, parent_widget);
        break;
    case LEFTBOTTOM:
        handleResizeLeftBottom(valid_point, rectMove, parent_widget);
        break;
    default:
        break;
    }

    this->setGeometry(rectMove);
}

8个方向处理放缩其实本质是一致的,都是计算新的几何形状,所以只举2个例子

以向上为例 handleResizeUp

向上改变形状

如图,最大值在传入该函数的时候就做了限制,所以该函数做了此方向上的最小值判断

  • 当放缩方式是 自由 放缩的时候,等于只要改变几何形状的 的值
  • 当放缩方式是 1:1 或者固定长宽比 放缩的时候,此时长宽同时变化,所有直接调用 handleResizeRightTop() 函数,此时 向上放缩 等同于 向右上放缩,这个是我自己规定的,可以根据实际自己定义

所以代码如下:

void CropBox::handleResizeUp(QPoint &valid_point, QRect &rectNew, const QWidget *parent_widget)
{
    if (m_zoomMode !=  Free) {
        handleResizeRightTop(valid_point, rectNew, parent_widget);
        return;
    }

    if (rectNew.bottom() - valid_point.y() + 1  <= m_minHeight)
        valid_point.setY( rectNew.bottom() - m_minHeight + 1);

    rectNew.setTop( valid_point.y() );
}

这里有个坑 rectNew.bottom() - valid_point.y() + 1 <= m_minHeight 计算长度的时候 +1, 之前缩放到最小高度的时候,例如80时,裁切框得到的最小高度永远是 81,查看 Qt 的文档时,可以看到这样文档描述

QRect

大致意思就是,因为历史原因

right() - left() + 1 = width()
bottom() - top() + 1 = height()

以右上为例 handleResizeRightTop

向右上改变形状

  • 当放缩时 自由 放缩的时候,此时等于是同时改变几何形状的 的, 一样判断最小值
  • 当放缩方式是 1:1 或者 固定长宽比 放缩时,难点依旧是对于如何不让裁切框出边界的问题

再次强调一下 1:1放缩 就是裁切框一直保持是一个正方形,也就是长宽比是 1, 而 固定长宽比 此时的长宽比是任意值,所以可以使用 m_heightwidthRatio 值记录放缩前的长宽比
并且此时放缩的长宽的最小值会跟用户设置的最小值是有出入的,长或宽在长宽比限制的情况下,很难同时到达最小点 (除非用户设置的最小值长宽比和放缩时的长宽比一样),所以需要单独记录

m_ratioMinWidth = m_minWidth * m_heightwidthRatio > m_minHeight? m_minWidth : m_minHeight / m_heightwidthRatio;
m_ratioMinHeight = m_minWidth * m_heightwidthRatio > m_minHeight? m_minWidth * m_heightwidthRatio : m_minHeight;
  • 先将鼠标的位置转换成合理的图像内的坐标
  • 使用鼠标某个方向上的坐标得出 长或者宽,根据长宽比反推出 宽或者长
  • 然后在判断新的 几何形状 是否在图像的内部
    • 满足,直接设置新的 几何形状
    • 不满足,重新计算一下新的 几何形状

代码如下:

void CropBox::handleResizeRightTop(QPoint &valid_point, QRect &rectNew, const QWidget *parent_widget)
{
    if (m_zoomMode !=  Free) {
        if (valid_point.x() - rectNew.left() + 1 <= m_ratioMinWidth)
            valid_point.setX( rectNew.left() + m_ratioMinWidth - 1);

        if (rectNew.bottom() - valid_point.y() + 1 <= m_ratioMinHeight)
            valid_point.setY( rectNew.bottom() - m_ratioMinHeight + 1);

        int right = (rectNew.bottom()- valid_point.y() + 1)/m_heightwidthRatio + rectNew.left() - 1 ;
        if ( right > parent_widget->width() ) {
            right = parent_widget->width();
            valid_point.setY( rectNew.bottom() - (parent_widget->width() - rectNew.left() + 1)*m_heightwidthRatio + 1 );
        }

        valid_point.setX( right );
    } else {
        if (valid_point.x() - rectNew.left() + 1 <= m_minWidth)
            valid_point.setX( rectNew.left() + m_minWidth - 1);

        if (rectNew.bottom() - valid_point.y() + 1 <= m_minHeight )
            valid_point.setY( rectNew.bottom() - m_minHeight + 1 );

    }

    rectNew.setRight(valid_point.x());
    rectNew.setTop(valid_point.y());
}

结合键盘按键放缩

  • ctrl 固定比例放缩
  • alt 1:1 放缩

CropBox 初始化的时候需要监听键盘事件 this->setEnableKeyPressEvent(true);

代码如下:

void CropBox::keyPressEvent(QKeyEvent *event)
{
    // m_keyPressZoomMode 记录按键前原始的放缩模式
    m_keyPressZoomMode = m_zoomMode;

    if (event->key() == Qt::Key_Control) {
        this->setZoomMode(FixedRatio);
        return;
    } else if(event->key() == Qt::Key_Alt) {
        this->setZoomMode(Square);
        return;
    } else {
        QWidget::keyPressEvent(event);
    }
}

void CropBox::keyReleaseEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Alt) {
        this->setZoomMode(m_keyPressZoomMode);
        return;
    }

    QWidget::keyPressEvent(event);
}

需要优化的地方

图片尺寸过大

当图片尺寸过大(超过了显示器的分辨率),ImageShowLabel 会显示不下,想到了以下2种解决方法

  • 使用一个 QScrollArea 包含了 ImageShowLabel, 这样会出现滑动轴,通过拖动来保证可以展示完整。
  • 限制用户输入图片的大小

这2种方法感觉都不是很好

计算放缩后的几何形状的坐标

因为使用 QRectright(), left(), top() , bottom() 这些函数,导致计算过程中总是出现 +1,-1 的代码, 但是改变形状,可以直接调用对应的 set 函数就能直接改变形状;

官方推荐的方法中 x(), width(), y(), height() 等计算有效范围会简洁很多,但是设置新的形状的时候,需要设置 x(), width(), y(), height(), 设置起来更麻烦了

例如向上放缩的函数 handleResizeUp

void CropBox::handleResizeUp(QPoint &valid_point, QRect &rectNew, const QWidget *parent_widget)
{
    if (m_zoomMode !=  Free) {
        handleResizeRightTop(valid_point, rectNew, parent_widget);
        return;
    }

    int oldHeight = rectNew.height();
    int oldY = rectNew.y();
    if ( oldY + oldHeight - valid_point.y() <= m_minHeight)
        valid_point.setY( oldY + oldHeight- m_minHeight);

    rectNew.setY( valid_point.y() );
    rectNew.setHeight( oldY + oldHeight - valid_point.y() );
}

代码地址

github 地址 :https://github.com/catcheroftime/CropPicture