上一篇《环境搭建与第一个程序》我们跑通了工具链,有了一个带原厂标题栏的窗口。这一篇的目标更进一步:做出和系统自带软件看不出差别的界面。先看成品的设备实拍截图:

“原厂风格“由三个部分组成,缺一不可:
- 系统标题栏:QPE 绘制的蓝灰色标题栏,带应用标题和系统按钮;
- 主题样式:BlueStorm 主题,决定控件的颜色和质感;
- 诺亚舟私有控件:
NM前缀的控件家族,和系统软件同源。
下面逐个拆解。
屏幕的分配:26 + 214 = 240
NP1380 的屏幕是 320x240。原厂程序的通用做法是:
- 应用程序只拥有 320x214 的客户区;
- QPE 在客户区上方补一条 26 像素的系统标题栏;
- 两者拼起来正好满屏。
对应的窗口代码(第一篇已经见过,这里讲透):
NMQWidget *window = new(storage)
NMQWidget(0, "demo", Qt::WType_TopLevel | Qt::WStyle_StaysOnTop, FALSE);
window->setGeometry(0, 0, 320, 214);
window->setFixedSize(320, 214);
window->setCaption("NP1380 Demo");
一个容易踩的坑:不要给窗口加 WStyle_Customize | WStyle_NoBorderEx。这两个标志会告诉 QWS“这个窗口不需要系统装饰“,于是标题栏整个消失。想要原厂标题栏,就让它自然创建。
标题栏还有两个控制接口,通过 NMQWidget 的内部接口取得(+0x9c 偏移的原理见第三篇):
NMTitleButtonsInterface *title = np1380TitleButtons(window);
title->SetCustomButtons(0); // 自定义按钮位掩码,0 表示不声明
title->SetTitleVisible(TRUE); // 显式打开标题栏
SetCustomButtons() 的位掩码语义尚未完全逆向确认,内置程序里观察到 1、2、4 三个位,实机对比过 0/1/2/4/5 的外观一致,所以就用 0,不给未确认的位编造用途。
最后是主窗口的呈现方式:
app->showMainWidget(window, TRUE);
第二个参数 TRUE 表示“不要让 QPE 再把客户区最大化“,保住我们精心计算的 320x214,否则拼不上那条 26 像素的标题栏。
主题:一键切换 BlueStorm
QPE 支持主题样式,设备默认主题叫 BlueStorm(蓝灰色调)。两行调用即可套用:
QPEApplication *app = new(storage) QPEApplication(argc, argv, QApplication::GuiClient);
app->applyStyle();
Global::setDefaultBlueStormStyle();
之后无论是标准 Qt 控件还是 NM 控件,都会呈现原厂的颜色和边框质感。
NM 控件家族逐个讲
所有 NM 控件都构造在 4 KiB placement 槽位里(第一篇的规矩,第三篇讲原理),并且建议直接以根 NMQWidget 为父控件——嵌套进普通容器后,这个固件的部分 Noah 控件会不绘制。
NMCheckBox:复选框
NMCheckBox *check = new(storage) NMCheckBox("Enabled", root, "nm_check");
check->setChecked(TRUE);
check->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
check->setGap(2); // 图标与文字的间距
check->setMargin(1); // 控件内边距
check->setGeometry(14, 28, 150, 26);
check->show();
QLineEdit 与输入法
输入框用标准 QLineEdit,但要多一步开启固件输入法,否则点进去无法输入:
QLineEdit *edit = new QLineEdit(root, "input_edit");
edit->setFrame(TRUE);
edit->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
edit->setMaxLength(32);
edit->setGeometry(14, 80, 242, 28);
edit->show();
np1380EnableLineEditInput(edit); // 开启设备输入法
edit->setFocus();
注意调用顺序:构造 → 设置属性和位置 → 显示 → 开输入法 → 给焦点。np1380EnableLineEditInput() 绑定的是 libqte.so 里两个没有出现在公开头文件中的扩展方法,绑定技巧在第三篇展开。
NMTapButton:分段选择按钮
适合做单选切换条,setSelected() 表示当前选中项:
NMTapButton *tapA = new(storage) NMTapButton("A", root, "nm_tap_a", 0);
tapA->setAlignment(Qt::AlignCenter);
tapA->setSelected(TRUE);
tapA->setGeometry(14, 136, 74, 26);
tapA->show();
NMPushButton:按钮
普通操作按钮,clicked() 信号和标准 Qt 一样用:
NMPushButton *apply = new(storage) NMPushButton("Apply", root, "nm_apply", 0);
apply->setAlignment(Qt::AlignCenter);
apply->setGeometry(14, 500, 114, 26);
apply->show();
QObject::connect(apply, SIGNAL(clicked()), qApp, SLOT(quit()));
NMSlider:滑块
诺亚舟自绘的滑块,不是标准 QSlider(标准版在这个固件上不稳定):
NMSlider *slider = new(storage) NMSlider(root, "nm_slider", 0, 100);
slider->setValue(60);
slider->setGeometry(14, 556, 238, 28);
slider->show();
NMRadiosView 与 NMImageSets:集合控件
单选组和图片组,构造参数是 Qt 2 风格的 QList<QString>——它实际保存的是字符串指针,必须保证这些字符串的寿命覆盖控件本身:
QList<QString> items;
items.append(new QString("Low"));
items.append(new QString("Medium"));
items.append(new QString("High"));
NMRadiosView *radios = new(storage) NMRadiosView(items, root, "nm_radios");
radios->setAllOff();
radios->setGeometry(14, 618, 238, 84);
radios->show();
NMImageSets 用法相同,但有一个硬件级坑:列表至少要给三个字符串,固件的构造路径会直接访问前三项,少于三项在实机上会触发信号 139(段错误):
QList<QString> images;
images.append(new QString("Image 1"));
images.append(new QString("Image 2"));
images.append(new QString("Image 3")); // 少这一项就会崩
NMImageSets *imageSets = new(storage) NMImageSets(images, root, "nm_image_sets");
imageSets->setAllOff();
上面这些控件在设备上的实际效果:


NMFollowList:原厂蓝色滚动条
这是原厂界面的灵魂控件——右侧那条蓝色滚动条:
NMFollowList *scroll = new(storage)
NMFollowList(root, 48, viewHeight, "content_follow", 0);
scroll->setGeometry(147, viewTop, 306, viewHeight);
QFrame *content = new QFrame(scroll, "follow_content");
content->setGeometry(0, 0, 24, contentHeight); // 内容总高度
scroll->setWidget(content, NMFollowListLayoutTop);
scroll->setWaitingTextVisble(FALSE); // Visble 是固件原始拼写,不能改
scroll->initScrollBar(scrollMax, 80); // 最大值与翻页步长
scroll->setScrollBarValue(1); // 首个有效位置是 1,不是 0
setWidget() 绑定的子控件只用于计算内容高度,不要把真正的业务控件嵌进去(嵌套的 Noah 控件可能不绘制)。让业务控件跟随滚动条移动、显示和隐藏的完整方案,是第三篇的重头戏。
布局:直接算坐标
Qt 2 时代的嵌入式开发没有花哨的布局器可用,而 320 像素的宽度让绝对坐标布局反而非常直观。三种基础排列的坐标算法:
垂直排列——同 x、同宽,y 等差递增:
row1->setGeometry(14, 224, 238, 22);
row2->setGeometry(14, 250, 238, 22); // y +26
row3->setGeometry(14, 276, 238, 22);
水平排列——同 y、同高,x 等差递增:
left->setGeometry(14, 328, 74, 26);
center->setGeometry(96, 328, 74, 26); // x +82
right->setGeometry(178, 328, 74, 26);
2x2 网格——两种步长的组合:
a1->setGeometry(14, 386, 114, 24);
a2->setGeometry(138, 386, 114, 24); // x +124
b1->setGeometry(14, 414, 114, 24); // y +28
b2->setGeometry(138, 414, 114, 24);
配合分组标签(QFrame::Panel | QFrame::Raised 样式的 QLabel)把界面切成一节一节,滚动时很容易定位。
固定页脚
屏幕底部 35 像素留给一个不随内容滚动的页脚:一个 QFrame::Panel | QFrame::Raised 面板上放“Top“和“Exit“两个 NMPushButton。“Top“把滚动条拨回位置 1,“Exit“连接 qApp->quit()。关键是页脚控件不要加入滚动管理体系,并最后 raise() 到最上层,这样内容滚动时它们始终可用。
小结与预告
现在我们的程序已经有了全套原厂行头:系统标题栏、BlueStorm 主题、NM 控件、蓝色滚动条、固定页脚。代码里那些“奇怪的规矩“——placement 槽位、+0x9c 偏移、不能嵌套的控件——都被我们照做了,但还没说为什么。
第三篇《ABI 兼容与滚动布局实战》揭开这些规矩的成因:在没有原厂头文件的世界里,如何用逆向出的最小声明和固件安全共处,以及让一屏内容顺滑滚动起来的完整方案。