Qt绘图:选中区域

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

  • 5
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
`setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint)` 是一个 Qt 函数调用,用于将窗口设置为工具提示窗口,并且始终保持在顶部,并且窗口没有边框。这个函数可以在创建窗口时调用,也可以在运行时调用。 下面是一个使用 `setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint)` 的例子。在这个例子,我们创建了一个简单的窗口,并将其设置为工具提示窗口、始终在顶部,并且没有边框: ```cpp #include <QtWidgets> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setGeometry(100, 100, 200, 200); window.setWindowTitle("Tool Window"); window.show(); // Set the window to be a tool window, always on top, and frameless window.setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint); return app.exec(); } ``` 在上面的例子,我们创建了一个名为 `window` 的 `QWidget` 对象,并通过 `setGeometry` 函数设置了窗口的位置和大小。随后,我们将窗口的标题设置为 `"Tool Window"`,并将其显示出来。最后,我们调用 `setWindowFlags` 函数,并将其参数设置为 `Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint`,以将窗口设置为工具提示窗口、始终在顶部,并且没有边框。 需要注意的是,如果您想取消窗口的 `Tool`、`WindowStaysOnTopHint`、`FramelessWindowHint` 标志位,可以使用 `clearMask(Qt::Tool | Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint)` 函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值