添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
  • 裁切框内部的样式线条数可以配置
  • 裁切框可以固定尺寸,或者设置放缩规则,等比(1:1)缩放或者自由缩放
  • 按住键盘 ctrl 缩放为固定比例缩放, 按住键盘 alt 缩放为 1:1 长宽缩放
  • 整体代码思路

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

  • 使用 QLabel 显示图片,我这里创建一个 ImageShowLabel 类来显示图片
  • QLabel 上添加一个 QWidget 作为裁切框, 在创建一个继承 QWidget CropBox 类来表示裁切框
  • 剩下的主要的就是裁切框 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 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);
            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 的样式

    // 绘制外边框线,外边框实线 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);
            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);
                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));
            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 的文档时,可以看到这样文档描述

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

    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