用Qt写软件系列四:定制个性化系统托盘菜单

导读

    一款流行的软件,往往会在功能渐趋完善的时候,通过改善交互界面来提高用户体验。毕竟,就算再牛逼的产品,躲藏在糟糕的用户界面之后总会让用户心生不满。界面设计需综合考虑审美学、心理学、设计学等多因素,是一份精细活。这篇博文仍然以Qt的使用为主旨,探讨一下在Qt中如何进行系统托盘的个性化定制。

介绍

    首先我们看看几款知名软件的系统托盘设计:

    上图是金山卫士的系统托盘菜单设计。我们稍作分析:整个托盘菜单窗口是个半透明的设计,窗口边框进行了圆角处理。底部的菜单项包含三个Button,倒数第二、三个菜单项的右部还加上了一个自定义的单选按钮。顶部菜单项则包含一个评级组件;其他菜单项则没有什么特别,加上对应的图标即可完成设计。但是可能由于整个背景色的缘故,导致整体效果看起来灰蒙蒙的,不太亮堂。

     上图是360安全卫士的托盘菜单。顶部和底部的两个菜单项都将背景色设置成了360安全卫士的主题色,加上了两个标签和按钮。其他菜单项保持不变。另外,菜单的背景色也被设置成了白色。整个菜单的设计较为简洁、清爽。虽然并不喜欢用360安全卫士,但是并不妨碍我对其产品外观设计的赞赏。

原型设计

     既然有了上述两款产品的参考,我们也可以试着设计下自己的系统托盘。首先我们需要一个原型设计工具,将草图绘制好我们才能用代码将最终结果显示出来。这里推荐一个原型设计工具:Balsqmiq mockup。这款工具使用简单,其提供的原型组件非常丰富,使用会觉得非常方便。

     根据初步设想,我设计了如下的一个原型草图:

      在布局方面基本上综合了金山卫士和360安全卫士的设计特点。顶部菜单项部署两个Label, 一个用来显示应用程序的窗口标题或产品名称,另一个显示为go to visit,可以响应鼠标点击事件。底部菜单项和金山卫士一样,设置了三个按钮:Update, about, exit,使用水平均匀布局。其他的菜单项则和普通菜单项没有区别。 基本上,一个自定义的托盘菜单已经跃然而出。

代码实现

      根据上述的原型设计,我们要做的准备工作显然就是准备好图片。对于没有美工技能的程序员来说,寻找界面图片素材显然是一大难题。做不出图片显然只好去网上搜索了。本人在网上下载了一堆的图片压缩包,有一个值得推荐:异次元图标。另外还有一个图片搜索网站也值得推荐。在这里我准备的图片如下:

      每个图片都取了一个别名,这样在代码中我们直接使用图片别名,从而消除与图片具体名称的藕合性。资源准备好之后我们需要开始编码了。参考本人曾经写过的一篇博文(使用Qt创建系统托盘),可以实现一个默认主题的系统托盘菜单。但是这里我们要实现自定义托盘菜单,我们从QSystemTray派生一个子类,并定义好相关的类成员变量:

QMenu* m_trayMenu;

QWidget* m_topWidget;
QWidgetAction* m_topWidgetAction;
QLabel* m_topLabel;
QLabel* m_homeBtn;

QWidget* m_bottomWidget;
QWidgetAction* m_bottomWidgetAction;
QPushButton* m_updateBtn;
QPushButton* m_aboutBtn;
QPushButton* m_exitBtn;

QAction* m_runOnSystemBoot;
QAction* m_helpOnline;
QAction* m_homePage;
QAction* m_notification;
QAction* m_settings;

  显然,我们注意到一个平时没有接触到的:QWidgetAction。这个类自Qt 4.2引入,继承自QAction。根据类名也可以推测出其含义:使用QWidget来充当Menu的Action。于是,我们似乎明白了自定义菜单的精髓:用Widget来做Action。这里我们主要定义顶部菜单项和底部菜单项。因此我们定义了两个QWidgetAction。另外,我们还有一个疑问就是:布局好的Widget如何"伪装"成Action插入到菜单项中去呢?我们可以使用QWidgetAction的setDefaultWidget()方法来完成这项工作。后面的代码将会有说明。

      此外,我们还注意到:360安全卫士的底部菜单项和顶部菜单项的背景色都是绿色的这又该如何实现呢?一种可行的方法是,安装一个事件过滤器(Event Filter)。当过滤到绘制事件并且绘制的组件是顶部菜单项和底部菜单项时,我们改变绘制方式。代码如下:

bool SystemTray::eventFilter(QObject *obj, QEvent *event)
{
	if (obj == m_topWidget && event->type() == QEvent::Paint)
	{
		QPainter painter(m_topWidget);
		painter.setPen(Qt::NoPen);
		painter.setBrush(QColor(42, 120, 192));
		painter.drawRect(m_topWidget->rect());
	}
	return QSystemTrayIcon::eventFilter(obj, event);
}

  在完成了我们自己的绘制工作之后,还得再调用父类的事件过滤器,以免漏掉其他过滤工作。eventFilter()是一个protected方法,我们要在头文件中进行重写。

      接下来要做的工作就是完成顶部和底部菜单项的绘制工作。先看看顶部菜单项如何绘制:

void SystemTray::createTopWidget()
{
	m_topWidget = new QWidget();
	m_topWidgetAction = new QWidgetAction(m_trayMenu);
	m_topLabel = new QLabel(QStringLiteral("HUST Information Security Lab"));
	m_topLabel->setObjectName(QStringLiteral("WhiteLabel"));
	m_homeBtn = new QLabel(QStringLiteral("Visit"));
	m_homeBtn->setCursor(Qt::PointingHandCursor);
	m_homeBtn->setObjectName(QStringLiteral("WhiteLabel"));

	QVBoxLayout* m_topLayout = new QVBoxLayout();
	m_topLayout->addWidget(m_topLabel, 0, Qt::AlignLeft|Qt::AlignVCenter);
	m_topLayout->addWidget(m_homeBtn, 0, Qt::AlignRight|Qt::AlignVCenter);

	m_topLayout->setSpacing(5);
	m_topLayout->setContentsMargins(5, 5, 5, 5);

	m_topWidget->setLayout(m_topLayout);
	m_topWidget->installEventFilter(this);
	m_topWidgetAction->setDefaultWidget(m_topWidget);
}

  我们声明了两个Label标签,作用在上文已说明。然后用垂直布局管理器将两个标签分左右放置。注意语句:m_topWidget->installEventFilter(this)。这条语句完成了过滤器的安装。指针this表明窗口事件将先发往当前类的eventFilter()方法进行处理,如果不处理再发往其他类的过滤器进行处理。底部菜单项的初始化大致类似:

void SystemTray::createBottomWidget()
{
	m_bottomWidget = new QWidget();
	m_bottomWidgetAction = new QWidgetAction(m_trayMenu);

	m_updateBtn = new QPushButton(QIcon(":/menu/update"), QStringLiteral("Update"));
	m_updateBtn->setObjectName(QStringLiteral("TrayButton"));
	m_updateBtn->setFixedSize(60, 25);

	m_aboutBtn = new QPushButton(QIcon(":/menu/about"), QStringLiteral("About"));
	m_aboutBtn->setObjectName(QStringLiteral("TrayButton"));
	m_aboutBtn->setFixedSize(60, 25);

	m_exitBtn = new QPushButton(QIcon(":/menu/quit"), QStringLiteral("Exit"));
	m_exitBtn->setObjectName(QStringLiteral("TrayButton"));
	m_exitBtn->setFixedSize(60, 25);

	QHBoxLayout* m_bottomLayout = new QHBoxLayout();
	m_bottomLayout->addWidget(m_updateBtn, 0, Qt::AlignCenter);
	m_bottomLayout->addWidget(m_aboutBtn, 0, Qt::AlignCenter);
	m_bottomLayout->addWidget(m_exitBtn, 0, Qt::AlignCenter);

	m_bottomLayout->setSpacing(5);
	m_bottomLayout->setContentsMargins(5,5,5,5);

	m_bottomWidget->setLayout(m_bottomLayout);
	m_bottomWidgetAction->setDefaultWidget(m_bottomWidget);
}

  分别对三个按钮设置了大小和图标。具体的外观样式则使用了QSS来进行控制,因此我们还为每个按钮设置了一个Object Name。这个Object Name在QSS中充当ID选择器,便于样式控制。那么样式文件该如何编写呢?具体参看如下所示:

QMenu{
    background:white;
    border:1px solid lightgray; # 边框为灰色
}

QMenu::item{
    padding:0px 20px 0px 20px;
    margin-left: 5px;
    height:25px;
}

QMenu::item:selected:enabled{
    background: lightgray;   # 菜单项选中时背景色设置为浅灰色
    color: white;            # 文本颜色设置为白色,否则看不清文本内容了
}

QMenu::separator{
    height:1px;
    background: lightgray;   # 菜单分割线也设置为浅灰色
    margin:2px 0px 2px 0px;
}

QMenu::item:selected:!enabled{
    background:transparent;
}

QPushButton#TrayButton {
    border: none;    # 无边框按钮
    background: transparent;  # 按钮背景设置为透明,这样不会受到默认主题颜色干扰
}

QPushButton#TrayButton:hover {
    background: rgb(233, 237, 252);  # 鼠标悬停时,按钮背景色设为淡色
    color: rgb(42, 120, 192);    # 鼠标悬停时,文本颜色不变
}

  基本上,使用上面的样式设置就可完成基本样式设置。其他代码就不再详细叙述。到此,我们的托盘菜单就完成了个性化定制工作。

效果图

      根据上述代码,我们实现的最终效果图如下:

       前面也说过:界面设计是一门学问,综合了设计学、心理学、审美学等多学科。要设计出让人耳目一新的产品界面,需要设计师具备相当的设计功力。但不管最终设计的怎么样,我们已经知道了,如何实现具备个人特点的托盘菜单!

参考

posted @ 2014-03-30 14:29  24K纯开源  阅读(6340)  评论(9编辑  收藏  举报