Qt-图片裁切

Qt-图片裁切

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

已实现功能简介

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

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

加载图片,并在图片上添加裁切框,右下角显示了裁切框大小

裁切框大小和位置均可调整,且在图片内部,不超出图片

裁切框中央颜色不变,裁切框外部颜色变深

裁切框内部的样式线条数可以配置

裁切框可以固定尺寸,或者设置放缩规则,等比(1:1)缩放或者自由缩放

按住键盘 ctrl 缩放为固定比例缩放, 按住键盘 alt 缩放为 1:1 长宽缩放

整体代码思路

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

使用 QLabel 显示图片,我这里创建一个 ImageShowLabel 类来显示图片

在 QLabel 上添加一个 QWidget 作为裁切框, 在创建一个继承 QWidget 的 CropBox 类来表示裁切框

剩下的主要的就是裁切框 CropBox 大小位置,放缩等逻辑

ImageShowLabel 的实现

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

#include

#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 实际上裁切框实现的类,这个类需要实现

裁切框的样式

形状

圆形

方形

背景,内部的线条,边框的线条,边框的一些标志点,以及裁切框大小的显示

裁切框在图片内移动

裁切框在图片内放缩

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);

}

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

绘制边框的 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

int width = this->width() / m_widthCount;

painter.drawLine( i*width, SPACING , i*width , this->height()-SPACING);

}

for (int i=1; 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 的文档时,可以看到这样文档描述

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

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种方法感觉都不是很好

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

因为使用 QRect 的 right(), 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

相关推荐

剑网3丐帮门派特色与实力怎么样?
国内在365投注

剑网3丐帮门派特色与实力怎么样?

📅 10-03 👁️ 9689
骆驼有哪些特点
国内在365投注

骆驼有哪些特点

📅 10-14 👁️ 5029
克洛泽踩着巴西创16球纪录 12年苦涩快感唯他懂
365bet线上攻略

克洛泽踩着巴西创16球纪录 12年苦涩快感唯他懂

📅 07-01 👁️ 8920