如何在Qt中创建类似于PC开始菜单的状态栏菜单

3
在我的Qt应用程序中,我设计了带有几个图标的状态栏。在我的状态栏左侧,我添加了菜单图标(QPixmap),当我点击该图标时,我需要显示类似于PC开始菜单的菜单。我搜索了很多但没有找到适用于此的QWidget。
这是我的Qt应用程序窗口。
根据我最近的评论,我在下面添加了我的编辑代码,因为我不能在评论部分添加代码,请建议一个解决方案。
//Menu Button
    menuBtn->setIcon(QPixmap(":/new/prefix1/ic_menu_black.png"));
    statusBar()->addWidget(menuBtn);
    connect(menuBtn,SIGNAL(clicked(bool)),this,SLOT(showPopupMenu()));

void MainWindow::showPopupMenu(){


    QMenu qMenuStart;

    QAction qCmdStart1(QString::fromUtf8("Product Details"), &qMenuStart);
    qMenuStart.addAction(&qCmdStart1);

    QAction qCmdStart2(QString::fromUtf8("Memory Status"), &qMenuStart);
    qMenuStart.addAction(&qCmdStart2);

    QObject::connect(&qCmdStart1, &QAction::triggered,[](bool){
        qDebug() << "Product Details triggered"; }
    );
    QObject::connect(&qCmdStart2, &QAction::triggered,[](bool){
        qDebug() << "Memory Status triggered"; }
    );

    qMenuStart.exec();

    menuBtn->setMenu(&qMenuStart);

    qMenuStart.show();
    qMenuStart.popup(mapToGlobal(pos() - QPoint(0, qMenuStart.height())));

    qDebug()<<"Menu Clicked!";

}

在菜单按钮上单击后弹出菜单。


在查看了我自己的源代码之后,我注意到有两个值得一提的问题需要考虑,关于如何在QTreeView中放置上下文菜单。第一个是使用QMenu::popup()打开菜单,并接受一个位置参数。另一个是QWidget::mapToGlobal(),这个方法非常有用。如果菜单应该在按钮上方打开,则必须适当地考虑这一点(例如通过从标签/按钮的全局映射y位置中减去菜单的高度来实现)。 - Scheff's Cat
我不确定 qMenuStart.exec() 的作用。我担心在将菜单分配给 menuBtn 之前,它会打开菜单,因此任何定位都不会被考虑。为什么不将 QMenu 设为 MainWindow 的成员,并一次性配置它(与 menuBtnstatusBar 一起)。这样,qMenuStart.exec() 就变得过时了。 - Scheff's Cat
我相信,我知道你为什么插入了 qMenuStart.exec();。你可能在想菜单不会显示在 qMenuStart.popup() 上。实际上,它确实显示了,但是它立即被销毁(从 MainWindow::showPopupMenu() 返回时)因为它是一个局部实例。(我的第一个猜测是崩溃,但现在我相信 Qt 能够管理它。)然而,解决方案仍然像我之前的评论建议的那样。 - Scheff's Cat
2个回答

4
我将使用您提供的示例代码,并进行更改以说明我在最新评论中所描述的内容:
MainWindow声明中添加一个成员变量:
QMenu *menuStart;

修改后的示例代码:

// configure start menu:
menuStart = new QMenu(menuBtn);
menuStart->addAction(QString::fromUtf8("Product Details"),
  [](bool){
    qDebug() << "Product Details triggered"; }
  );
menuStart->addAction(QString::fromUtf8("Memory Status"),
  [](bool){
    qDebug() << "Memory Status triggered"; }
  );
//Menu Button
menuBtn->setIcon(QPixmap(":/new/prefix1/ic_menu_black.png"));
menuBtn->setMenu(menuStart);
statusBar()->addWidget(menuBtn);
connect(menuBtn,SIGNAL(clicked(bool)),this,SLOT(showPopupMenu()));

注意:
我将menuBtn设置为menuStart的父级。因此,当menuBtn自身被销毁时,应该由其管理menuStart的销毁。
相应地,信号处理程序变得更短:
void MainWindow::showPopupMenu()
{
  menuStart->show(); // <-- trick to force layout of the menu before height() is called
  // position menu relative to menuBtn at popup
  menuStart->popup(
    mapToGlobal(menuBtn->pos() - QPoint(0, menuStart->height())));

  qDebug()<<"Menu Clicked!";
}

我仔细地做了,但无法测试它(因为它不是MCVE)。所以,请抱着一颗“谨慎”的心态看待这个。

注意:

我刚刚意识到你使用了“pre-Qt5”风格的连接信号处理程序。Qt5引入了一个很好的扩展其信号支持函数指针以及方法指针。由于这个原因,我能够简单地将信号处理程序做成lambdas。(这些lambda就是我连接到操作插槽的[](bool){/*...*/}。我一直对这些新的lambda持怀疑态度,直到我意识到它们是编写非常简单的适配器以及编写相当紧凑的示例代码的绝佳工具。)

关于Lambdas的更新:

括号[]可用于向lambda的环境(或上下文)中添加变量。如果为空(如我所使用的),则只能使用全局变量和函数。使用[this]意味着当前this指针被添加到环境中。因此,lambda可以访问当前类实例的成员变量。
当我第一次认识lambda时,经常看到[=]表示lambda获取调用函数的当前上下文(例如每个本地函数变量的副本)。我个人认为这很危险,特别是对于信号处理程序。我稍微解释一下这个问题:
Lambda的环境允许在lambda主体中访问“外部变量”。变量可以按值或引用添加。如果一个变量是通过引用提供的,则可以在lambda主体中访问它,但这并不意味着“外部”变量的生命周期足够长。(如果不是,它将导致一个“悬空引用”,具有与悬空指针相同的未定义行为。)是否完全避免引用有所帮助?不是。对于原始类型(例如int或float),这很好,但对于对象可能效率低下(甚至在语义上错误)。使用对象指针同样危险,因此应谨慎使用lambda的环境。
作为经验法则,我个人使用以下方法来实现lambda:我总是从空环境([])开始。如果我发现需要在lambda中使用的变量,我会将其添加到环境中,同时考虑其足够的生命周期(或寻找替代方案)。
更新:
由于我是SVG的粉丝,我准备了一个(类似的)菜单图标作为SVG文件:
<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
  "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  width="32" height="32">
  <title>Start Menu</title>
  <desc>Start Menu</desc>
  <rect id="line"
    x="4" y="6" width="24" height="4"
    style="stroke:none;fill:#444"/>
  <use xlink:href="#line" transform="translate(0,8)"/>
  <use xlink:href="#line" transform="translate(0,16)"/>
 </svg>

你可以将这个文件保存在文本文件中ic_menu_black.svg,然后尝试在你的应用程序中加载它。
不幸的是,Qt的导入器只支持有限的SVG。因此,并非在SVG中可能的每个技巧都能在Qt中正确工作。因此,我修改了我们Qt应用程序中图标的副本,以确保这个能够正常工作。

相同的Qt应用程序在Windows 10上运行良好,但是当我们在由ARM编译器编译的Linux(嵌入式设备)上运行相同的Qt应用程序时,成功编译后,只有QPixmap图像无法显示并抛出以下错误:“ QPixmap :: scaled:Pixmap是一个空的pixmap”。请建议。 - Durga Sekar
1
@DurgaSekar 图像文件(用于QIconQPixMap等)的加载是通过插件完成的(例如JPEG的qjpeg.dll)。可能在Linux(嵌入式设备)上缺少相应的插件。顺便问一下,我为什么没有找到qpng.dll(尽管它在我的第一个示例中确实起作用)?我发现了Qt图像格式。因此,某些加载器似乎是“内置的”。也就是说,将文件转换为png格式(并相应更改文件名)应该使您独立于插件。 - Scheff's Cat
我在../plugins/imageformats文件夹中搜索,发现只有3个.so文件(libqico.so、libqsvg.so和libqtga.so),没有找到任何与png相关的文件。我正在寻找相应的SO文件。 - Durga Sekar
不知道为什么我错过了 menuBtn->setIcon(QPixmap(":/new/prefix1/ic_menu_black.png")); 这一行代码。(可能是因为我在我的专业工作之外做这个...)你确定路径在 Linux 上可以正确解析吗?顺便说一下,我已经用菜单图标的 SVG 版本进行了更新。 - Scheff's Cat
@DurgaSekar,我添加了一些关于lambda的注释,并意识到它们可能会变得有些棘手。在您获得了一些实践经验之前,为什么不使用Qt5之前的预置信号处理程序呢?您还可以更改productdetails()函数的签名并直接使用它(而不是lambda)。您还可以编写一个专用的信号处理程序(具有匹配的函数签名),该程序调用productdetails()(即包装函数)。 - Scheff's Cat
显示剩余8条评论

2
阅读您的问题,我不太确定您实际的问题是什么。不幸的是,由于我们公司的安全政策,i.stack.imgur.com被封锁了,而我现在只能在办公室里。也许,如果您提供快照,这可以为我澄清问题。(当我回家时,我可以看一下。)
然而,出于好奇心,我试着做了一些尝试,并想展示一下我的成果:
实际上,使用QStatusBar::insertPermanentWidget()向状态栏添加小部件非常容易。
不幸的是,永久性小部件通常会放置在右侧。如果必须将其放置在左侧,则可以通过将两个状态栏嵌套到彼此中来进行调整。(实际上,任何其他布局都应该可以,但是QMainWindow::setStatusBar()需要一个QStatusBar*。)
内部状态栏是用于临时消息的状态栏。为了防止重复大小调整手柄的有趣影响,内部状态栏禁用了大小调整手柄(使用QStatusBar::setSizeGripEnabled())。
嵌套状态栏的另一个副作用是在内部状态栏左右可视化分隔符。我认为这不是很糟糕(考虑到美学方面),并且对此不太关心。
也许,我本可以仅使用QLabel(而不是内部状态栏)来显示消息(失去QStatusBar提供的附加功能,例如自动清除消息的超时)。
为了提供启动按钮,我最初使用了一个QPushButton。后来想到必须以某种方式放置启动菜单,于是我将其替换为QToolBar,因为它可以包含一个QAction,而该操作又已准备好管理子菜单。
当我设置菜单时,我注意到在启动按钮处出现了指示器。为了使示例尽可能简短,我决定暂时接受这种情况。
与提问者进行了一些沟通后,我添加了一个使用QLabel派生类的可选实现。菜单放置必须自行完成。但最终,这并不那么复杂...
如果在源代码开头附近定义了USE_LABEL,则会激活第二个版本。
我的示例 testQStartBtn.cc
#include <QtWidgets>

#define USE_LABEL

#ifdef USE_LABEL
class StartButton: public QLabel {

  private:
    QMenu *_pQMenu;

  public:
    StartButton(QWidget *pQParent = 0): QLabel(pQParent), _pQMenu(nullptr) { }

    QMenu* menu() { return _pQMenu; }
    QMenu* setMenu(QMenu *pQMenu) { std::swap(_pQMenu, pQMenu); return pQMenu; }

  protected:
    virtual void mousePressEvent(QMouseEvent *pQEvent);
};

void StartButton::mousePressEvent(QMouseEvent *pQEvent)
{
  if (_pQMenu && pQEvent->button() == Qt::LeftButton) {
    if (!_pQMenu->isVisible()) {
      _pQMenu->show(); // a trick to force computation of height() before
      _pQMenu->popup(mapToGlobal(pos()) - QPoint(0, _pQMenu->height()));
    } else _pQMenu->hide();
  }
}
#endif // USE_LABEL

int main(int argc, char **argv)
{
  qDebug() << "Qt Version: " << QT_VERSION_STR;
  // main application
#undef qApp // undef macro qApp out of the way
  QApplication qApp(argc, argv);
  // setup GUI
  QMainWindow qWin;
  QLabel qLblCentral(QString::fromUtf8("Central\nWidget"));
  qLblCentral.setAlignment(Qt::AlignCenter);
  qWin.setCentralWidget(&qLblCentral);
  QStatusBar qStBarLayout;
  // the "Windows Start Menu" alike
#ifdef USE_LABEL
  StartButton qBtnStart;
  qBtnStart.setPixmap(QPixmap(
    QApplication::applicationDirPath() + QString::fromLatin1("/Start.png")));
#else // (not) USE_LABEL
  QToolBar qToolbarStart;
  QIcon qIcnStart(
    QApplication::applicationDirPath() + QString::fromLatin1("/Start.png"));
  QAction qCmdStart(qIcnStart, QString::fromUtf8("Start"), &qToolbarStart);
#endif // USE_LABEL
  QMenu qMenuStart;
  QAction qCmdStart1(QString::fromUtf8("Command 1"), &qMenuStart);
  qMenuStart.addAction(&qCmdStart1);
  QAction qCmdStart2(QString::fromUtf8("Command 2"), &qMenuStart);
  qMenuStart.addAction(&qCmdStart2);
  QAction qCmdStart3(QString::fromUtf8("Command 3"), &qMenuStart);
  qMenuStart.addAction(&qCmdStart3);
#ifdef USE_LABEL
  qBtnStart.setMenu(&qMenuStart);
  qStBarLayout.insertPermanentWidget(0, &qBtnStart);
#else // (not) USE_LABEL
  qCmdStart.setMenu(&qMenuStart);
  qToolbarStart.addAction(&qCmdStart);
  qStBarLayout.insertPermanentWidget(0, &qToolbarStart);
#endif // USE_LABEL
  // the rest of status bar
  QStatusBar qStBar;
  qStBar.showMessage(QString::fromUtf8("<- Start Menu"), 3000 /* ms */);
  qStBar.setSizeGripEnabled(false);
  qStBarLayout.insertPermanentWidget(1, &qStBar, 1);
  qWin.setStatusBar(&qStBarLayout);
  qWin.show();
  // install signal handlers
  QObject::connect(&qCmdStart1, &QAction::triggered,
    [](bool){ qDebug() << "Command 1 triggered"; });
  QObject::connect(&qCmdStart2, &QAction::triggered,
    [](bool){ qDebug() << "Command 2 triggered"; });
  QObject::connect(&qCmdStart3, &QAction::triggered,
    [](bool){ qDebug() << "Command 3 triggered"; });
  // run application
  return qApp.exec();
}

我在Windows 10(64位)上使用VS2013和Qt5.6编译。这是它的样子:

Snapshot of testQStartBtn on Windows 10

更新后的版本,带有派生的 QLabel,看起来像这样:

Snapshot of updated testQStartBtn on Windows 10


解决方案很好,但实际上我需要实现的是在状态栏左上角图标上的单击事件。我已经通过QPixmap在qlabel中创建了状态栏中的startmenu图标。//菜单图标 menuIcon->setPixmap(QPixmap(":/new/prefix1/ic_menu_black.png")); menuIcon->setScaledContents(false); statusBar()->addWidget(menuIcon);有没有可能在这个menuIcon(Qlabel)上添加单击操作? - Durga Sekar
我相信这是可能的:1. 制作一个QLabel的派生类。2. 重载至少mousePressEvent()方法。在这种情况下,您必须在重载的方法中show()菜单,并可能计算和设置其正确的位置。我不太确定是否需要其他一些东西来使QLabel接受鼠标事件。我相信 - 不需要。 - Scheff's Cat
因此,我的第一个想法是使用 QPushButton。这提供了一个现成的信号 clicked。因此,不需要进行重载。如果必要,可以通过修改其属性/样式来调整按钮的外观。但是,菜单的放置和显示仍然必须在 clicked 信号处理程序中完成。一般来说:如果我有疑问,我会在 woboq.com 上搜索 Qt 的原始实现方式,但有时有点繁琐。 - Scheff's Cat
我根据我们的沟通更新了答案。 - Scheff's Cat
@DurgaSekar 你有注意到 if (!_pQMenu->isVisible()) 这个技巧吗?我这样做是为了在第二次点击“开始按钮”而没有选择菜单项时再次隐藏菜单。我认为这样做很漂亮,就像 Windows 一样。 - Scheff's Cat
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接