Cursor blinking

多窗口页面管理

Qt 基础|字数 1,143|阅读时长≈ 3 分钟

前言

在项目开发过程中,我们常常面对多窗口页面联动或单窗口内包含多个页面容器的设计。随着需求的不断迭代,页面数量逐渐增加,页面之间的关联也变得愈发复杂,导致页面管理的难度显著提升。在这种情况下,开发新需求往往变得异常艰难,经常需要添加大量的 if else 语句来处理错误的页面跳转逻辑,这又使得页面逻辑更加混乱。如果我们不能妥善的处理,一点小小的改动,都会给我们布置了一个新的迷宫。

fig.1
fig.1

复杂性

在软件开发中,我们通常面临三种复杂性:业务复杂性(源于现实世界)、工具复杂性(包括开发语言、框架等)和团队(人员)复杂性。

针对多窗口页面管理,我们需要一个路由工具来简化页面跳转的操作。应对业务复杂性,我们需要对页面进行梳理,找出规律并定义合适的规则。在引入新工具和新规则对现有项目进行改造时,复杂性会急剧增加(两种工具、两中规则并行时的混乱)。因此,工具设计必须符合开发人员的直觉和习惯,规则需要足够简单且有效,并且团队需要能够轻松使用这些新的设计和规则。

示例程序

本篇博客通过一个 Qt Quick 示例程序,使用 StackView 作为容器管理页面,结合 routing 机制,遵循数据驱动显示和关注点分离的原则,提供了一个多窗口页面管理解决方式,希望读者在面对相似问题是能有所启示。

fig.3: 示例程序页面布局
fig.3: 示例程序页面布局
fig.4 示例程序页面层级结构
fig.4 示例程序页面层级结构
fig.5 示例程序演示
fig.5 示例程序演示

统一管理所有的页面资源

这里的路由与我们通常所讲的网络路由不一样,类别 Android 开发种 ARouter 的实现机制,示例中,路由特指基于页面的唯一标识,可以从一个页面出发,跳转到目标页面。

Qt 中可以使用枚举定义所有的页面,并且枚举值映射对应的页面资源路径。

Code
/** * 页面常量 */class PageConstants : public QObject{    Q_OBJECT    QML_ELEMENT public:    static PageConstants &getInstance()    {        static PageConstants instance;         return instance;    }     enum PageEnum    {        MainContainerPage = 10000,        HomeFirstPage,        HomeSecondPage,        HomeThirdPage,        HomeTwinContainerPage,        LeftFirstPage=20000,        LeftSecondPage,        RightFirstPage,        RightSecondPage,        SmallContainerPage = 30000,        SmallFirstPage,        SmallSecondPage,     };    Q_ENUM(PageEnum)     Q_INVOKABLE QString toQString(const PageEnum value)    {        return QMetaEnum::fromType<PageEnum>().valueToKey(value);    }     Q_INVOKABLE QString getPageQmlPath(const PageEnum value)    {        return m_pageMap[value];    } private:    explicit PageConstants(QObject *parent = nullptr) : QObject(parent)    {        initPageMap();    }    ~PageConstants() {};     void initPageMap()    {        m_pageMap[MainContainerPage] = "page/MainContainerPage.qml";        m_pageMap[HomeFirstPage] = "page/HomeFirstPage.qml";        m_pageMap[HomeSecondPage] = "page/HomeSecondPage.qml";        m_pageMap[HomeThirdPage] = "page/HomeThirdPage.qml";        m_pageMap[HomeTwinContainerPage] = "page/HomeTwinContainerPage.qml";        m_pageMap[LeftFirstPage] = "page/secondary/LeftFirstPage.qml";        m_pageMap[LeftSecondPage] = "page/secondary/LeftSecondPage.qml";        m_pageMap[RightFirstPage] = "page/secondary/RightFirstPage.qml";        m_pageMap[RightSecondPage] = "page/secondary/RightSecondPage.qml";        m_pageMap[SmallContainerPage] = "page/small/SmallContainerPage.qml";        m_pageMap[SmallFirstPage] = "page/small/SmallFirstPage.qml";        m_pageMap[SmallSecondPage] = "page/small/SmallSecondPage.qml";    }     std::map<int, QString> m_pageMap;}; 

页面跳转的管理

使用 Qt StackView 作为容器装载页面,基于 StackView 提供的 api 和其特性,对其进行封装。

Code
 // 一组参数定义目标页面、启动模式、传递参数、动画function createNavigateMap(pageEnum, launchMode, params = {}, operation = -1) {    var map = {        "pageEnum": pageEnum,        "launchMode": launchMode,        "params": params,        "operation": operation    };    return map;} // 关闭页面function closePage(stackView, pageEnum, launchMode) {    console.log("------ " + stackView.objectName + " closePage -> pageEnum:" + pageEnum + " pageEnumName:" + pageConstants.toQString(pageEnum) + " launchMode:" + launchMode + " launchModeName:" + launchConstants.toQString(launchMode));    if (stackView.depth === 0) {        return;    }    if (launchMode === LaunchConstants.Standard) {        if (stackView.currentItem.objectName === pageConstants.toQString(pageEnum)) {            stackView.pop();        }        return;    }} // 动态创建页面对象function createPageObject(stackView, pageEnum, params = {}) {    var pageComponent = Qt.createComponent(pageConstants.getPageQmlPath(pageEnum));    if (pageComponent && pageComponent.status === Component.Ready) {        var pageObject = pageComponent.createObject(stackView, params);        if (pageObject === null) {            console.log("------ createPageObject error: " + pageComponent.errorString());            return null;        }        return pageObject;    } else {        console.log("------ createPageObject error");    }    return null;} // 打开页面function navigateToPage(stackView, pageEnum, launchMode, params = {}, operation = -1) {    console.log("------ " + stackView.objectName + " navigateTo -> pageEnum:" + pageEnum + " pageEnumName:" + pageConstants.toQString(pageEnum) + " launchMode:" + launchMode + " launchModeName:" + launchConstants.toQString(launchMode) + " params:" + JSON.stringify(params) + " operation:" + operation);    if (launchMode === LaunchConstants.SingleInstance) {        stackView.clear();        stackView.push(createPageObject(stackView, pageEnum, params), params, operation);        return;    } else if (launchMode === LaunchConstants.SingleTask) {        if (stackView.depth === 0) {            stackView.push(createPageObject(stackView, pageEnum, params), params, operation);            return;        }        var targetIndex = -1;        stackView.find(function (item, index) {            if (item.objectName === pageConstants.toQString(pageEnum)) {                targetIndex = index;                return true;            }            return false;        });        if (targetIndex > -1) {            stackView.popToIndex(targetIndex);            // 判断当前页面,是否包含 updateData() 函数            if (stackView.currentItem.updateData) {                stackView.currentItem.updateData(params);            }            return;        }    }    stackView.push(createPageObject(stackView, pageEnum, params), params, operation);} 

借鉴 Android 中 Activity 的启动模式,页面入栈时,定义了三种模式:

  • Standard,页面添加到栈顶
  • SingleInstance,清空栈后添加
  • SingleTask,如果栈里面已经存在目标页面,弹出其上的页面,并调用目标页面的刷新函数

当然还要支持跳转页面时传递参数、设置动画。

Code
 mainPageRouter.navigateToPage(										 PageConstants.HomeThirdPage, LaunchConstants.SingleTask, {                   "bgColor": "lightred",                   "userInfo": {                        "name": "刷新 name",                        "age": -100	                    }                   });

页面跳转规则定义

在实现了页面的统一管理和路由工具的支持后,还有一个关键步骤:定义页面跳转的规则。

需求设计人员,通常不会考虑页面之间的关联关系。面对不断堆叠的业务场景,我们需要确保无论新添加多少页面,页面与页面直之间有多少组合显示形式,我们都能应对。定义简单且有效的规则,保障我们软件的可维护性和扩展性。

fig.6 页面跳转的管理
fig.6 页面跳转的管理
  • 每个页面由最近父级别的 router 所管理(一般情况下一个 StackView 容器对应一个 router 管理类)
  • 页面跳转由目标页面所在最小范围的 router 执行
  • 示例一:RightFirstPage → RightSecondPage,页面跳转由 HomeTwinPageRouter 执行
  • 示例二:HomeFirstPage→HomeTwinContainerPage,页面跳转由 MainPageRouter 执行
  • 示例三:大屏 HomeFirstPage→HomeTwinContainerPage,同时小屏 SamllFirstPage → SmallSecondPage,页面跳转由 DoubleScreenPageRouter 执行。
Code
class SmallPageRouter : public QObject{    Q_OBJECT     QML_ELEMENT public:    static SmallPageRouter &getInstance();    ~SmallPageRouter();     Q_INVOKABLE void navigateToPage(const PageConstants::PageEnum pageEnum, const LaunchConstants::LaunchMode launchMode, const QVariantMap &map = QVariantMap(), int operation = -1);     Q_INVOKABLE void closePage(const PageConstants::PageEnum pageEnum);     Q_INVOKABLE QVariantMap createNavigateMap(const PageConstants::PageEnum pageEnum, const LaunchConstants::LaunchMode launchMode, const QVariantMap &map = QVariantMap(), int operation = -1); signals:    void signalNavigateToPage(int pageEnum, int launchMode, QVariantMap map, int operation);     void signalClosePage(int pageEnum); private:    explicit SmallPageRouter(QObject *parent = nullptr);}; 

通过以上方法,就能让示例程序的页面管理顺利运行了!然而在实际项目中,页面结构可能更为复杂,还需要我们在实践中进一步完善和扩展工具,不断运用新的技巧来解决意想不到的问题。

参考文档&资料