0.前言
选区操作是比较常见的需求,如截图、图表等。一般矩形选区居多,有的是四个边都可编辑,有的是横向可编辑。单个选区的操作逻辑也是比较简单的,之后会在此基础上增加更复杂的需求。
本文代码源码链接及实现效果如下:
github 链接(SimpleSelection类):https://github.com/gongjianbo/EasyQPainter
1.实现细节
矩形区域可以用 QRect 来保存,先对这个类有个基本的认识:
QRect有四个成员变量,分别对应左上角和右下角点坐标
x1-左上角坐标x
x2-等于x1+width-1
y1-左上角坐标y
y2-等于y1+height-1
即QRect(50,50,200,200)时,topLeft=(50,50)bottomRight=(249,249)
fillRect会填充整个区域
drawRect在画笔宽度奇数时,右下角会多1px,绘制时整体宽度先减去1px
选区的编辑主要借助三个鼠标事件来完成:
void mousePressEvent(QMouseEvent *event) override; //鼠标按下
void mouseMoveEvent(QMouseEvent *event) override; //鼠标移动
void mouseReleaseEvent(QMouseEvent *event) override; //鼠标释放
如果要在未按下鼠标时触发moveEvent,可以给组件设置setMouseTracking(true);
因为编辑有移动和拉伸边界等,定义一个枚举进行区分当前的操作类型:
//当前编辑类型
enum EditType : int
{
EditNone, //无操作
PressInside, //在选区范围内按下
PressOutside, //在选区范围外按下
DrawSelection, //绘制
MoveSelection, //拖动
EditSelection //拉伸编辑
};
有选区之后,需要判断鼠标在哪个边界,定义一个枚举来区分当前鼠标所在区域:
//鼠标在区域的哪个位置
enum AreaPosition : int
{
Outside = 0x00,
Inside = 0xFF, //任意值
AtLeft = 0x01,
AtRight = 0x02,
AtTop = 0x10,
AtBottom = 0x20,
AtTopLeft = 0x11, //AtLeft|AtTop
AtTopRight = 0x12, //AtRight|AtTop
AtBottomLeft = 0x21, //AtLeft|AtBottom
AtBottomRight = 0x22 //AtRight|AtBottom
};
主要有三个操作:拖拽绘制出一个选区、拖拽移动选区、拖拽边界拉伸选区。
绘制流程:鼠标按下时判断当前是否在空白区域,如果是则在移动鼠标时更新选区的信息(通过按下时坐标和当前光标坐标两个点确定一个矩形),鼠标释放时再判定下选区大小是否符合。
移动流程:鼠标按下时判断当前是否在选区非边界上,如果是则在移动鼠标时移动选区。可以在按下时保存光标相对选区位置,移动时拿相对位置加上光标位置就是选区当前位置了。
拉伸流程:鼠标按下时判断当前是否在选区边界,如果是则在移动鼠标时设置对应边的位置,鼠标释放时再判定下选区大小是否符合。
目前的操作都是在屏幕坐标系上进行,像素和选区位置是对应的。但在处理如波形图选区时,坐标轴的范围是可以缩放和移动的,此时选区也需要跟着坐标刻度变动。一般情况下,可以在编辑选区时取屏幕坐标,编辑完后根据坐标计算出对应的刻度值,在坐标轴变动时再根据这个值反过来计算屏幕坐标。
2.主要代码
#pragma once
#include <QWidget>
//选区绘制和交互
class SimpleSelection : public QWidget
{
Q_OBJECT
public:
//鼠标在区域的哪个位置
enum AreaPosition : int
{
Outside = 0x00,
Inside = 0xFF, //任意值
AtLeft = 0x01,
AtRight = 0x02,
AtTop = 0x10,
AtBottom = 0x20,
AtTopLeft = 0x11, //AtLeft|AtTop
AtTopRight = 0x12, //AtRight|AtTop
AtBottomLeft = 0x21, //AtLeft|AtBottom
AtBottomRight = 0x22 //AtRight|AtBottom
};
//当前编辑类型
enum EditType : int
{
EditNone, //无操作
PressInside, //在选区范围内按下
PressOutside, //在选区范围外按下
DrawSelection, //绘制
MoveSelection, //拖动
EditSelection //拉伸编辑
};
public:
explicit SimpleSelection(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
//计算鼠标相对区域的位置
AreaPosition calcPosition(const QPoint &pos);
//当前鼠标对应选区的位置
void setCurPosition(AreaPosition position);
//根据鼠标当前位置更新鼠标样式
void updateCursor();
private:
//当前选区
QRect selection;
//是否有选区
bool hasSelection{false};
//鼠标当前操作位置
AreaPosition curPosition{AreaPosition::Outside};
//当前操作类型
EditType curEditType{EditType::EditNone};
//鼠标按下标志
bool pressFlag{false};
//鼠标按下位置
QPoint pressPos;
//目前用于记录press时鼠标与选区左上角的坐标差值
QPoint tempPos;
//鼠标当前位置
QPoint mousePos;
//最小宽度
static const int Min_Width{5};
};
#include "SimpleSelection.h"
#include <cmath>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QCursor>
#include <QDebug>
SimpleSelection::SimpleSelection(QWidget *parent)
: QWidget(parent)
{
setMouseTracking(true);
selection = QRect(50, 50, 200, 200);
hasSelection = true;
}
void SimpleSelection::paintEvent(QPaintEvent *event)
{
event->accept();
QPainter painter(this);
//黑底
painter.fillRect(this->rect(), Qt::black);
if (!hasSelection)
return;
painter.save();
if (pressFlag && curPosition != AreaPosition::Outside)
{ //点击样式,选用纯正的原谅绿主题
painter.setPen(QColor(0, 255, 255));
painter.setBrush(QColor(0, 180, 0));
}
else if (curPosition != AreaPosition::Outside)
{ //悬停样式
painter.setPen(QColor(0, 255, 255));
painter.setBrush(QColor(0, 160, 0));
}
else
{ //未选中样式
painter.setPen(QColor(0, 150, 255));
painter.setBrush(QColor(0, 140, 0));
}
//-1是为了边界在rect范围内
painter.drawRect(selection.adjusted(0, 0, -1, -1));
painter.restore();
}
void SimpleSelection::mousePressEvent(QMouseEvent *event)
{
event->accept();
mousePos = event->pos();
if (event->button() == Qt::LeftButton)
{
//鼠标左键进行编辑操作
pressFlag = true;
pressPos = event->pos();
if (curPosition == AreaPosition::Inside)
{
curEditType = PressInside;
//鼠标相对选区左上角的位置
tempPos = mousePos - selection.topLeft();
}
else if (curPosition != AreaPosition::Outside)
{
curEditType = EditSelection;
}
else
{
curEditType = PressOutside;
}
}
else
{
//非单独按左键时的操作
}
update();
}
void SimpleSelection::mouseMoveEvent(QMouseEvent *event)
{
event->accept();
mousePos = event->pos();
if (pressFlag)
{
if (curEditType == PressInside)
{
//在选区内点击且移动,则移动选区
if (QPoint(pressPos - mousePos).manhattanLength() > 3)
{
curEditType = MoveSelection;
}
}
else if (curEditType == PressOutside)
{
//在选区外点击且移动,则绘制选区
if (QPoint(pressPos - mousePos).manhattanLength() > 3)
{
hasSelection = true;
curEditType = DrawSelection;
}
}
QPoint mouse_p = mousePos;
//限制范围在可视区域
if (mouse_p.x() < 0)
{
mouse_p.setX(0);
}
else if (mouse_p.x() > width() - 1)
{
mouse_p.setX(width() - 1);
}
if (mouse_p.y() < 0)
{
mouse_p.setY(0);
}
else if (mouse_p.y() > height() - 1)
{
mouse_p.setY(height() - 1);
}
if (curEditType == DrawSelection)
{
//根据按下时位置和当前位置确定一个选区
selection = QRect(pressPos, mouse_p);
}
else if (curEditType == MoveSelection)
{
//移动选区
selection.moveTopLeft(mousePos - tempPos);
//限制范围在可视区域
if (selection.left() < 0)
{
selection.moveLeft(0);
}
else if (selection.right() > width() - 1)
{
selection.moveRight(width() - 1);
}
if (selection.top() < 0)
{
selection.moveTop(0);
}
else if (selection.bottom() > height() - 1)
{
selection.moveBottom(height() - 1);
}
}
else if (curEditType == EditSelection)
{
//拉伸选区边界
int position = curPosition;
if (position & AtLeft)
{
if (mouse_p.x() < selection.right())
{
selection.setLeft(mouse_p.x());
}
else
{
selection.setLeft(selection.right() - 1);
}
}
else if (position & AtRight)
{
if (mouse_p.x() > selection.left())
{
selection.setRight(mouse_p.x());
}
else
{
selection.setRight(selection.left() + 1);
}
}
if (position & AtTop)
{
if (mouse_p.y() < selection.bottom())
{
selection.setTop(mouse_p.y());
}
else
{
selection.setTop(selection.bottom() - 1);
}
}
else if (position & AtBottom)
{
if (mouse_p.y() > selection.top())
{
selection.setBottom(mouse_p.y());
}
else
{
selection.setBottom(selection.top() + 1);
}
}
}
}
else
{
setCurPosition(calcPosition(mousePos));
}
update();
}
void SimpleSelection::mouseReleaseEvent(QMouseEvent *event)
{
event->accept();
mousePos = event->pos();
pressFlag = false;
if (curEditType != EditNone)
{
//编辑结束后判断是否小于最小宽度,是则取消选区
if (curEditType == DrawSelection)
{
selection = selection.normalized();
if (selection.width() < Min_Width || selection.height() < Min_Width)
{
hasSelection = false;
}
}
else if (curEditType == MoveSelection)
{
}
else if (curEditType == EditSelection)
{
if (selection.width() < Min_Width || selection.height() < Min_Width)
{
hasSelection = false;
}
}
curEditType = EditNone;
}
setCurPosition(calcPosition(mousePos));
update();
}
SimpleSelection::AreaPosition SimpleSelection::calcPosition(const QPoint &pos)
{
//一条线太窄,不好触发,增加判断范围又会出现边界太近时交叠在一起
//目前的策略是从右下开始判断,左上的优先级更低一点
static const int check_radius = 3;
int position = AreaPosition::Outside;
QRect check_rect = selection.adjusted(-check_radius, -check_radius, check_radius-1, check_radius-1);
//无选区,或者不在选区判定范围则返回outside
if (!hasSelection || !check_rect.contains(pos))
{
return (SimpleSelection::AreaPosition)position;
}
//判断是否在某个边界上
if (std::abs(pos.x() - selection.right()) < check_radius)
{
position |= AreaPosition::AtRight;
}
else if (std::abs(pos.x() - selection.left()) < check_radius)
{
position |= AreaPosition::AtLeft;
}
if (std::abs(pos.y() - selection.bottom()) < check_radius)
{
position |= AreaPosition::AtBottom;
}
else if (std::abs(pos.y() - selection.top()) < check_radius)
{
position |= AreaPosition::AtTop;
}
//没在边界上就判断是否在内部
if (position == AreaPosition::Outside && selection.contains(pos))
{
position = AreaPosition::Inside;
}
return (SimpleSelection::AreaPosition)position;
}
void SimpleSelection::setCurPosition(AreaPosition position)
{
if (position != curPosition)
{
curPosition = position;
updateCursor();
}
}
void SimpleSelection::updateCursor()
{
switch (curPosition)
{
case AtLeft:
case AtRight:
setCursor(Qt::SizeHorCursor);
break;
case AtTop:
case AtBottom:
setCursor(Qt::SizeVerCursor);
break;
case AtTopLeft:
case AtBottomRight:
setCursor(Qt::SizeFDiagCursor);
break;
case AtTopRight:
case AtBottomLeft:
setCursor(Qt::SizeBDiagCursor);
break;
default:
setCursor(Qt::ArrowCursor);
break;
}
}