使用Qt C++创建自定义饼状菜单控件与交互效果

1. 引言

用户界面的创新性和易用性对于现代应用程序至关重要。饼状菜单作为一种创新的选项选择方式,为用户提供了一种高效且引人注目的交互体验。本篇博客将详细介绍如何使用Qt C++框架,创建一个高度可定制的自定义饼状菜单控件,并深入了解其过程。

2. 初始化自定义饼状菜单控件

在创建自定义饼状菜单控件之前,我们需要进行一系列的初始化步骤,以确保控件的各项属性和功能正确设置。以下是构造函数的关键部分,展示了如何进行初始化:

PieMenu::PieMenu(QWidget *parent, bool isSubMenu) : QDialog(parent)
{
    // 初始化窗口属性和变量
    setFixedSize(300, 300); // 设置固定尺寸
    setWindowFlag(Qt::FramelessWindowHint); // 隐藏窗口边框
    setAttribute(Qt::WA_TranslucentBackground); // 使用透明背景

    // 设置菜单项数量和角度计算
    menuItemNum = 6;
    perTheta = 360 / menuItemNum;

    // 初始化角度和其他参数
    startTheta = -90;
    originTheta = startTheta;
    stepTheta = 10;

    // 创建计时器用于动画
    timer = new QTimer();
    timer->setInterval(10);
    QObject::connect(timer, SIGNAL(timeout()), this, SLOT(updateValue()));

    // 设置初始饼状菜单尺寸
    setPieSize(260, 260);

    // 执行其他初始化步骤,如加载菜单项、图标等
    init();
}

在这个构造函数中,我们首先设置窗口的固定尺寸,并隐藏了窗口边框以及启用透明背景。接着,我们计算菜单项数量和每个扇形的角度,初始化起始角度和步长。我们创建了计时器用于动画效果,以及连接到更新函数。最后,我们调用了init()函数,用于执行其他的初始化步骤,例如加载菜单项和图标等。

在初始化过程中,执行其他的初始化步骤非常重要,它们将为饼状菜单的正确显示和交互提供必要的数据。以下是一个示例init()函数的部分代码,演示了如何加载菜单项和图标:

void PieMenu::init()
{
    // 设置中心
    centerPoint = QPoint(width() / 2, height() / 2);
    centerIconSize = QSize(width() / 5, height() / 5);

    // 设置半径
    indicatorHeight = 5;
    centerCircleRadius = width() / 10;
    innerCircleRadius = qRound(double(width()) / 2.5);
    outerCircleRadius = width() / 2 - indicatorHeight;

    // 设置菜单
    menuItemRho = width() / 4;
    menuIconSize = QSize(width() / 6, height() / 6);
}

在这个函数中,我们通过创建MenuItem对象并将其添加到menuItemList列表中,加载了一些菜单项。我们还设置了中心点和中心图标的大小,这些信息将在后续绘制和交互过程中使用。

3.绘制自定义饼状菜单的视图

在自定义控件中,绘制是至关重要的一步,它决定了控件的外观和交互效果。本章将详细介绍如何使用Qt C++框架的绘图功能,为自定义饼状菜单控件添加视觉效果。

3.1. 绘制中心图标

在饼状菜单控件中,中心图标是用户操作的关键部分。下面是如何使用绘图函数绘制中心图标的代码:

void PieMenu::drawCenterCircle(QPainter *painter)
{
    painter->save();
    QPixmap pixmap = isSubMenu ? QPixmap(":/img/img/Back.svg") : QPixmap(":/img/img/Click.svg");
    painter->drawPixmap(centerPoint.x() - centerIconSize.width() / 2,
                        centerPoint.y() - centerIconSize.width() / 2,
                        centerIconSize.width(),
                        centerIconSize.height(),
                        pixmap);
    painter->restore();
}

在这个函数中,我们使用QPainter对象绘制中心图标。根据isSubMenu的值,我们选择不同的图标。通过drawPixmap函数,我们将图标绘制在中心点的位置。

3.2. 绘制内圆和指示弧

接下来,我们绘制内圆和指示弧,这些将增加视觉效果和交互性。以下是部分代码,展示了如何绘制内圆和指示弧:

void PieMenu::drawInnerCircle(QPainter *painter)
{
    painter->save();

    // 绘制内圆
    painter->setPen(Qt::NoPen);
    painter->setBrush(Qt::white);
    painter->drawEllipse(centerPoint, innerCircleRadius, innerCircleRadius);

    // 绘制指示弧
    if (selectedIndex != -1) {
        QRectF rectF = QRectF(QPoint(centerPoint.x() - innerCircleRadius, centerPoint.y() - innerCircleRadius),
                              QPoint(centerPoint.x() + innerCircleRadius, centerPoint.y() + innerCircleRadius));
        painter->setBrush(QBrush(QColor(128, 0, 128, 95)));
        painter->drawPie(rectF, -(startTheta - perTheta / 2 + selectedIndex * perTheta) * 16, -(perTheta - 1) * 16);
        painter->setBrush(Qt::white);
        painter->drawEllipse(centerPoint, innerCircleRadius - indicatorHeight, innerCircleRadius - indicatorHeight);
    }

    // 绘制菜单项图标
    for (int i = 0; i < menuItemList.size(); i++) {
        QPoint point = pol2cart(startTheta + i * perTheta, menuItemRho);
        painter->drawPixmap(point.x() - menuIconSize.width() / 2,
                            point.y() - menuIconSize.height() / 2,
                            menuIconSize.width(),
                            menuIconSize.height(),
                            menuItemList.at(i).icon);
    }

    painter->restore();
}

在这个函数中,我们首先绘制了一个白色的内圆,然后绘制了指示弧,用于标记选定的菜单项。如果有菜单项被选定,我们使用drawPie函数绘制一个带有半透明颜色的扇形,用于指示选中的菜单项。最后,我们绘制了菜单项的图标,通过pol2cart函数将极坐标转换为屏幕坐标。

3.3. 绘制外圆和箭头

最后,我们绘制外圆和箭头,为整个饼状菜单增添更多的视觉效果。以下是如何绘制外圆和箭头的代码:

void PieMenu::drawOuterCircle(QPainter *painter)
{
    painter->save();

    // 绘制外圆
    painter->setPen(Qt::NoPen);
    painter->setBrush(Qt::lightGray);
    painter->drawEllipse(centerPoint, outerCircleRadius, outerCircleRadius);

    // 绘制扇区
    painter->setBrush(Qt::gray);
    for (int i = 0; i < menuItemNum; i++) {
        QRectF rectF = i == activeIndex
                ? QRectF(QPoint(0, 0), QPoint(width(), height()))
                : QRectF(QPoint(indicatorHeight, indicatorHeight),
                         QPoint(width() - indicatorHeight, height() - indicatorHeight));
        painter->drawPie(rectF, -(startTheta - perTheta / 2 + i * perTheta) * 16, -(perTheta - 1) * 16);
    }

    // 绘制箭头
    painter->setPen(QPen(Qt::white, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    for (int i = 0; i < menuItemList.size(); i++) {
        if (!menuItemList.at(i).subFlag) {
            break;
        }
        int rho = i == activeIndex
                ? width() / 2 - (outerCircleRadius - innerCircleRadius) / 2 + 2
                : outerCircleRadius - (outerCircleRadius - innerCircleRadius) / 2 + 2;
        drawAllow(painter, i * perTheta + startTheta, rho);
    }

    painter->restore();
}

在这个函数中,我们首先绘制了一个灰色的外圆,然后绘制了一系列的扇区,表示菜单项。通过使用drawPie函数,我们根据菜单项的活跃状态绘制不同的扇区。最后,我们使用drawAllow函数绘制箭头,箭头指向具有子菜单的菜单项。

4.坐标换算与鼠标事件响应

4.1. 坐标换算

坐标换算是将不同坐标系之间的位置进行转换,以便正确地处理鼠标事件。以下是一个用于将极坐标转换为屏幕坐标的函数示例:

QPoint PieMenu::pol2cart(int theta, int rho)
{
    double x, y;
    x = rho * qCos(qDegreesToRadians(double(theta)));
    y = rho * qSin(qDegreesToRadians(double(theta)));
    x = centerPoint.x() + x;
    y = centerPoint.y() + y;
    return QPoint(qRound(x), qRound(y));
}

int PieMenu::calcTheta(QPoint point)
{
    point.setX(point.x() - centerPoint.x());
    point.setY(centerPoint.y() - point.y());
    qreal radian = -qAtan2(point.y(), point.x());
    qreal degree = qRadiansToDegrees(radian); // 将弧度值转换为角度值,并调整为顺时针坐标系下的角度范围(0 到 360 度)
    if (degree < 0) {
        degree += 360;
    }
    return qRound(degree);
}

int PieMenu::calcRho(QPoint point)
{
    double val = qPow(point.x() - centerPoint.x(), 2) + qPow(point.y() - centerPoint.y(), 2);
    return qRound(qSqrt(val));
}

int PieMenu::getIndexFromPos(QPoint pos)
{
    int theta = calcTheta(pos);
    int realStartTheta = -perTheta / 2 + startTheta;
    if (theta < realStartTheta) theta += 360;
    if (theta> realStartTheta+360) theta -= 360;
    for (int i = 0; i < menuItemNum; i++) {
        if(qAbs(theta - (startTheta + i * perTheta)) < perTheta / 2) {
            return i;
        }
    }
    return -1;
}

这里的函数中,我们使用三角函数将极坐标转换为屏幕坐标。qDegreesToRadians函数将角度转换为弧度,然后我们通过简单的数学计算,计算出屏幕上的点坐标。

4.2. 鼠标事件响应

为了使饼状菜单实现交互效果,我们需要处理鼠标事件。以下是一些重要的鼠标事件处理函数示例:

void PieMenu::mousePressEvent(QMouseEvent *ev)
{
    if(ev->button() == Qt::LeftButton) {
        lastPosition = frameGeometry().topLeft();
        dragPosition = ev->globalPos() - lastPosition;
    }
}

void PieMenu::mouseMoveEvent(QMouseEvent *ev)
{
    if (ev->buttons() == Qt::LeftButton) {
        move(ev->globalPos() - dragPosition);
    }
    else if (ev->buttons() == Qt::NoButton) {
        if (calcRho(ev->pos()) > innerCircleRadius) { // 设置扇区active状态
            int index = getIndexFromPos(ev->pos());
            if (index == -1 || index > menuItemList.size() - 1 || index == activeIndex || !menuItemList.at(index).subFlag) {
                return;
            }
            activeIndex = index;
            update();
        }
        else {
            if(activeIndex != -1) {
                activeIndex = -1;
                update();
            }
        }
    }
}

void PieMenu::mouseReleaseEvent(QMouseEvent *ev)
{
    QPoint delta = frameGeometry().topLeft() - lastPosition;
    if (qAbs(delta.x()) > 0 || qAbs(delta.y()) > 0) { // 如果偏移,认为是拖动
        return;
    }

    QPoint pos = ev->pos();
    if (calcRho(pos) < centerCircleRadius) { // 点击中心
        if(isSubMenu) {
            emit menuClosed(this->geometry());
            this->close();
        } else {
            timer->start();
        }
    } else if (calcRho(pos) < innerCircleRadius) { // 点击内圆
        int index = getIndexFromPos(pos);
        if (index == -1 || index > menuItemList.size() - 1) {
            return;
        }
        selectedIndex = index;
        update();
        emit(itemClicked(index));
    } else {
        int index = getIndexFromPos(pos);
        if (index == -1 || index > menuItemList.size() - 1) { // 点击外圆
            return;
        }
        selectedIndex = index;
        update();
        emit(openSubMenu(index));
    }
}

void PieMenu::enterEvent(QEvent *ev)
{
    Q_UNUSED(ev);
    setMouseTracking(true);
}

void PieMenu::leaveEvent(QEvent *ev)
{
    Q_UNUSED(ev);
    setMouseTracking(false);
    activeIndex = -1;
    update();
}

在这些函数中,我们分别处理了鼠标进入、离开、按下、移动和释放事件。在mouseMoveEvent中,我们根据鼠标的位置处理菜单项的活跃状态和更新视图。在mouseReleaseEvent中,我们根据点击的位置处理不同的交互情况,例如点击中心、内圆或外圆。

5.过渡动画与重绘视图

在自定义控件中,过渡动画可以提升用户体验,而重绘视图则保证了控件的正确呈现。本章将介绍如何通过过渡动画实现平滑的效果,并详细解释何时需要重新绘制视图。

5.1. 过渡动画

过渡动画是控制控件外观平滑变化的关键。在饼状菜单控件中,我们可以使用过渡动画来实现菜单的展开和收起效果。以下是如何实现过渡动画的代码片段:

void PieMenu::updateValue()
{
    if (isExpanded) {
        startTheta -= stepTheta;
        setPieSize(width() - 2, height() - 2);
        if(originTheta - startTheta >= 180) { // 旋转180°后收起
            isExpanded = false;
            isVisible = false;
            timer->stop();
        }
    } else {
        isVisible = true;
        startTheta += stepTheta;
        setPieSize(width() + 2, height() + 2);
        if(originTheta == startTheta) { // 反向操作
            isExpanded = true;
            timer->stop();
        }
    }

    update(); // 重绘视图
}

void PieMenu::setPieSize(int width, int height)
{
    if (width < 200 || width != height) {
        return;
    }
    QPoint originCenter = geometry().center();
    setFixedSize(width, height);
    init();
    update();
    QPoint delta = originCenter - geometry().center();
    move(geometry().topLeft() + delta); // 缩放窗口时保持中心不变
}

在这个函数中,我们使用定时器不断调用updateValue函数,以实现平滑的过渡动画效果。通过改变startTheta的值,我们改变了菜单项的角度,从而实现菜单的展开和收起。在展开和收起的过程中,我们通过调整窗口的大小来实现过渡效果。

5.2. 重绘视图

在任何需要更新控件外观的情况下,我们都需要重新绘制视图。在饼状菜单控件中,当菜单项的状态发生变化,或者需要展示不同的扇区时,我们需要重新绘制视图。以下是重新绘制视图的相关代码片段:

void PieMenu::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHints(QPainter::HighQualityAntialiasing | QPainter::TextAntialiasing);

    if (isVisible) {
        drawOuterCircle(&painter);
        drawInnerCircle(&painter);
    }
    drawCenterCircle(&painter);
}

paintEvent函数中,我们使用QPainter对象进行绘制操作。我们根据菜单的可见性绘制外圆和内圆,然后绘制中心图标。通过调用不同的绘制函数,我们可以根据菜单项的状态和用户的交互,动态地更新视图。

6.与主窗口集成

将自定义饼状菜单控件与主窗口集成,可以为应用程序带来更丰富的交互体验。本章将介绍如何在主窗口中使用自定义控件,以及如何处理控件之间的交互。

6.1. 在主窗口中使用自定义控件

首先,我们需要在主窗口中创建并使用自定义饼状菜单控件。以下是如何在主窗口中初始化并显示饼状菜单的代码片段:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    setWindowFlag(Qt::FramelessWindowHint);
    setAttribute(Qt::WA_TranslucentBackground);
    setVisible(false);

    pieMenu = new PieMenu(this);

    // 添加菜单项
    pieMenu->addMenuItem(MenuItem("Pen", QPixmap(":/img/img/Pen.svg"), true));
    pieMenu->addMenuItem(MenuItem("Ruler", QPixmap(":/img/img/Ruler.svg"), true));
    // ...

    pieMenu->show();

    // 连接菜单项点击和子菜单打开的信号
    QObject::connect(pieMenu, SIGNAL(itemClicked(int)), this, SLOT(onItemClicked(int)));
    QObject::connect(pieMenu, SIGNAL(openSubMenu(int)), this, SLOT(onSubMenuOpened(int)));
}

在这个代码片段中,我们在主窗口的构造函数中创建了自定义饼状菜单控件,并添加了菜单项。然后,我们将控件显示出来,并连接了菜单项点击和子菜单打开的信号。

6.2. 处理控件之间的交互

在主窗口中,我们可以根据自定义饼状菜单控件的信号来处理不同的交互情况。以下是如何处理菜单项点击和子菜单打开的信号的代码片段:

void MainWindow::onItemClicked(int index)
{
    if (index == 1) { // "Ruler" 被点击
        ruler->show(); // 显示标尺控件
    }
}

void MainWindow::onSubMenuOpened(int index)
{
    if(index == 1) { // "Ruler" 的子菜单被打开
        subMenu = new PieMenu(this, true);

        // 添加子菜单项
        subMenu->addMenuItem(MenuItem("Direction", QPixmap(":/img/img/Arrows.svg"), false));
        // ...

        pieMenu->hide(); // 隐藏主菜单
        subMenu->move(pieMenu->geometry().topLeft());
        subMenu->show();
        subMenu->setExpanded(true);

        // 连接子菜单项点击和菜单关闭的信号
        QObject::connect(subMenu, SIGNAL(itemClicked(int)), this, SLOT(onSubMenuItemClicked(int)));
        QObject::connect(subMenu, SIGNAL(menuClosed(QRect)), this, SLOT(onSubMenuClosed(QRect)));
    }
}

在这个代码片段中,我们根据不同的信号处理函数,实现了菜单项点击和子菜单打开的逻辑。当菜单项被点击时,我们显示相关的控件(如标尺控件)。当子菜单被打开时,我们创建并显示子菜单,并连接了子菜单项点击和菜单关闭的信号。

7.总结与展望

通过本技术博客,我们详细地介绍了如何基于Qt和C++实现一个自定义的饼状菜单控件。从初始化到绘制视图、坐标换算、鼠标事件响应、过渡动画到与主窗口的集成,我们一步步深入探讨了控件的各个方面。

成果与重要方法

在我们的自定义饼状菜单控件中,我们成功实现了一个可交互的、具有动态效果的菜单系统。重要的实现方法包括:

  • 初始化:在构造函数中设置控件的初始属性,包括大小、透明背景等。

  • 绘制视图:通过重写paintEvent函数,使用QPainter进行高质量的绘制,实现各种菜单元素的展示。

  • 坐标换算:通过极坐标和屏幕坐标之间的转换,将鼠标事件的位置映射到控件上,以正确处理用户的交互。

  • 事件响应:通过重写鼠标事件处理函数,实现菜单项的活跃状态切换、拖动、点击等交互效果。

  • 过渡动画与重绘视图:通过定时器实现过渡动画,平滑地展开和收起菜单;通过重绘视图保证了控件的正确呈现。

  • 与主窗口集成:通过信号和槽机制,实现主窗口与自定义控件之间的交互,包括菜单项点击和子菜单的打开等。

应用与未来展望

自定义饼状菜单控件在各种应用场景中都有潜在的用途,例如图形编辑器、游戏界面、工具栏等。通过进一步的优化和扩展,我们可以实现更多有趣的功能,例如动态添加菜单项、自定义菜单样式等。

结语

本技术博客从头到尾详细地介绍了如何基于Qt和C++实现一个自定义的饼状菜单控件。通过逐步解释代码和关键方法,我们希望读者可以更好地理解自定义控件的开发过程和原理。希望这个博客能够对您有所帮助,激发出更多的创意和想法。

如果您有任何问题、建议或意见,都欢迎随时与我分享。感谢您的阅读!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值